View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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     public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
186     {   
187         HttpServletRequest request = (HttpServletRequest)req;
188         HttpServletResponse response = (HttpServletResponse)res;
189         String uri = request.getRequestURI();
190         if (uri==null)
191             uri=URIUtil.SLASH;
192 
193         mandatory|=isJSecurityCheck(uri);
194         if (!mandatory)
195             return new DeferredAuthentication(this);
196 
197         if (isLoginOrErrorPage(URIUtil.addPaths(request.getServletPath(),request.getPathInfo())) &&!DeferredAuthentication.isDeferred(response))
198             return new DeferredAuthentication(this);
199 
200         HttpSession session = request.getSession(true);
201             
202         try
203         {
204             // Handle a request for authentication.
205             if (isJSecurityCheck(uri))
206             {
207                 final String username = request.getParameter(__J_USERNAME);
208                 final String password = request.getParameter(__J_PASSWORD);
209                 
210                 UserIdentity user = _loginService.login(username,password);
211                 if (user!=null)
212                 {
213                     session=renewSession(request,response);
214                     
215                     // Redirect to original request
216                     String nuri;
217                     synchronized(session)
218                     {
219                         nuri = (String) session.getAttribute(__J_URI);
220 
221                         if (nuri == null || nuri.length() == 0)
222                         {
223                             nuri = request.getContextPath();
224                             if (nuri.length() == 0) 
225                                 nuri = URIUtil.SLASH;
226                         }
227 
228                         Authentication cached=new SessionAuthentication(getAuthMethod(),user,password);
229                         session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
230                     }
231                     response.setContentLength(0);   
232                     response.sendRedirect(response.encodeRedirectURL(nuri));
233                     
234                     return new FormAuthentication(getAuthMethod(),user);
235                 }
236                 
237                 // not authenticated
238                 if (LOG.isDebugEnabled()) 
239                     LOG.debug("Form authentication FAILED for " + StringUtil.printable(username));
240                 if (_formErrorPage == null)
241                 {
242                     if (response != null) 
243                         response.sendError(HttpServletResponse.SC_FORBIDDEN);
244                 }
245                 else if (_dispatch)
246                 {
247                     RequestDispatcher dispatcher = request.getRequestDispatcher(_formErrorPage);
248                     response.setHeader(HttpHeaders.CACHE_CONTROL,"No-cache");
249                     response.setDateHeader(HttpHeaders.EXPIRES,1);
250                     dispatcher.forward(new FormRequest(request), new FormResponse(response));
251                 }
252                 else
253                 {
254                     response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formErrorPage)));
255                 }
256                 
257                 return Authentication.SEND_FAILURE;
258             }
259             
260             // Look for cached authentication
261             Authentication authentication = (Authentication) session.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
262             if (authentication != null) 
263             {
264                 // Has authentication been revoked?
265                 if (authentication instanceof Authentication.User && 
266                     _loginService!=null &&
267                     !_loginService.validate(((Authentication.User)authentication).getUserIdentity()))
268                 {
269                 
270                     session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
271                 }
272                 else
273                 {
274                     String j_uri=(String)session.getAttribute(__J_URI);
275                     if (j_uri!=null)
276                     {
277                         MultiMap<String> j_post = (MultiMap<String>)session.getAttribute(__J_POST);
278                         if (j_post!=null)
279                         {
280                             StringBuffer buf = request.getRequestURL();
281                             if (request.getQueryString() != null)
282                                 buf.append("?").append(request.getQueryString());
283 
284                             if (j_uri.equals(buf.toString()))
285                             {
286                                 // This is a retry of an original POST request
287                                 // so restore method and parameters
288 
289                                 session.removeAttribute(__J_POST);                        
290                                 Request base_request = (req instanceof Request)?(Request)req:AbstractHttpConnection.getCurrentConnection().getRequest();
291                                 base_request.setMethod(HttpMethods.POST);
292                                 base_request.setParameters(j_post);
293                             }
294                         }
295                         else
296                             session.removeAttribute(__J_URI);
297                             
298                     }
299                     return authentication;
300                 }
301             }
302 
303             // if we can't send challenge
304             if (DeferredAuthentication.isDeferred(response))
305             {
306                 LOG.debug("auth deferred {}",session.getId());
307                 return Authentication.UNAUTHENTICATED;
308             }
309 
310             // remember the current URI
311             synchronized (session)
312             {
313                 // But only if it is not set already, or we save every uri that leads to a login form redirect
314                 if (session.getAttribute(__J_URI)==null || _alwaysSaveUri)
315                 {  
316                     StringBuffer buf = request.getRequestURL();
317                     if (request.getQueryString() != null)
318                         buf.append("?").append(request.getQueryString());
319                     session.setAttribute(__J_URI, buf.toString());
320                     
321                     if (MimeTypes.FORM_ENCODED.equalsIgnoreCase(req.getContentType()) && HttpMethods.POST.equals(request.getMethod()))
322                     {
323                         Request base_request = (req instanceof Request)?(Request)req:AbstractHttpConnection.getCurrentConnection().getRequest();
324                         base_request.extractParameters();                        
325                         session.setAttribute(__J_POST, new MultiMap<String>(base_request.getParameters()));
326                     }
327                 }
328             }
329             
330             // send the the challenge
331             if (_dispatch)
332             {
333                 RequestDispatcher dispatcher = request.getRequestDispatcher(_formLoginPage);
334                 response.setHeader(HttpHeaders.CACHE_CONTROL,"No-cache");
335                 response.setDateHeader(HttpHeaders.EXPIRES,1);
336                 dispatcher.forward(new FormRequest(request), new FormResponse(response));
337             }
338             else
339             {
340                 response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formLoginPage)));
341             }
342             return Authentication.SEND_CONTINUE;
343             
344          
345         }
346         catch (IOException e)
347         {
348             throw new ServerAuthException(e);
349         }
350         catch (ServletException e)
351         {
352             throw new ServerAuthException(e);
353         }
354     }
355     
356     /* ------------------------------------------------------------ */
357     public boolean isJSecurityCheck(String uri)
358     {
359         int jsc = uri.indexOf(__J_SECURITY_CHECK);
360         
361         if (jsc<0)
362             return false;
363         int e=jsc+__J_SECURITY_CHECK.length();
364         if (e==uri.length())
365             return true;
366         char c = uri.charAt(e);
367         return c==';'||c=='#'||c=='/'||c=='?';
368     }
369     
370     /* ------------------------------------------------------------ */
371     public boolean isLoginOrErrorPage(String pathInContext)
372     {
373         return pathInContext != null && (pathInContext.equals(_formErrorPath) || pathInContext.equals(_formLoginPath));
374     }
375     
376     /* ------------------------------------------------------------ */
377     public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
378     {
379         return true;
380     }
381 
382     /* ------------------------------------------------------------ */
383     /* ------------------------------------------------------------ */
384     protected static class FormRequest extends HttpServletRequestWrapper
385     {
386         public FormRequest(HttpServletRequest request)
387         {
388             super(request);
389         }
390 
391         @Override
392         public long getDateHeader(String name)
393         {
394             if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
395                 return -1;
396             return super.getDateHeader(name);
397         }
398         
399         @Override
400         public String getHeader(String name)
401         {
402             if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
403                 return null;
404             return super.getHeader(name);
405         }
406 
407         @Override
408         public Enumeration getHeaderNames()
409         {
410             return Collections.enumeration(Collections.list(super.getHeaderNames()));
411         }
412 
413         @Override
414         public Enumeration getHeaders(String name)
415         {
416             if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
417                 return Collections.enumeration(Collections.EMPTY_LIST);
418             return super.getHeaders(name);
419         }
420     }
421 
422     /* ------------------------------------------------------------ */
423     /* ------------------------------------------------------------ */
424     protected static class FormResponse extends HttpServletResponseWrapper
425     {
426         public FormResponse(HttpServletResponse response)
427         {
428             super(response);
429         }
430 
431         @Override
432         public void addDateHeader(String name, long date)
433         {
434             if (notIgnored(name))
435                 super.addDateHeader(name,date);
436         }
437 
438         @Override
439         public void addHeader(String name, String value)
440         {
441             if (notIgnored(name))
442                 super.addHeader(name,value);
443         }
444 
445         @Override
446         public void setDateHeader(String name, long date)
447         {
448             if (notIgnored(name))
449                 super.setDateHeader(name,date);
450         }
451         
452         @Override
453         public void setHeader(String name, String value)
454         {
455             if (notIgnored(name))
456                 super.setHeader(name,value);
457         }
458         
459         private boolean notIgnored(String name)
460         {
461             if (HttpHeaders.CACHE_CONTROL.equalsIgnoreCase(name) ||
462                 HttpHeaders.PRAGMA.equalsIgnoreCase(name) ||
463                 HttpHeaders.ETAG.equalsIgnoreCase(name) ||
464                 HttpHeaders.EXPIRES.equalsIgnoreCase(name) ||
465                 HttpHeaders.LAST_MODIFIED.equalsIgnoreCase(name) ||
466                 HttpHeaders.AGE.equalsIgnoreCase(name))
467                 return false;
468             return true;
469         }
470     }
471     
472     /* ------------------------------------------------------------ */
473     /** This Authentication represents a just completed Form authentication.
474      * Subsequent requests from the same user are authenticated by the presents 
475      * of a {@link SessionAuthentication} instance in their session.
476      */
477     public static class FormAuthentication extends UserAuthentication implements Authentication.ResponseSent
478     {
479         public FormAuthentication(String method, UserIdentity userIdentity)
480         {
481             super(method,userIdentity);
482         }
483         
484         @Override
485         public String toString()
486         {
487             return "Form"+super.toString();
488         }
489     }
490 }