View Javadoc

1   /* Copyright 2004, 2005 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  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   *  &lt;property name="loginConfig"&gt;
81   *  &lt;value&gt;/WEB-INF/login.conf&lt;/value&gt;
82   *  &lt;/property&gt;
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   *  &lt;property name="loginContextName"&gt;
99   *  &lt;value&gt;JAASTest&lt;/value&gt;
100  *  &lt;/property&gt;
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  *  &lt;property name="callbackHandlers"&gt;
123  *  &lt;list&gt;
124  *  &lt;bean class="org.acegisecurity.providers.jaas.TestCallbackHandler"/&gt;
125  *  &lt;bean class="{@link JaasNameCallbackHandler org.acegisecurity.providers.jaas.JaasNameCallbackHandler}"/&gt;
126  *  &lt;bean class="{@link JaasPasswordCallbackHandler org.acegisecurity.providers.jaas.JaasPasswordCallbackHandler}"/&gt;
127  *  &lt;/list&gt;
128  *  &lt;/property&gt;
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  *  &lt;property name="authorityGranters"&gt;
148  *  &lt;list&gt;
149  *  &lt;bean class="org.acegisecurity.providers.jaas.TestAuthorityGranter"/&gt;
150  *  &lt;/list&gt;
151  *  &lt;/property&gt;
152  *  <p/>
153  *  </pre>
154  * </p>
155  *
156  * A configuration note:
157  * The JaasAuthenticationProvider uses the security properites &quote;login.config.url.X&quote; 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     //~ Static fields/initializers =============================================
166 
167     protected static final Log log = LogFactory.getLog(JaasAuthenticationProvider.class);
168 
169     //~ Instance fields ========================================================
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     //~ Methods ================================================================
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                 //Create the LoginContext object, and pass our InternallCallbackHandler
347                 LoginContext loginContext = new LoginContext(loginContextName,
348                         new InternalCallbackHandler(auth));
349 
350                 //Attempt to login the user, the LoginContext will call our InternalCallbackHandler at this point.
351                 loginContext.login();
352 
353                 //create a set to hold the authorities, and add any that have already been applied.
354                 Set authorities = new HashSet();
355 
356                 if (request.getAuthorities() != null) {
357                     authorities.addAll(Arrays.asList(request.getAuthorities()));
358                 }
359 
360                 //get the subject principals and pass them to each of the AuthorityGranters
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                         //If the granter doesn't wish to grant any authorities, it should return null.
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                 //Convert the authorities set back to an array and apply it to the token.
384                 JaasAuthenticationToken result = new JaasAuthenticationToken(request
385                         .getPrincipal(), request.getCredentials(),
386                         (GrantedAuthority[]) authorities.toArray(
387                                 new GrantedAuthority[authorities.size()]), loginContext);
388 
389                 //Publish the success event
390                 publishSuccessEvent(result);
391 
392                 //we're done, return the token.
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     //~ Inner Classes ==========================================================
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 }