View Javadoc

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  
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  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;
34  
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;
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   *
69   */
70  public class FormAuthenticator extends LoginAuthenticator
71  {
72      private static final Logger LOG = Log.getLogger(FormAuthenticator.class);
73  
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";
83  
84      private String _formErrorPage;
85      private String _formErrorPath;
86      private String _formLoginPage;
87      private String _formLoginPath;
88      private boolean _dispatch;
89      private boolean _alwaysSaveUri;
90  
91      public FormAuthenticator()
92      {
93      }
94  
95      /* ------------------------------------------------------------ */
96      public FormAuthenticator(String login,String error,boolean dispatch)
97      {
98          this();
99          if (login!=null)
100             setLoginPage(login);
101         if (error!=null)
102             setErrorPage(error);
103         _dispatch=dispatch;
104     }
105 
106     /* ------------------------------------------------------------ */
107     /**
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
112      * @param alwaysSave
113      */
114     public void setAlwaysSaveUri (boolean alwaysSave)
115     {
116         _alwaysSaveUri = alwaysSave;
117     }
118 
119 
120     /* ------------------------------------------------------------ */
121     public boolean getAlwaysSaveUri ()
122     {
123         return _alwaysSaveUri;
124     }
125 
126     /* ------------------------------------------------------------ */
127     /**
128      * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration)
129      */
130     @Override
131     public void setConfiguration(AuthConfiguration configuration)
132     {
133         super.setConfiguration(configuration);
134         String login=configuration.getInitParameter(FormAuthenticator.__FORM_LOGIN_PAGE);
135         if (login!=null)
136             setLoginPage(login);
137         String error=configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
138         if (error!=null)
139             setErrorPage(error);
140         String dispatch=configuration.getInitParameter(FormAuthenticator.__FORM_DISPATCH);
141         _dispatch = dispatch==null?_dispatch:Boolean.valueOf(dispatch);
142     }
143 
144     /* ------------------------------------------------------------ */
145     @Override
146     public String getAuthMethod()
147     {
148         return Constraint.__FORM_AUTH;
149     }
150 
151     /* ------------------------------------------------------------ */
152     private void setLoginPage(String path)
153     {
154         if (!path.startsWith("/"))
155         {
156             LOG.warn("form-login-page must start with /");
157             path = "/" + path;
158         }
159         _formLoginPage = path;
160         _formLoginPath = path;
161         if (_formLoginPath.indexOf('?') > 0)
162             _formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?'));
163     }
164 
165     /* ------------------------------------------------------------ */
166     private void setErrorPage(String path)
167     {
168         if (path == null || path.trim().length() == 0)
169         {
170             _formErrorPath = null;
171             _formErrorPage = null;
172         }
173         else
174         {
175             if (!path.startsWith("/"))
176             {
177                 LOG.warn("form-error-page must start with /");
178                 path = "/" + path;
179             }
180             _formErrorPage = path;
181             _formErrorPath = path;
182 
183             if (_formErrorPath.indexOf('?') > 0)
184                 _formErrorPath = _formErrorPath.substring(0, _formErrorPath.indexOf('?'));
185         }
186     }
187     
188     
189     /* ------------------------------------------------------------ */
190     @Override
191     public UserIdentity login(String username, Object password, ServletRequest request)
192     {
193         
194         UserIdentity user = super.login(username,password,request);
195         if (user!=null)
196         {
197             HttpSession session = ((HttpServletRequest)request).getSession(true);
198             Authentication cached=new SessionAuthentication(getAuthMethod(),user,password);
199             session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
200         }
201         return user;
202     }
203     
204     
205     /* ------------------------------------------------------------ */
206     @Override
207     public void prepareRequest(ServletRequest request)
208     {
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).
214         //
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
220         
221         String juri = (String)session.getAttribute(__J_URI);
222         if (juri == null || juri.length() == 0)
223             return; //no original uri saved
224         
225         String method = (String)session.getAttribute(__J_METHOD);
226         if (method == null || method.length() == 0)
227             return; //didn't save original request method
228        
229         StringBuffer buf = httpRequest.getRequestURL();
230         if (httpRequest.getQueryString() != null)
231             buf.append("?").append(httpRequest.getQueryString());
232         
233         if (!juri.equals(buf.toString()))
234             return; //this request is not for the same url as the original
235         
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());
241     }
242 
243     /* ------------------------------------------------------------ */
244     @Override
245     public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
246     {
247         HttpServletRequest request = (HttpServletRequest)req;
248         HttpServletResponse response = (HttpServletResponse)res;
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                     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));
297                     return form_auth;
298                 }
299 
300                 // not authenticated
301                 if (LOG.isDebugEnabled())
302                     LOG.debug("Form authentication FAILED for " + StringUtil.printable(username));
303                 if (_formErrorPage == null)
304                 {
305                     LOG.debug("auth failed {}->403",username);
306                     if (response != null)
307                         response.sendError(HttpServletResponse.SC_FORBIDDEN);
308                 }
309                 else if (_dispatch)
310                 {
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));
316                 }
317                 else
318                 {
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)));
324                 }
325 
326                 return Authentication.SEND_FAILURE;
327             }
328 
329             // Look for cached authentication
330             Authentication authentication = (Authentication) session.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
331             if (authentication != null)
332             {
333                 // Has authentication been revoked?
334                 if (authentication instanceof Authentication.User &&
335                     _loginService!=null &&
336                     !_loginService.validate(((Authentication.User)authentication).getUserIdentity()))
337                 {
338                     LOG.debug("auth revoked {}",authentication);
339                     session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
340                 }
341                 else
342                 {
343                     synchronized (session)
344                     {
345                         String j_uri=(String)session.getAttribute(__J_URI);
346                         if (j_uri!=null)
347                         {
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());
354 
355                             if (j_uri.equals(buf.toString()))
356                             {
357                                 MultiMap<String> j_post = (MultiMap<String>)session.getAttribute(__J_POST);
358                                 if (j_post!=null)
359                                 {
360                                     LOG.debug("auth rePOST {}->{}",authentication,j_uri);
361                                     Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
362                                     base_request.setContentParameters(j_post);
363                                 }
364                                 session.removeAttribute(__J_URI);
365                                 session.removeAttribute(__J_METHOD);
366                                 session.removeAttribute(__J_POST);
367                             }
368                         }
369                     }
370                     LOG.debug("auth {}",authentication);
371                     return authentication;
372                 }
373             }
374 
375             // if we can't send challenge
376             if (DeferredAuthentication.isDeferred(response))
377             {
378                 LOG.debug("auth deferred {}",session.getId());
379                 return Authentication.UNAUTHENTICATED;
380             }
381 
382             // remember the current URI
383             synchronized (session)
384             {
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)
387                 {
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());
393 
394                     if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod()))
395                     {
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);
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 }