From 58d9903ebcdfa738ecdfefa098df9a965fc4392d Mon Sep 17 00:00:00 2001 From: rwinch Date: Fri, 10 Sep 2010 20:17:22 -0500 Subject: [PATCH] SEC-1564: JAAS Configuration can now be injected into DefaultJaasAuthenticationProvider --- core/core.gradle | 3 +- .../AbstractJaasAuthenticationProvider.java | 365 ++++++++++++++++++ .../DefaultJaasAuthenticationProvider.java | 123 ++++++ .../jaas/JaasAuthenticationProvider.java | 260 +------------ .../jaas/memory/InMemoryConfiguration.java | 102 +++++ .../jaas/memory/package-info.java | 4 + ...efaultJaasAuthenticationProviderTests.java | 273 +++++++++++++ .../memory/InMemoryConfigurationTests.java | 98 +++++ ...DefaultJaasAuthenticationProviderTests.xml | 41 ++ .../manual/src/docbook/jaas-auth-provider.xml | 258 ++++++++----- docs/manual/src/docbook/samples.xml | 6 + samples/jaas/jaas.gradle | 25 ++ .../jaas/RoleUserAuthorityGranter.java | 34 ++ .../UsernameEqualsPasswordLoginModule.java | 85 ++++ .../resources/applicationContext-security.xml | 52 +++ samples/jaas/src/main/webapp/WEB-INF/web.xml | 63 +++ samples/jaas/src/main/webapp/index.jsp | 18 + samples/jaas/src/main/webapp/secure/index.jsp | 40 ++ settings.gradle | 3 +- 19 files changed, 1521 insertions(+), 332 deletions(-) create mode 100644 core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java create mode 100644 core/src/main/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProvider.java create mode 100644 core/src/main/java/org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.java create mode 100644 core/src/main/java/org/springframework/security/authentication/jaas/memory/package-info.java create mode 100644 core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java create mode 100644 core/src/test/java/org/springframework/security/authentication/jaas/memory/InMemoryConfigurationTests.java create mode 100644 core/src/test/resources/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.xml create mode 100644 samples/jaas/jaas.gradle create mode 100644 samples/jaas/src/main/java/samples/jaas/RoleUserAuthorityGranter.java create mode 100644 samples/jaas/src/main/java/samples/jaas/UsernameEqualsPasswordLoginModule.java create mode 100644 samples/jaas/src/main/resources/applicationContext-security.xml create mode 100644 samples/jaas/src/main/webapp/WEB-INF/web.xml create mode 100644 samples/jaas/src/main/webapp/index.jsp create mode 100644 samples/jaas/src/main/webapp/secure/index.jsp diff --git a/core/core.gradle b/core/core.gradle index 4b3bd7db78..b9aa212458 100644 --- a/core/core.gradle +++ b/core/core.gradle @@ -14,7 +14,8 @@ dependencies { 'javax.annotation:jsr250-api:1.0' testCompile 'commons-collections:commons-collections:3.2', - "org.springframework:spring-test:$springVersion" + "org.springframework:spring-test:$springVersion", + "ch.qos.logback:logback-classic:$logbackVersion" testRuntime "hsqldb:hsqldb:$hsqlVersion" } diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java new file mode 100644 index 0000000000..01a63a20b9 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java @@ -0,0 +1,365 @@ +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.authentication.jaas; + +import java.io.IOException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.jaas.event.JaasAuthenticationFailedEvent; +import org.springframework.security.authentication.jaas.event.JaasAuthenticationSuccessEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.session.SessionDestroyedEvent; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * An {@link AuthenticationProvider} implementation that retrieves user details from a JAAS login configuration. + * + *

This AuthenticationProvider is capable of validating {@link + * org.springframework.security.authentication.UsernamePasswordAuthenticationToken} requests contain the correct username and + * password.

+ *

This implementation is backed by a JAAS configuration that is provided by + * a subclass's implementation of {@link #createLoginContext(CallbackHandler)}. + * + *

When using JAAS login modules as the authentication source, sometimes the + * LoginContext will + * require CallbackHandlers. The AbstractJaasAuthenticationProvider uses an internal + * CallbackHandler + * to wrap the {@link JaasAuthenticationCallbackHandler}s configured in the ApplicationContext. + * When the LoginContext calls the internal CallbackHandler, control is passed to each + * {@link JaasAuthenticationCallbackHandler} for each Callback passed. + *

+ *

