View Javadoc

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