1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.acegisecurity.providers.jaas;
17
18 import org.acegisecurity.AcegiSecurityException;
19 import org.acegisecurity.Authentication;
20 import org.acegisecurity.AuthenticationException;
21 import org.acegisecurity.GrantedAuthority;
22 import org.acegisecurity.context.HttpSessionContextIntegrationFilter;
23 import org.acegisecurity.context.SecurityContext;
24 import org.acegisecurity.providers.AuthenticationProvider;
25 import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
26 import org.acegisecurity.providers.jaas.event.JaasAuthenticationFailedEvent;
27 import org.acegisecurity.providers.jaas.event.JaasAuthenticationSuccessEvent;
28 import org.acegisecurity.ui.session.HttpSessionDestroyedEvent;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.springframework.beans.BeansException;
32 import org.springframework.beans.factory.InitializingBean;
33 import org.springframework.context.ApplicationContext;
34 import org.springframework.context.ApplicationContextAware;
35 import org.springframework.context.ApplicationEvent;
36 import org.springframework.context.ApplicationListener;
37 import org.springframework.core.io.Resource;
38 import org.springframework.util.Assert;
39
40 import javax.security.auth.callback.Callback;
41 import javax.security.auth.callback.CallbackHandler;
42 import javax.security.auth.callback.UnsupportedCallbackException;
43 import javax.security.auth.login.Configuration;
44 import javax.security.auth.login.LoginContext;
45 import javax.security.auth.login.LoginException;
46 import java.io.IOException;
47 import java.security.Principal;
48 import java.security.Security;
49 import java.util.Arrays;
50 import java.util.HashSet;
51 import java.util.Iterator;
52 import java.util.Set;
53
54
55 /***
56 * An {@link AuthenticationProvider} implementation that retrieves user details
57 * from a JAAS login configuration.
58 *
59 * <p>
60 * This <code>AuthenticationProvider</code> is capable of validating {@link
61 * org.acegisecurity.providers.UsernamePasswordAuthenticationToken}
62 * requests contain the correct username and password.
63 * </p>
64 *
65 * <p>
66 * This implementation is backed by a <a
67 * href="http://java.sun.com/j2se/1.4.2/docs/guide/security/jaas/JAASRefGuide.html">JAAS</a>
68 * configuration. The loginConfig property must be set to a given JAAS
69 * configuration file. This setter accepts a Spring {@link
70 * org.springframework.core.io.Resource} instance. It should point to a JAAS
71 * configuration file containing an index matching the {@link
72 * #setLoginContextName(java.lang.String) loginContextName} property.
73 * </p>
74 *
75 * <p>
76 * For example: If this JaasAuthenticationProvider were configured in a Spring
77 * WebApplicationContext the xml to set the loginConfiguration could be as
78 * follows...
79 * <pre>
80 * <property name="loginConfig">
81 * <value>/WEB-INF/login.conf</value>
82 * </property>
83 * </pre>
84 * </p>
85 *
86 * <p>
87 * The loginContextName should coincide with a given index in the loginConfig
88 * specifed. The loginConfig file used in the JUnit tests appears as the
89 * following...
90 * <pre>
91 * JAASTest {
92 * org.acegisecurity.providers.jaas.TestLoginModule required;
93 * };
94 * </pre>
95 * Using the example login configuration above, the loginContextName property
96 * would be set as <i>JAASTest</i>...
97 * <pre>
98 * <property name="loginContextName">
99 * <value>JAASTest</value>
100 * </property>
101 * </pre>
102 * </p>
103 *
104 * <p>
105 * When using JAAS login modules as the authentication source, sometimes the <a
106 * href="http://java.sun.com/j2se/1.4.2/docs/api/javax/security/auth/login/LoginContext.html">LoginContext</a>
107 * will require <i>CallbackHandler</i>s. The JaasAuthenticationProvider uses
108 * an internal <a
109 * href="http://java.sun.com/j2se/1.4.2/docs/api/javax/security/auth/callback/CallbackHandler.html">CallbackHandler</a>
110 * to wrap the {@link JaasAuthenticationCallbackHandler}s configured in the
111 * ApplicationContext. When the LoginContext calls the internal
112 * CallbackHandler, control is passed to each {@link
113 * JaasAuthenticationCallbackHandler} for each Callback passed.
114 * </p>
115 *
116 * <p>
117 * {{@link JaasAuthenticationCallbackHandler}s are passed to the
118 * JaasAuthenticationProvider through the {@link
119 * #setCallbackHandlers(org.acegisecurity.providers.jaas.JaasAuthenticationCallbackHandler[])
120 * callbackHandlers} property. }
121 * <pre>
122 * <property name="callbackHandlers">
123 * <list>
124 * <bean class="org.acegisecurity.providers.jaas.TestCallbackHandler"/>
125 * <bean class="{@link JaasNameCallbackHandler org.acegisecurity.providers.jaas.JaasNameCallbackHandler}"/>
126 * <bean class="{@link JaasPasswordCallbackHandler org.acegisecurity.providers.jaas.JaasPasswordCallbackHandler}"/>
127 * </list>
128 * </property>
129 * </pre>
130 * </p>
131 *
132 * <p>
133 * After calling LoginContext.login(), the JaasAuthenticationProvider will
134 * retrieve the returned Principals from the Subject
135 * (LoginContext.getSubject().getPrincipals). Each returned principal is then
136 * passed to the configured {@link AuthorityGranter}s. An AuthorityGranter is
137 * a mapping between a returned Principal, and a role name. If an
138 * AuthorityGranter wishes to grant an Authorization a role, it returns that
139 * role name from it's {@link AuthorityGranter#grant(java.security.Principal)}
140 * method. The returned role will be applied to the Authorization object as a
141 * {@link GrantedAuthority}.
142 * </p>
143 *
144 * <p>
145 * AuthorityGranters are configured in spring xml as follows...
146 * <pre>
147 * <property name="authorityGranters">
148 * <list>
149 * <bean class="org.acegisecurity.providers.jaas.TestAuthorityGranter"/>
150 * </list>
151 * </property>
152 * <p/>
153 * </pre>
154 * </p>
155 *
156 * A configuration note:
157 * The JaasAuthenticationProvider uses the security properites "e;login.config.url.X"e; to configure jaas.
158 * If you would like to customize the way Jaas gets configured, create a subclass of this and override the {@link #configureJaas(Resource)} method.
159 *
160 * @author Ray Krueger
161 * @version $Id: JaasAuthenticationProvider.java,v 1.15 2005/11/17 00:55:52 benalex Exp $
162 */
163 public class JaasAuthenticationProvider implements AuthenticationProvider,
164 InitializingBean, ApplicationContextAware, ApplicationListener {
165
166
167 protected static final Log log = LogFactory.getLog(JaasAuthenticationProvider.class);
168
169
170
171 private ApplicationContext context;
172 private LoginExceptionResolver loginExceptionResolver = new DefaultLoginExceptionResolver();
173 private Resource loginConfig;
174
175 private String loginContextName = "ACEGI";
176 private AuthorityGranter[] authorityGranters;
177 private JaasAuthenticationCallbackHandler[] callbackHandlers;
178
179
180
181 public void setApplicationContext(ApplicationContext applicationContext)
182 throws BeansException {
183 this.context = applicationContext;
184 }
185
186 public ApplicationContext getApplicationContext() {
187 return context;
188 }
189
190 /***
191 * Set the AuthorityGranters that should be consulted for role names to be
192 * granted to the Authentication.
193 *
194 * @param authorityGranters AuthorityGranter array
195 * @see JaasAuthenticationProvider
196 */
197 public void setAuthorityGranters(AuthorityGranter[] authorityGranters) {
198 this.authorityGranters = authorityGranters;
199 }
200
201 /***
202 * Returns the AuthorityGrannter array that was passed to the {@link
203 * #setAuthorityGranters(AuthorityGranter[])} method, or null if it none
204 * were ever set.
205 *
206 * @return The AuthorityGranter array, or null
207 * @see #setAuthorityGranters(AuthorityGranter[])
208 */
209 public AuthorityGranter[] getAuthorityGranters() {
210 return authorityGranters;
211 }
212
213 /***
214 * Set the JAASAuthentcationCallbackHandler array to handle callback
215 * objects generated by the LoginContext.login method.
216 *
217 * @param callbackHandlers Array of JAASAuthenticationCallbackHandlers
218 */
219 public void setCallbackHandlers(
220 JaasAuthenticationCallbackHandler[] callbackHandlers) {
221 this.callbackHandlers = callbackHandlers;
222 }
223
224 /***
225 * Returns the current JaasAuthenticationCallbackHandler array, or null if
226 * none are set.
227 *
228 * @return the JAASAuthenticationCallbackHandlers.
229 * @see #setCallbackHandlers(JaasAuthenticationCallbackHandler[])
230 */
231 public JaasAuthenticationCallbackHandler[] getCallbackHandlers() {
232 return callbackHandlers;
233 }
234
235 /***
236 * Set the JAAS login configuration file.
237 *
238 * @param loginConfig <a
239 * href="http://www.springframework.org/docs/api/org/springframework/core/io/Resource.html">Spring
240 * Resource</a>
241 * @see <a
242 * href="http://java.sun.com/j2se/1.4.2/docs/guide/security/jaas/JAASRefGuide.html">JAAS
243 * Reference</a>
244 */
245 public void setLoginConfig(Resource loginConfig) {
246 this.loginConfig = loginConfig;
247 }
248
249 public Resource getLoginConfig() {
250 return loginConfig;
251 }
252
253 /***
254 * Set the loginContextName, this name is used as the index to the
255 * configuration specified in the loginConfig property.
256 *
257 * @param loginContextName
258 */
259 public void setLoginContextName(String loginContextName) {
260 this.loginContextName = loginContextName;
261 }
262
263 public String getLoginContextName() {
264 return loginContextName;
265 }
266
267 public void setLoginExceptionResolver(
268 LoginExceptionResolver loginExceptionResolver) {
269 this.loginExceptionResolver = loginExceptionResolver;
270 }
271
272 public LoginExceptionResolver getLoginExceptionResolver() {
273 return loginExceptionResolver;
274 }
275
276 public void afterPropertiesSet() throws Exception {
277 Assert.notNull(loginConfig, "loginConfig must be set on " + getClass());
278 Assert.hasLength(loginContextName,
279 "loginContextName must be set on " + getClass());
280
281 configureJaas(loginConfig);
282
283 Assert.notNull(Configuration.getConfiguration(),
284 "As per http://java.sun.com/j2se/1.5.0/docs/api/javax/security/auth/login/Configuration.html \"If a Configuration object was set via the Configuration.setConfiguration method, then that object is returned. Otherwise, a default Configuration object is returned\". Your JRE returned null to Configuration.getConfiguration().");
285 }
286
287 /***
288 * Hook method for configuring Jaas
289 *
290 * @param loginConfigStr URL to Jaas login configuration
291 */
292 protected void configureJaas(Resource loginConfig) throws IOException {
293 configureJaasUsingLoop();
294 }
295
296 /***
297 * Loops through the login.config.url.1,login.config.url.2 properties
298 * looking for the login configuration. If it is not set, it will be set
299 * to the last available login.config.url.X property.
300 */
301 private void configureJaasUsingLoop() throws IOException {
302 String loginConfigUrl = loginConfig.getURL().toString();
303 boolean alreadySet = false;
304
305 int n = 1;
306 String prefix = "login.config.url.";
307 String existing = null;
308
309 while ((existing = Security.getProperty(prefix + n)) != null) {
310 alreadySet = existing.equals(loginConfigUrl);
311
312 if (alreadySet) {
313 break;
314 }
315
316 n++;
317 }
318
319 if (!alreadySet) {
320 String key = prefix + n;
321 log.debug("Setting security property [" + key + "] to: "
322 + loginConfigUrl);
323 Security.setProperty(key, loginConfigUrl);
324 }
325 }
326
327 /***
328 * Attempts to login the user given the Authentication objects principal
329 * and credential
330 *
331 * @param auth The Authentication object to be authenticated.
332 * @return The authenticated Authentication object, with it's
333 * grantedAuthorities set.
334 * @throws AuthenticationException This implementation does not handle
335 * 'locked' or 'disabled' accounts. This method only throws a
336 * AuthenticationServiceException, with the message of the
337 * LoginException that will be thrown, should the
338 * loginContext.login() method fail.
339 */
340 public Authentication authenticate(Authentication auth)
341 throws AuthenticationException {
342 if (auth instanceof UsernamePasswordAuthenticationToken) {
343 UsernamePasswordAuthenticationToken request = (UsernamePasswordAuthenticationToken) auth;
344
345 try {
346
347 LoginContext loginContext = new LoginContext(loginContextName,
348 new InternalCallbackHandler(auth));
349
350
351 loginContext.login();
352
353
354 Set authorities = new HashSet();
355
356 if (request.getAuthorities() != null) {
357 authorities.addAll(Arrays.asList(request.getAuthorities()));
358 }
359
360
361 Set principals = loginContext.getSubject().getPrincipals();
362
363 for (Iterator iterator = principals.iterator();
364 iterator.hasNext();) {
365 Principal principal = (Principal) iterator.next();
366
367 for (int i = 0; i < authorityGranters.length; i++) {
368 AuthorityGranter granter = authorityGranters[i];
369 Set roles = granter.grant(principal);
370
371
372 if ((roles != null) && !roles.isEmpty()) {
373 for (Iterator roleIterator = roles.iterator();
374 roleIterator.hasNext();) {
375 String role = roleIterator.next().toString();
376 authorities.add(new JaasGrantedAuthority(role,
377 principal));
378 }
379 }
380 }
381 }
382
383
384 JaasAuthenticationToken result = new JaasAuthenticationToken(request
385 .getPrincipal(), request.getCredentials(),
386 (GrantedAuthority[]) authorities.toArray(
387 new GrantedAuthority[authorities.size()]), loginContext);
388
389
390 publishSuccessEvent(result);
391
392
393 return result;
394 } catch (LoginException loginException) {
395 AcegiSecurityException ase = loginExceptionResolver
396 .resolveException(loginException);
397
398 publishFailureEvent(request, ase);
399 throw ase;
400 }
401 }
402
403 return null;
404 }
405
406 public boolean supports(Class aClass) {
407 return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
408 }
409
410 public void onApplicationEvent(ApplicationEvent applicationEvent) {
411 if (applicationEvent instanceof HttpSessionDestroyedEvent) {
412 HttpSessionDestroyedEvent event = (HttpSessionDestroyedEvent) applicationEvent;
413 handleLogout(event);
414 }
415 }
416
417 /***
418 * Handles the logout by getting the SecurityContext for the session that was destroyed.
419 * <b>MUST NOT use SecurityContextHolder we are logging out a session that is not related to the current user.</b>
420 * @param event
421 */
422 protected void handleLogout(HttpSessionDestroyedEvent event) {
423 SecurityContext context = (SecurityContext) event.getSession().getAttribute(HttpSessionContextIntegrationFilter.ACEGI_SECURITY_CONTEXT_KEY);
424 Authentication auth = context.getAuthentication();
425 if (auth instanceof JaasAuthenticationToken) {
426 JaasAuthenticationToken token = (JaasAuthenticationToken) auth;
427 try {
428 LoginContext loginContext = token.getLoginContext();
429 if (loginContext != null) {
430 log.debug("Logging principal: [" + token.getPrincipal() + "] out of LoginContext");
431 loginContext.logout();
432 } else {
433 log.debug("Cannot logout principal: [" + token.getPrincipal() + "] from LoginContext. " +
434 "The LoginContext is unavailable");
435 }
436 } catch (LoginException e) {
437 log.warn("Error error logging out of LoginContext", e);
438 }
439 }
440 }
441
442 /***
443 * Publishes the {@link JaasAuthenticationFailedEvent}. Can be overridden
444 * by subclasses for different functionality
445 *
446 * @param token The {@link UsernamePasswordAuthenticationToken} being
447 * processed
448 * @param ase The {@link AcegiSecurityException} that caused the failure
449 */
450 protected void publishFailureEvent(
451 UsernamePasswordAuthenticationToken token, AcegiSecurityException ase) {
452 getApplicationContext().publishEvent(new JaasAuthenticationFailedEvent(
453 token, ase));
454 }
455
456 /***
457 * Publishes the {@link JaasAuthenticationSuccessEvent}. Can be overridden
458 * by subclasses for different functionality.
459 *
460 * @param token The {@link UsernamePasswordAuthenticationToken} being
461 * processed
462 */
463 protected void publishSuccessEvent(
464 UsernamePasswordAuthenticationToken token) {
465 getApplicationContext().publishEvent(new JaasAuthenticationSuccessEvent(
466 token));
467 }
468
469
470
471 /***
472 * Wrapper class for JAASAuthenticationCallbackHandlers
473 */
474 private class InternalCallbackHandler implements CallbackHandler {
475 private Authentication authentication;
476
477 public InternalCallbackHandler(Authentication authentication) {
478 this.authentication = authentication;
479 }
480
481 public void handle(Callback[] callbacks)
482 throws IOException, UnsupportedCallbackException {
483 for (int i = 0; i < callbackHandlers.length; i++) {
484 JaasAuthenticationCallbackHandler handler = callbackHandlers[i];
485
486 for (int j = 0; j < callbacks.length; j++) {
487 Callback callback = callbacks[j];
488
489 handler.handle(callback, authentication);
490 }
491 }
492 }
493 }
494 }