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 }