1// 2// ======================================================================== 3// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. 4// ------------------------------------------------------------------------ 5// All rights reserved. This program and the accompanying materials 6// are made available under the terms of the Eclipse Public License v1.0 7// and Apache License v2.0 which accompanies this distribution. 8// 9// The Eclipse Public License is available at 10// http://www.eclipse.org/legal/epl-v10.html 11// 12// The Apache License v2.0 is available at 13// http://www.opensource.org/licenses/apache2.0.php 14// 15// You may elect to redistribute this code under either of these licenses. 16// ======================================================================== 17// 18 19package org.eclipse.jetty.security.authentication; 20 21import java.io.IOException; 22import java.util.Collections; 23import java.util.Enumeration; 24import java.util.Locale; 25 26import javax.servlet.RequestDispatcher; 27import javax.servlet.ServletException; 28import javax.servlet.ServletRequest; 29import javax.servlet.ServletResponse; 30import javax.servlet.http.HttpServletRequest; 31import javax.servlet.http.HttpServletRequestWrapper; 32import javax.servlet.http.HttpServletResponse; 33import javax.servlet.http.HttpServletResponseWrapper; 34import javax.servlet.http.HttpSession; 35 36import org.eclipse.jetty.http.HttpHeaders; 37import org.eclipse.jetty.http.HttpMethods; 38import org.eclipse.jetty.http.MimeTypes; 39import org.eclipse.jetty.security.ServerAuthException; 40import org.eclipse.jetty.security.UserAuthentication; 41import org.eclipse.jetty.server.AbstractHttpConnection; 42import org.eclipse.jetty.server.Authentication; 43import org.eclipse.jetty.server.Authentication.User; 44import org.eclipse.jetty.server.Request; 45import org.eclipse.jetty.server.UserIdentity; 46import org.eclipse.jetty.util.MultiMap; 47import org.eclipse.jetty.util.StringUtil; 48import org.eclipse.jetty.util.URIUtil; 49import org.eclipse.jetty.util.log.Log; 50import org.eclipse.jetty.util.log.Logger; 51import org.eclipse.jetty.util.security.Constraint; 52 53/** 54 * FORM Authenticator. 55 * 56 * <p>This authenticator implements form authentication will use dispatchers to 57 * the login page if the {@link #__FORM_DISPATCH} init parameter is set to true. 58 * Otherwise it will redirect.</p> 59 * 60 * <p>The form authenticator redirects unauthenticated requests to a log page 61 * which should use a form to gather username/password from the user and send them 62 * to the /j_security_check URI within the context. FormAuthentication uses 63 * {@link SessionAuthentication} to wrap Authentication results so that they 64 * are associated with the session.</p> 65 * 66 * 67 */ 68public class FormAuthenticator extends LoginAuthenticator 69{ 70 private static final Logger LOG = Log.getLogger(FormAuthenticator.class); 71 72 public final static String __FORM_LOGIN_PAGE="org.eclipse.jetty.security.form_login_page"; 73 public final static String __FORM_ERROR_PAGE="org.eclipse.jetty.security.form_error_page"; 74 public final static String __FORM_DISPATCH="org.eclipse.jetty.security.dispatch"; 75 public final static String __J_URI = "org.eclipse.jetty.security.form_URI"; 76 public final static String __J_POST = "org.eclipse.jetty.security.form_POST"; 77 public final static String __J_SECURITY_CHECK = "/j_security_check"; 78 public final static String __J_USERNAME = "j_username"; 79 public final static String __J_PASSWORD = "j_password"; 80 81 private String _formErrorPage; 82 private String _formErrorPath; 83 private String _formLoginPage; 84 private String _formLoginPath; 85 private boolean _dispatch; 86 private boolean _alwaysSaveUri; 87 88 public FormAuthenticator() 89 { 90 } 91 92 /* ------------------------------------------------------------ */ 93 public FormAuthenticator(String login,String error,boolean dispatch) 94 { 95 this(); 96 if (login!=null) 97 setLoginPage(login); 98 if (error!=null) 99 setErrorPage(error); 100 _dispatch=dispatch; 101 } 102 103 /* ------------------------------------------------------------ */ 104 /** 105 * If true, uris that cause a redirect to a login page will always 106 * be remembered. If false, only the first uri that leads to a login 107 * page redirect is remembered. 108 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=379909 109 * @param alwaysSave 110 */ 111 public void setAlwaysSaveUri (boolean alwaysSave) 112 { 113 _alwaysSaveUri = alwaysSave; 114 } 115 116 117 /* ------------------------------------------------------------ */ 118 public boolean getAlwaysSaveUri () 119 { 120 return _alwaysSaveUri; 121 } 122 123 /* ------------------------------------------------------------ */ 124 /** 125 * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration) 126 */ 127 @Override 128 public void setConfiguration(AuthConfiguration configuration) 129 { 130 super.setConfiguration(configuration); 131 String login=configuration.getInitParameter(FormAuthenticator.__FORM_LOGIN_PAGE); 132 if (login!=null) 133 setLoginPage(login); 134 String error=configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE); 135 if (error!=null) 136 setErrorPage(error); 137 String dispatch=configuration.getInitParameter(FormAuthenticator.__FORM_DISPATCH); 138 _dispatch = dispatch==null?_dispatch:Boolean.valueOf(dispatch); 139 } 140 141 /* ------------------------------------------------------------ */ 142 public String getAuthMethod() 143 { 144 return Constraint.__FORM_AUTH; 145 } 146 147 /* ------------------------------------------------------------ */ 148 private void setLoginPage(String path) 149 { 150 if (!path.startsWith("/")) 151 { 152 LOG.warn("form-login-page must start with /"); 153 path = "/" + path; 154 } 155 _formLoginPage = path; 156 _formLoginPath = path; 157 if (_formLoginPath.indexOf('?') > 0) 158 _formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?')); 159 } 160 161 /* ------------------------------------------------------------ */ 162 private void setErrorPage(String path) 163 { 164 if (path == null || path.trim().length() == 0) 165 { 166 _formErrorPath = null; 167 _formErrorPage = null; 168 } 169 else 170 { 171 if (!path.startsWith("/")) 172 { 173 LOG.warn("form-error-page must start with /"); 174 path = "/" + path; 175 } 176 _formErrorPage = path; 177 _formErrorPath = path; 178 179 if (_formErrorPath.indexOf('?') > 0) 180 _formErrorPath = _formErrorPath.substring(0, _formErrorPath.indexOf('?')); 181 } 182 } 183 184 185 /* ------------------------------------------------------------ */ 186 @Override 187 public UserIdentity login(String username, Object password, ServletRequest request) 188 { 189 190 UserIdentity user = super.login(username,password,request); 191 if (user!=null) 192 { 193 HttpSession session = ((HttpServletRequest)request).getSession(true); 194 Authentication cached=new SessionAuthentication(getAuthMethod(),user,password); 195 session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached); 196 } 197 return user; 198 } 199 200 /* ------------------------------------------------------------ */ 201 public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException 202 { 203 HttpServletRequest request = (HttpServletRequest)req; 204 HttpServletResponse response = (HttpServletResponse)res; 205 String uri = request.getRequestURI(); 206 if (uri==null) 207 uri=URIUtil.SLASH; 208 209 mandatory|=isJSecurityCheck(uri); 210 if (!mandatory) 211 return new DeferredAuthentication(this); 212 213 if (isLoginOrErrorPage(URIUtil.addPaths(request.getServletPath(),request.getPathInfo())) &&!DeferredAuthentication.isDeferred(response)) 214 return new DeferredAuthentication(this); 215 216 HttpSession session = request.getSession(true); 217 218 try 219 { 220 // Handle a request for authentication. 221 if (isJSecurityCheck(uri)) 222 { 223 final String username = request.getParameter(__J_USERNAME); 224 final String password = request.getParameter(__J_PASSWORD); 225 226 UserIdentity user = login(username, password, request); 227 session = request.getSession(true); 228 if (user!=null) 229 { 230 // Redirect to original request 231 String nuri; 232 synchronized(session) 233 { 234 nuri = (String) session.getAttribute(__J_URI); 235 236 if (nuri == null || nuri.length() == 0) 237 { 238 nuri = request.getContextPath(); 239 if (nuri.length() == 0) 240 nuri = URIUtil.SLASH; 241 } 242 } 243 response.setContentLength(0); 244 response.sendRedirect(response.encodeRedirectURL(nuri)); 245 246 return new FormAuthentication(getAuthMethod(),user); 247 } 248 249 // not authenticated 250 if (LOG.isDebugEnabled()) 251 LOG.debug("Form authentication FAILED for " + StringUtil.printable(username)); 252 if (_formErrorPage == null) 253 { 254 if (response != null) 255 response.sendError(HttpServletResponse.SC_FORBIDDEN); 256 } 257 else if (_dispatch) 258 { 259 RequestDispatcher dispatcher = request.getRequestDispatcher(_formErrorPage); 260 response.setHeader(HttpHeaders.CACHE_CONTROL,"No-cache"); 261 response.setDateHeader(HttpHeaders.EXPIRES,1); 262 dispatcher.forward(new FormRequest(request), new FormResponse(response)); 263 } 264 else 265 { 266 response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formErrorPage))); 267 } 268 269 return Authentication.SEND_FAILURE; 270 } 271 272 // Look for cached authentication 273 Authentication authentication = (Authentication) session.getAttribute(SessionAuthentication.__J_AUTHENTICATED); 274 if (authentication != null) 275 { 276 // Has authentication been revoked? 277 if (authentication instanceof Authentication.User && 278 _loginService!=null && 279 !_loginService.validate(((Authentication.User)authentication).getUserIdentity())) 280 { 281 282 session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED); 283 } 284 else 285 { 286 String j_uri=(String)session.getAttribute(__J_URI); 287 if (j_uri!=null) 288 { 289 MultiMap<String> j_post = (MultiMap<String>)session.getAttribute(__J_POST); 290 if (j_post!=null) 291 { 292 StringBuffer buf = request.getRequestURL(); 293 if (request.getQueryString() != null) 294 buf.append("?").append(request.getQueryString()); 295 296 if (j_uri.equals(buf.toString())) 297 { 298 // This is a retry of an original POST request 299 // so restore method and parameters 300 301 session.removeAttribute(__J_POST); 302 Request base_request = (req instanceof Request)?(Request)req:AbstractHttpConnection.getCurrentConnection().getRequest(); 303 base_request.setMethod(HttpMethods.POST); 304 base_request.setParameters(j_post); 305 } 306 } 307 else 308 session.removeAttribute(__J_URI); 309 310 } 311 return authentication; 312 } 313 } 314 315 // if we can't send challenge 316 if (DeferredAuthentication.isDeferred(response)) 317 { 318 LOG.debug("auth deferred {}",session.getId()); 319 return Authentication.UNAUTHENTICATED; 320 } 321 322 // remember the current URI 323 synchronized (session) 324 { 325 // But only if it is not set already, or we save every uri that leads to a login form redirect 326 if (session.getAttribute(__J_URI)==null || _alwaysSaveUri) 327 { 328 StringBuffer buf = request.getRequestURL(); 329 if (request.getQueryString() != null) 330 buf.append("?").append(request.getQueryString()); 331 session.setAttribute(__J_URI, buf.toString()); 332 333 if (MimeTypes.FORM_ENCODED.equalsIgnoreCase(req.getContentType()) && HttpMethods.POST.equals(request.getMethod())) 334 { 335 Request base_request = (req instanceof Request)?(Request)req:AbstractHttpConnection.getCurrentConnection().getRequest(); 336 base_request.extractParameters(); 337 session.setAttribute(__J_POST, new MultiMap<String>(base_request.getParameters())); 338 } 339 } 340 } 341 342 // send the the challenge 343 if (_dispatch) 344 { 345 RequestDispatcher dispatcher = request.getRequestDispatcher(_formLoginPage); 346 response.setHeader(HttpHeaders.CACHE_CONTROL,"No-cache"); 347 response.setDateHeader(HttpHeaders.EXPIRES,1); 348 dispatcher.forward(new FormRequest(request), new FormResponse(response)); 349 } 350 else 351 { 352 response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formLoginPage))); 353 } 354 return Authentication.SEND_CONTINUE; 355 356 357 } 358 catch (IOException e) 359 { 360 throw new ServerAuthException(e); 361 } 362 catch (ServletException e) 363 { 364 throw new ServerAuthException(e); 365 } 366 } 367 368 /* ------------------------------------------------------------ */ 369 public boolean isJSecurityCheck(String uri) 370 { 371 int jsc = uri.indexOf(__J_SECURITY_CHECK); 372 373 if (jsc<0) 374 return false; 375 int e=jsc+__J_SECURITY_CHECK.length(); 376 if (e==uri.length()) 377 return true; 378 char c = uri.charAt(e); 379 return c==';'||c=='#'||c=='/'||c=='?'; 380 } 381 382 /* ------------------------------------------------------------ */ 383 public boolean isLoginOrErrorPage(String pathInContext) 384 { 385 return pathInContext != null && (pathInContext.equals(_formErrorPath) || pathInContext.equals(_formLoginPath)); 386 } 387 388 /* ------------------------------------------------------------ */ 389 public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException 390 { 391 return true; 392 } 393 394 /* ------------------------------------------------------------ */ 395 /* ------------------------------------------------------------ */ 396 protected static class FormRequest extends HttpServletRequestWrapper 397 { 398 public FormRequest(HttpServletRequest request) 399 { 400 super(request); 401 } 402 403 @Override 404 public long getDateHeader(String name) 405 { 406 if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) 407 return -1; 408 return super.getDateHeader(name); 409 } 410 411 @Override 412 public String getHeader(String name) 413 { 414 if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) 415 return null; 416 return super.getHeader(name); 417 } 418 419 @Override 420 public Enumeration getHeaderNames() 421 { 422 return Collections.enumeration(Collections.list(super.getHeaderNames())); 423 } 424 425 @Override 426 public Enumeration getHeaders(String name) 427 { 428 if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) 429 return Collections.enumeration(Collections.EMPTY_LIST); 430 return super.getHeaders(name); 431 } 432 } 433 434 /* ------------------------------------------------------------ */ 435 /* ------------------------------------------------------------ */ 436 protected static class FormResponse extends HttpServletResponseWrapper 437 { 438 public FormResponse(HttpServletResponse response) 439 { 440 super(response); 441 } 442 443 @Override 444 public void addDateHeader(String name, long date) 445 { 446 if (notIgnored(name)) 447 super.addDateHeader(name,date); 448 } 449 450 @Override 451 public void addHeader(String name, String value) 452 { 453 if (notIgnored(name)) 454 super.addHeader(name,value); 455 } 456 457 @Override 458 public void setDateHeader(String name, long date) 459 { 460 if (notIgnored(name)) 461 super.setDateHeader(name,date); 462 } 463 464 @Override 465 public void setHeader(String name, String value) 466 { 467 if (notIgnored(name)) 468 super.setHeader(name,value); 469 } 470 471 private boolean notIgnored(String name) 472 { 473 if (HttpHeaders.CACHE_CONTROL.equalsIgnoreCase(name) || 474 HttpHeaders.PRAGMA.equalsIgnoreCase(name) || 475 HttpHeaders.ETAG.equalsIgnoreCase(name) || 476 HttpHeaders.EXPIRES.equalsIgnoreCase(name) || 477 HttpHeaders.LAST_MODIFIED.equalsIgnoreCase(name) || 478 HttpHeaders.AGE.equalsIgnoreCase(name)) 479 return false; 480 return true; 481 } 482 } 483 484 /* ------------------------------------------------------------ */ 485 /** This Authentication represents a just completed Form authentication. 486 * Subsequent requests from the same user are authenticated by the presents 487 * of a {@link SessionAuthentication} instance in their session. 488 */ 489 public static class FormAuthentication extends UserAuthentication implements Authentication.ResponseSent 490 { 491 public FormAuthentication(String method, UserIdentity userIdentity) 492 { 493 super(method,userIdentity); 494 } 495 496 @Override 497 public String toString() 498 { 499 return "Form"+super.toString(); 500 } 501 } 502} 503