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