View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 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  
19  package org.eclipse.jetty.security.authentication;
20  
21  import java.io.IOException;
22  import java.util.Collections;
23  import java.util.Enumeration;
24  import java.util.Locale;
25  
26  import javax.servlet.RequestDispatcher;
27  import javax.servlet.ServletException;
28  import javax.servlet.ServletRequest;
29  import javax.servlet.ServletResponse;
30  import javax.servlet.http.HttpServletRequest;
31  import javax.servlet.http.HttpServletRequestWrapper;
32  import javax.servlet.http.HttpServletResponse;
33  import javax.servlet.http.HttpServletResponseWrapper;
34  import javax.servlet.http.HttpSession;
35  
36  import org.eclipse.jetty.http.HttpHeader;
37  import org.eclipse.jetty.http.HttpHeaderValue;
38  import org.eclipse.jetty.http.HttpMethod;
39  import org.eclipse.jetty.http.HttpVersion;
40  import org.eclipse.jetty.http.MimeTypes;
41  import org.eclipse.jetty.security.ServerAuthException;
42  import org.eclipse.jetty.security.UserAuthentication;
43  import org.eclipse.jetty.server.Authentication;
44  import org.eclipse.jetty.server.Authentication.User;
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;
54  
55  /**
56   * FORM Authenticator.
57   *
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>
61   *
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>
67   */
68  public 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_METHOD = "org.eclipse.jetty.security.form_METHOD";
78      public final static String __J_SECURITY_CHECK = "/j_security_check";
79      public final static String __J_USERNAME = "j_username";
80      public final static String __J_PASSWORD = "j_password";
81  
82      private String _formErrorPage;
83      private String _formErrorPath;
84      private String _formLoginPage;
85      private String _formLoginPath;
86      private boolean _dispatch;
87      private boolean _alwaysSaveUri;
88  
89      public FormAuthenticator()
90      {
91      }
92  
93      /* ------------------------------------------------------------ */
94      public FormAuthenticator(String login,String error,boolean dispatch)
95      {
96          this();
97          if (login!=null)
98              setLoginPage(login);
99          if (error!=null)
100             setErrorPage(error);
101         _dispatch=dispatch;
102     }
103 
104     /* ------------------------------------------------------------ */
105     /**
106      * If true, uris that cause a redirect to a login page will always
107      * be remembered. If false, only the first uri that leads to a login
108      * page redirect is remembered.
109      * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=379909
110      * @param alwaysSave true to always save the uri
111      */
112     public void setAlwaysSaveUri (boolean alwaysSave)
113     {
114         _alwaysSaveUri = alwaysSave;
115     }
116 
117 
118     /* ------------------------------------------------------------ */
119     public boolean getAlwaysSaveUri ()
120     {
121         return _alwaysSaveUri;
122     }
123 
124     /* ------------------------------------------------------------ */
125     /**
126      * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration)
127      */
128     @Override
129     public void setConfiguration(AuthConfiguration configuration)
130     {
131         super.setConfiguration(configuration);
132         String login=configuration.getInitParameter(FormAuthenticator.__FORM_LOGIN_PAGE);
133         if (login!=null)
134             setLoginPage(login);
135         String error=configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
136         if (error!=null)
137             setErrorPage(error);
138         String dispatch=configuration.getInitParameter(FormAuthenticator.__FORM_DISPATCH);
139         _dispatch = dispatch==null?_dispatch:Boolean.valueOf(dispatch);
140     }
141 
142     /* ------------------------------------------------------------ */
143     @Override
144     public String getAuthMethod()
145     {
146         return Constraint.__FORM_AUTH;
147     }
148 
149     /* ------------------------------------------------------------ */
150     private void setLoginPage(String path)
151     {
152         if (!path.startsWith("/"))
153         {
154             LOG.warn("form-login-page must start with /");
155             path = "/" + path;
156         }
157         _formLoginPage = path;
158         _formLoginPath = path;
159         if (_formLoginPath.indexOf('?') > 0)
160             _formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?'));
161     }
162 
163     /* ------------------------------------------------------------ */
164     private void setErrorPage(String path)
165     {
166         if (path == null || path.trim().length() == 0)
167         {
168             _formErrorPath = null;
169             _formErrorPage = null;
170         }
171         else
172         {
173             if (!path.startsWith("/"))
174             {
175                 LOG.warn("form-error-page must start with /");
176                 path = "/" + path;
177             }
178             _formErrorPage = path;
179             _formErrorPath = path;
180 
181             if (_formErrorPath.indexOf('?') > 0)
182                 _formErrorPath = _formErrorPath.substring(0, _formErrorPath.indexOf('?'));
183         }
184     }
185     
186     
187     /* ------------------------------------------------------------ */
188     @Override
189     public UserIdentity login(String username, Object password, ServletRequest request)
190     {
191         
192         UserIdentity user = super.login(username,password,request);
193         if (user!=null)
194         {
195             HttpSession session = ((HttpServletRequest)request).getSession(true);
196             Authentication cached=new SessionAuthentication(getAuthMethod(),user,password);
197             session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
198         }
199         return user;
200     }
201     
202     
203     /* ------------------------------------------------------------ */
204     @Override
205     public void prepareRequest(ServletRequest request)
206     {
207         //if this is a request resulting from a redirect after auth is complete
208         //(ie its from a redirect to the original request uri) then due to 
209         //browser handling of 302 redirects, the method may not be the same as
210         //that of the original request. Replace the method and original post
211         //params (if it was a post).
212         //
213         //See Servlet Spec 3.1 sec 13.6.3
214         HttpServletRequest httpRequest = (HttpServletRequest)request;
215         HttpSession session = httpRequest.getSession(false);
216         if (session == null || session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null)
217             return; //not authenticated yet
218         
219         String juri = (String)session.getAttribute(__J_URI);
220         if (juri == null || juri.length() == 0)
221             return; //no original uri saved
222         
223         String method = (String)session.getAttribute(__J_METHOD);
224         if (method == null || method.length() == 0)
225             return; //didn't save original request method
226        
227         StringBuffer buf = httpRequest.getRequestURL();
228         if (httpRequest.getQueryString() != null)
229             buf.append("?").append(httpRequest.getQueryString());
230         
231         if (!juri.equals(buf.toString()))
232             return; //this request is not for the same url as the original
233         
234         //restore the original request's method on this request
235         if (LOG.isDebugEnabled()) LOG.debug("Restoring original method {} for {} with method {}", method, juri,httpRequest.getMethod());
236         Request base_request = Request.getBaseRequest(request);
237         base_request.setMethod(method);
238     }
239 
240     /* ------------------------------------------------------------ */
241     @Override
242     public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
243     {
244         HttpServletRequest request = (HttpServletRequest)req;
245         HttpServletResponse response = (HttpServletResponse)res;
246         Request base_request = Request.getBaseRequest(request);
247         Response base_response = base_request.getResponse();
248         
249         String uri = request.getRequestURI();
250         if (uri==null)
251             uri=URIUtil.SLASH;
252 
253         mandatory|=isJSecurityCheck(uri);
254         if (!mandatory)
255             return new DeferredAuthentication(this);
256 
257         if (isLoginOrErrorPage(URIUtil.addPaths(request.getServletPath(),request.getPathInfo())) &&!DeferredAuthentication.isDeferred(response))
258             return new DeferredAuthentication(this);
259 
260         HttpSession session = request.getSession(true);
261 
262         try
263         {
264             // Handle a request for authentication.
265             if (isJSecurityCheck(uri))
266             {
267                 final String username = request.getParameter(__J_USERNAME);
268                 final String password = request.getParameter(__J_PASSWORD);
269 
270                 UserIdentity user = login(username, password, request);
271                 LOG.debug("jsecuritycheck {} {}",username,user);
272                 session = request.getSession(true);
273                 if (user!=null)
274                 {                    
275                     // Redirect to original request
276                     String nuri;
277                     FormAuthentication form_auth;
278                     synchronized(session)
279                     {
280                         nuri = (String) session.getAttribute(__J_URI);
281 
282                         if (nuri == null || nuri.length() == 0)
283                         {
284                             nuri = request.getContextPath();
285                             if (nuri.length() == 0)
286                                 nuri = URIUtil.SLASH;
287                         }
288                         form_auth = new FormAuthentication(getAuthMethod(),user);
289                     }
290                     LOG.debug("authenticated {}->{}",form_auth,nuri);
291 
292                     response.setContentLength(0);
293                     int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
294                     base_response.sendRedirect(redirectCode, response.encodeRedirectURL(nuri));
295                     return form_auth;
296                 }
297 
298                 // not authenticated
299                 if (LOG.isDebugEnabled())
300                     LOG.debug("Form authentication FAILED for " + StringUtil.printable(username));
301                 if (_formErrorPage == null)
302                 {
303                     LOG.debug("auth failed {}->403",username);
304                     if (response != null)
305                         response.sendError(HttpServletResponse.SC_FORBIDDEN);
306                 }
307                 else if (_dispatch)
308                 {
309                     LOG.debug("auth failed {}=={}",username,_formErrorPage);
310                     RequestDispatcher dispatcher = request.getRequestDispatcher(_formErrorPage);
311                     response.setHeader(HttpHeader.CACHE_CONTROL.asString(),HttpHeaderValue.NO_CACHE.asString());
312                     response.setDateHeader(HttpHeader.EXPIRES.asString(),1);
313                     dispatcher.forward(new FormRequest(request), new FormResponse(response));
314                 }
315                 else
316                 {
317                     LOG.debug("auth failed {}->{}",username,_formErrorPage);
318                     int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
319                     base_response.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formErrorPage)));
320                 }
321 
322                 return Authentication.SEND_FAILURE;
323             }
324 
325             // Look for cached authentication
326             Authentication authentication = (Authentication) session.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
327             if (authentication != null)
328             {
329                 // Has authentication been revoked?
330                 if (authentication instanceof Authentication.User &&
331                     _loginService!=null &&
332                     !_loginService.validate(((Authentication.User)authentication).getUserIdentity()))
333                 {
334                     LOG.debug("auth revoked {}",authentication);
335                     session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
336                 }
337                 else
338                 {
339                     synchronized (session)
340                     {
341                         String j_uri=(String)session.getAttribute(__J_URI);
342                         if (j_uri!=null)
343                         {
344                             //check if the request is for the same url as the original and restore
345                             //params if it was a post
346                             LOG.debug("auth retry {}->{}",authentication,j_uri);
347                             StringBuffer buf = request.getRequestURL();
348                             if (request.getQueryString() != null)
349                                 buf.append("?").append(request.getQueryString());
350 
351                             if (j_uri.equals(buf.toString()))
352                             {
353                                 MultiMap<String> j_post = (MultiMap<String>)session.getAttribute(__J_POST);
354                                 if (j_post!=null)
355                                 {
356                                     LOG.debug("auth rePOST {}->{}",authentication,j_uri);
357                                     base_request.setContentParameters(j_post);
358                                 }
359                                 session.removeAttribute(__J_URI);
360                                 session.removeAttribute(__J_METHOD);
361                                 session.removeAttribute(__J_POST);
362                             }
363                         }
364                     }
365                     LOG.debug("auth {}",authentication);
366                     return authentication;
367                 }
368             }
369 
370             // if we can't send challenge
371             if (DeferredAuthentication.isDeferred(response))
372             {
373                 LOG.debug("auth deferred {}",session.getId());
374                 return Authentication.UNAUTHENTICATED;
375             }
376 
377             // remember the current URI
378             synchronized (session)
379             {
380                 // But only if it is not set already, or we save every uri that leads to a login form redirect
381                 if (session.getAttribute(__J_URI)==null || _alwaysSaveUri)
382                 {
383                     StringBuffer buf = request.getRequestURL();
384                     if (request.getQueryString() != null)
385                         buf.append("?").append(request.getQueryString());
386                     session.setAttribute(__J_URI, buf.toString());
387                     session.setAttribute(__J_METHOD, request.getMethod());
388 
389                     if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod()))
390                     {
391                         MultiMap<String> formParameters = new MultiMap<>();
392                         base_request.extractFormParameters(formParameters);
393                         session.setAttribute(__J_POST, formParameters);
394                     }
395                 }
396             }
397 
398             // send the the challenge
399             if (_dispatch)
400             {
401                 LOG.debug("challenge {}=={}",session.getId(),_formLoginPage);
402                 RequestDispatcher dispatcher = request.getRequestDispatcher(_formLoginPage);
403                 response.setHeader(HttpHeader.CACHE_CONTROL.asString(),HttpHeaderValue.NO_CACHE.asString());
404                 response.setDateHeader(HttpHeader.EXPIRES.asString(),1);
405                 dispatcher.forward(new FormRequest(request), new FormResponse(response));
406             }
407             else
408             {
409                 LOG.debug("challenge {}->{}",session.getId(),_formLoginPage);
410                 int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
411                 base_response.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formLoginPage)));
412             }
413             return Authentication.SEND_CONTINUE;
414         }
415         catch (IOException | ServletException e)
416         {
417             throw new ServerAuthException(e);
418         }
419     }
420 
421     /* ------------------------------------------------------------ */
422     public boolean isJSecurityCheck(String uri)
423     {
424         int jsc = uri.indexOf(__J_SECURITY_CHECK);
425 
426         if (jsc<0)
427             return false;
428         int e=jsc+__J_SECURITY_CHECK.length();
429         if (e==uri.length())
430             return true;
431         char c = uri.charAt(e);
432         return c==';'||c=='#'||c=='/'||c=='?';
433     }
434 
435     /* ------------------------------------------------------------ */
436     public boolean isLoginOrErrorPage(String pathInContext)
437     {
438         return pathInContext != null && (pathInContext.equals(_formErrorPath) || pathInContext.equals(_formLoginPath));
439     }
440 
441     /* ------------------------------------------------------------ */
442     @Override
443     public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
444     {
445         return true;
446     }
447 
448     /* ------------------------------------------------------------ */
449     /* ------------------------------------------------------------ */
450     protected static class FormRequest extends HttpServletRequestWrapper
451     {
452         public FormRequest(HttpServletRequest request)
453         {
454             super(request);
455         }
456 
457         @Override
458         public long getDateHeader(String name)
459         {
460             if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
461                 return -1;
462             return super.getDateHeader(name);
463         }
464 
465         @Override
466         public String getHeader(String name)
467         {
468             if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
469                 return null;
470             return super.getHeader(name);
471         }
472 
473         @Override
474         public Enumeration<String> getHeaderNames()
475         {
476             return Collections.enumeration(Collections.list(super.getHeaderNames()));
477         }
478 
479         @Override
480         public Enumeration<String> getHeaders(String name)
481         {
482             if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
483                 return Collections.<String>enumeration(Collections.<String>emptyList());
484             return super.getHeaders(name);
485         }
486     }
487 
488     /* ------------------------------------------------------------ */
489     /* ------------------------------------------------------------ */
490     protected static class FormResponse extends HttpServletResponseWrapper
491     {
492         public FormResponse(HttpServletResponse response)
493         {
494             super(response);
495         }
496 
497         @Override
498         public void addDateHeader(String name, long date)
499         {
500             if (notIgnored(name))
501                 super.addDateHeader(name,date);
502         }
503 
504         @Override
505         public void addHeader(String name, String value)
506         {
507             if (notIgnored(name))
508                 super.addHeader(name,value);
509         }
510 
511         @Override
512         public void setDateHeader(String name, long date)
513         {
514             if (notIgnored(name))
515                 super.setDateHeader(name,date);
516         }
517 
518         @Override
519         public void setHeader(String name, String value)
520         {
521             if (notIgnored(name))
522                 super.setHeader(name,value);
523         }
524 
525         private boolean notIgnored(String name)
526         {
527             if (HttpHeader.CACHE_CONTROL.is(name) ||
528                 HttpHeader.PRAGMA.is(name) ||
529                 HttpHeader.ETAG.is(name) ||
530                 HttpHeader.EXPIRES.is(name) ||
531                 HttpHeader.LAST_MODIFIED.is(name) ||
532                 HttpHeader.AGE.is(name))
533                 return false;
534             return true;
535         }
536     }
537 
538     /* ------------------------------------------------------------ */
539     /** This Authentication represents a just completed Form authentication.
540      * Subsequent requests from the same user are authenticated by the presents
541      * of a {@link SessionAuthentication} instance in their session.
542      */
543     public static class FormAuthentication extends UserAuthentication implements Authentication.ResponseSent
544     {
545         public FormAuthentication(String method, UserIdentity userIdentity)
546         {
547             super(method,userIdentity);
548         }
549 
550         @Override
551         public String toString()
552         {
553             return "Form"+super.toString();
554         }
555     }
556 }