1 /**
2  * Licensed under the Apache License, Version 2.0 (the "License");
3  * you may not use this file except in compliance with the License.
4  * You may obtain a copy of the License at
5  *
6  *   http://www.apache.org/licenses/LICENSE-2.0
7  *
8  * Unless required by applicable law or agreed to in writing, software
9  * distributed under the License is distributed on an "AS IS" BASIS,
10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  * See the License for the specific language governing permissions and
12  * limitations under the License. See accompanying LICENSE file.
13  */
14 package org.apache.hadoop.security.authentication.server;
15 
16 import org.apache.hadoop.classification.InterfaceAudience;
17 import org.apache.hadoop.classification.InterfaceStability;
18 import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
19 import org.apache.hadoop.security.authentication.client.AuthenticationException;
20 import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
21 import org.apache.hadoop.security.authentication.util.*;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24 
25 import javax.servlet.Filter;
26 import javax.servlet.FilterChain;
27 import javax.servlet.FilterConfig;
28 import javax.servlet.ServletContext;
29 import javax.servlet.ServletException;
30 import javax.servlet.ServletRequest;
31 import javax.servlet.ServletResponse;
32 import javax.servlet.http.Cookie;
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpServletRequestWrapper;
35 import javax.servlet.http.HttpServletResponse;
36 
37 import java.io.IOException;
38 import java.security.Principal;
39 import java.text.SimpleDateFormat;
40 import java.util.*;
41 
42 /**
43  * <p>The {@link AuthenticationFilter} enables protecting web application
44  * resources with different (pluggable)
45  * authentication mechanisms and signer secret providers.
46  * </p>
47  * <p>
48  * Out of the box it provides 2 authentication mechanisms: Pseudo and Kerberos SPNEGO.
49  * </p>
50  * Additional authentication mechanisms are supported via the {@link AuthenticationHandler} interface.
51  * <p>
52  * This filter delegates to the configured authentication handler for authentication and once it obtains an
53  * {@link AuthenticationToken} from it, sets a signed HTTP cookie with the token. For client requests
54  * that provide the signed HTTP cookie, it verifies the validity of the cookie, extracts the user information
55  * and lets the request proceed to the target resource.
56  * </p>
57  * The supported configuration properties are:
58  * <ul>
59  * <li>config.prefix: indicates the prefix to be used by all other configuration properties, the default value
60  * is no prefix. See below for details on how/why this prefix is used.</li>
61  * <li>[#PREFIX#.]type: simple|kerberos|#CLASS#, 'simple' is short for the
62  * {@link PseudoAuthenticationHandler}, 'kerberos' is short for {@link KerberosAuthenticationHandler}, otherwise
63  * the full class name of the {@link AuthenticationHandler} must be specified.</li>
64  * <li>[#PREFIX#.]signature.secret: when signer.secret.provider is set to
65  * "string" or not specified, this is the value for the secret used to sign the
66  * HTTP cookie.</li>
67  * <li>[#PREFIX#.]token.validity: time -in seconds- that the generated token is
68  * valid before a new authentication is triggered, default value is
69  * <code>3600</code> seconds. This is also used for the rollover interval for
70  * the "random" and "zookeeper" SignerSecretProviders.</li>
71  * <li>[#PREFIX#.]cookie.domain: domain to use for the HTTP cookie that stores the authentication token.</li>
72  * <li>[#PREFIX#.]cookie.path: path to use for the HTTP cookie that stores the authentication token.</li>
73  * </ul>
74  * <p>
75  * The rest of the configuration properties are specific to the {@link AuthenticationHandler} implementation and the
76  * {@link AuthenticationFilter} will take all the properties that start with the prefix #PREFIX#, it will remove
77  * the prefix from it and it will pass them to the the authentication handler for initialization. Properties that do
78  * not start with the prefix will not be passed to the authentication handler initialization.
79  * </p>
80  * <p>
81  * Out of the box it provides 3 signer secret provider implementations:
82  * "string", "random", and "zookeeper"
83  * </p>
84  * Additional signer secret providers are supported via the
85  * {@link SignerSecretProvider} class.
86  * <p>
87  * For the HTTP cookies mentioned above, the SignerSecretProvider is used to
88  * determine the secret to use for signing the cookies. Different
89  * implementations can have different behaviors.  The "string" implementation
90  * simply uses the string set in the [#PREFIX#.]signature.secret property
91  * mentioned above.  The "random" implementation uses a randomly generated
92  * secret that rolls over at the interval specified by the
93  * [#PREFIX#.]token.validity mentioned above.  The "zookeeper" implementation
94  * is like the "random" one, except that it synchronizes the random secret
95  * and rollovers between multiple servers; it's meant for HA services.
96  * </p>
97  * The relevant configuration properties are:
98  * <ul>
99  * <li>signer.secret.provider: indicates the name of the SignerSecretProvider
100  * class to use. Possible values are: "string", "random", "zookeeper", or a
101  * classname. If not specified, the "string" implementation will be used with
102  * [#PREFIX#.]signature.secret; and if that's not specified, the "random"
103  * implementation will be used.</li>
104  * <li>[#PREFIX#.]signature.secret: When the "string" implementation is
105  * specified, this value is used as the secret.</li>
106  * <li>[#PREFIX#.]token.validity: When the "random" or "zookeeper"
107  * implementations are specified, this value is used as the rollover
108  * interval.</li>
109  * </ul>
110  * <p>
111  * The "zookeeper" implementation has additional configuration properties that
112  * must be specified; see {@link ZKSignerSecretProvider} for details.
113  * </p>
114  * For subclasses of AuthenticationFilter that want additional control over the
115  * SignerSecretProvider, they can use the following attribute set in the
116  * ServletContext:
117  * <ul>
118  * <li>signer.secret.provider.object: A SignerSecretProvider implementation can
119  * be passed here that will be used instead of the signer.secret.provider
120  * configuration property. Note that the class should already be
121  * initialized.</li>
122  * </ul>
123  */
124 
125 @InterfaceAudience.Private
126 @InterfaceStability.Unstable
127 public class AuthenticationFilter implements Filter {
128 
129   private static Logger LOG = LoggerFactory.getLogger(AuthenticationFilter.class);
130 
131   /**
132    * Constant for the property that specifies the configuration prefix.
133    */
134   public static final String CONFIG_PREFIX = "config.prefix";
135 
136   /**
137    * Constant for the property that specifies the authentication handler to use.
138    */
139   public static final String AUTH_TYPE = "type";
140 
141   /**
142    * Constant for the property that specifies the secret to use for signing the HTTP Cookies.
143    */
144   public static final String SIGNATURE_SECRET = "signature.secret";
145 
146   public static final String SIGNATURE_SECRET_FILE = SIGNATURE_SECRET + ".file";
147 
148   /**
149    * Constant for the configuration property that indicates the validity of the generated token.
150    */
151   public static final String AUTH_TOKEN_VALIDITY = "token.validity";
152 
153   /**
154    * Constant for the configuration property that indicates the domain to use in the HTTP cookie.
155    */
156   public static final String COOKIE_DOMAIN = "cookie.domain";
157 
158   /**
159    * Constant for the configuration property that indicates the path to use in the HTTP cookie.
160    */
161   public static final String COOKIE_PATH = "cookie.path";
162 
163   /**
164    * Constant for the configuration property that indicates the name of the
165    * SignerSecretProvider class to use.
166    * Possible values are: "string", "random", "zookeeper", or a classname.
167    * If not specified, the "string" implementation will be used with
168    * SIGNATURE_SECRET; and if that's not specified, the "random" implementation
169    * will be used.
170    */
171   public static final String SIGNER_SECRET_PROVIDER =
172           "signer.secret.provider";
173 
174   /**
175    * Constant for the ServletContext attribute that can be used for providing a
176    * custom implementation of the SignerSecretProvider. Note that the class
177    * should already be initialized. If not specified, SIGNER_SECRET_PROVIDER
178    * will be used.
179    */
180   public static final String SIGNER_SECRET_PROVIDER_ATTRIBUTE =
181       "signer.secret.provider.object";
182 
183   private Properties config;
184   private Signer signer;
185   private SignerSecretProvider secretProvider;
186   private AuthenticationHandler authHandler;
187   private long validity;
188   private String cookieDomain;
189   private String cookiePath;
190   private boolean isInitializedByTomcat;
191 
192   /**
193    * <p>Initializes the authentication filter and signer secret provider.</p>
194    * It instantiates and initializes the specified {@link
195    * AuthenticationHandler}.
196    *
197    * @param filterConfig filter configuration.
198    *
199    * @throws ServletException thrown if the filter or the authentication handler could not be initialized properly.
200    */
201   @Override
init(FilterConfig filterConfig)202   public void init(FilterConfig filterConfig) throws ServletException {
203     String configPrefix = filterConfig.getInitParameter(CONFIG_PREFIX);
204     configPrefix = (configPrefix != null) ? configPrefix + "." : "";
205     config = getConfiguration(configPrefix, filterConfig);
206     String authHandlerName = config.getProperty(AUTH_TYPE, null);
207     String authHandlerClassName;
208     if (authHandlerName == null) {
209       throw new ServletException("Authentication type must be specified: " +
210           PseudoAuthenticationHandler.TYPE + "|" +
211           KerberosAuthenticationHandler.TYPE + "|<class>");
212     }
213     if (authHandlerName.toLowerCase(Locale.ENGLISH).equals(
214         PseudoAuthenticationHandler.TYPE)) {
215       authHandlerClassName = PseudoAuthenticationHandler.class.getName();
216     } else if (authHandlerName.toLowerCase(Locale.ENGLISH).equals(
217         KerberosAuthenticationHandler.TYPE)) {
218       authHandlerClassName = KerberosAuthenticationHandler.class.getName();
219     } else {
220       authHandlerClassName = authHandlerName;
221     }
222 
223     validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, "36000"))
224         * 1000; //10 hours
225     initializeSecretProvider(filterConfig);
226 
227     initializeAuthHandler(authHandlerClassName, filterConfig);
228 
229     cookieDomain = config.getProperty(COOKIE_DOMAIN, null);
230     cookiePath = config.getProperty(COOKIE_PATH, null);
231   }
232 
initializeAuthHandler(String authHandlerClassName, FilterConfig filterConfig)233   protected void initializeAuthHandler(String authHandlerClassName, FilterConfig filterConfig)
234       throws ServletException {
235     try {
236       Class<?> klass = Thread.currentThread().getContextClassLoader().loadClass(authHandlerClassName);
237       authHandler = (AuthenticationHandler) klass.newInstance();
238       authHandler.init(config);
239     } catch (ClassNotFoundException | InstantiationException |
240         IllegalAccessException ex) {
241       throw new ServletException(ex);
242     }
243   }
244 
initializeSecretProvider(FilterConfig filterConfig)245   protected void initializeSecretProvider(FilterConfig filterConfig)
246       throws ServletException {
247     secretProvider = (SignerSecretProvider) filterConfig.getServletContext().
248         getAttribute(SIGNER_SECRET_PROVIDER_ATTRIBUTE);
249     if (secretProvider == null) {
250       // As tomcat cannot specify the provider object in the configuration.
251       // It'll go into this path
252       try {
253         secretProvider = constructSecretProvider(
254             filterConfig.getServletContext(),
255             config, false);
256         isInitializedByTomcat = true;
257       } catch (Exception ex) {
258         throw new ServletException(ex);
259       }
260     }
261     signer = new Signer(secretProvider);
262   }
263 
constructSecretProvider( ServletContext ctx, Properties config, boolean disallowFallbackToRandomSecretProvider)264   public static SignerSecretProvider constructSecretProvider(
265       ServletContext ctx, Properties config,
266       boolean disallowFallbackToRandomSecretProvider) throws Exception {
267     String name = config.getProperty(SIGNER_SECRET_PROVIDER, "file");
268     long validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY,
269                                                       "36000")) * 1000;
270 
271     if (!disallowFallbackToRandomSecretProvider
272         && "file".equals(name)
273         && config.getProperty(SIGNATURE_SECRET_FILE) == null) {
274       name = "random";
275     }
276 
277     SignerSecretProvider provider;
278     if ("file".equals(name)) {
279       provider = new FileSignerSecretProvider();
280       try {
281         provider.init(config, ctx, validity);
282       } catch (Exception e) {
283         if (!disallowFallbackToRandomSecretProvider) {
284           LOG.info("Unable to initialize FileSignerSecretProvider, " +
285                        "falling back to use random secrets.");
286           provider = new RandomSignerSecretProvider();
287           provider.init(config, ctx, validity);
288         } else {
289           throw e;
290         }
291       }
292     } else if ("random".equals(name)) {
293       provider = new RandomSignerSecretProvider();
294       provider.init(config, ctx, validity);
295     } else if ("zookeeper".equals(name)) {
296       provider = new ZKSignerSecretProvider();
297       provider.init(config, ctx, validity);
298     } else {
299       provider = (SignerSecretProvider) Thread.currentThread().
300           getContextClassLoader().loadClass(name).newInstance();
301       provider.init(config, ctx, validity);
302     }
303     return provider;
304   }
305 
306   /**
307    * Returns the configuration properties of the {@link AuthenticationFilter}
308    * without the prefix. The returned properties are the same that the
309    * {@link #getConfiguration(String, FilterConfig)} method returned.
310    *
311    * @return the configuration properties.
312    */
getConfiguration()313   protected Properties getConfiguration() {
314     return config;
315   }
316 
317   /**
318    * Returns the authentication handler being used.
319    *
320    * @return the authentication handler being used.
321    */
getAuthenticationHandler()322   protected AuthenticationHandler getAuthenticationHandler() {
323     return authHandler;
324   }
325 
326   /**
327    * Returns if a random secret is being used.
328    *
329    * @return if a random secret is being used.
330    */
isRandomSecret()331   protected boolean isRandomSecret() {
332     return secretProvider.getClass() == RandomSignerSecretProvider.class;
333   }
334 
335   /**
336    * Returns if a custom implementation of a SignerSecretProvider is being used.
337    *
338    * @return if a custom implementation of a SignerSecretProvider is being used.
339    */
isCustomSignerSecretProvider()340   protected boolean isCustomSignerSecretProvider() {
341     Class<?> clazz = secretProvider.getClass();
342     return clazz != FileSignerSecretProvider.class && clazz !=
343         RandomSignerSecretProvider.class && clazz != ZKSignerSecretProvider
344         .class;
345   }
346 
347   /**
348    * Returns the validity time of the generated tokens.
349    *
350    * @return the validity time of the generated tokens, in seconds.
351    */
getValidity()352   protected long getValidity() {
353     return validity / 1000;
354   }
355 
356   /**
357    * Returns the cookie domain to use for the HTTP cookie.
358    *
359    * @return the cookie domain to use for the HTTP cookie.
360    */
getCookieDomain()361   protected String getCookieDomain() {
362     return cookieDomain;
363   }
364 
365   /**
366    * Returns the cookie path to use for the HTTP cookie.
367    *
368    * @return the cookie path to use for the HTTP cookie.
369    */
getCookiePath()370   protected String getCookiePath() {
371     return cookiePath;
372   }
373 
374   /**
375    * Destroys the filter.
376    * <p>
377    * It invokes the {@link AuthenticationHandler#destroy()} method to release any resources it may hold.
378    */
379   @Override
destroy()380   public void destroy() {
381     if (authHandler != null) {
382       authHandler.destroy();
383       authHandler = null;
384     }
385     if (secretProvider != null && isInitializedByTomcat) {
386       secretProvider.destroy();
387       secretProvider = null;
388     }
389   }
390 
391   /**
392    * Returns the filtered configuration (only properties starting with the specified prefix). The property keys
393    * are also trimmed from the prefix. The returned {@link Properties} object is used to initialized the
394    * {@link AuthenticationHandler}.
395    * <p>
396    * This method can be overriden by subclasses to obtain the configuration from other configuration source than
397    * the web.xml file.
398    *
399    * @param configPrefix configuration prefix to use for extracting configuration properties.
400    * @param filterConfig filter configuration object
401    *
402    * @return the configuration to be used with the {@link AuthenticationHandler} instance.
403    *
404    * @throws ServletException thrown if the configuration could not be created.
405    */
getConfiguration(String configPrefix, FilterConfig filterConfig)406   protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException {
407     Properties props = new Properties();
408     Enumeration<?> names = filterConfig.getInitParameterNames();
409     while (names.hasMoreElements()) {
410       String name = (String) names.nextElement();
411       if (name.startsWith(configPrefix)) {
412         String value = filterConfig.getInitParameter(name);
413         props.put(name.substring(configPrefix.length()), value);
414       }
415     }
416     return props;
417   }
418 
419   /**
420    * Returns the full URL of the request including the query string.
421    * <p>
422    * Used as a convenience method for logging purposes.
423    *
424    * @param request the request object.
425    *
426    * @return the full URL of the request including the query string.
427    */
getRequestURL(HttpServletRequest request)428   protected String getRequestURL(HttpServletRequest request) {
429     StringBuffer sb = request.getRequestURL();
430     if (request.getQueryString() != null) {
431       sb.append("?").append(request.getQueryString());
432     }
433     return sb.toString();
434   }
435 
436   /**
437    * Returns the {@link AuthenticationToken} for the request.
438    * <p>
439    * It looks at the received HTTP cookies and extracts the value of the {@link AuthenticatedURL#AUTH_COOKIE}
440    * if present. It verifies the signature and if correct it creates the {@link AuthenticationToken} and returns
441    * it.
442    * <p>
443    * If this method returns <code>null</code> the filter will invoke the configured {@link AuthenticationHandler}
444    * to perform user authentication.
445    *
446    * @param request request object.
447    *
448    * @return the Authentication token if the request is authenticated, <code>null</code> otherwise.
449    *
450    * @throws IOException thrown if an IO error occurred.
451    * @throws AuthenticationException thrown if the token is invalid or if it has expired.
452    */
getToken(HttpServletRequest request)453   protected AuthenticationToken getToken(HttpServletRequest request) throws IOException, AuthenticationException {
454     AuthenticationToken token = null;
455     String tokenStr = null;
456     Cookie[] cookies = request.getCookies();
457     if (cookies != null) {
458       for (Cookie cookie : cookies) {
459         if (cookie.getName().equals(AuthenticatedURL.AUTH_COOKIE)) {
460           tokenStr = cookie.getValue();
461           try {
462             tokenStr = signer.verifyAndExtract(tokenStr);
463           } catch (SignerException ex) {
464             throw new AuthenticationException(ex);
465           }
466           break;
467         }
468       }
469     }
470     if (tokenStr != null) {
471       token = AuthenticationToken.parse(tokenStr);
472       if (!token.getType().equals(authHandler.getType())) {
473         throw new AuthenticationException("Invalid AuthenticationToken type");
474       }
475       if (token.isExpired()) {
476         throw new AuthenticationException("AuthenticationToken expired");
477       }
478     }
479     return token;
480   }
481 
482   /**
483    * If the request has a valid authentication token it allows the request to continue to the target resource,
484    * otherwise it triggers an authentication sequence using the configured {@link AuthenticationHandler}.
485    *
486    * @param request the request object.
487    * @param response the response object.
488    * @param filterChain the filter chain object.
489    *
490    * @throws IOException thrown if an IO error occurred.
491    * @throws ServletException thrown if a processing error occurred.
492    */
493   @Override
doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)494   public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
495       throws IOException, ServletException {
496     boolean unauthorizedResponse = true;
497     int errCode = HttpServletResponse.SC_UNAUTHORIZED;
498     AuthenticationException authenticationEx = null;
499     HttpServletRequest httpRequest = (HttpServletRequest) request;
500     HttpServletResponse httpResponse = (HttpServletResponse) response;
501     boolean isHttps = "https".equals(httpRequest.getScheme());
502     try {
503       boolean newToken = false;
504       AuthenticationToken token;
505       try {
506         token = getToken(httpRequest);
507       }
508       catch (AuthenticationException ex) {
509         LOG.warn("AuthenticationToken ignored: " + ex.getMessage());
510         // will be sent back in a 401 unless filter authenticates
511         authenticationEx = ex;
512         token = null;
513       }
514       if (authHandler.managementOperation(token, httpRequest, httpResponse)) {
515         if (token == null) {
516           if (LOG.isDebugEnabled()) {
517             LOG.debug("Request [{}] triggering authentication", getRequestURL(httpRequest));
518           }
519           token = authHandler.authenticate(httpRequest, httpResponse);
520           if (token != null && token.getExpires() != 0 &&
521               token != AuthenticationToken.ANONYMOUS) {
522             token.setExpires(System.currentTimeMillis() + getValidity() * 1000);
523           }
524           newToken = true;
525         }
526         if (token != null) {
527           unauthorizedResponse = false;
528           if (LOG.isDebugEnabled()) {
529             LOG.debug("Request [{}] user [{}] authenticated", getRequestURL(httpRequest), token.getUserName());
530           }
531           final AuthenticationToken authToken = token;
532           httpRequest = new HttpServletRequestWrapper(httpRequest) {
533 
534             @Override
535             public String getAuthType() {
536               return authToken.getType();
537             }
538 
539             @Override
540             public String getRemoteUser() {
541               return authToken.getUserName();
542             }
543 
544             @Override
545             public Principal getUserPrincipal() {
546               return (authToken != AuthenticationToken.ANONYMOUS) ? authToken : null;
547             }
548           };
549           if (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) {
550             String signedToken = signer.sign(token.toString());
551             createAuthCookie(httpResponse, signedToken, getCookieDomain(),
552                     getCookiePath(), token.getExpires(), isHttps);
553           }
554           doFilter(filterChain, httpRequest, httpResponse);
555         }
556       } else {
557         unauthorizedResponse = false;
558       }
559     } catch (AuthenticationException ex) {
560       // exception from the filter itself is fatal
561       errCode = HttpServletResponse.SC_FORBIDDEN;
562       authenticationEx = ex;
563       if (LOG.isDebugEnabled()) {
564         LOG.debug("Authentication exception: " + ex.getMessage(), ex);
565       } else {
566         LOG.warn("Authentication exception: " + ex.getMessage());
567       }
568     }
569     if (unauthorizedResponse) {
570       if (!httpResponse.isCommitted()) {
571         createAuthCookie(httpResponse, "", getCookieDomain(),
572                 getCookiePath(), 0, isHttps);
573         // If response code is 401. Then WWW-Authenticate Header should be
574         // present.. reset to 403 if not found..
575         if ((errCode == HttpServletResponse.SC_UNAUTHORIZED)
576             && (!httpResponse.containsHeader(
577                 KerberosAuthenticator.WWW_AUTHENTICATE))) {
578           errCode = HttpServletResponse.SC_FORBIDDEN;
579         }
580         if (authenticationEx == null) {
581           httpResponse.sendError(errCode, "Authentication required");
582         } else {
583           httpResponse.sendError(errCode, authenticationEx.getMessage());
584         }
585       }
586     }
587   }
588 
589   /**
590    * Delegates call to the servlet filter chain. Sub-classes my override this
591    * method to perform pre and post tasks.
592    */
doFilter(FilterChain filterChain, HttpServletRequest request, HttpServletResponse response)593   protected void doFilter(FilterChain filterChain, HttpServletRequest request,
594       HttpServletResponse response) throws IOException, ServletException {
595     filterChain.doFilter(request, response);
596   }
597 
598   /**
599    * Creates the Hadoop authentication HTTP cookie.
600    *
601    * @param token authentication token for the cookie.
602    * @param expires UNIX timestamp that indicates the expire date of the
603    *                cookie. It has no effect if its value &lt; 0.
604    *
605    * XXX the following code duplicate some logic in Jetty / Servlet API,
606    * because of the fact that Hadoop is stuck at servlet 2.5 and jetty 6
607    * right now.
608    */
createAuthCookie(HttpServletResponse resp, String token, String domain, String path, long expires, boolean isSecure)609   public static void createAuthCookie(HttpServletResponse resp, String token,
610                                       String domain, String path, long expires,
611                                       boolean isSecure) {
612     StringBuilder sb = new StringBuilder(AuthenticatedURL.AUTH_COOKIE)
613                            .append("=");
614     if (token != null && token.length() > 0) {
615       sb.append("\"").append(token).append("\"");
616     }
617 
618     if (path != null) {
619       sb.append("; Path=").append(path);
620     }
621 
622     if (domain != null) {
623       sb.append("; Domain=").append(domain);
624     }
625 
626     if (expires >= 0) {
627       Date date = new Date(expires);
628       SimpleDateFormat df = new SimpleDateFormat("EEE, " +
629               "dd-MMM-yyyy HH:mm:ss zzz");
630       df.setTimeZone(TimeZone.getTimeZone("GMT"));
631       sb.append("; Expires=").append(df.format(date));
632     }
633 
634     if (isSecure) {
635       sb.append("; Secure");
636     }
637 
638     sb.append("; HttpOnly");
639     resp.addHeader("Set-Cookie", sb.toString());
640   }
641 }
642