View Javadoc

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