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.jaspi.modules;
20  
21  import java.io.IOException;
22  import java.io.Serializable;
23  import java.security.Principal;
24  import java.util.Arrays;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import javax.security.auth.Subject;
29  import javax.security.auth.callback.CallbackHandler;
30  import javax.security.auth.callback.UnsupportedCallbackException;
31  import javax.security.auth.message.AuthException;
32  import javax.security.auth.message.AuthStatus;
33  import javax.security.auth.message.MessageInfo;
34  import javax.security.auth.message.MessagePolicy;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  import javax.servlet.http.HttpSession;
38  import javax.servlet.http.HttpSessionBindingEvent;
39  import javax.servlet.http.HttpSessionBindingListener;
40  
41  import org.eclipse.jetty.util.security.Constraint;
42  import org.eclipse.jetty.util.security.Password;
43  import org.eclipse.jetty.security.CrossContextPsuedoSession;
44  import org.eclipse.jetty.security.authentication.DeferredAuthentication;
45  import org.eclipse.jetty.security.authentication.LoginCallbackImpl;
46  import org.eclipse.jetty.server.Authentication;
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  
52  /**
53   * @deprecated use *ServerAuthentication
54   * @version $Rev: 4792 $ $Date: 2009-03-18 22:55:52 +0100 (Wed, 18 Mar 2009) $
55   */
56  public class FormAuthModule extends BaseAuthModule
57  {
58      private static final Logger LOG = Log.getLogger(FormAuthModule.class);
59  
60      /* ------------------------------------------------------------ */
61      public final static String __J_URI = "org.eclipse.jetty.util.URI";
62  
63      public final static String __J_AUTHENTICATED = "org.eclipse.jetty.server.Auth";
64  
65      public final static String __J_SECURITY_CHECK = "/j_security_check";
66  
67      public final static String __J_USERNAME = "j_username";
68  
69      public final static String __J_PASSWORD = "j_password";
70  
71      // private String realmName;
72      public static final String LOGIN_PAGE_KEY = "org.eclipse.jetty.security.jaspi.modules.LoginPage";
73  
74      public static final String ERROR_PAGE_KEY = "org.eclipse.jetty.security.jaspi.modules.ErrorPage";
75  
76      public static final String SSO_SOURCE_KEY = "org.eclipse.jetty.security.jaspi.modules.SsoSource";
77  
78      private String _formErrorPage;
79  
80      private String _formErrorPath;
81  
82      private String _formLoginPage;
83  
84      private String _formLoginPath;
85  
86      private CrossContextPsuedoSession<UserInfo> ssoSource;
87  
88      public FormAuthModule()
89      {
90      }
91  
92      public FormAuthModule(CallbackHandler callbackHandler, String loginPage, String errorPage)
93      {
94          super(callbackHandler);
95          setLoginPage(loginPage);
96          setErrorPage(errorPage);
97      }
98  
99      public FormAuthModule(CallbackHandler callbackHandler, CrossContextPsuedoSession<UserInfo> ssoSource, 
100                           String loginPage, String errorPage)
101     {
102         super(callbackHandler);
103         this.ssoSource = ssoSource;
104         setLoginPage(loginPage);
105         setErrorPage(errorPage);
106     }
107 
108     @Override
109     public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, 
110                            CallbackHandler handler, Map options) 
111     throws AuthException
112     {
113         super.initialize(requestPolicy, responsePolicy, handler, options);
114         setLoginPage((String) options.get(LOGIN_PAGE_KEY));
115         setErrorPage((String) options.get(ERROR_PAGE_KEY));
116         ssoSource = (CrossContextPsuedoSession<UserInfo>) options.get(SSO_SOURCE_KEY);
117     }
118 
119     private void setLoginPage(String path)
120     {
121         if (!path.startsWith("/"))
122         {
123             LOG.warn("form-login-page must start with /");
124             path = "/" + path;
125         }
126         _formLoginPage = path;
127         _formLoginPath = path;
128         if (_formLoginPath.indexOf('?') > 0) _formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?'));
129     }
130 
131     /* ------------------------------------------------------------ */
132     private void setErrorPage(String path)
133     {
134         if (path == null || path.trim().length() == 0)
135         {
136             _formErrorPath = null;
137             _formErrorPage = null;
138         }
139         else
140         {
141             if (!path.startsWith("/"))
142             {
143                 LOG.warn("form-error-page must start with /");
144                 path = "/" + path;
145             }
146             _formErrorPage = path;
147             _formErrorPath = path;
148 
149             if (_formErrorPath.indexOf('?') > 0) _formErrorPath = _formErrorPath.substring(0, _formErrorPath.indexOf('?'));
150         }
151     }
152 
153     @Override
154     public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException
155     {
156        
157         HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage();
158         HttpServletResponse response = (HttpServletResponse) messageInfo.getResponseMessage();
159         String uri = request.getRequestURI();
160         if (uri==null)
161             uri=URIUtil.SLASH;
162         
163         boolean mandatory = isMandatory(messageInfo);  
164         mandatory |= isJSecurityCheck(uri);
165         HttpSession session = request.getSession(mandatory);
166         
167         // not mandatory or its the login or login error page don't authenticate
168         if (!mandatory || isLoginOrErrorPage(URIUtil.addPaths(request.getServletPath(),request.getPathInfo()))) return AuthStatus.SUCCESS;
169 
170         try
171         {
172             // Handle a request for authentication.
173             if (isJSecurityCheck(uri))
174             {
175                 final String username = request.getParameter(__J_USERNAME);
176                 final String password = request.getParameter(__J_PASSWORD);
177             
178                 boolean success = tryLogin(messageInfo, clientSubject, response, session, username, new Password(password));
179                 if (success)
180                 {
181                     // Redirect to original request                    
182                     String nuri=null;
183                     synchronized(session)
184                     {
185                         nuri = (String) session.getAttribute(__J_URI);
186                     }
187                     
188                     if (nuri == null || nuri.length() == 0)
189                     {
190                         nuri = request.getContextPath();
191                         if (nuri.length() == 0) 
192                             nuri = URIUtil.SLASH;
193                     }
194                    
195                     response.setContentLength(0);   
196                     response.sendRedirect(response.encodeRedirectURL(nuri));
197                     return AuthStatus.SEND_CONTINUE;
198                 }
199                 // not authenticated
200                 if (LOG.isDebugEnabled()) LOG.debug("Form authentication FAILED for " + StringUtil.printable(username));
201                 if (_formErrorPage == null)
202                 {
203                     if (response != null) response.sendError(HttpServletResponse.SC_FORBIDDEN);
204                 }
205                 else
206                 {
207                     response.setContentLength(0);
208                     response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _formErrorPage)));
209                 }
210                 // TODO is this correct response if isMandatory false??? Can
211                 // that occur?
212                 return AuthStatus.SEND_FAILURE;
213             }
214             
215             
216             // Check if the session is already authenticated.
217             FormCredential form_cred = (FormCredential) session.getAttribute(__J_AUTHENTICATED);
218             if (form_cred != null)
219             {                
220                 //TODO: ideally we would like the form auth module to be able to invoke the 
221                 //loginservice.validate() method to check the previously authed user, but it is not visible
222                 //to FormAuthModule
223                 if (form_cred._subject == null)
224                     return AuthStatus.SEND_FAILURE;
225                 Set<Object> credentials = form_cred._subject.getPrivateCredentials();
226                 if (credentials == null || credentials.isEmpty())
227                     return AuthStatus.SEND_FAILURE; //if no private credentials, assume it cannot be authenticated
228 
229                 clientSubject.getPrivateCredentials().addAll(credentials);
230 
231                 //boolean success = tryLogin(messageInfo, clientSubject, response, session, form_cred._jUserName, new Password(new String(form_cred._jPassword)));
232                 return AuthStatus.SUCCESS;  
233             }
234             else if (ssoSource != null)
235             {
236                 UserInfo userInfo = ssoSource.fetch(request);
237                 if (userInfo != null)
238                 {
239                     boolean success = tryLogin(messageInfo, clientSubject, response, session, userInfo.getUserName(), new Password(new String(userInfo.getPassword())));
240                     if (success) { return AuthStatus.SUCCESS; }
241                 }
242             }
243             
244            
245 
246             // if we can't send challenge
247             if (DeferredAuthentication.isDeferred(response))
248                 return AuthStatus.SUCCESS; 
249             
250 
251             // redirect to login page  
252             StringBuffer buf = request.getRequestURL();
253             if (request.getQueryString() != null)
254                 buf.append("?").append(request.getQueryString());
255 
256             synchronized (session)
257             {
258                 session.setAttribute(__J_URI, buf.toString());
259             }
260             
261             response.setContentLength(0);
262             response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _formLoginPage)));
263             return AuthStatus.SEND_CONTINUE;
264         }
265         catch (IOException e)
266         {
267             throw new AuthException(e.getMessage());
268         }
269         catch (UnsupportedCallbackException e)
270         {
271             throw new AuthException(e.getMessage());
272         }
273 
274     }
275     
276     /* ------------------------------------------------------------ */
277     public boolean isJSecurityCheck(String uri)
278     {
279         int jsc = uri.indexOf(__J_SECURITY_CHECK);
280         
281         if (jsc<0)
282             return false;
283         int e=jsc+__J_SECURITY_CHECK.length();
284         if (e==uri.length())
285             return true;
286         char c = uri.charAt(e);
287         return c==';'||c=='#'||c=='/'||c=='?';
288     }
289 
290     private boolean tryLogin(MessageInfo messageInfo, Subject clientSubject, 
291                              HttpServletResponse response, HttpSession session, 
292                              String username, Password password) 
293     throws AuthException, IOException, UnsupportedCallbackException
294     {
295         if (login(clientSubject, username, password, Constraint.__FORM_AUTH, messageInfo))
296         {
297             char[] pwdChars = password.toString().toCharArray();
298             Set<LoginCallbackImpl> loginCallbacks = clientSubject.getPrivateCredentials(LoginCallbackImpl.class);
299            
300             if (!loginCallbacks.isEmpty())
301             {
302                 LoginCallbackImpl loginCallback = loginCallbacks.iterator().next();
303                 FormCredential form_cred = new FormCredential(username, pwdChars, loginCallback.getUserPrincipal(), loginCallback.getSubject());
304                 session.setAttribute(__J_AUTHENTICATED, form_cred);
305             }
306 
307             // Sign-on to SSO mechanism
308             if (ssoSource != null)
309             {
310                 UserInfo userInfo = new UserInfo(username, pwdChars);
311                 ssoSource.store(userInfo, response);
312             }
313             return true;
314         }
315         return false;
316     }
317 
318     public boolean isLoginOrErrorPage(String pathInContext)
319     {
320         return pathInContext != null && (pathInContext.equals(_formErrorPath) || pathInContext.equals(_formLoginPath));
321     }
322 
323     /* ------------------------------------------------------------ */
324     /**
325      * FORM Authentication credential holder.
326      */
327     private static class FormCredential implements Serializable, HttpSessionBindingListener
328     {
329         String _jUserName;
330 
331         char[] _jPassword;
332 
333         transient Principal _userPrincipal;
334         
335         transient Subject _subject;
336 
337         private FormCredential(String _jUserName, char[] _jPassword, Principal _userPrincipal, Subject subject)
338         {
339             this._jUserName = _jUserName;
340             this._jPassword = _jPassword;
341             this._userPrincipal = _userPrincipal;
342             this._subject = subject;
343         }
344 
345         public void valueBound(HttpSessionBindingEvent event)
346         {
347         }
348 
349         public void valueUnbound(HttpSessionBindingEvent event)
350         {
351             if (LOG.isDebugEnabled()) LOG.debug("Logout " + _jUserName);
352 
353             // TODO jaspi call cleanSubject()
354             // if (_realm instanceof SSORealm)
355             // ((SSORealm) _realm).clearSingleSignOn(_jUserName);
356             //
357             // if (_realm != null && _userPrincipal != null)
358             // _realm.logout(_userPrincipal);
359         }
360 
361         public int hashCode()
362         {
363             return _jUserName.hashCode() + _jPassword.hashCode();
364         }
365 
366         public boolean equals(Object o)
367         {
368             if (!(o instanceof FormCredential)) return false;
369             FormCredential fc = (FormCredential) o;
370             return _jUserName.equals(fc._jUserName) && Arrays.equals(_jPassword, fc._jPassword);
371         }
372 
373         public String toString()
374         {
375             return "Cred[" + _jUserName + "]";
376         }
377 
378     }
379 
380 }