View Javadoc

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