From de819378fc096641da05c4d7c29b1c42a8b134ac Mon Sep 17 00:00:00 2001 From: rwinch Date: Mon, 13 Sep 2010 13:12:45 -0500 Subject: [PATCH] SEC-1536: added JAAS API Integration, updated doc, updated jaas sample --- .../http/DefaultFilterChainValidator.java | 2 + .../config/http/HttpConfigurationBuilder.java | 23 +- .../security/config/http/SecurityFilters.java | 1 + .../security/config/spring-security-3.1.rnc | 3 + .../security/config/spring-security-3.1.xsd | 5 + .../config/http/MiscHttpConfigTests.groovy | 9 + .../manual/src/docbook/appendix-namespace.xml | 7 + .../manual/src/docbook/jaas-auth-provider.xml | 16 ++ docs/manual/src/docbook/namespace-config.xml | 5 + docs/manual/src/docbook/samples.xml | 3 +- .../src/docbook/security-filter-chain.xml | 7 + samples/jaas/jaas.gradle | 1 - .../UsernameEqualsPasswordLoginModule.java | 5 +- .../resources/applicationContext-security.xml | 2 +- samples/jaas/src/main/webapp/index.jsp | 5 + samples/jaas/src/main/webapp/secure/index.jsp | 11 + .../jaas/JaasApiIntegrationFilter.java | 163 ++++++++++++++ .../jaas/JaasApiIntegrationFilterTest.java | 211 ++++++++++++++++++ web/web.gradle | 3 +- 19 files changed, 476 insertions(+), 6 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/authentication/jaas/JaasApiIntegrationFilter.java create mode 100644 web/src/test/java/org/springframework/security/web/authentication/jaas/JaasApiIntegrationFilterTest.java diff --git a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java index a1093692fa..02a7be95c4 100644 --- a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java +++ b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java @@ -17,6 +17,7 @@ import org.springframework.security.web.access.intercept.FilterSecurityIntercept import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.jaas.JaasApiIntegrationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; @@ -52,6 +53,7 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain checkForDuplicates(SessionManagementFilter.class, filters); checkForDuplicates(BasicAuthenticationFilter.class, filters); checkForDuplicates(SecurityContextHolderAwareRequestFilter.class, filters); + checkForDuplicates(JaasApiIntegrationFilter.class, filters); checkForDuplicates(ExceptionTranslationFilter.class, filters); checkForDuplicates(FilterSecurityInterceptor.class, filters); } diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index f3ac8d6a42..5b2ccfa8da 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -4,7 +4,6 @@ import static org.springframework.security.config.http.HttpSecurityBeanDefinitio import static org.springframework.security.config.http.SecurityFilters.*; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.springframework.beans.factory.config.BeanDefinition; @@ -35,6 +34,7 @@ import org.springframework.security.web.access.expression.WebExpressionVoter; import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.jaas.JaasApiIntegrationFilter; import org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy; import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; @@ -90,6 +90,7 @@ class HttpConfigurationBuilder { private BeanReference sessionStrategyRef; private RootBeanDefinition sfpf; private BeanDefinition servApiFilter; + private BeanDefinition jaasApiFilter; private final String portMapperName; private BeanReference fsi; private BeanReference requestCache; @@ -123,6 +124,7 @@ class HttpConfigurationBuilder { createSessionManagementFilters(); createRequestCacheFilter(); createServletApiFilter(); + createJaasApiFilter(); createChannelProcessingFilter(); createFilterSecurityInterceptor(authenticationManager); } @@ -337,7 +339,22 @@ class HttpConfigurationBuilder { servApiFilter = new RootBeanDefinition(SecurityContextHolderAwareRequestFilter.class); } } + + // Adds the jaas-api integration filter if required + private void createJaasApiFilter() { + final String ATT_JAAS_API_PROVISION = "jaas-api-provision"; + final String DEF_JAAS_API_PROVISION = "false"; + String provideJaasApi = httpElt.getAttribute(ATT_JAAS_API_PROVISION); + if (!StringUtils.hasText(provideJaasApi)) { + provideJaasApi = DEF_JAAS_API_PROVISION; + } + + if ("true".equals(provideJaasApi)) { + jaasApiFilter = new RootBeanDefinition(JaasApiIntegrationFilter.class); + } + } + private void createChannelProcessingFilter() { ManagedMap channelRequestMap = parseInterceptUrlsForChannelSecurity(); @@ -514,6 +531,10 @@ class HttpConfigurationBuilder { filters.add(new OrderDecorator(servApiFilter, SERVLET_API_SUPPORT_FILTER)); } + if (jaasApiFilter != null) { + filters.add(new OrderDecorator(jaasApiFilter, JAAS_API_SUPPORT_FILTER)); + } + if (sfpf != null) { filters.add(new OrderDecorator(sfpf, SESSION_MANAGEMENT_FILTER)); } diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index af9ea1926c..0e7568bb9d 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -23,6 +23,7 @@ enum SecurityFilters { BASIC_AUTH_FILTER, REQUEST_CACHE_FILTER, SERVLET_API_SUPPORT_FILTER, + JAAS_API_SUPPORT_FILTER, REMEMBER_ME_FILTER, ANONYMOUS_FILTER, SESSION_MANAGEMENT_FILTER, diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc index 1ee992a4f2..b138557177 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc @@ -289,6 +289,9 @@ http.attlist &= http.attlist &= ## Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". attribute servlet-api-provision {boolean}? +http.attlist &= + ## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". + attribute jaas-api-provision {boolean}? http.attlist &= ## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. attribute access-decision-manager-ref {xsd:token}? diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd index a5754eba1a..e950e484f0 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd @@ -759,6 +759,11 @@ Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". + + + If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". + + Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. diff --git a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy index c21e56c1df..919d2d386b 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy @@ -30,6 +30,7 @@ import org.springframework.security.web.access.intercept.FilterSecurityIntercept import org.springframework.security.web.authentication.AnonymousAuthenticationFilter import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.authentication.jaas.JaasApiIntegrationFilter import org.springframework.security.web.authentication.logout.LogoutFilter import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter @@ -568,6 +569,14 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { getFilter(BasicAuthenticationFilter).authenticationDetailsSource == adsr getFilter(X509AuthenticationFilter).authenticationDetailsSource == adsr } + + def includeJaasApiIntegrationFilter() { + xml.http(['auto-config':'true','jaas-api-provision':'true']) + createAppContext() + expect: + getFilter(JaasApiIntegrationFilter.class) != null + + } } class MockEntryPoint extends LoginUrlAuthenticationEntryPoint { diff --git a/docs/manual/src/docbook/appendix-namespace.xml b/docs/manual/src/docbook/appendix-namespace.xml index 773caead62..63f3573b8a 100644 --- a/docs/manual/src/docbook/appendix-namespace.xml +++ b/docs/manual/src/docbook/appendix-namespace.xml @@ -70,6 +70,13 @@ SecurityContextHolderAwareRequestFilter bean to the stack. Defaults to "true". +
+ <literal>jaas-api-provision</literal> + If available, runs the request as the Subject acquired from + the JaasAuthenticationToken which is implemented by + adding a JaasApiIntegrationFilter bean to the stack. + Defaults to "false". +
<literal>request-matcher</literal> Defines the RequestMatcher strategy used in diff --git a/docs/manual/src/docbook/jaas-auth-provider.xml b/docs/manual/src/docbook/jaas-auth-provider.xml index 39f3a01b9e..e50652a69e 100644 --- a/docs/manual/src/docbook/jaas-auth-provider.xml +++ b/docs/manual/src/docbook/jaas-auth-provider.xml @@ -213,4 +213,20 @@ JAASTest { ]]>
+
+ + Running as a Subject + + If configured, the JaasApiIntegrationFilter will attempt to + run as the Subject on the + JaasAuthenticationToken. This means that the + Subject can be accessed using: + + This integration can easily be configured using the + jaas-api-provision attribute. This + feature is useful when integrating with legacy or external API's that rely on the + JAAS Subject being populated. +
\ No newline at end of file diff --git a/docs/manual/src/docbook/namespace-config.xml b/docs/manual/src/docbook/namespace-config.xml index 67d5df92ce..51a3baf3bd 100644 --- a/docs/manual/src/docbook/namespace-config.xml +++ b/docs/manual/src/docbook/namespace-config.xml @@ -691,6 +691,11 @@ List<OpenIDAttribute> attributes = token.getAttributes();The SecurityContextHolderAwareFilter http/@servlet-api-provision + + JAAS_API_SUPPORT_FILTER + JaasApiIntegrationFilter + http/@jaas-api-provision + REMEMBER_ME_FILTER RememberMeAuthenticationFilter diff --git a/docs/manual/src/docbook/samples.xml b/docs/manual/src/docbook/samples.xml index 0e21e81b22..227c6d2590 100644 --- a/docs/manual/src/docbook/samples.xml +++ b/docs/manual/src/docbook/samples.xml @@ -133,7 +133,8 @@ Success! Your web filters appear to be properly configured! 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. + used in this example always grants the role ROLE_USER. The sample application also demonstrates how to run as the JAAS Subject + returned by the LoginModule by setting jaas-api-provision equal to "true".
Pre-Authentication Sample diff --git a/docs/manual/src/docbook/security-filter-chain.xml b/docs/manual/src/docbook/security-filter-chain.xml index 181af8763c..af192d7870 100644 --- a/docs/manual/src/docbook/security-filter-chain.xml +++ b/docs/manual/src/docbook/security-filter-chain.xml @@ -151,6 +151,13 @@ using it to install a Spring Security aware HttpServletRequestWrapper into your servlet container + + The JaasApiIntegrationFilter, if a + JaasAuthenticationToken is in the + SecurityContextHolder this will process the + FilterChain as the Subject in the + JaasAuthenticationToken + RememberMeAuthenticationFilter, so that if no earlier authentication processing mechanism updated the diff --git a/samples/jaas/jaas.gradle b/samples/jaas/jaas.gradle index c6a6031108..0714ace274 100644 --- a/samples/jaas/jaas.gradle +++ b/samples/jaas/jaas.gradle @@ -13,7 +13,6 @@ dependencies { runtime project(':spring-security-web'), project(':spring-security-config'), - project(':spring-security-taglibs'), "org.springframework:spring-context-support:$springVersion", "javax.servlet:jstl:$jstlVersion", "org.slf4j:jcl-over-slf4j:$slf4jVersion", diff --git a/samples/jaas/src/main/java/samples/jaas/UsernameEqualsPasswordLoginModule.java b/samples/jaas/src/main/java/samples/jaas/UsernameEqualsPasswordLoginModule.java index 213ed42b8b..2dca7dbeae 100644 --- a/samples/jaas/src/main/java/samples/jaas/UsernameEqualsPasswordLoginModule.java +++ b/samples/jaas/src/main/java/samples/jaas/UsernameEqualsPasswordLoginModule.java @@ -67,7 +67,7 @@ public class UsernameEqualsPasswordLoginModule implements LoginModule { } public boolean login() throws LoginException { - if (username != null && !username.equals(password)) { + if (username == null || !username.equals(password)) { throw new LoginException("username is not equal to password"); } @@ -75,6 +75,9 @@ public class UsernameEqualsPasswordLoginModule implements LoginModule { public String getName() { return username; } + public String toString() { + return "Principal [name="+getName()+"]"; + } }); return true; } diff --git a/samples/jaas/src/main/resources/applicationContext-security.xml b/samples/jaas/src/main/resources/applicationContext-security.xml index d658938c7b..34b854e325 100644 --- a/samples/jaas/src/main/resources/applicationContext-security.xml +++ b/samples/jaas/src/main/resources/applicationContext-security.xml @@ -9,7 +9,7 @@ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd"> - + diff --git a/samples/jaas/src/main/webapp/index.jsp b/samples/jaas/src/main/webapp/index.jsp index f0dfc20ac8..06e2b29ee6 100644 --- a/samples/jaas/src/main/webapp/index.jsp +++ b/samples/jaas/src/main/webapp/index.jsp @@ -1,3 +1,5 @@ +<%@ page import="javax.security.auth.Subject" %> +<%@ page import="java.security.AccessController" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> @@ -9,6 +11,9 @@ Anyone can view this page. Your principal object is....: <%= request.getUserPrincipal() %>

+Subject.getSubject(AccessController.getContext()) is....: <%= Subject.getSubject(AccessController.getContext()) %> +

+

You can currently access "/secure" URLs.

diff --git a/samples/jaas/src/main/webapp/secure/index.jsp b/samples/jaas/src/main/webapp/secure/index.jsp index 553bc3a15a..6ddcc61e07 100644 --- a/samples/jaas/src/main/webapp/secure/index.jsp +++ b/samples/jaas/src/main/webapp/secure/index.jsp @@ -1,3 +1,5 @@ +<%@ page import="javax.security.auth.Subject" %> +<%@ page import="java.security.AccessController" %> <%@ page import="org.springframework.security.core.context.SecurityContextHolder" %> <%@ page import="org.springframework.security.core.Authentication" %> <%@ page import="org.springframework.security.core.GrantedAuthority" %> @@ -10,6 +12,15 @@

Security Debug Information

+<% + + Subject subject = Subject.getSubject(AccessController.getContext()); + if(subject != null) { %> +

+ Subject.getSubject(AccessController.getContext()) is....: <%= subject %> +

+ <%} %> + <% Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { %> diff --git a/web/src/main/java/org/springframework/security/web/authentication/jaas/JaasApiIntegrationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/jaas/JaasApiIntegrationFilter.java new file mode 100644 index 0000000000..b2bdba2ce1 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/jaas/JaasApiIntegrationFilter.java @@ -0,0 +1,163 @@ +/* + * 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.web.authentication.jaas; + +import java.io.IOException; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginContext; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.security.authentication.jaas.JaasAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +/** + *

+ * A Filter which attempts to obtain a JAAS Subject + * and continue the FilterChain running as that + * Subject. + *

+ *

+ * By using this Filter in conjunction with Spring's + * JaasAuthenticationProvider both Spring's + * SecurityContext and a JAAS Subject can be populated + * simultaneously. This is useful when integrating with code that requires a + * JAAS Subject to be populated. + *

+ * + * @author Rob Winch + * @see #doFilter(ServletRequest, ServletResponse, FilterChain) + * @see #obtainSubject(ServletRequest) + */ +public class JaasApiIntegrationFilter extends GenericFilterBean { + //~ Instance fields ================================================================================================ + + private boolean createEmptySubject; + + //~ Methods ======================================================================================================== + + /** + *

+ * Attempts to obtain and run as a JAAS Subject using + * {@link #obtainSubject(ServletRequest)}. + *

+ * + *

+ * If the Subject is null and + * createEmptySubject is true, an empty, writeable + * Subject is used. This allows for the Subject to + * be populated at the time of login. If the Subject is + * null, the FilterChain continues with no + * additional processing. If the Subject is not + * null, the FilterChain is ran with + * {@link Subject#doAs(Subject, PrivilegedExceptionAction)} in conjunction + * with the Subject obtained. + *

+ */ + public final void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws ServletException, IOException { + + Subject subject = obtainSubject(request); + if (subject == null && createEmptySubject) { + if (logger.isDebugEnabled()) { + logger.debug("Subject returned was null and createEmtpySubject is true; creating new empty subject to run as."); + } + subject = new Subject(); + } + if (subject == null) { + if (logger.isDebugEnabled()) { + logger.debug("Subject is null continue running with no Subject."); + } + chain.doFilter(request, response); + return; + } + final PrivilegedExceptionAction continueChain = new PrivilegedExceptionAction() { + public Object run() throws IOException, ServletException { + chain.doFilter(request, response); + return null; + } + }; + + if (logger.isDebugEnabled()) { + logger.debug("Running as Subject " + subject); + } + try { + Subject.doAs(subject, continueChain); + } catch (PrivilegedActionException e) { + throw new ServletException(e.getMessage(), e); + } + } + + /** + *

+ * Obtains the Subject to run as or null if no + * Subject is available. + *

+ *

+ * The default implementation attempts to obtain the Subject + * from the SecurityContext's Authentication. If + * it is of type JaasAuthenticationToken and is authenticated, + * the Subject is returned from it. Otherwise, + * null is returned. + *

+ * + * @param request + * the current ServletRequest + * @return the Subject to run as or null if no + * Subject is available. + */ + protected Subject obtainSubject(ServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (logger.isDebugEnabled()) { + logger.debug("Attempting to obtainSubject using authentication : " + authentication); + } + if (authentication == null) { + return null; + } + if (!authentication.isAuthenticated()) { + return null; + } + if (!(authentication instanceof JaasAuthenticationToken)) { + return null; + } + JaasAuthenticationToken token = (JaasAuthenticationToken) authentication; + LoginContext loginContext = token.getLoginContext(); + if (loginContext == null) { + return null; + } + return loginContext.getSubject(); + } + + /** + * Sets createEmptySubject. If the value is true, + * and {@link #obtainSubject(ServletRequest)} returns null, an + * empty, writeable Subject is created instead. Otherwise no + * Subject is used. The default is false. + * + * @param createEmptySubject + * the new value + */ + public final void setCreateEmptySubject(boolean createEmptySubject) { + this.createEmptySubject = createEmptySubject; + } +} \ No newline at end of file diff --git a/web/src/test/java/org/springframework/security/web/authentication/jaas/JaasApiIntegrationFilterTest.java b/web/src/test/java/org/springframework/security/web/authentication/jaas/JaasApiIntegrationFilterTest.java new file mode 100644 index 0000000000..c6f187f25f --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/jaas/JaasApiIntegrationFilterTest.java @@ -0,0 +1,211 @@ +/* + * 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.web.authentication.jaas; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.security.AccessController; +import java.security.Principal; +import java.util.HashMap; + +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.callback.TextInputCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +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.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authentication.jaas.JaasAuthenticationToken; +import org.springframework.security.authentication.jaas.TestLoginModule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Tests the JaasApiIntegrationFilter. + * + * @author Rob Winch + */ +public class JaasApiIntegrationFilterTest { + //~ Instance fields ================================================================================================ + private JaasApiIntegrationFilter filter; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private Authentication token; + private Subject authenticatedSubject; + private Configuration testConfiguration; + private CallbackHandler callbackHandler; + //~ Methods ======================================================================================================== + + @Before + public void onBeforeTests() throws Exception { + this.filter = new JaasApiIntegrationFilter(); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + + authenticatedSubject = new Subject(); + authenticatedSubject.getPrincipals().add(new Principal() { + public String getName() { + return "principal"; + } + }); + authenticatedSubject.getPrivateCredentials().add("password"); + authenticatedSubject.getPublicCredentials().add("username"); + callbackHandler = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName("user"); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword("password".toCharArray()); + } else if (callback instanceof TextInputCallback) { + // ignore + } else { + throw new UnsupportedCallbackException(callback, "Unrecognized Callback " + callback); + } + } + } + }; + testConfiguration = new Configuration() { + public void refresh() { + } + + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return new AppConfigurationEntry[] { new AppConfigurationEntry(TestLoginModule.class.getName(), + LoginModuleControlFlag.REQUIRED, new HashMap()) }; + } + }; + LoginContext ctx = new LoginContext("SubjectDoAsFilterTest", authenticatedSubject, callbackHandler, + testConfiguration); + ctx.login(); + token = new JaasAuthenticationToken("username", "password", AuthorityUtils.createAuthorityList("ROLE_ADMIN"), + ctx); + + // just in case someone forgot to clear the context + SecurityContextHolder.clearContext(); + } + + @After + public void onAfterTests() { + SecurityContextHolder.clearContext(); + } + + /** + * Ensure a Subject was not setup in some other manner. + */ + @Test + public void currentSubjectNull() { + assertNull(Subject.getSubject(AccessController.getContext())); + } + + @Test + public void obtainSubjectNullAuthentication() { + assertNullSubject(filter.obtainSubject(request)); + } + + @Test + public void obtainSubjectNonJaasAuthentication() { + Authentication authentication = new TestingAuthenticationToken("un", "pwd"); + authentication.setAuthenticated(true); + SecurityContextHolder.getContext().setAuthentication(authentication); + assertNullSubject(filter.obtainSubject(request)); + } + + @Test + public void obtainSubjectNullLoginContext() { + token = new JaasAuthenticationToken("un", "pwd", AuthorityUtils.createAuthorityList("ROLE_ADMIN"), null); + SecurityContextHolder.getContext().setAuthentication(token); + assertNullSubject(filter.obtainSubject(request)); + } + + @Test + public void obtainSubjectNullSubject() throws Exception { + LoginContext ctx = new LoginContext("obtainSubjectNullSubject", null, callbackHandler, testConfiguration); + assertNull(ctx.getSubject()); + token = new JaasAuthenticationToken("un", "pwd", AuthorityUtils.createAuthorityList("ROLE_ADMIN"), ctx); + SecurityContextHolder.getContext().setAuthentication(token); + assertNullSubject(filter.obtainSubject(request)); + } + + @Test + public void obtainSubject() throws Exception { + SecurityContextHolder.getContext().setAuthentication(token); + assertEquals(authenticatedSubject, filter.obtainSubject(request)); + } + + @Test + public void doFilterCurrentSubjectPopulated() throws Exception { + SecurityContextHolder.getContext().setAuthentication(token); + assertJaasSubjectEquals(authenticatedSubject); + } + + @Test + public void doFilterAuthenticationNotAuthenticated() throws Exception { + // Authentication is null, so no Subject is populated. + token.setAuthenticated(false); + SecurityContextHolder.getContext().setAuthentication(token); + assertJaasSubjectEquals(null); + filter.setCreateEmptySubject(true); + assertJaasSubjectEquals(new Subject()); + } + + @Test + public void doFilterAuthenticationNull() throws Exception { + assertJaasSubjectEquals(null); + filter.setCreateEmptySubject(true); + assertJaasSubjectEquals(new Subject()); + } + + //~ Helper Methods ==================================================================================================== + + private void assertJaasSubjectEquals(final Subject expectedValue) throws Exception { + MockFilterChain chain = new MockFilterChain() { + public void doFilter(ServletRequest request, ServletResponse response) { + // See if the subject was updated + Subject currentSubject = Subject.getSubject(AccessController.getContext()); + assertEquals(expectedValue, currentSubject); + + // run so we know the chain was executed + super.doFilter(request, response); + } + }; + filter.doFilter(request, response, chain); + // ensure that the chain was actually invoked + assertNotNull(chain.getRequest()); + } + + private void assertNullSubject(Subject subject) { + assertNull("Subject is expected to be null, but is not. Got " + subject, subject); + } +} \ No newline at end of file diff --git a/web/web.gradle b/web/web.gradle index 39bfb6daec..2cc1a35a73 100644 --- a/web/web.gradle +++ b/web/web.gradle @@ -13,7 +13,8 @@ dependencies { provided 'javax.servlet:servlet-api:2.5' - testCompile 'commons-codec:commons-codec:1.3', + testCompile project(':spring-security-core').sourceSets.test.classes, + 'commons-codec:commons-codec:1.3', "org.springframework:spring-test:$springVersion" testRuntime "hsqldb:hsqldb:$hsqlVersion" }