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