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 < 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