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