浏览代码

SEC-1536: added JAAS API Integration, updated doc, updated jaas sample

rwinch 15 年之前
父节点
当前提交
de819378fc

+ 2 - 0
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);
     }

+ 22 - 1
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<BeanDefinition,BeanDefinition> 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));
         }

+ 1 - 0
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,

+ 3 - 0
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}?

+ 5 - 0
config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd

@@ -759,6 +759,11 @@
         <xs:documentation>Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true".</xs:documentation>
       </xs:annotation>
     </xs:attribute>
+    <xs:attribute name="jaas-api-provision" type="security:boolean">
+      <xs:annotation>
+        <xs:documentation>If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false".</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
     <xs:attribute name="access-decision-manager-ref" type="xs:token">
       <xs:annotation>
         <xs:documentation>Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests.</xs:documentation>

+ 9 - 0
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 {

+ 7 - 0
docs/manual/src/docbook/appendix-namespace.xml

@@ -70,6 +70,13 @@
                     <classname>SecurityContextHolderAwareRequestFilter</classname> bean to the
                     stack. Defaults to "true".</para>
             </section>
+            <section xml:id="nsa-jaas-api-provision">
+                <title><literal>jaas-api-provision</literal></title>
+                <para>If available, runs the request as the <literal>Subject</literal> acquired from 
+                    the <classname>JaasAuthenticationToken</classname> which is implemented by 
+                    adding a <classname>JaasApiIntegrationFilter</classname> bean to the stack. 
+                    Defaults to "false".</para>
+            </section>
             <section xml:id="nsa-path-type">
                 <title><literal>request-matcher</literal></title>
                 <para> Defines the <interfacename>RequestMatcher</interfacename> strategy used in

+ 16 - 0
docs/manual/src/docbook/jaas-auth-provider.xml

@@ -213,4 +213,20 @@ JAASTest {
 </bean>
 ]]></programlisting></para>
     </section>
+    <section xml:id="jaas-apiprovision">
+        <info>
+            <title xml:id="jaas-api-provision">Running as a Subject</title>
+        </info>
+        <para>If configured, the <classname>JaasApiIntegrationFilter</classname> will attempt to 
+            run as the <literal>Subject</literal> on the 
+            <classname>JaasAuthenticationToken</classname>. This means that the 
+            <literal>Subject</literal> can be accessed using: 
+            <programlisting language="java"><![CDATA[
+            Subject subject = Subject.getSubject(AccessController.getContext());
+]]></programlisting>
+            This integration can easily be configured using the 
+            <link xlink:href="#nsa-jaas-api-provision">jaas-api-provision</link> attribute. This
+            feature is useful when integrating with legacy or external API's that rely on the
+            JAAS Subject being populated.</para>
+    </section>
 </chapter>

+ 5 - 0
docs/manual/src/docbook/namespace-config.xml

@@ -691,6 +691,11 @@ List&lt;OpenIDAttribute> attributes = token.getAttributes();</programlisting>The
                             <entry><literal>SecurityContextHolderAwareFilter</literal></entry>
                             <entry><literal>http/@servlet-api-provision</literal></entry>
                         </row>
+                        <row>
+                            <entry>JAAS_API_SUPPORT_FILTER</entry>
+                            <entry><literal>JaasApiIntegrationFilter</literal></entry>
+                            <entry><literal>http/@jaas-api-provision</literal></entry>
+                        </row>
                         <row>
                             <entry> REMEMBER_ME_FILTER </entry>
                             <entry><classname>RememberMeAuthenticationFilter</classname></entry>

+ 2 - 1
docs/manual/src/docbook/samples.xml

@@ -133,7 +133,8 @@ Success! Your web filters appear to be properly configured!
         <title>JAAS Sample</title>
         <para>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.</para>
+            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 <link xlink:href="#nsa-jaas-api-provision">jaas-api-provision</link> equal to "true".</para>
     </section>
     <section xml:id="preauth-sample">
         <title>Pre-Authentication Sample</title>

+ 7 - 0
docs/manual/src/docbook/security-filter-chain.xml

@@ -151,6 +151,13 @@
                     using it to install a Spring Security aware
                     <literal>HttpServletRequestWrapper</literal> into your servlet container</para>
             </listitem>
+            <listitem>
+                <para>The <classname>JaasApiIntegrationFilter</classname>, if a 
+                    <classname>JaasAuthenticationToken</classname> is in the 
+                    <classname>SecurityContextHolder</classname> this will process the 
+                    <classname>FilterChain</classname> as the <classname>Subject</classname> in the 
+                    <classname>JaasAuthenticationToken</classname></para> 
+            </listitem>
             <listitem>
                 <para><classname>RememberMeAuthenticationFilter</classname>, so that if no earlier
                     authentication processing mechanism updated the

