View Javadoc

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