View Javadoc

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