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;
68 private final static int DEFAULT_MAX_LOGIN_DURATION = 1000 * 3600 * 24;
69 private final static Timer clockDaemon = new Timer(
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
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
155 }
156
157 public void doFail() {
158
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
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
220 }
221 try {
222 module.abort();
223 } catch (LoginException e) {
224
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 {
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
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 }