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