View Javadoc

1   /*
2    * Copyright (c) 2005-2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.danube.http.auth;
14  
15  import org.abstracthorizon.danube.connection.Connection;
16  import org.abstracthorizon.danube.connection.ConnectionException;
17  import org.abstracthorizon.danube.connection.ConnectionHandler;
18  import org.abstracthorizon.danube.http.HTTPConnection;
19  import org.abstracthorizon.danube.http.Status;
20  import org.abstracthorizon.danube.http.session.HTTPSessionManager;
21  import org.abstracthorizon.danube.http.session.Session;
22  import org.abstracthorizon.danube.http.session.SimpleSessionManager;
23  import org.abstracthorizon.danube.http.util.Base64;
24  import org.abstracthorizon.danube.support.RuntimeIOException;
25  
26  import java.io.IOException;
27  import java.security.PrivilegedActionException;
28  import java.security.PrivilegedExceptionAction;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.Map;
32  
33  import javax.security.auth.Subject;
34  import javax.security.auth.callback.Callback;
35  import javax.security.auth.callback.CallbackHandler;
36  import javax.security.auth.callback.ConfirmationCallback;
37  import javax.security.auth.callback.NameCallback;
38  import javax.security.auth.callback.PasswordCallback;
39  import javax.security.auth.callback.TextOutputCallback;
40  import javax.security.auth.callback.UnsupportedCallbackException;
41  import javax.security.auth.login.LoginContext;
42  import javax.security.auth.login.LoginException;
43  
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  /**
48   * <p>
49   * This wrapper forces JAAS authentication to happen at client side:
50   * if &quot;Authorization&quot; header is missing it would return 401
51   * code requesting one. This class performs only basic http authentication.
52   * </p>
53   * <p>
54   * When user is authenticated {@link Subject} object is placed in the user session
55   * under {@link #AUTHORIZATION_DATA_ATTRIBUTE} name. That is only going to happen if
56   * session manager is passed to this object.
57   * </p>
58   * @author Daniel Sendula
59   */
60  public class JAASAuthenticator implements ConnectionHandler {
61  
62      /** Logger */
63      protected final Logger logger = LoggerFactory.getLogger(JAASAuthenticator.class);
64  
65  
66      /** Authorisation data session attribute */
67      public static final String AUTHORIZATION_DATA_ATTRIBUTE = "org.abstracthorizon.danube.http.auth.Subject";
68  
69      /** Client request header for authorisation  */
70      public static final String AUTHORIZATION_REQUEST_HEADER = "Authorization";
71  
72      /** Server response header for authorisation  */
73      public static final String AUTHORIZATION_RESPONSE_HEADER = "WWW-Authenticate";
74  
75      /** Default cache timeout */
76      public static final int DEFAULT_CACHE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
77  
78      /** Default minimum scan period */
79      public static final int DEFAULT_MINIMUM_SCAN_PERIOD = 10 * 1000; // 10 seconds
80  
81      /** Wrapped handler */
82      protected ConnectionHandler handler;
83  
84      /** Session manager */
85      protected HTTPSessionManager sessionManager;
86  
87      /** Realm name */
88      protected String realm;
89  
90      /** Login context name */
91      protected String loginContextName;
92  
93      /** Login context */
94      protected LoginContext loginContext;
95  
96      /** Cache to hold authorisation information for a while */
97      protected Map<String, AuthData> cachedAuth = new HashMap<String, AuthData>();
98  
99      /** Cache timeout */
100     protected int cacheTimeout = DEFAULT_CACHE_TIMEOUT;
101 
102     /** Minimum scan period */
103     protected int minScanPeriod = DEFAULT_MINIMUM_SCAN_PERIOD;
104 
105     /** When was cache scanned last time for expired entries */
106     protected long lastScan;
107 
108     /**
109      * Constructor
110      */
111     public JAASAuthenticator() {
112     }
113 
114     /**
115      * Constructor
116      */
117     public JAASAuthenticator(ConnectionHandler handler) {
118         setHandler(handler);
119     }
120 
121     /**
122      * This method creates sets context path to be same as context path
123      * up to here plus this component's path. Component's path is reset
124      * to &quot;<code>/<code>&quot;
125      *
126      * @param connection socket connection
127      * @throws ConnectionException
128      */
129     public void handleConnection(final Connection connection) throws ConnectionException {
130         Subject subject = null;
131         Session session = null;
132         boolean fromSession = false;
133         HTTPSessionManager sessionManager = getSessionManager();
134         if (sessionManager != null) {
135             session = (Session)sessionManager.findSession(connection, false);
136             if (session != null) {
137                 subject = (Subject)session.getAttributes().get(AUTHORIZATION_DATA_ATTRIBUTE);
138                 if (subject != null) {
139                     fromSession = true;
140                 }
141             }
142         }
143 
144         HTTPConnection httpConnection = (HTTPConnection)connection.adapt(HTTPConnection.class);
145         if (subject == null) {
146 
147             String authHeader = httpConnection.getRequestHeaders().getOnly(AUTHORIZATION_REQUEST_HEADER);
148             if (authHeader != null) {
149                 if (authHeader.startsWith("Basic ")) {
150                     String base64 = authHeader.substring(6);
151                     subject = authorise(base64);
152                 }
153             }
154         }
155 
156         if (subject != null) {
157             if (!fromSession && (session != null)) {
158                 session.getAttributes().put(AUTHORIZATION_DATA_ATTRIBUTE, subject);
159             }
160             try {
161                 Subject.doAs(subject, new PrivilegedExceptionAction<Object>() {
162                     public Object run() throws Exception {
163                         getHandler().handleConnection(connection);
164                         return null;
165                     }
166                 });
167             } catch (PrivilegedActionException e) {
168                 if (e.getException() instanceof ConnectionException) {
169                     throw (ConnectionException)e.getException();
170                 } else if (e.getException() instanceof IOException) {
171                     throw new RuntimeIOException((IOException)e.getException());
172                 } else {
173                     throw new ConnectionException(e);
174                 }
175             }
176         } else {
177             String oldComponentPath = httpConnection.getComponentPath();
178 
179             String realm = getRealm();
180             if (realm == null) {
181                 realm = oldComponentPath;
182             }
183             httpConnection.getResponseHeaders().putOnly(AUTHORIZATION_RESPONSE_HEADER, "Basic realm=\"" + realm + "\"");
184             httpConnection.setResponseStatus(Status.UNAUTHORIZED);
185         }
186     }
187 
188     /**
189      * Obtains subject object from base 64 encoded username and password
190      * @param base64 base 64 encoded username and password
191      * @return subject or <code>null</code>
192      */
193     protected Subject authorise(String base64) {
194         AuthData authData = null;
195         // TODO Maybe we need to keep queried AuthData even if it is expired
196         synchronized (cachedAuth) {
197             long now = System.currentTimeMillis() - minScanPeriod;
198             if (lastScan + minScanPeriod < now) {
199                 Iterator<AuthData> it = cachedAuth.values().iterator();
200                 while (it.hasNext()) {
201                     authData = it.next();
202                     if (authData.lastAccessed + cacheTimeout < now) {
203                         it.remove();
204                     }
205                 }
206                 lastScan = System.currentTimeMillis();
207             }
208             authData = cachedAuth.get(base64);
209         }
210 
211         if (authData != null) {
212             authData.lastAccessed = System.currentTimeMillis();
213             return authData.subject;
214         }
215 
216         String userPass = Base64.decode(base64);
217         int i = userPass.indexOf(':');
218         if (i < 0) {
219             return null;
220         }
221 
222         final String user = userPass.substring(0, i);
223         final char[] pass = userPass.substring(i+1).toCharArray();
224 
225         LoginContext loginContext; // = getLoginContext();
226         try {
227             // if (loginContext == null) {
228                 loginContext = new LoginContext(getLoginContextName(), new CallbackHandler() {
229 
230                     public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
231                         for (int i = 0; i < callbacks.length; i++) {
232                             if (callbacks[i] instanceof TextOutputCallback) {
233                             } else if (callbacks[i] instanceof ConfirmationCallback) {
234                             } else if (callbacks[i] instanceof NameCallback) {
235                                 NameCallback nameCallback = (NameCallback)callbacks[i];
236                                 nameCallback.setName(user);
237                             } else if (callbacks[i] instanceof PasswordCallback) {
238                                 PasswordCallback passwordCallback = (PasswordCallback)callbacks[i];
239                                 passwordCallback.setPassword(pass);
240                             } else {
241                                 throw new UnsupportedCallbackException
242                                  (callbacks[i], "Unrecognized Callback");
243                             }
244                           }
245                     }
246                 });
247                 if (loginContext == null) {
248                     return null;
249                 } else {
250                     setLoginContext(loginContext);
251                 }
252             // }
253             logger.debug("Trying to authenticate user " + user);
254             loginContext.login();
255             logger.debug("Successfully authenticated user " + user);
256         } catch (LoginException e) {
257             logger.debug("Exception trying to get LoginContext " + getLoginContextName(), e);
258             return null;
259         }
260         Subject subject = loginContext.getSubject();
261         synchronized (cachedAuth) {
262             authData = new AuthData();
263             authData.lastAccessed = System.currentTimeMillis();
264             authData.subject = subject;
265             cachedAuth.put(base64, authData);
266         }
267         return subject;
268     }
269 
270     /**
271      * Returns wrapped handler
272      * @return wrapped handler
273      */
274     public ConnectionHandler getHandler() {
275         return handler;
276     }
277 
278     /**
279      * Sets wrapped handler
280      * @param handler wrapped handler
281      */
282     public void setHandler(ConnectionHandler handler) {
283         this.handler = handler;
284     }
285 
286     /**
287      * Returns session manaager
288      * @return http session manager
289      */
290     public HTTPSessionManager getSessionManager() {
291         if (sessionManager == null) {
292             sessionManager = new SimpleSessionManager();
293         }
294         return sessionManager;
295     }
296 
297     /**
298      * Sets session manager
299      * @param sessionManager http session manager
300      */
301     public void setSessionManager(HTTPSessionManager sessionManager) {
302         this.sessionManager = sessionManager;
303     }
304 
305     /**
306      * Returns realm to be used. If not set then component path will be used.
307      * @return realm
308      */
309     public String getRealm() {
310         return realm;
311     }
312 
313     /**
314      * Sets realm.
315      *
316      * @param realm realm
317      */
318     public void setRealm(String realm) {
319         this.realm = realm;
320     }
321 
322     /**
323      * Returns login context name
324      * @return login context name
325      */
326     public String getLoginContextName() {
327         return loginContextName;
328     }
329 
330     /**
331      * Sets login context name
332      * @param loginContextName login context name
333      */
334     public void setLoginContextName(String loginContextName) {
335         this.loginContextName = loginContextName;
336     }
337 
338     /**
339      * Returns login context
340      * @return login context
341      */
342     public LoginContext getLoginContext() {
343         return loginContext;
344     }
345 
346     /**
347      * Sets login context
348      * @param loginContext login context
349      */
350     public void setLoginContext(LoginContext loginContext) {
351         this.loginContext = loginContext;
352     }
353 
354     /**
355      * Returns cache timeout
356      * @return cache timeout
357      */
358     public int getCacheTimeout() {
359         return cacheTimeout;
360     }
361 
362     /**
363      * Sets cache timeout
364      * @param cacheTimeout cache timeout
365      */
366     public void setCacheTimeout(int cacheTimeout) {
367         this.cacheTimeout = cacheTimeout;
368     }
369 
370     /**
371      * Return minimum scan period
372      * @return minimum scan period
373      */
374     public int getMinimumScanPeriod() {
375         return minScanPeriod;
376     }
377 
378     /**
379      * Sets minimum scan period
380      * @param minScanPeriod minimum scan period
381      */
382     public void setMinimumScanPeriod(int minScanPeriod) {
383         this.minScanPeriod = minScanPeriod;
384     }
385 
386 
387     /**
388      * Class holding cached authorisation data
389      */
390     protected class AuthData {
391 
392         public long lastAccessed;
393         public Subject subject;
394 
395     }
396 }