+ 0 - 1
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",

+ 4 - 1
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;
     }

+ 1 - 1
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">
 
 
-	<sec:http auto-config="true" use-expressions="true">
+	<sec:http auto-config="true" use-expressions="true" jaas-api-provision="true">
 		<sec:intercept-url pattern="/secure/**" access="isAuthenticated()"/>
 	</sec:http>
 	

+ 5 - 0
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" %>
 <html>
 <body>
@@ -9,6 +11,9 @@ Anyone can view this page.
 Your principal object is....: <%= request.getUserPrincipal() %>
 </p>
 <p>
+Subject.getSubject(AccessController.getContext()) is....: <%= Subject.getSubject(AccessController.getContext()) %>
+</p>
+<p>
 <sec:authorize url='/secure/index.jsp'>You can currently access "/secure" URLs.</sec:authorize>
 </p>
 

+ 11 - 0
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 @@
 
 <h3>Security Debug Information</h3>
 
+<% 
+
+		Subject subject = Subject.getSubject(AccessController.getContext());  
+		if(subject != null) { %>
+<p>
+			Subject.getSubject(AccessController.getContext()) is....: <%= subject %>
+</p>
+		<%} %>
+
 <%
         Authentication auth = SecurityContextHolder.getContext().getAuthentication();
         if (auth != null) { %>

+ 163 - 0
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;
+
+/**
+ * <p>
+ * A <code>Filter</code> which attempts to obtain a JAAS <code>Subject</code>
+ * and continue the <code>FilterChain</code> running as that
+ * <code>Subject</code>.
+ * </p>
+ * <p>
+ * By using this <code>Filter</code> in conjunction with Spring's
+ * <code>JaasAuthenticationProvider</code> both Spring's
+ * <code>SecurityContext</code> and a JAAS <code>Subject</code> can be populated
+ * simultaneously. This is useful when integrating with code that requires a
+ * JAAS <code>Subject</code> to be populated.
+ * </p>
+ * 
+ * @author Rob Winch
+ * @see #doFilter(ServletRequest, ServletResponse, FilterChain)
+ * @see #obtainSubject(ServletRequest)
+ */
+public class JaasApiIntegrationFilter extends GenericFilterBean {
+    //~ Instance fields ================================================================================================
+
+    private boolean createEmptySubject;
+
+    //~ Methods ========================================================================================================
+
+    /**
+     * <p>
+     * Attempts to obtain and run as a JAAS <code>Subject</code> using
+     * {@link #obtainSubject(ServletRequest)}.
+     * </p>
+     * 
+     * <p>
+     * If the <code>Subject</code> is <code>null</code> and
+     * <tt>createEmptySubject</tt> is <code>true</code>, an empty, writeable
+     * <code>Subject</code> is used. This allows for the <code>Subject</code> to
+     * be populated at the time of login. If the <code>Subject</code> is
+     * <code>null</code>, the <code>FilterChain</code> continues with no
+     * additional processing. If the <code>Subject</code> is not
+     * <code>null</code>, the <code>FilterChain</code> is ran with
+     * {@link Subject#doAs(Subject, PrivilegedExceptionAction)} in conjunction
+     * with the <code>Subject</code> obtained.
+     * </p>
+     */
+    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<Object> continueChain = new PrivilegedExceptionAction<Object>() {
+            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);
+        }
+    }
+
+    /**
+     * <p>
+     * Obtains the <code>Subject</code> to run as or <code>null</code> if no
+     * <code>Subject</code> is available.
+     * </p>
+     * <p>
+     * The default implementation attempts to obtain the <code>Subject</code>
+     * from the <code>SecurityContext</code>'s <code>Authentication</code>. If
+     * it is of type <code>JaasAuthenticationToken</code> and is authenticated,
+     * the <code>Subject</code> is returned from it. Otherwise,
+     * <code>null</code> is returned.
+     * </p>
+     * 
+     * @param request
+     *            the current <code>ServletRequest</code>
+     * @return the Subject to run as or <code>null</code> if no
+     *         <code>Subject</code> 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 <code>createEmptySubject</code>. If the value is <code>true</code>,
+     * and {@link #obtainSubject(ServletRequest)} returns <code>null</code>, an
+     * empty, writeable <code>Subject</code> is created instead. Otherwise no
+     * <code>Subject</code> is used. The default is <code>false</code>.
+     * 
+     * @param createEmptySubject
+     *            the new value
+     */
+    public final void setCreateEmptySubject(boolean createEmptySubject) {
+        this.createEmptySubject = createEmptySubject;
+    }
+}

+ 211 - 0
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<String, String>()) };
+            }
+        };
+        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);
+    }
+}

+ 2 - 1
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"
 }