View Javadoc

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