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.http.HTTPConnection;
18  import org.abstracthorizon.danube.http.HTTPContext;
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   * This class marks start of "web application" by setting
49   * context path. All, potential, session handling will be done using
50   * context path - path this component is defined on.
51   *
52   * @author Daniel Sendula
53   */
54  public class JAASAuthenticatedHTTPContext extends HTTPContext {
55  
56  
57      /** Logger */
58      protected final Logger logger = LoggerFactory.getLogger(JAASAuthenticatedHTTPContext.class);
59  
60  
61      /** Authorisation data session attribute */
62      public static final String AUTHORIZATION_DATA_ATTRIBUTE = "org.abstracthorizon.danube.http.auth.Subject";
63  
64      /** Client request header for authorisation  */
65      public static final String AUTHORIZATION_REQUEST_HEADER = "Authorization";
66  
67      /** Server response header for authorisation  */
68      public static final String AUTHORIZATION_RESPONSE_HEADER = "WWW-Authenticate";
69  
70      /** Default cache timeout */
71      public static final int DEFAULT_CACHE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
72  
73      /** Default minimum scan period */
74      public static final int DEFAULT_MINIMUM_SCAN_PERIOD = 10 * 1000; // 10 seconds
75  
76      /** Session manager */
77      protected HTTPSessionManager sessionManager;
78  
79      /** Realm name */
80      protected String realm;
81  
82      /** Login context name */
83      protected String loginContextName;
84  
85      /** Login context */
86      protected LoginContext loginContext;
87  
88      /** Cache to hold authorisation information for a while */
89      protected Map<String, AuthData> cachedAuth = new HashMap<String, AuthData>();
90  
91      /** Cache timeout */
92      protected int cacheTimeout = DEFAULT_CACHE_TIMEOUT;
93  
94      /** Minimum scan period */
95      protected int minScanPeriod = DEFAULT_MINIMUM_SCAN_PERIOD;
96  
97      /** When was cache scanned last time for expired entries */
98      protected long lastScan;
99  
100     /** Force authorisation */
101     protected boolean forceAuthorisation = true;
102     
103     /**
104      * Constructor
105      */
106     public JAASAuthenticatedHTTPContext() {
107     }
108 
109     /**
110      * This method creates sets context path to be same as context path
111      * up to here plus this component's path. Component's path is reset
112      * to &quot;<code>/<code>&quot;
113      *
114      * @param connection socket connection
115      * @throws ConnectionException
116      */
117     public void handleConnection(final Connection connection) throws ConnectionException {
118         Subject subject = null;
119         Session session = null;
120         boolean fromSession = false;
121         HTTPSessionManager sessionManager = getSessionManager();
122         if (sessionManager != null) {
123             session = (Session)sessionManager.findSession(connection, false);
124             if (session != null) {
125                 subject = (Subject)session.getAttributes().get(AUTHORIZATION_DATA_ATTRIBUTE);
126                 if (subject != null) {
127                     fromSession = true;
128                 }
129             }
130         }
131 
132         HTTPConnection httpConnection = (HTTPConnection)connection.adapt(HTTPConnection.class);
133         if (subject == null) {
134 
135             String authHeader = httpConnection.getRequestHeaders().getOnly(AUTHORIZATION_REQUEST_HEADER);
136             if (authHeader != null) {
137                 if (authHeader.startsWith("Basic ")) {
138                     String base64 = authHeader.substring(6);
139                     subject = authorise(base64);
140                 }
141             }
142         }
143 
144         if (subject != null) {
145             if (!fromSession && (session != null)) {
146                 session.getAttributes().put(AUTHORIZATION_DATA_ATTRIBUTE, subject);
147             }
148             try {
149                 Subject.doAs(subject, new PrivilegedExceptionAction<Object>() {
150                     public Object run() throws Exception {
151                         superHandleConnection(connection);
152                         return null;
153                     }
154                 });
155             } catch (PrivilegedActionException e) {
156                 if (e.getException() instanceof ConnectionException) {
157                     throw (ConnectionException)e.getException();
158                 } else if (e.getException() instanceof IOException) {
159                     throw new RuntimeIOException((IOException)e.getException());
160                 } else {
161                     throw new ConnectionException(e);
162                 }
163             }
164         } else {
165             if (forceAuthorisation) {
166                 String oldComponentPath = httpConnection.getComponentPath();
167                 httpConnection.addComponentPathToContextPath();
168     //            httpConnection.addToContextPath(httpConnection.getComponentPath());
169     //            httpConnection.setComponentPath("/");
170     
171                 String realm = getRealm();
172                 if (realm == null) {
173                     realm = oldComponentPath;
174                 }
175                 httpConnection.getResponseHeaders().putOnly(AUTHORIZATION_RESPONSE_HEADER, "Basic realm=\"" + realm + "\"");
176                 httpConnection.setResponseStatus(Status.UNAUTHORIZED);
177             } else {
178                 try {
179                     superHandleConnection(connection);
180                 } catch (IOException e) {
181                     throw new RuntimeIOException(e);
182                 }
183             }
184         }
185     }
186 
187     /**
188      * Calls super class handle connection method.
189      * @param connection connection
190      * @throws IOException if thrown by super {@link #handleConnection(Connection)}
191      */
192     protected void superHandleConnection(Connection connection) throws IOException {
193         super.handleConnection(connection);
194     }
195 
196     /**
197      * Obtains subject object from base 64 encoded username and password
198      * @param base64 base 64 encoded username and password
199      * @return subject or <code>null</code>
200      */
201     protected Subject authorise(String base64) {
202         AuthData authData = null;
203         // TODO Maybe we need to keep queried AuthData even if it is expired
204         synchronized (cachedAuth) {
205             long now = System.currentTimeMillis() - minScanPeriod;
206             if (lastScan + minScanPeriod < now) {
207                 Iterator<AuthData> it = cachedAuth.values().iterator();
208                 while (it.hasNext()) {
209                     authData = it.next();
210                     if (authData.lastAccessed + cacheTimeout < now) {
211                         it.remove();
212                     }
213                 }
214                 lastScan = System.currentTimeMillis();
215             }
216             authData = cachedAuth.get(base64);
217         }
218 
219         if (authData != null) {
220             authData.lastAccessed = System.currentTimeMillis();
221             return authData.subject;
222         }
223 
224         String userPass = Base64.decode(base64);
225         int i = userPass.indexOf(':');
226         if (i < 0) {
227             return null;
228         }
229 
230         final String user = userPass.substring(0, i);
231         final char[] pass = userPass.substring(i+1).toCharArray();
232 
233         LoginContext loginContext; // = getLoginContext();
234         try {
235             // if (loginContext == null) {
236                 loginContext = new LoginContext(getLoginContextName(), new CallbackHandler() {
237 
238                     public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
239                         for (int i = 0; i < callbacks.length; i++) {
240                             if (callbacks[i] instanceof TextOutputCallback) {
241                             } else if (callbacks[i] instanceof ConfirmationCallback) {
242                             } else if (callbacks[i] instanceof NameCallback) {
243                                 NameCallback nameCallback = (NameCallback)callbacks[i];
244                                 nameCallback.setName(user);
245                             } else if (callbacks[i] instanceof PasswordCallback) {
246                                 PasswordCallback passwordCallback = (PasswordCallback)callbacks[i];
247                                 passwordCallback.setPassword(pass);
248                             } else {
249                                 throw new UnsupportedCallbackException
250                                  (callbacks[i], "Unrecognized Callback");
251                             }
252                           }
253                     }
254                 });
255                 if (loginContext == null) {
256                     return null;
257                 } else {
258                     setLoginContext(loginContext);
259                 }
260             // }
261             logger.debug("Trying to authenticate user " + user);
262             loginContext.login();
263             logger.debug("Successfully authenticated user " + user);
264         } catch (LoginException e) {
265             logger.debug("Exception trying to get LoginContext " + getLoginContextName(), e);
266             return null;
267         }
268         Subject subject = loginContext.getSubject();
269         synchronized (cachedAuth) {
270             authData = new AuthData();
271             authData.lastAccessed = System.currentTimeMillis();
272             authData.subject = subject;
273             cachedAuth.put(base64, authData);
274         }
275         return subject;
276     }
277 
278     /**
279      * Returns session manaager
280      * @return http session manager
281      */
282     public HTTPSessionManager getSessionManager() {
283         if (sessionManager == null) {
284             sessionManager = new SimpleSessionManager();
285         }
286         return sessionManager;
287     }
288 
289     /**
290      * Sets session manager
291      * @param sessionManager http session manager
292      */
293     public void setSessionManager(HTTPSessionManager sessionManager) {
294         this.sessionManager = sessionManager;
295     }
296 
297     /**
298      * Returns realm to be used. If not set then component path will be used.
299      * @return realm
300      */
301     public String getRealm() {
302         return realm;
303     }
304 
305     /**
306      * Sets realm.
307      *
308      * @param realm realm
309      */
310     public void setRealm(String realm) {
311         this.realm = realm;
312     }
313 
314     /**
315      * Returns login context name
316      * @return login context name
317      */
318     public String getLoginContextName() {
319         return loginContextName;
320     }
321 
322     /**
323      * Sets login context name
324      * @param loginContextName login context name
325      */
326     public void setLoginContextName(String loginContextName) {
327         this.loginContextName = loginContextName;
328     }
329 
330     /**
331      * Returns login context
332      * @return login context
333      */
334     public LoginContext getLoginContext() {
335         return loginContext;
336     }
337 
338     /**
339      * Sets login context
340      * @param loginContext login context
341      */
342     public void setLoginContext(LoginContext loginContext) {
343         this.loginContext = loginContext;
344     }
345 
346     /**
347      * Returns cache timeout
348      * @return cache timeout
349      */
350     public int getCacheTimeout() {
351         return cacheTimeout;
352     }
353 
354     /**
355      * Sets cache timeout
356      * @param cacheTimeout cache timeout
357      */
358     public void setCacheTimeout(int cacheTimeout) {
359         this.cacheTimeout = cacheTimeout;
360     }
361 
362     /**
363      * Return minimum scan period
364      * @return minimum scan period
365      */
366     public int getMinimumScanPeriod() {
367         return minScanPeriod;
368     }
369 
370     /**
371      * Sets minimum scan period
372      * @param minScanPeriod minimum scan period
373      */
374     public void setMinimumScanPeriod(int minScanPeriod) {
375         this.minScanPeriod = minScanPeriod;
376     }
377     
378     public boolean getForceAuthorisation() {
379         return forceAuthorisation;
380     }
381     
382     public void setForceAuthorisation(boolean forceAuthorisation) {
383         this.forceAuthorisation = forceAuthorisation;
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 }