View Javadoc

1   /**
2    *
3    *  Licensed to the Apache Software Foundation (ASF) under one or more
4    *  contributor license agreements.  See the NOTICE file distributed with
5    *  this work for additional information regarding copyright ownership.
6    *  The ASF licenses this file to You under the Apache License, Version 2.0
7    *  (the "License"); you may not use this file except in compliance with
8    *  the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing, software
13   *  distributed under the License is distributed on an "AS IS" BASIS,
14   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   *  See the License for the specific language governing permissions and
16   *  limitations under the License.
17   */
18  
19  package org.apache.geronimo.security.jaas.server;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.geronimo.common.GeronimoSecurityException;
24  import org.apache.geronimo.gbean.GBeanInfo;
25  import org.apache.geronimo.gbean.GBeanInfoBuilder;
26  import org.apache.geronimo.gbean.GBeanLifecycle;
27  import org.apache.geronimo.j2ee.j2eeobjectnames.NameFactory;
28  import org.apache.geronimo.security.ContextManager;
29  import org.apache.geronimo.security.IdentificationPrincipal;
30  import org.apache.geronimo.security.SubjectId;
31  import org.apache.geronimo.security.jaas.LoginUtils;
32  import org.apache.geronimo.security.realm.SecurityRealm;
33  
34  import javax.crypto.Mac;
35  import javax.crypto.SecretKey;
36  import javax.crypto.spec.SecretKeySpec;
37  
38  import javax.security.auth.Subject;
39  import javax.security.auth.callback.Callback;
40  import javax.security.auth.login.LoginException;
41  import javax.security.auth.spi.LoginModule;
42  import java.security.InvalidKeyException;
43  import java.security.NoSuchAlgorithmException;
44  import java.security.Principal;
45  
46  import java.util.Collection;
47  import java.util.HashMap;
48  import java.util.Hashtable;
49  import java.util.Iterator;
50  import java.util.LinkedList;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Set;
54  import java.util.Timer;
55  import java.util.TimerTask;
56  
57  /**
58   * The single point of contact for Geronimo JAAS realms.  Instead of attempting
59   * to interact with JAAS realms directly, a client should either interact with
60   * this service, or use a LoginModule implementation that interacts with this
61   * service.
62   *
63   * @version $Rev: 472291 $ $Date: 2006-11-07 13:51:35 -0800 (Tue, 07 Nov 2006) $
64   */
65  public class JaasLoginService implements GBeanLifecycle, JaasLoginServiceMBean {
66      public static final Log log = LogFactory.getLog(JaasLoginService.class);
67      private final static int DEFAULT_EXPIRED_LOGIN_SCAN_INTERVAL = 300000; // 5 mins
68      private final static int DEFAULT_MAX_LOGIN_DURATION = 1000 * 3600 * 24; // 1 day
69      private final static Timer clockDaemon = new Timer(/* Name requires JDK 1.5 "LoginService login modules monitor", */ true);
70      private static long nextLoginModuleId = System.currentTimeMillis();
71      private Collection realms;
72      private final String objectName;
73      private final SecretKey key;
74      private final String algorithm;
75      private final ClassLoader classLoader;
76      private final Map activeLogins = new Hashtable();
77      private int expiredLoginScanIntervalMillis = DEFAULT_EXPIRED_LOGIN_SCAN_INTERVAL;
78      private int maxLoginDurationMillis = DEFAULT_MAX_LOGIN_DURATION;
79      private ExpirationMonitor expirationMonitor;
80  
81      public JaasLoginService(String algorithm, String password, ClassLoader classLoader, String objectName) {
82          this.classLoader = classLoader;
83          this.algorithm = algorithm;
84          key = new SecretKeySpec(password.getBytes(), algorithm);
85          this.objectName = objectName;
86      }
87  
88      public String getObjectName() {
89          return objectName;
90      }
91  
92      /**
93       * GBean property
94       */
95      public Collection getRealms() {
96          return realms;
97      }
98  
99      /**
100      * GBean property
101      */
102     public void setRealms(Collection realms) {
103         this.realms = realms;
104         //todo: add listener to drop logins when realm is removed
105     }
106 
107     /**
108      * GBean property
109      */
110     public int getMaxLoginDurationMillis() {
111         return maxLoginDurationMillis;
112     }
113 
114     /**
115      * GBean property
116      */
117     public void setMaxLoginDurationMillis(int maxLoginDurationMillis) {
118         if (maxLoginDurationMillis == 0) {
119             maxLoginDurationMillis = DEFAULT_MAX_LOGIN_DURATION;
120         }
121         this.maxLoginDurationMillis = maxLoginDurationMillis;
122     }
123 
124     /**
125      * GBean property
126      */
127     public int getExpiredLoginScanIntervalMillis() {
128         return expiredLoginScanIntervalMillis;
129     }
130 
131     /**
132      * GBean property
133      */
134     public void setExpiredLoginScanIntervalMillis(int expiredLoginScanIntervalMillis) {
135         if (expiredLoginScanIntervalMillis == 0) {
136             expiredLoginScanIntervalMillis = DEFAULT_EXPIRED_LOGIN_SCAN_INTERVAL;
137         }
138         this.expiredLoginScanIntervalMillis = expiredLoginScanIntervalMillis;
139     }
140 
141     public void doStart() throws Exception {
142         expirationMonitor = new ExpirationMonitor();
143 
144         clockDaemon.scheduleAtFixedRate(
145                 expirationMonitor, expiredLoginScanIntervalMillis, expiredLoginScanIntervalMillis);
146     }
147 
148     public void doStop() throws Exception {
149         if (expirationMonitor != null) {
150             expirationMonitor.cancel();
151             expirationMonitor = null;
152         }
153 
154         //todo: shut down all logins
155     }
156 
157     public void doFail() {
158         //todo: shut down all logins
159     }
160 
161     /**
162      * Starts a new authentication process on behalf of an end user.  The
163      * returned ID will identify that user throughout the user's interaction
164      * with the server.  On the server side, that means maintaining the
165      * Subject and Principals for the user.
166      *
167      * @return The client handle used as an argument for the rest of the
168      *         methods in this class.
169      */
170     public JaasSessionId connectToRealm(String realmName) {
171         SecurityRealm realm;
172         realm = getRealm(realmName);
173         if (realm == null) {
174             throw new GeronimoSecurityException("No such realm (" + realmName + ")");
175         } else {
176             return initializeClient(realm);
177         }
178     }
179 
180     /**
181      * Gets the login module configuration for the specified realm.  The
182      * caller needs that in order to perform the authentication process.
183      */
184     public JaasLoginModuleConfiguration[] getLoginConfiguration(JaasSessionId sessionHandle) throws LoginException {
185         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
186         if (session == null) {
187             throw new ExpiredLoginModuleException();
188         }
189         JaasLoginModuleConfiguration[] config = session.getModules();
190         // strip out non-serializable configuration options
191         JaasLoginModuleConfiguration[] result = new JaasLoginModuleConfiguration[config.length];
192         for (int i = 0; i < config.length; i++) {
193             result[i] = LoginUtils.getSerializableCopy(config[i]);
194         }
195         return result;
196     }
197 
198     /**
199      * Retrieves callbacks for a server side login module.  When the client
200      * is going through the configured login modules, if a specific login
201      * module is client-side, it will be handled directly.  If it is
202      * server-side, the client gets the callbacks (using this method),
203      * populates them, and sends them back to the server.
204      */
205     public Callback[] getServerLoginCallbacks(JaasSessionId sessionHandle, int loginModuleIndex) throws LoginException {
206         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
207         checkContext(session, loginModuleIndex);
208         LoginModule module = session.getLoginModule(loginModuleIndex);
209 
210         session.getHandler().setExploring();
211         try {
212             module.initialize(session.getSubject(), session.getHandler(), new HashMap(), session.getOptions(loginModuleIndex));
213         } catch (Exception e) {
214             log.error("Failed to initialize module", e);
215         }
216         try {
217             module.login();
218         } catch (LoginException e) {
219             //expected
220         }
221         try {
222             module.abort();
223         } catch (LoginException e) {
224             //makes no difference
225         }
226         return session.getHandler().finalizeCallbackList();
227     }
228 
229     /**
230      * Returns populated callbacks for a server side login module.  When the
231      * client is going through the configured login modules, if a specific
232      * login module is client-side, it will be handled directly.  If it is
233      * server-side, the client gets the callbacks, populates them, and sends
234      * them back to the server (using this method).
235      */
236     public boolean performLogin(JaasSessionId sessionHandle, int loginModuleIndex, Callback[] results) throws LoginException {
237         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
238         checkContext(session, loginModuleIndex);
239         try {
240             session.getHandler().setClientResponse(results);
241         } catch (IllegalArgumentException iae) {
242             throw new LoginException(iae.toString());
243         }
244         return session.getLoginModule(loginModuleIndex).login();
245     }
246 
247     /**
248      * Indicates that the overall login succeeded, and some principals were
249      * generated by a client-side login module.  This method needs to be called
250      * once for each client-side login module, to specify Principals for each
251      * module.
252      */
253     public boolean performCommit(JaasSessionId sessionHandle, int loginModuleIndex) throws LoginException {
254         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
255         checkContext(session, loginModuleIndex);
256         return session.getLoginModule(loginModuleIndex).commit();
257     }
258 
259     /**
260      * Indicates that the overall login failed.  This method needs to be called
261      * once for each client-side login module.
262      */
263     public boolean performAbort(JaasSessionId sessionHandle, int loginModuleIndex) throws LoginException {
264         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
265         checkContext(session, loginModuleIndex);
266         return session.getLoginModule(loginModuleIndex).abort();
267     }
268 
269     /**
270      * Indicates that the overall login succeeded.  All login modules that were
271      * touched should have been logged in and committed before calling this.
272      */
273     public Principal loginSucceeded(JaasSessionId sessionHandle) throws LoginException {
274         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
275         if (session == null) {
276             throw new ExpiredLoginModuleException();
277         }
278 
279         Subject subject = session.getSubject();
280         ContextManager.registerSubject(subject);
281         SubjectId id = ContextManager.getSubjectId(subject);
282         IdentificationPrincipal principal = new IdentificationPrincipal(id);
283         subject.getPrincipals().add(principal);
284         return principal;
285     }
286 
287     /**
288      * Indicates that the overall login failed, and the server should release
289      * any resources associated with the user ID.
290      */
291     public void loginFailed(JaasSessionId sessionHandle) {
292         activeLogins.remove(sessionHandle);
293     }
294 
295     /**
296      * Indicates that the client has logged out, and the server should release
297      * any resources associated with the user ID.
298      */
299     public void logout(JaasSessionId sessionHandle) throws LoginException {
300         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
301         if (session == null) {
302             throw new ExpiredLoginModuleException();
303         }
304         ContextManager.unregisterSubject(session.getSubject());
305         activeLogins.remove(sessionHandle);
306         for (int i = 0; i < session.getModules().length; i++) {
307             if (session.isServerSide(i)) {
308                 session.getLoginModule(i).logout();
309             }
310         }
311     }
312 
313     /**
314      * Syncs the shared state that's on thye client with the shared state that
315      * is on the server.
316      *
317      * @param sessionHandle
318      * @param sharedState   the shared state that is on the client
319      * @return the sync'd shared state that is on the server
320      */
321     public Map syncShareState(JaasSessionId sessionHandle, Map sharedState) throws LoginException {
322         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
323         if (session == null) {
324             throw new ExpiredLoginModuleException();
325         }
326         session.getSharedContext().putAll(sharedState);
327         return LoginUtils.getSerializableCopy(session.getSharedContext());
328     }
329 
330     /**
331      * Syncs the set of principals that are on the client with the set of principals that
332      * are on the server.
333      *
334      * @param sessionHandle
335      * @param principals    the set of principals that are on the client side
336      * @return the sync'd set of principals that are on the server
337      */
338     public Set syncPrincipals(JaasSessionId sessionHandle, Set principals) throws LoginException {
339         JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(sessionHandle);
340         if (session == null) {
341             throw new ExpiredLoginModuleException();
342         }
343         session.getSubject().getPrincipals().addAll(principals);
344 
345         return LoginUtils.getSerializableCopy(session.getSubject().getPrincipals());
346     }
347 
348     private void checkContext(JaasSecuritySession session, int loginModuleIndex) throws LoginException {
349         if (session == null) {
350             throw new ExpiredLoginModuleException();
351         }
352         if (loginModuleIndex < 0 || loginModuleIndex >= session.getModules().length || !session.isServerSide(loginModuleIndex)) {
353             throw new LoginException("Invalid login module specified");
354         }
355     }
356 
357     /**
358      * Prepares a new security context for a new client.  Each client uses a
359      * unique security context to sture their authentication progress,
360      * principals, etc.
361      *
362      * @param realm The realm the client is authenticating to
363      */
364     private JaasSessionId initializeClient(SecurityRealm realm) {
365         long id;
366         synchronized (JaasLoginService.class) {
367             id = ++nextLoginModuleId;
368         }
369         JaasSessionId sessionHandle = new JaasSessionId(id, hash(id));
370         JaasLoginModuleConfiguration[] modules = realm.getAppConfigurationEntries();
371         JaasSecuritySession session = new JaasSecuritySession(realm.getRealmName(), modules, new HashMap(), classLoader);
372         activeLogins.put(sessionHandle, session);
373         return sessionHandle;
374     }
375 
376     private SecurityRealm getRealm(String realmName) {
377         for (Iterator it = realms.iterator(); it.hasNext();) {
378             SecurityRealm test = (SecurityRealm) it.next();
379             if (test.getRealmName().equals(realmName)) {
380                 return test;
381             }
382         }
383         return null;
384     }
385 
386     /**
387      * Hashes a unique ID.  The client keeps an object around with the ID and
388      * the hash of the ID.  That way it's not so easy to forge an ID and steal
389      * someone else's account.
390      */
391     private byte[] hash(long id) {
392         byte[] bytes = new byte[8];
393         for (int i = 7; i >= 0; i--) {
394             bytes[i] = (byte) (id);
395             id >>>= 8;
396         }
397 
398         try {
399             Mac mac = Mac.getInstance(algorithm);
400             mac.init(key);
401             mac.update(bytes);
402 
403             return mac.doFinal();
404         } catch (NoSuchAlgorithmException e) {
405         } catch (InvalidKeyException e) {
406         }
407         assert false : "Should never have reached here";
408         return null;
409     }
410 
411     private class ExpirationMonitor extends TimerTask { //todo: different timeouts per realm?
412 
413         public void run() {
414             long now = System.currentTimeMillis();
415             List list = new LinkedList();
416             synchronized (activeLogins) {
417                 for (Iterator it = activeLogins.keySet().iterator(); it.hasNext();) {
418                     JaasSessionId id = (JaasSessionId) it.next();
419                     JaasSecuritySession session = (JaasSecuritySession) activeLogins.get(id);
420                     int age = (int) (now - session.getCreated());
421                     if (session.isDone() || age > maxLoginDurationMillis) {
422                         list.add(session);
423                         session.setDone(true);
424                         it.remove();
425                     }
426                 }
427             }
428             for (Iterator it = list.iterator(); it.hasNext();) {
429                 JaasSecuritySession session = (JaasSecuritySession) it.next();
430                 ContextManager.unregisterSubject(session.getSubject());
431             }
432         }
433     }
434 
435 
436     // This stuff takes care of making this object into a GBean
437     public static final GBeanInfo GBEAN_INFO;
438 
439     static {
440         GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic(JaasLoginService.class, "JaasLoginService");
441 
442         infoFactory.addAttribute("algorithm", String.class, true);
443         infoFactory.addAttribute("password", String.class, true);
444         infoFactory.addAttribute("classLoader", ClassLoader.class, false);
445         infoFactory.addAttribute("maxLoginDurationMillis", int.class, true);
446         infoFactory.addAttribute("expiredLoginScanIntervalMillis", int.class, true);
447         infoFactory.addAttribute("objectName", String.class, false);
448 
449         infoFactory.addOperation("connectToRealm", new Class[]{String.class});
450         infoFactory.addOperation("getLoginConfiguration", new Class[]{JaasSessionId.class});
451         infoFactory.addOperation("getServerLoginCallbacks", new Class[]{JaasSessionId.class, int.class});
452         infoFactory.addOperation("performLogin", new Class[]{JaasSessionId.class, int.class, Callback[].class});
453         infoFactory.addOperation("performCommit", new Class[]{JaasSessionId.class, int.class});
454         infoFactory.addOperation("loginSucceeded", new Class[]{JaasSessionId.class});
455         infoFactory.addOperation("loginFailed", new Class[]{JaasSessionId.class});
456         infoFactory.addOperation("logout", new Class[]{JaasSessionId.class});
457         infoFactory.addOperation("syncShareState", new Class[]{JaasSessionId.class, Map.class});
458         infoFactory.addOperation("syncPrincipals", new Class[]{JaasSessionId.class, Set.class});
459 
460         infoFactory.addReference("Realms", SecurityRealm.class, NameFactory.SECURITY_REALM);
461         infoFactory.addInterface(JaasLoginServiceMBean.class);
462 
463         infoFactory.setConstructor(new String[]{"algorithm", "password", "classLoader", "objectName"});
464 
465         GBEAN_INFO = infoFactory.getBeanInfo();
466     }
467 
468     public static GBeanInfo getGBeanInfo() {
469         return GBEAN_INFO;
470     }
471 }