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.
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
19 package org.eclipse.jetty.security.authentication;
21 import java.io.IOException;
22 import java.util.Collections;
23 import java.util.Enumeration;
24 import java.util.Locale;
25 import javax.servlet.RequestDispatcher;
26 import javax.servlet.ServletException;
27 import javax.servlet.ServletRequest;
28 import javax.servlet.ServletResponse;
29 import javax.servlet.http.HttpServletRequest;
30 import javax.servlet.http.HttpServletRequestWrapper;
31 import javax.servlet.http.HttpServletResponse;
32 import javax.servlet.http.HttpServletResponseWrapper;
33 import javax.servlet.http.HttpSession;
35 import org.eclipse.jetty.http.HttpHeader;
36 import org.eclipse.jetty.http.HttpHeaderValue;
37 import org.eclipse.jetty.http.HttpMethod;
38 import org.eclipse.jetty.http.HttpVersion;
39 import org.eclipse.jetty.http.MimeTypes;
40 import org.eclipse.jetty.security.ServerAuthException;
41 import org.eclipse.jetty.security.UserAuthentication;
42 import org.eclipse.jetty.server.Authentication;
43 import org.eclipse.jetty.server.Authentication.User;
44 import org.eclipse.jetty.server.HttpChannel;
45 import org.eclipse.jetty.server.Request;
46 import org.eclipse.jetty.server.Response;
47 import org.eclipse.jetty.server.UserIdentity;
48 import org.eclipse.jetty.util.MultiMap;
49 import org.eclipse.jetty.util.StringUtil;
50 import org.eclipse.jetty.util.URIUtil;
51 import org.eclipse.jetty.util.log.Log;
52 import org.eclipse.jetty.util.log.Logger;
53 import org.eclipse.jetty.util.security.Constraint;
58 * <p>This authenticator implements form authentication will use dispatchers to
59 * the login page if the {@link #__FORM_DISPATCH} init parameter is set to true.
60 * Otherwise it will redirect.</p>
62 * <p>The form authenticator redirects unauthenticated requests to a log page
63 * which should use a form to gather username/password from the user and send them
64 * to the /j_security_check URI within the context. FormAuthentication uses
65 * {@link SessionAuthentication} to wrap Authentication results so that they
66 * are associated with the session.</p>
70 public class FormAuthenticator extends LoginAuthenticator
72 private static final Logger LOG = Log.getLogger(FormAuthenticator.class);
74 public final static String __FORM_LOGIN_PAGE="org.eclipse.jetty.security.form_login_page";
75 public final static String __FORM_ERROR_PAGE="org.eclipse.jetty.security.form_error_page";
76 public final static String __FORM_DISPATCH="org.eclipse.jetty.security.dispatch";
77 public final static String __J_URI = "org.eclipse.jetty.security.form_URI";
78 public final static String __J_POST = "org.eclipse.jetty.security.form_POST";
79 public final static String __J_METHOD = "org.eclipse.jetty.security.form_METHOD";
80 public final static String __J_SECURITY_CHECK = "/j_security_check";
81 public final static String __J_USERNAME = "j_username";
82 public final static String __J_PASSWORD = "j_password";
84 private String _formErrorPage;
85 private String _formErrorPath;
86 private String _formLoginPage;
87 private String _formLoginPath;
88 private boolean _dispatch;
89 private boolean _alwaysSaveUri;
91 public FormAuthenticator()
95 /* ------------------------------------------------------------ */
96 public FormAuthenticator(String login,String error,boolean dispatch)
106 /* ------------------------------------------------------------ */
108 * If true, uris that cause a redirect to a login page will always
109 * be remembered. If false, only the first uri that leads to a login
110 * page redirect is remembered.
111 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=379909
114 public void setAlwaysSaveUri (boolean alwaysSave)
116 _alwaysSaveUri = alwaysSave;
120 /* ------------------------------------------------------------ */
121 public boolean getAlwaysSaveUri ()
123 return _alwaysSaveUri;
126 /* ------------------------------------------------------------ */
128 * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration)
131 public void setConfiguration(AuthConfiguration configuration)
133 super.setConfiguration(configuration);
134 String login=configuration.getInitParameter(FormAuthenticator.__FORM_LOGIN_PAGE);
137 String error=configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
140 String dispatch=configuration.getInitParameter(FormAuthenticator.__FORM_DISPATCH);
141 _dispatch = dispatch==null?_dispatch:Boolean.valueOf(dispatch);
144 /* ------------------------------------------------------------ */
146 public String getAuthMethod()
148 return Constraint.__FORM_AUTH;
151 /* ------------------------------------------------------------ */
152 private void setLoginPage(String path)
154 if (!path.startsWith("/"))
156 LOG.warn("form-login-page must start with /");
159 _formLoginPage = path;
160 _formLoginPath = path;
161 if (_formLoginPath.indexOf('?') > 0)
162 _formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?'));
165 /* ------------------------------------------------------------ */
166 private void setErrorPage(String path)
168 if (path == null || path.trim().length() == 0)
170 _formErrorPath = null;
171 _formErrorPage = null;
175 if (!path.startsWith("/"))
177 LOG.warn("form-error-page must start with /");
180 _formErrorPage = path;
181 _formErrorPath = path;
183 if (_formErrorPath.indexOf('?') > 0)
184 _formErrorPath = _formErrorPath.substring(0, _formErrorPath.indexOf('?'));
189 /* ------------------------------------------------------------ */
191 public UserIdentity login(String username, Object password, ServletRequest request)
194 UserIdentity user = super.login(username,password,request);
197 HttpSession session = ((HttpServletRequest)request).getSession(true);
198 Authentication cached=new SessionAuthentication(getAuthMethod(),user,password);
199 session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
205 /* ------------------------------------------------------------ */
207 public void prepareRequest(ServletRequest request)
209 //if this is a request resulting from a redirect after auth is complete
210 //(ie its from a redirect to the original request uri) then due to
211 //browser handling of 302 redirects, the method may not be the same as
212 //that of the original request. Replace the method and original post
213 //params (if it was a post).
215 //See Servlet Spec 3.1 sec 13.6.3
216 HttpServletRequest httpRequest = (HttpServletRequest)request;
217 HttpSession session = httpRequest.getSession(false);
218 if (session == null || session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null)
219 return; //not authenticated yet
221 String juri = (String)session.getAttribute(__J_URI);
222 if (juri == null || juri.length() == 0)
223 return; //no original uri saved
225 String method = (String)session.getAttribute(__J_METHOD);
226 if (method == null || method.length() == 0)
227 return; //didn't save original request method
229 StringBuffer buf = httpRequest.getRequestURL();
230 if (httpRequest.getQueryString() != null)
231 buf.append("?").append(httpRequest.getQueryString());
233 if (!juri.equals(buf.toString()))
234 return; //this request is not for the same url as the original
236 //restore the original request's method on this request
237 if (LOG.isDebugEnabled()) LOG.debug("Restoring original method {} for {} with method {}", method, juri,httpRequest.getMethod());
238 Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
239 HttpMethod m = HttpMethod.fromString(method);
240 base_request.setMethod(m,m.asString());
243 /* ------------------------------------------------------------ */
245 public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
247 HttpServletRequest request = (HttpServletRequest)req;
248 HttpServletResponse response = (HttpServletResponse)res;
249 String uri = request.getRequestURI();
253 mandatory|=isJSecurityCheck(uri);
255 return new DeferredAuthentication(this);
257 if (isLoginOrErrorPage(URIUtil.addPaths(request.getServletPath(),request.getPathInfo())) &&!DeferredAuthentication.isDeferred(response))
258 return new DeferredAuthentication(this);
260 HttpSession session = request.getSession(true);
264 // Handle a request for authentication.
265 if (isJSecurityCheck(uri))
267 final String username = request.getParameter(__J_USERNAME);
268 final String password = request.getParameter(__J_PASSWORD);
270 UserIdentity user = login(username, password, request);
271 LOG.debug("jsecuritycheck {} {}",username,user);
272 session = request.getSession(true);
275 // Redirect to original request
277 FormAuthentication form_auth;
278 synchronized(session)
280 nuri = (String) session.getAttribute(__J_URI);
282 if (nuri == null || nuri.length() == 0)
284 nuri = request.getContextPath();
285 if (nuri.length() == 0)
286 nuri = URIUtil.SLASH;
288 form_auth = new FormAuthentication(getAuthMethod(),user);
290 LOG.debug("authenticated {}->{}",form_auth,nuri);
292 response.setContentLength(0);
293 Response base_response = HttpChannel.getCurrentHttpChannel().getResponse();
294 Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
295 int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
296 base_response.sendRedirect(redirectCode, response.encodeRedirectURL(nuri));
301 if (LOG.isDebugEnabled())
302 LOG.debug("Form authentication FAILED for " + StringUtil.printable(username));
303 if (_formErrorPage == null)
305 LOG.debug("auth failed {}->403",username);
306 if (response != null)
307 response.sendError(HttpServletResponse.SC_FORBIDDEN);
311 LOG.debug("auth failed {}=={}",username,_formErrorPage);
312 RequestDispatcher dispatcher = request.getRequestDispatcher(_formErrorPage);
313 response.setHeader(HttpHeader.CACHE_CONTROL.asString(),HttpHeaderValue.NO_CACHE.asString());
314 response.setDateHeader(HttpHeader.EXPIRES.asString(),1);
315 dispatcher.forward(new FormRequest(request), new FormResponse(response));
319 LOG.debug("auth failed {}->{}",username,_formErrorPage);
320 Response base_response = HttpChannel.getCurrentHttpChannel().getResponse();
321 Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
322 int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
323 base_response.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formErrorPage)));
326 return Authentication.SEND_FAILURE;
329 // Look for cached authentication
330 Authentication authentication = (Authentication) session.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
331 if (authentication != null)
333 // Has authentication been revoked?
334 if (authentication instanceof Authentication.User &&
335 _loginService!=null &&
336 !_loginService.validate(((Authentication.User)authentication).getUserIdentity()))
338 LOG.debug("auth revoked {}",authentication);
339 session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
343 synchronized (session)
345 String j_uri=(String)session.getAttribute(__J_URI);
348 //check if the request is for the same url as the original and restore
349 //params if it was a post
350 LOG.debug("auth retry {}->{}",authentication,j_uri);
351 StringBuffer buf = request.getRequestURL();
352 if (request.getQueryString() != null)
353 buf.append("?").append(request.getQueryString());
355 if (j_uri.equals(buf.toString()))
357 MultiMap<String> j_post = (MultiMap<String>)session.getAttribute(__J_POST);
360 LOG.debug("auth rePOST {}->{}",authentication,j_uri);
361 Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
362 base_request.setContentParameters(j_post);
364 session.removeAttribute(__J_URI);
365 session.removeAttribute(__J_METHOD);
366 session.removeAttribute(__J_POST);
370 LOG.debug("auth {}",authentication);
371 return authentication;
375 // if we can't send challenge
376 if (DeferredAuthentication.isDeferred(response))
378 LOG.debug("auth deferred {}",session.getId());
379 return Authentication.UNAUTHENTICATED;
382 // remember the current URI
383 synchronized (session)
385 // But only if it is not set already, or we save every uri that leads to a login form redirect
386 if (session.getAttribute(__J_URI)==null || _alwaysSaveUri)
388 StringBuffer buf = request.getRequestURL();
389 if (request.getQueryString() != null)
390 buf.append("?").append(request.getQueryString());
391 session.setAttribute(__J_URI, buf.toString());
392 session.setAttribute(__J_METHOD, request.getMethod());
394 if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod()))
396 Request base_request = (req instanceof Request)?(Request)req:HttpChannel.getCurrentHttpChannel().getRequest();
397 MultiMap<String> formParameters = new MultiMap<>();
398 base_request.extractFormParameters(formParameters);
399 session.setAttribute(__J_POST, formParameters);
404 // send the the challenge
407 LOG.debug("challenge {}=={}",session.getId(),_formLoginPage);
408 RequestDispatcher dispatcher = request.getRequestDispatcher(_formLoginPage);
409 response.setHeader(HttpHeader.CACHE_CONTROL.asString(),HttpHeaderValue.NO_CACHE.asString());
410 response.setDateHeader(HttpHeader.EXPIRES.asString(),1);
411 dispatcher.forward(new FormRequest(request), new FormResponse(response));
415 LOG.debug("challenge {}->{}",session.getId(),_formLoginPage);
416 Response base_response = HttpChannel.getCurrentHttpChannel().getResponse();
417 Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
418 int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
419 base_response.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formLoginPage)));
421 return Authentication.SEND_CONTINUE;
423 catch (IOException | ServletException e)
425 throw new ServerAuthException(e);
429 /* ------------------------------------------------------------ */
430 public boolean isJSecurityCheck(String uri)
432 int jsc = uri.indexOf(__J_SECURITY_CHECK);
436 int e=jsc+__J_SECURITY_CHECK.length();
439 char c = uri.charAt(e);
440 return c==';'||c=='#'||c=='/'||c=='?';
443 /* ------------------------------------------------------------ */
444 public boolean isLoginOrErrorPage(String pathInContext)
446 return pathInContext != null && (pathInContext.equals(_formErrorPath) || pathInContext.equals(_formLoginPath));
449 /* ------------------------------------------------------------ */
451 public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
456 /* ------------------------------------------------------------ */
457 /* ------------------------------------------------------------ */
458 protected static class FormRequest extends HttpServletRequestWrapper
460 public FormRequest(HttpServletRequest request)
466 public long getDateHeader(String name)
468 if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
470 return super.getDateHeader(name);
474 public String getHeader(String name)
476 if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
478 return super.getHeader(name);
482 public Enumeration<String> getHeaderNames()
484 return Collections.enumeration(Collections.list(super.getHeaderNames()));
488 public Enumeration<String> getHeaders(String name)
490 if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
491 return Collections.<String>enumeration(Collections.<String>emptyList());
492 return super.getHeaders(name);
496 /* ------------------------------------------------------------ */
497 /* ------------------------------------------------------------ */
498 protected static class FormResponse extends HttpServletResponseWrapper
500 public FormResponse(HttpServletResponse response)
506 public void addDateHeader(String name, long date)
508 if (notIgnored(name))
509 super.addDateHeader(name,date);
513 public void addHeader(String name, String value)
515 if (notIgnored(name))
516 super.addHeader(name,value);
520 public void setDateHeader(String name, long date)
522 if (notIgnored(name))
523 super.setDateHeader(name,date);
527 public void setHeader(String name, String value)
529 if (notIgnored(name))
530 super.setHeader(name,value);
533 private boolean notIgnored(String name)
535 if (HttpHeader.CACHE_CONTROL.is(name) ||
536 HttpHeader.PRAGMA.is(name) ||
537 HttpHeader.ETAG.is(name) ||
538 HttpHeader.EXPIRES.is(name) ||
539 HttpHeader.LAST_MODIFIED.is(name) ||
540 HttpHeader.AGE.is(name))
546 /* ------------------------------------------------------------ */
547 /** This Authentication represents a just completed Form authentication.
548 * Subsequent requests from the same user are authenticated by the presents
549 * of a {@link SessionAuthentication} instance in their session.
551 public static class FormAuthentication extends UserAuthentication implements Authentication.ResponseSent
553 public FormAuthentication(String method, UserIdentity userIdentity)
555 super(method,userIdentity);
559 public String toString()
561 return "Form"+super.toString();