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