{@link JaasAuthenticationCallbackHandler}s are passed to the AbstractJaasAuthenticationProvider through the {@link + * #setCallbackHandlers(org.springframework.security.authentication.jaas.JaasAuthenticationCallbackHandler[]) callbackHandlers} + * property. + *

+ * <property name="callbackHandlers">
+ *   <list>
+ *     <bean class="org.springframework.security.authentication.jaas.TestCallbackHandler"/>
+ *     <bean class="{@link JaasNameCallbackHandler org.springframework.security.authentication.jaas.JaasNameCallbackHandler}"/>
+ *     <bean class="{@link JaasPasswordCallbackHandler org.springframework.security.authentication.jaas.JaasPasswordCallbackHandler}"/>
+ *  </list>
+ * </property>
+ * 
+ *

+ *

+ * After calling LoginContext.login(), the AbstractJaasAuthenticationProvider will retrieve the returned Principals + * from the Subject (LoginContext.getSubject().getPrincipals). Each returned principal is then passed to the + * configured {@link AuthorityGranter}s. An AuthorityGranter is a mapping between a returned Principal, and a role + * name. If an AuthorityGranter wishes to grant an Authorization a role, it returns that role name from it's {@link + * AuthorityGranter#grant(java.security.Principal)} method. The returned role will be applied to the Authorization + * object as a {@link GrantedAuthority}.

+ *

AuthorityGranters are configured in spring xml as follows... + *

+ * <property name="authorityGranters">
+ *   <list>
+ *     <bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
+ *   </list>
+ *  </property>
+ * 
+ * + * @author Ray Krueger + * @author Rob Winch + */ +public abstract class AbstractJaasAuthenticationProvider implements AuthenticationProvider, +ApplicationEventPublisherAware, InitializingBean, ApplicationListener { + //~ Instance fields ================================================================================================ + + private ApplicationEventPublisher applicationEventPublisher; + private AuthorityGranter[] authorityGranters; + private JaasAuthenticationCallbackHandler[] callbackHandlers; + protected final Log log = LogFactory.getLog(getClass()); + private LoginExceptionResolver loginExceptionResolver = new DefaultLoginExceptionResolver(); + private String loginContextName = "SPRINGSECURITY"; + + //~ Methods ======================================================================================================== + + /** + * Validates the required properties are set. In addition, if + * {@link #setCallbackHandlers(JaasAuthenticationCallbackHandler[])} has not + * been called with valid handlers, initializes to use + * {@link JaasNameCallbackHandler} and {@link JaasPasswordCallbackHandler}. + */ + public void afterPropertiesSet() throws Exception { + Assert.hasLength(loginContextName, "loginContextName cannot be null or empty"); + Assert.notEmpty(authorityGranters, "authorityGranters cannot be null or empty"); + if (ObjectUtils.isEmpty(callbackHandlers)) { + setCallbackHandlers(new JaasAuthenticationCallbackHandler[] { new JaasNameCallbackHandler(), + new JaasPasswordCallbackHandler() }); + } + Assert.notNull(loginExceptionResolver, "loginExceptionResolver cannot be null"); + } + + /** + * Attempts to login the user given the Authentication objects principal and credential + * + * @param auth The Authentication object to be authenticated. + * + * @return The authenticated Authentication object, with it's grantedAuthorities set. + * + * @throws AuthenticationException This implementation does not handle 'locked' or 'disabled' accounts. This method + * only throws a AuthenticationServiceException, with the message of the LoginException that will be + * thrown, should the loginContext.login() method fail. + */ + public Authentication authenticate(Authentication auth) throws AuthenticationException { + if (!(auth instanceof UsernamePasswordAuthenticationToken)) { + return null; + } + + UsernamePasswordAuthenticationToken request = (UsernamePasswordAuthenticationToken) auth; + Set authorities; + + try { + // Create the LoginContext object, and pass our InternallCallbackHandler + LoginContext loginContext = createLoginContext(new InternalCallbackHandler(auth)); + + // Attempt to login the user, the LoginContext will call our InternalCallbackHandler at this point. + loginContext.login(); + + // Create a set to hold the authorities, and add any that have already been applied. + authorities = new HashSet(); + authorities.addAll(request.getAuthorities()); + + // Get the subject principals and pass them to each of the AuthorityGranters + Set principals = loginContext.getSubject().getPrincipals(); + + for (Principal principal : principals) { + for (AuthorityGranter granter : authorityGranters) { + Set roles = granter.grant(principal); + + // If the granter doesn't wish to grant any authorities, it should return null. + if ((roles != null) && !roles.isEmpty()) { + for (String role : roles) { + authorities.add(new JaasGrantedAuthority(role, principal)); + } + } + } + } + + //Convert the authorities set back to an array and apply it to the token. + JaasAuthenticationToken result = new JaasAuthenticationToken(request.getPrincipal(), + request.getCredentials(), new ArrayList(authorities), loginContext); + + //Publish the success event + publishSuccessEvent(result); + + //we're done, return the token. + return result; + + } catch (LoginException loginException) { + AuthenticationException ase = loginExceptionResolver.resolveException(loginException); + + publishFailureEvent(request, ase); + throw ase; + } + } + + /** + * Creates the LoginContext to be used for authentication. + * + * @param handler The CallbackHandler that should be used for the LoginContext (never null). + * @return the LoginContext to use for authentication. + * @throws LoginException + */ + protected abstract LoginContext createLoginContext(CallbackHandler handler) throws LoginException; + + /** + * Handles the logout by getting the SecurityContext for the session that was destroyed. MUST NOT use + * SecurityContextHolder as we are logging out a session that is not related to the current user. + * + * @param event + */ + protected void handleLogout(SessionDestroyedEvent event) { + SecurityContext context = event.getSecurityContext(); + + if (context == null) { + log.debug("The destroyed session has no SecurityContext"); + + return; + } + + Authentication auth = context.getAuthentication(); + + if ((auth != null) && (auth instanceof JaasAuthenticationToken)) { + JaasAuthenticationToken token = (JaasAuthenticationToken) auth; + + try { + LoginContext loginContext = token.getLoginContext(); + boolean debug = log.isDebugEnabled(); + if (loginContext != null) { + if (debug) { + log.debug("Logging principal: [" + token.getPrincipal() + "] out of LoginContext"); + } + loginContext.logout(); + } else if (debug) { + log.debug("Cannot logout principal: [" + token.getPrincipal() + "] from LoginContext. " + + "The LoginContext is unavailable"); + } + } catch (LoginException e) { + log.warn("Error error logging out of LoginContext", e); + } + } + } + + public void onApplicationEvent(SessionDestroyedEvent event) { + handleLogout(event); + } + + /** + * Publishes the {@link JaasAuthenticationFailedEvent}. Can be overridden by subclasses for different + * functionality + * + * @param token The authentication token being processed + * @param ase The excetion that caused the authentication failure + */ + protected void publishFailureEvent(UsernamePasswordAuthenticationToken token, AuthenticationException ase) { + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new JaasAuthenticationFailedEvent(token, ase)); + } + } + + /** + * Publishes the {@link JaasAuthenticationSuccessEvent}. Can be overridden by subclasses for different + * functionality. + * + * @param token The token being processed + */ + protected void publishSuccessEvent(UsernamePasswordAuthenticationToken token) { + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new JaasAuthenticationSuccessEvent(token)); + } + } + + /** + * Returns the AuthorityGrannter array that was passed to the {@link + * #setAuthorityGranters(AuthorityGranter[])} method, or null if it none were ever set. + * + * @return The AuthorityGranter array, or null + * + * @see #setAuthorityGranters(AuthorityGranter[]) + */ + AuthorityGranter[] getAuthorityGranters() { + return authorityGranters; + } + + /** + * Set the AuthorityGranters that should be consulted for role names to be granted to the Authentication. + * + * @param authorityGranters AuthorityGranter array + * + * @see JaasAuthenticationProvider + */ + public void setAuthorityGranters(AuthorityGranter[] authorityGranters) { + this.authorityGranters = authorityGranters; + } + + /** + * Returns the current JaasAuthenticationCallbackHandler array, or null if none are set. + * + * @return the JAASAuthenticationCallbackHandlers. + * + * @see #setCallbackHandlers(JaasAuthenticationCallbackHandler[]) + */ + JaasAuthenticationCallbackHandler[] getCallbackHandlers() { + return callbackHandlers; + } + + /** + * Set the JAASAuthentcationCallbackHandler array to handle callback objects generated by the + * LoginContext.login method. + * + * @param callbackHandlers Array of JAASAuthenticationCallbackHandlers + */ + public void setCallbackHandlers(JaasAuthenticationCallbackHandler[] callbackHandlers) { + this.callbackHandlers = callbackHandlers; + } + + String getLoginContextName() { + return loginContextName; + } + + /** + * Set the loginContextName, this name is used as the index to the configuration specified in the + * loginConfig property. + * + * @param loginContextName + */ + public void setLoginContextName(String loginContextName) { + this.loginContextName = loginContextName; + } + + LoginExceptionResolver getLoginExceptionResolver() { + return loginExceptionResolver; + } + + public void setLoginExceptionResolver(LoginExceptionResolver loginExceptionResolver) { + this.loginExceptionResolver = loginExceptionResolver; + } + + public boolean supports(Class aClass) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass); + } + + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + protected ApplicationEventPublisher getApplicationEventPublisher() { + return applicationEventPublisher; + } + + //~ Inner Classes ================================================================================================== + + /** + * Wrapper class for JAASAuthenticationCallbackHandlers + */ + private class InternalCallbackHandler implements CallbackHandler { + private final Authentication authentication; + + public InternalCallbackHandler(Authentication authentication) { + this.authentication = authentication; + } + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (JaasAuthenticationCallbackHandler handler : callbackHandlers) { + for (Callback callback : callbacks) { + handler.handle(callback, authentication); + } + } + } + } +} diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProvider.java new file mode 100644 index 0000000000..e0f6d45013 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProvider.java @@ -0,0 +1,123 @@ +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.authentication.jaas; + +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.springframework.security.authentication.jaas.memory.InMemoryConfiguration; +import org.springframework.util.Assert; + +/** + *

+ * Creates a LoginContext using the Configuration provided to it. This allows + * the configuration to be injected regardless of the value of + * {@link Configuration#getConfiguration()}. + *

+ *

+ * While not bound to any particular Configuration implementation, an in memory version of a JAAS + * configuration can be represented using {@link InMemoryConfiguration}. + *

+ *

+ * The following JAAS configuration: + *

+ * + *
+ * SPRINGSECURITY {
+ *    sample.SampleLoginModule required;
+ *  };
+ * 
+ * + *

+ * Can be represented as follows: + *

+ * + *
+ * <bean id="jaasAuthProvider" class="org.springframework.security.authentication.jaas.DefaultJaasAuthenticationProvider">
+ *   <property name="configuration">
+ *     <bean class="org.springframework.security.authentication.jaas.memory.InMemoryConfiguration">
+ *       <constructor-arg>
+ *         <map>
+ *           <!-- SPRINGSECURITY is the default loginContextName for AbstractJaasAuthenticationProvider-->
+ *           <entry key="SPRINGSECURITY">
+ *             <array>
+ *               <bean class="javax.security.auth.login.AppConfigurationEntry">
+ *                 <constructor-arg value="sample.SampleLoginModule" />
+ *                 <constructor-arg>
+ *                   <util:constant static-field="javax.security.auth.login.AppConfigurationEntry$LoginModuleControlFlag.REQUIRED" />
+ *                 </constructor-arg>
+ *                 <constructor-arg>
+ *                   <map></map>
+ *                 </constructor-arg>
+ *               </bean>
+ *             </array>
+ *           </entry>
+ *         </map>
+ *       </constructor-arg>
+ *     </bean>
+ *   </property>
+ *   <property name="authorityGranters">
+ *     <list>
+ *       <!-- You will need to write your own implementation of AuthorityGranter -->
+ *       <bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
+ *     </list>
+ *   </property>
+ * </bean>
+ * 
+ * + * @author Rob Winch + * @see AbstractJaasAuthenticationProvider + * @see InMemoryConfiguration + */ +public class DefaultJaasAuthenticationProvider extends AbstractJaasAuthenticationProvider { + //~ Instance fields ================================================================================================ + + private Configuration configuration; + + //~ Methods ======================================================================================================== + + @Override + public void afterPropertiesSet() throws Exception { + super.afterPropertiesSet(); + Assert.notNull(configuration, "configuration cannot be null."); + } + + /** + * Creates a LoginContext using the Configuration that was specified in + * {@link #setConfiguration(Configuration)}. + */ + @Override + protected LoginContext createLoginContext(CallbackHandler handler) throws LoginException { + return new LoginContext(getLoginContextName(), null, handler, getConfiguration()); + } + + protected Configuration getConfiguration() { + return configuration; + } + + /** + * Sets the Configuration to use for Authentication. + * + * @param configuration + * the Configuration that is used when + * {@link #createLoginContext(CallbackHandler)} is called. + */ + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } +} diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationProvider.java index 1323d6dbd0..88a11dee23 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationProvider.java @@ -18,35 +18,21 @@ package org.springframework.security.authentication.jaas; import java.io.File; import java.io.IOException; import java.net.URL; -import java.security.Principal; import java.security.Security; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; -import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.context.ApplicationListener; import org.springframework.core.io.Resource; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.jaas.event.JaasAuthenticationFailedEvent; -import org.springframework.security.authentication.jaas.event.JaasAuthenticationSuccessEvent; -import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.session.SessionDestroyedEvent; import org.springframework.util.Assert; @@ -125,100 +111,38 @@ import org.springframework.util.Assert; *

* * @author Ray Krueger + * @author Rob Winch */ -public class JaasAuthenticationProvider implements AuthenticationProvider, ApplicationEventPublisherAware, - InitializingBean, ApplicationListener { +public class JaasAuthenticationProvider extends AbstractJaasAuthenticationProvider { //~ Static fields/initializers ===================================================================================== + // exists for passivity protected static final Log log = LogFactory.getLog(JaasAuthenticationProvider.class); //~ Instance fields ================================================================================================ - private LoginExceptionResolver loginExceptionResolver = new DefaultLoginExceptionResolver(); private Resource loginConfig; - private String loginContextName = "SPRINGSECURITY"; - private AuthorityGranter[] authorityGranters; - private JaasAuthenticationCallbackHandler[] callbackHandlers; - private ApplicationEventPublisher applicationEventPublisher; private boolean refreshConfigurationOnStartup = true; //~ Methods ======================================================================================================== public void afterPropertiesSet() throws Exception { + // the superclass is not called because it does additional checks that are non-passive + Assert.hasLength(getLoginContextName(), "loginContextName must be set on " + getClass()); Assert.notNull(loginConfig, "loginConfig must be set on " + getClass()); - Assert.hasLength(loginContextName, "loginContextName must be set on " + getClass()); - configureJaas(loginConfig); - Assert.notNull(Configuration.getConfiguration(), - "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()."); + Assert.notNull( + Configuration.getConfiguration(), + "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()."); } - /** - * Attempts to login the user given the Authentication objects principal and credential - * - * @param auth The Authentication object to be authenticated. - * - * @return The authenticated Authentication object, with it's grantedAuthorities set. - * - * @throws AuthenticationException This implementation does not handle 'locked' or 'disabled' accounts. This method - * only throws a AuthenticationServiceException, with the message of the LoginException that will be - * thrown, should the loginContext.login() method fail. - */ - public Authentication authenticate(Authentication auth) throws AuthenticationException { - if (!(auth instanceof UsernamePasswordAuthenticationToken)) { - return null; - } - - UsernamePasswordAuthenticationToken request = (UsernamePasswordAuthenticationToken) auth; - Set authorities; - - try { - // Create the LoginContext object, and pass our InternallCallbackHandler - LoginContext loginContext = new LoginContext(loginContextName, new InternalCallbackHandler(auth)); - - // Attempt to login the user, the LoginContext will call our InternalCallbackHandler at this point. - loginContext.login(); - - // Create a set to hold the authorities, and add any that have already been applied. - authorities = new HashSet(); - authorities.addAll(request.getAuthorities()); - - // Get the subject principals and pass them to each of the AuthorityGranters - Set principals = loginContext.getSubject().getPrincipals(); - - for (Principal principal : principals) { - for (AuthorityGranter granter : authorityGranters) { - Set roles = granter.grant(principal); - - // If the granter doesn't wish to grant any authorities, it should return null. - if ((roles != null) && !roles.isEmpty()) { - for (String role : roles) { - authorities.add(new JaasGrantedAuthority(role, principal)); - } - } - } - } - - //Convert the authorities set back to an array and apply it to the token. - JaasAuthenticationToken result = new JaasAuthenticationToken(request.getPrincipal(), - request.getCredentials(), new ArrayList(authorities), loginContext); - - //Publish the success event - publishSuccessEvent(result); - - //we're done, return the token. - return result; - - } catch (LoginException loginException) { - AuthenticationException ase = loginExceptionResolver.resolveException(loginException); - - publishFailureEvent(request, ase); - throw ase; - } + @Override + protected LoginContext createLoginContext(CallbackHandler handler) throws LoginException { + return new LoginContext(getLoginContextName(), handler); } /** @@ -277,47 +201,7 @@ public class JaasAuthenticationProvider implements AuthenticationProvider, Appli return new URL("file", "", loginConfigPath).toString(); } - - /** - * Handles the logout by getting the SecurityContext for the session that was destroyed. MUST NOT use - * SecurityContextHolder as we are logging out a session that is not related to the current user. - * - * @param event - */ - protected void handleLogout(SessionDestroyedEvent event) { - SecurityContext context = event.getSecurityContext(); - - if (context == null) { - log.debug("The destroyed session has no SecurityContext"); - - return; - } - - Authentication auth = context.getAuthentication(); - - if ((auth != null) && (auth instanceof JaasAuthenticationToken)) { - JaasAuthenticationToken token = (JaasAuthenticationToken) auth; - - try { - LoginContext loginContext = token.getLoginContext(); - - if (loginContext != null) { - log.debug("Logging principal: [" + token.getPrincipal() + "] out of LoginContext"); - loginContext.logout(); - } else { - log.debug("Cannot logout principal: [" + token.getPrincipal() + "] from LoginContext. " - + "The LoginContext is unavailable"); - } - } catch (LoginException e) { - log.warn("Error error logging out of LoginContext", e); - } - } - } - - public void onApplicationEvent(SessionDestroyedEvent event) { - handleLogout(event); - } - + /** * Publishes the {@link JaasAuthenticationFailedEvent}. Can be overridden by subclasses for different * functionality @@ -326,63 +210,8 @@ public class JaasAuthenticationProvider implements AuthenticationProvider, Appli * @param ase The excetion that caused the authentication failure */ protected void publishFailureEvent(UsernamePasswordAuthenticationToken token, AuthenticationException ase) { - applicationEventPublisher.publishEvent(new JaasAuthenticationFailedEvent(token, ase)); - } - - /** - * Publishes the {@link JaasAuthenticationSuccessEvent}. Can be overridden by subclasses for different - * functionality. - * - * @param token The token being processed - */ - protected void publishSuccessEvent(UsernamePasswordAuthenticationToken token) { - if (applicationEventPublisher != null) { - applicationEventPublisher.publishEvent(new JaasAuthenticationSuccessEvent(token)); - } - } - - /** - * Returns the AuthorityGrannter array that was passed to the {@link - * #setAuthorityGranters(AuthorityGranter[])} method, or null if it none were ever set. - * - * @return The AuthorityGranter array, or null - * - * @see #setAuthorityGranters(AuthorityGranter[]) - */ - AuthorityGranter[] getAuthorityGranters() { - return authorityGranters; - } - - /** - * Set the AuthorityGranters that should be consulted for role names to be granted to the Authentication. - * - * @param authorityGranters AuthorityGranter array - * - * @see JaasAuthenticationProvider - */ - public void setAuthorityGranters(AuthorityGranter[] authorityGranters) { - this.authorityGranters = authorityGranters; - } - - /** - * Returns the current JaasAuthenticationCallbackHandler array, or null if none are set. - * - * @return the JAASAuthenticationCallbackHandlers. - * - * @see #setCallbackHandlers(JaasAuthenticationCallbackHandler[]) - */ - JaasAuthenticationCallbackHandler[] getCallbackHandlers() { - return callbackHandlers; - } - - /** - * Set the JAASAuthentcationCallbackHandler array to handle callback objects generated by the - * LoginContext.login method. - * - * @param callbackHandlers Array of JAASAuthenticationCallbackHandlers - */ - public void setCallbackHandlers(JaasAuthenticationCallbackHandler[] callbackHandlers) { - this.callbackHandlers = callbackHandlers; + // exists for passivity (the superclass does a null check before publishing) + getApplicationEventPublisher().publishEvent(new JaasAuthenticationFailedEvent(token, ase)); } public Resource getLoginConfig() { @@ -400,28 +229,6 @@ public class JaasAuthenticationProvider implements AuthenticationProvider, Appli this.loginConfig = loginConfig; } - String getLoginContextName() { - return loginContextName; - } - - /** - * Set the loginContextName, this name is used as the index to the configuration specified in the - * loginConfig property. - * - * @param loginContextName - */ - public void setLoginContextName(String loginContextName) { - this.loginContextName = loginContextName; - } - - LoginExceptionResolver getLoginExceptionResolver() { - return loginExceptionResolver; - } - - public void setLoginExceptionResolver(LoginExceptionResolver loginExceptionResolver) { - this.loginExceptionResolver = loginExceptionResolver; - } - /** * If set, a call to {@code Configuration#refresh()} will be made by {@code #configureJaas(Resource) } * method. Defaults to {@code true}. @@ -434,37 +241,4 @@ public class JaasAuthenticationProvider implements AuthenticationProvider, Appli public void setRefreshConfigurationOnStartup(boolean refresh) { this.refreshConfigurationOnStartup = refresh; } - - public boolean supports(Class aClass) { - return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass); - } - - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - this.applicationEventPublisher = applicationEventPublisher; - } - - protected ApplicationEventPublisher getApplicationEventPublisher() { - return applicationEventPublisher; - } - - //~ Inner Classes ================================================================================================== - - /** - * Wrapper class for JAASAuthenticationCallbackHandlers - */ - private class InternalCallbackHandler implements CallbackHandler { - private final Authentication authentication; - - public InternalCallbackHandler(Authentication authentication) { - this.authentication = authentication; - } - - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - for (JaasAuthenticationCallbackHandler handler : callbackHandlers) { - for (Callback callback : callbacks) { - handler.handle(callback, authentication); - } - } - } - } } diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.java b/core/src/main/java/org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.java new file mode 100644 index 0000000000..2746364e18 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.java @@ -0,0 +1,102 @@ +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.authentication.jaas.memory; + +import java.util.Collections; +import java.util.Map; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +import org.springframework.util.Assert; + +/** + *

+ * An in memory representation of a JAAS configuration. The constructor accepts + * a Map where the key represents the name of the login context name and the + * value is an Array of {@link AppConfigurationEntry} for that login context + * name. A default Array of {@link AppConfigurationEntry}s can be specified + * which will be returned if a login context is specified which is undefined. + *

+ * + * @author Rob Winch + */ +public class InMemoryConfiguration extends Configuration { + //~ Instance fields ================================================================================================ + + private final AppConfigurationEntry[] defaultConfiguration; + private final Map mappedConfigurations; + + //~ Constructors =================================================================================================== + + /** + * Creates a new instance with only a defaultConfiguration. Any + * configuration name will result in defaultConfiguration being returned. + * + * @param defaultConfiguration + * The result for any calls to + * {@link #getAppConfigurationEntry(String)}. Can be + * null. + */ + public InMemoryConfiguration(AppConfigurationEntry[] defaultConfiguration) { + this(Collections. emptyMap(), defaultConfiguration); + } + + /** + * Creates a new instance with a mapping of login context name to an array + * of {@link AppConfigurationEntry}s. + * + * @param mappedConfigurations + * each key represents a login context name and each value is an + * Array of {@link AppConfigurationEntry}s that should be used. + */ + public InMemoryConfiguration(Map mappedConfigurations) { + this(mappedConfigurations, null); + } + + /** + * Creates a new instance with a mapping of login context name to an array + * of {@link AppConfigurationEntry}s along with a default configuration that + * will be used if no mapping is found for the given login context name. + * + * @param mappedConfigurations + * each key represents a login context name and each value is an + * Array of {@link AppConfigurationEntry}s that should be used. + * @param defaultConfiguration The result for any calls to + * {@link #getAppConfigurationEntry(String)}. Can be + * null. + */ + public InMemoryConfiguration(Map mappedConfigurations, + AppConfigurationEntry[] defaultConfiguration) { + Assert.notNull(mappedConfigurations, "mappedConfigurations cannot be null."); + this.mappedConfigurations = mappedConfigurations; + this.defaultConfiguration = defaultConfiguration; + } + + //~ Methods ======================================================================================================== + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + AppConfigurationEntry[] mappedResult = mappedConfigurations.get(name); + return mappedResult == null ? defaultConfiguration : mappedResult; + } + + /** + * Does nothing, but required for JDK5 + */ + public void refresh() { + } +} \ No newline at end of file diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/memory/package-info.java b/core/src/main/java/org/springframework/security/authentication/jaas/memory/package-info.java new file mode 100644 index 0000000000..fd08f58c5a --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/jaas/memory/package-info.java @@ -0,0 +1,4 @@ +/** + * An in memory JAAS implementation. + */ +package org.springframework.security.authentication.jaas.memory; diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java new file mode 100644 index 0000000000..3effeb7621 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.authentication.jaas; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.Collections; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.apache.commons.logging.Log; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.jaas.DefaultJaasAuthenticationProvider; +import org.springframework.security.authentication.jaas.event.JaasAuthenticationFailedEvent; +import org.springframework.security.authentication.jaas.event.JaasAuthenticationSuccessEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.session.SessionDestroyedEvent; +import org.springframework.test.util.ReflectionTestUtils; + +public class DefaultJaasAuthenticationProviderTests { + private DefaultJaasAuthenticationProvider provider; + private UsernamePasswordAuthenticationToken token; + private ApplicationEventPublisher publisher; + private Log log; + + @Before + public void setUp() throws Exception { + Configuration configuration = mock(Configuration.class); + publisher = mock(ApplicationEventPublisher.class); + log = mock(Log.class); + provider = new DefaultJaasAuthenticationProvider(); + provider.setConfiguration(configuration); + provider.setApplicationEventPublisher(publisher); + provider.setAuthorityGranters(new AuthorityGranter[] { new TestAuthorityGranter() }); + provider.afterPropertiesSet(); + AppConfigurationEntry[] aces = new AppConfigurationEntry[] { new AppConfigurationEntry( + TestLoginModule.class.getName(), LoginModuleControlFlag.REQUIRED, + Collections. emptyMap()) }; + when(configuration.getAppConfigurationEntry(provider.getLoginContextName())).thenReturn(aces); + token = new UsernamePasswordAuthenticationToken("user", "password"); + ReflectionTestUtils.setField(provider, "log", log); + + } + + @Test(expected = IllegalArgumentException.class) + public void afterPropertiesSetNullConfiguration() throws Exception { + provider.setConfiguration(null); + provider.afterPropertiesSet(); + } + + @Test(expected = IllegalArgumentException.class) + public void afterPropertiesSetNullAuthorityGranters() throws Exception { + provider.setAuthorityGranters(null); + provider.afterPropertiesSet(); + } + + @Test + public void authenticateUnsupportedAuthentication() { + assertEquals(null, provider.authenticate(new TestingAuthenticationToken("user", "password"))); + } + + @Test + public void authenticateSuccess() throws Exception { + Authentication auth = provider.authenticate(token); + assertEquals(token.getPrincipal(), auth.getPrincipal()); + assertEquals(token.getCredentials(), auth.getCredentials()); + assertEquals(true, auth.isAuthenticated()); + assertEquals(false, auth.getAuthorities().isEmpty()); + verify(publisher).publishEvent(isA(JaasAuthenticationSuccessEvent.class)); + verifyNoMoreInteractions(publisher); + } + + @Test + public void authenticateBadPassword() { + try { + provider.authenticate(new UsernamePasswordAuthenticationToken("user", "asdf")); + fail("LoginException should have been thrown for the bad password"); + } catch (AuthenticationException success) { + } + + verifyFailedLogin(); + } + + @Test + public void authenticateBadUser() { + try { + provider.authenticate(new UsernamePasswordAuthenticationToken("asdf", "password")); + fail("LoginException should have been thrown for the bad user"); + } catch (AuthenticationException success) { + } + + verifyFailedLogin(); + } + + @Test + public void logout() throws Exception { + SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); + SecurityContext securityContext = mock(SecurityContext.class); + JaasAuthenticationToken token = mock(JaasAuthenticationToken.class); + LoginContext context = mock(LoginContext.class); + + when(event.getSecurityContext()).thenReturn(securityContext); + when(securityContext.getAuthentication()).thenReturn(token); + when(token.getLoginContext()).thenReturn(context); + + provider.onApplicationEvent(event); + + verify(event).getSecurityContext(); + verify(securityContext).getAuthentication(); + verify(token).getLoginContext(); + verify(context).logout(); + verifyNoMoreInteractions(event, securityContext, token, context); + } + + @Test + public void logoutNullSession() { + SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); + + provider.handleLogout(event); + + verify(event).getSecurityContext(); + verify(log).debug(anyString()); + verifyNoMoreInteractions(event); + } + + @Test + public void logoutNullAuthentication() { + SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); + SecurityContext securityContext = mock(SecurityContext.class); + + when(event.getSecurityContext()).thenReturn(securityContext); + + provider.handleLogout(event); + + verify(event).getSecurityContext(); + verify(event).getSecurityContext(); + verify(securityContext).getAuthentication(); + verifyNoMoreInteractions(event, securityContext); + } + + @Test + public void logoutNonJaasAuthentication() { + SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); + SecurityContext securityContext = mock(SecurityContext.class); + + when(event.getSecurityContext()).thenReturn(securityContext); + when(securityContext.getAuthentication()).thenReturn(token); + + provider.handleLogout(event); + + verify(event).getSecurityContext(); + verify(event).getSecurityContext(); + verify(securityContext).getAuthentication(); + verifyNoMoreInteractions(event, securityContext); + } + + @Test + public void logoutNullLoginContext() throws Exception { + SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); + SecurityContext securityContext = mock(SecurityContext.class); + JaasAuthenticationToken token = mock(JaasAuthenticationToken.class); + + when(event.getSecurityContext()).thenReturn(securityContext); + when(securityContext.getAuthentication()).thenReturn(token); + + provider.onApplicationEvent(event); + verify(event).getSecurityContext(); + verify(securityContext).getAuthentication(); + verify(token).getLoginContext(); + + verifyNoMoreInteractions(event, securityContext, token); + } + + @Test + public void logoutLoginException() throws Exception { + SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); + SecurityContext securityContext = mock(SecurityContext.class); + JaasAuthenticationToken token = mock(JaasAuthenticationToken.class); + LoginContext context = mock(LoginContext.class); + LoginException loginException = new LoginException("Failed Login"); + + when(event.getSecurityContext()).thenReturn(securityContext); + when(securityContext.getAuthentication()).thenReturn(token); + when(token.getLoginContext()).thenReturn(context); + doThrow(loginException).when(context).logout(); + + provider.onApplicationEvent(event); + + verify(event).getSecurityContext(); + verify(securityContext).getAuthentication(); + verify(token).getLoginContext(); + verify(context).logout(); + verify(log).warn(anyString(), eq(loginException)); + verifyNoMoreInteractions(event, securityContext, token, context); + } + + @Test + public void publishNullPublisher() { + provider.setApplicationEventPublisher(null); + AuthenticationException ae = new BadCredentialsException("Failed to login", token); + + provider.publishFailureEvent(token, ae); + provider.publishSuccessEvent(token); + } + + @Test + public void javadocExample() { + String resName = "/" + getClass().getName().replace('.', '/') + ".xml"; + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(resName); + context.registerShutdownHook(); + try { + provider = context.getBean(DefaultJaasAuthenticationProvider.class); + Authentication auth = provider.authenticate(token); + assertEquals(true, auth.isAuthenticated()); + assertEquals(token.getPrincipal(), auth.getPrincipal()); + } finally { + context.close(); + } + } + + private void verifyFailedLogin() { + verify(publisher).publishEvent(argThat(new BaseMatcher() { + public void describeTo(Description desc) { + desc.appendText("isA(org.springframework.security.authentication.jaas.event.JaasAuthenticationFailedEvent)"); + desc.appendText(" && event.getException() != null"); + } + + public boolean matches(Object arg) { + JaasAuthenticationFailedEvent e = (JaasAuthenticationFailedEvent) arg; + return e.getException() != null; + } + + })); + verifyNoMoreInteractions(publisher); + } +} diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/memory/InMemoryConfigurationTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/memory/InMemoryConfigurationTests.java new file mode 100644 index 0000000000..02688d7eb1 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/jaas/memory/InMemoryConfigurationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.authentication.jaas.memory; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.authentication.jaas.TestLoginModule; + +/** + * Tests {@link InMemoryConfiguration}. + * + * @author Rob Winch + */ +public class InMemoryConfigurationTests { + + private AppConfigurationEntry[] defaultEntries; + private Map mappedEntries; + + @Before + public void setUp() { + defaultEntries = new AppConfigurationEntry[] { new AppConfigurationEntry(TestLoginModule.class.getName(), + LoginModuleControlFlag.REQUIRED, Collections. emptyMap()) }; + + mappedEntries = Collections. singletonMap("name", + new AppConfigurationEntry[] { new AppConfigurationEntry(TestLoginModule.class.getName(), + LoginModuleControlFlag.OPTIONAL, Collections. emptyMap()) }); + } + + @Test + public void constructorNullDefault() { + assertNull(new InMemoryConfiguration((AppConfigurationEntry[]) null).getAppConfigurationEntry("name")); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullMapped() { + new InMemoryConfiguration((Map) null); + } + + @Test + public void constructorEmptyMap() { + assertNull(new InMemoryConfiguration(Collections. emptyMap()) + .getAppConfigurationEntry("name")); + } + + @Test + public void constructorEmptyMapNullDefault() { + assertNull(new InMemoryConfiguration(Collections. emptyMap(), null) + .getAppConfigurationEntry("name")); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullMapNullDefault() { + new InMemoryConfiguration(null, null); + } + + @Test + public void nonnullDefault() { + InMemoryConfiguration configuration = new InMemoryConfiguration(defaultEntries); + assertArrayEquals(defaultEntries, configuration.getAppConfigurationEntry("name")); + } + + @Test + public void mappedNonnullDefault() { + InMemoryConfiguration configuration = new InMemoryConfiguration(mappedEntries, defaultEntries); + assertArrayEquals(defaultEntries, configuration.getAppConfigurationEntry("missing")); + assertArrayEquals(mappedEntries.get("name"), configuration.getAppConfigurationEntry("name")); + } + + @Test + public void jdk5Compatable() throws Exception { + Method method = InMemoryConfiguration.class.getDeclaredMethod("refresh"); + assertEquals(InMemoryConfiguration.class, method.getDeclaringClass()); + } +} diff --git a/core/src/test/resources/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.xml b/core/src/test/resources/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.xml new file mode 100644 index 0000000000..c1303be7fb --- /dev/null +++ b/core/src/test/resources/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/manual/src/docbook/jaas-auth-provider.xml b/docs/manual/src/docbook/jaas-auth-provider.xml index 675e5d750a..39f3a01b9e 100644 --- a/docs/manual/src/docbook/jaas-auth-provider.xml +++ b/docs/manual/src/docbook/jaas-auth-provider.xml @@ -1,28 +1,188 @@ - + Java Authentication and Authorization Service (JAAS) Provider -
+
Overview Spring Security provides a package able to delegate authentication requests to the Java Authentication and Authorization Service (JAAS). This package is discussed in detail below. +
+ +
+ + AbstractJaasAuthenticationProvider + + The AbstractJaasAuthenticationProvider is the basis for the + provided JAAS AuthenticationProvider implementations. Subclasses + must implement a method that creates the LoginContext. The + AbstractJaasAuthenticationProvider has a number of dependencies that can + be injected into it that are discussed below. - Central to JAAS operation are login configuration files. To learn more about JAAS - login configuration files, consult the JAAS reference documentation available from Sun - Microsystems. We expect you to have a basic understanding of JAAS and its login - configuration file syntax in order to understand this section. +
+ + JAAS CallbackHandler + + + Most JAAS LoginModules require a callback of some sort. These + callbacks are usually used to obtain the username and password from the user. + + In a Spring Security deployment, Spring Security is responsible for this user + interaction (via the authentication mechanism). Thus, by the time the authentication + request is delegated through to JAAS, Spring Security's authentication mechanism + will already have fully-populated an Authentication + object containing all the information required by the JAAS + LoginModule. + + Therefore, the JAAS package for Spring Security provides two default callback + handlers, JaasNameCallbackHandler and + JaasPasswordCallbackHandler. Each of these callback handlers + implement JaasAuthenticationCallbackHandler. In most cases these + callback handlers can simply be used without understanding the internal + mechanics. + + For those needing full control over the callback behavior, internally + AbstractJaasAuthenticationProvider wraps these + JaasAuthenticationCallbackHandlers with an + InternalCallbackHandler. The + InternalCallbackHandler is the class that actually implements + JAAS’ normal CallbackHandler interface. Any time that the JAAS + LoginModule is used, it is passed a list of application context + configured InternalCallbackHandlers. If the + LoginModule requests a callback against the + InternalCallbackHandlers, the callback is in-turn passed to the + JaasAuthenticationCallbackHandlers being wrapped. +
+ +
+ + JAAS AuthorityGranter + + + JAAS works with principals. Even "roles" are represented as principals in JAAS. + Spring Security, on the other hand, works with + Authentication objects. Each + Authentication object contains a single principal, + and multiple GrantedAuthoritys. To facilitate + mapping between these different concepts, Spring Security's JAAS package includes an + AuthorityGranter interface. + + An AuthorityGranter is responsible for inspecting a JAAS + principal and returning a set of Strings, representing the + authorities assigned to the principal. For each returned authority string, the + AbstractJaasAuthenticationProvider creates a + JaasGrantedAuthority (which implements Spring Security’s + GrantedAuthority interface) containing the authority + string and the JAAS principal that the + AuthorityGranter was passed. The + AbstractJaasAuthenticationProvider obtains the JAAS principals by + firstly successfully authenticating the user’s credentials using the JAAS + LoginModule, and then accessing the + LoginContext it returns. A call to + LoginContext.getSubject().getPrincipals() is made, with each + resulting principal passed to each AuthorityGranter + defined against the + AbstractJaasAuthenticationProvider.setAuthorityGranters(List) + property. + + Spring Security does not include any production + AuthorityGranters given that every JAAS principal has + an implementation-specific meaning. However, there is a + TestAuthorityGranter in the unit tests that demonstrates a simple + AuthorityGranter implementation. +
+
+
+ + DefaultJaasAuthenticationProvider + + The DefaultJaasAuthenticationProvider allows a JAAS + Configuration object to be injected into it as a dependency. It then + creates a LoginContext using the injected JAAS Configuration. + This means that DefaultJaasAuthenticationProvider is not bound any particular implementation + of Configuration as JaasAuthenticationProvider is. + +
+ + InMemoryConfiguration + + In order to make it easy to inject a Configuration into + DefaultJaasAuthenticationProvider, a default in memory + implementation named InMemoryConfiguration is provided. The + implementation constructor accepts a Map where each key represents a + login configuration name and the value represents an Array of + AppConfigurationEntrys. + InMemoryConfiguration also supports a default + Array of AppConfigurationEntry objects that + will be used if no mapping is found within the provided Map. For + details, refer to the class level javadoc of InMemoryConfiguration. +
+ +
+ + DefaultJaasAuthenticationProvider Example Configuration + + While the Spring configuration for InMemoryConfiguration can be + more verbose than the standarad JAAS configuration files, using it in conjuction with + DefaultJaasAuthenticationProvider is more flexible than + JaasAuthenticationProvider since it not dependant on the default + Configuration implementation. + An example configuration of DefaultJaasAuthenticationProvider using + InMemoryConfiguration is provided below. Note that custom implementations of + Configuration can easily be injected into + DefaultJaasAuthenticationProvider as well. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> +
-
+
- Configuration + JaasAuthenticationProvider - The JaasAuthenticationProvider attempts to authenticate a user’s - principal and credentials through JAAS. + The JaasAuthenticationProvider assumes the default Configuration is an instance of + + ConfigFile. This assumption is made in order to attempt to update the Configuration. The + JaasAuthenticationProvider then uses the default Configuration to create the + LoginContext. Let’s assume we have a JAAS login configuration file, /WEB-INF/login.conf, with the following contents: @@ -52,81 +212,5 @@ JAASTest { ]]> - - The CallbackHandlers and - AuthorityGranters are discussed below. - -
- - JAAS CallbackHandler - - - Most JAAS LoginModules require a callback of some sort. These - callbacks are usually used to obtain the username and password from the user. - - In a Spring Security deployment, Spring Security is responsible for this user - interaction (via the authentication mechanism). Thus, by the time the authentication - request is delegated through to JAAS, Spring Security's authentication mechanism - will already have fully-populated an Authentication - object containing all the information required by the JAAS - LoginModule. - - Therefore, the JAAS package for Spring Security provides two default callback - handlers, JaasNameCallbackHandler and - JaasPasswordCallbackHandler. Each of these callback handlers - implement JaasAuthenticationCallbackHandler. In most cases these - callback handlers can simply be used without understanding the internal - mechanics. - - For those needing full control over the callback behavior, internally - JaasAuthenticationProvider wraps these - JaasAuthenticationCallbackHandlers with an - InternalCallbackHandler. The - InternalCallbackHandler is the class that actually implements - JAAS’ normal CallbackHandler interface. Any time that the JAAS - LoginModule is used, it is passed a list of application context - configured InternalCallbackHandlers. If the - LoginModule requests a callback against the - InternalCallbackHandlers, the callback is in-turn passed to the - JaasAuthenticationCallbackHandlers being wrapped. -
- -
- - JAAS AuthorityGranter - - - JAAS works with principals. Even "roles" are represented as principals in JAAS. - Spring Security, on the other hand, works with - Authentication objects. Each - Authentication object contains a single principal, - and multiple GrantedAuthoritys. To facilitate - mapping between these different concepts, Spring Security's JAAS package includes an - AuthorityGranter interface. - - An AuthorityGranter is responsible for inspecting a JAAS - principal and returning a set of Strings, representing the - authorities assigned to the principal. For each returned authority string, the - JaasAuthenticationProvider creates a - JaasGrantedAuthority (which implements Spring Security’s - GrantedAuthority interface) containing the authority - string and the JAAS principal that the - AuthorityGranter was passed. The - JaasAuthenticationProvider obtains the JAAS principals by - firstly successfully authenticating the user’s credentials using the JAAS - LoginModule, and then accessing the - LoginContext it returns. A call to - LoginContext.getSubject().getPrincipals() is made, with each - resulting principal passed to each AuthorityGranter - defined against the - JaasAuthenticationProvider.setAuthorityGranters(List) - property. - - Spring Security does not include any production - AuthorityGranters given that every JAAS principal has - an implementation-specific meaning. However, there is a - TestAuthorityGranter in the unit tests that demonstrates a simple - AuthorityGranter implementation. -
- + \ No newline at end of file diff --git a/docs/manual/src/docbook/samples.xml b/docs/manual/src/docbook/samples.xml index 1530402c0a..0e21e81b22 100644 --- a/docs/manual/src/docbook/samples.xml +++ b/docs/manual/src/docbook/samples.xml @@ -129,6 +129,12 @@ Success! Your web filters appear to be properly configured! download the CAS Server web application (a war file) from the CAS site and drop it into the samples/cas/server directory.
+
+ JAAS Sample + The JAAS sample is very simple example of how to use a JAAS LoginModule with Spring Security. The provided LoginModule will + successfully authenticate a user if the username equals the password otherwise a LoginException is thrown. The AuthorityGranter + used in this example always grants the role ROLE_USER. +
Pre-Authentication Sample This sample application demonstrates how to wire up beans from the grant(Principal principal) { + return Collections.singleton("ROLE_USER"); + } +} diff --git a/samples/jaas/src/main/java/samples/jaas/UsernameEqualsPasswordLoginModule.java b/samples/jaas/src/main/java/samples/jaas/UsernameEqualsPasswordLoginModule.java new file mode 100644 index 0000000000..213ed42b8b --- /dev/null +++ b/samples/jaas/src/main/java/samples/jaas/UsernameEqualsPasswordLoginModule.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package samples.jaas; + +import java.security.Principal; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +/** + * A LoginModule that will allow login if the username equals the password. Upon + * successful authentication it adds the username as a Principal. + * + * @author Rob Winch + */ +public class UsernameEqualsPasswordLoginModule implements LoginModule { + //~ Instance fields ================================================================================================ + + private String password; + private String username; + private Subject subject; + + //~ Methods ======================================================================================================== + + public boolean abort() throws LoginException { + return true; + } + + public boolean commit() throws LoginException { + return true; + } + + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map options) { + this.subject = subject; + + try { + NameCallback nameCallback = new NameCallback("prompt"); + PasswordCallback passwordCallback = new PasswordCallback("prompt", false); + + callbackHandler.handle(new Callback[] { nameCallback, passwordCallback }); + + password = new String(passwordCallback.getPassword()); + username = nameCallback.getName(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public boolean login() throws LoginException { + if (username != null && !username.equals(password)) { + throw new LoginException("username is not equal to password"); + } + + subject.getPrincipals().add(new Principal() { + public String getName() { + return username; + } + }); + return true; + } + + public boolean logout() throws LoginException { + return true; + } +} diff --git a/samples/jaas/src/main/resources/applicationContext-security.xml b/samples/jaas/src/main/resources/applicationContext-security.xml new file mode 100644 index 0000000000..d658938c7b --- /dev/null +++ b/samples/jaas/src/main/resources/applicationContext-security.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/jaas/src/main/webapp/WEB-INF/web.xml b/samples/jaas/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..3af33ec5df --- /dev/null +++ b/samples/jaas/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,63 @@ + + + + + + JAAS Sample Application + + + + contextConfigLocation + + classpath:applicationContext-security.xml + + + + + + + webAppRootKey + jaas.root + + + + localizationFilter + org.springframework.web.filter.RequestContextFilter + + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + + localizationFilter + /* + + + + springSecurityFilterChain + /* + + + + + org.springframework.web.context.ContextLoaderListener + + + + index.jsp + + diff --git a/samples/jaas/src/main/webapp/index.jsp b/samples/jaas/src/main/webapp/index.jsp new file mode 100644 index 0000000000..f0dfc20ac8 --- /dev/null +++ b/samples/jaas/src/main/webapp/index.jsp @@ -0,0 +1,18 @@ +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> + + +

Home Page

+

+Anyone can view this page. +

+

+Your principal object is....: <%= request.getUserPrincipal() %> +

+

+You can currently access "/secure" URLs. +

+ +

+Secure page

+ + diff --git a/samples/jaas/src/main/webapp/secure/index.jsp b/samples/jaas/src/main/webapp/secure/index.jsp new file mode 100644 index 0000000000..553bc3a15a --- /dev/null +++ b/samples/jaas/src/main/webapp/secure/index.jsp @@ -0,0 +1,40 @@ +<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %> +<%@ page import="org.springframework.security.core.Authentication" %> +<%@ page import="org.springframework.security.core.GrantedAuthority" %> + + + +Security Debug Information + + + +

Security Debug Information

+ +<% + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { %> +

+ Authentication object is of type: <%= auth.getClass().getName() %> +

+

+ Authentication object as a String:

<%= auth.toString() %> +

+ + Authentication object holds the following granted authorities:

+<% + for (GrantedAuthority authority : auth.getAuthorities()) { %> + <%= authority %> (getAuthority(): <%= authority.getAuthority() %>)
+<% } +%> + +

Success! Your web filters appear to be properly configured!

+<% + } else { +%> + Authentication object is null.
+ This is an error and your Spring Security application will not operate properly until corrected.

+<% } +%> + + + diff --git a/settings.gradle b/settings.gradle index c6cc6aa60d..774a0af408 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,7 +19,8 @@ def String[] samples = [ 'dms', 'preauth', 'cas', - 'ldap' + 'ldap', + 'jaas' ] def String[] itest = [