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 }