1//
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}
503