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