diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/EnvironmentVariableAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/EnvironmentVariableAuthenticationFilter.java new file mode 100644 index 0000000000..c717f23ceb --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/EnvironmentVariableAuthenticationFilter.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2016 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.preauth; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.Assert; + +/** + * A simple pre-authenticated filter which obtains the username from an environment variable, for + * use with SSO systems such as Stanford WebAuth or Shibboleth. + *

+ * As with most pre-authenticated scenarios, it is essential that the external + * authentication system is set up correctly as this filter does no authentication + * whatsoever. + *

+ * The property {@code principalEnvironmentVariable} is the name of the request environment variable + * that contains the username. It defaults to "REMOTE_USER" for compatibility with WebAuth and Shibboleth. + *

+ * If the environment variable is missing from the request, {@code getPreAuthenticatedPrincipal} will + * throw an exception. You can override this behaviour by setting the + * {@code exceptionIfVariableMissing} property. + * + * + * @author Milan Sevcik + * @since 4.2 + */ +public class EnvironmentVariableAuthenticationFilter extends + AbstractPreAuthenticatedProcessingFilter { + private String principalEnvironmentVariable = "REMOTE_USER"; + private String credentialsEnvironmentVariable; + private boolean exceptionIfVariableMissing = true; + + /** + * Read and returns the variable named by {@code principalEnvironmentVariable} from the + * request. + * + * @throws PreAuthenticatedCredentialsNotFoundException if the environment variable + * is missing and {@code exceptionIfVariableMissing} is set to {@code true}. + */ + protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { + String principal = (String)request.getAttribute(principalEnvironmentVariable); + + if (principal == null && exceptionIfVariableMissing) { + throw new PreAuthenticatedCredentialsNotFoundException(principalEnvironmentVariable + + " variable not found in request."); + } + + return principal; + } + + /** + * Credentials aren't usually applicable, but if a {@code credentialsEnvironmentVariable} is + * set, this will be read and used as the credentials value. Otherwise a dummy value + * will be used. + */ + protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { + if (credentialsEnvironmentVariable != null) { + return request.getAttribute(credentialsEnvironmentVariable); + } + + return "N/A"; + } + + public void setPrincipalEnvironmentVariable(String principalEnvironmentVariable) { + Assert.hasText(principalEnvironmentVariable, + "principalEnvironmentVariable must not be empty or null"); + this.principalEnvironmentVariable = principalEnvironmentVariable; + } + + public void setCredentialsEnvironmentVariable(String credentialsEnvironmentVariable) { + Assert.hasText(credentialsEnvironmentVariable, + "credentialsEnvironmentVariable must not be empty or null"); + this.credentialsEnvironmentVariable = credentialsEnvironmentVariable; + } + + /** + * Defines whether an exception should be raised if the principal variable is missing. + * Defaults to {@code true}. + * + * @param exceptionIfVariableMissing set to {@code false} to override the default + * behaviour and allow the request to proceed if no variable is found. + */ + public void setExceptionIfVariableMissing(boolean exceptionIfVariableMissing) { + this.exceptionIfVariableMissing = exceptionIfVariableMissing; + } +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/envvariable/EnvironmentVariableAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/envvariable/EnvironmentVariableAuthenticationFilterTests.java new file mode 100644 index 0000000000..40afd9ac6c --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/envvariable/EnvironmentVariableAuthenticationFilterTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2016 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.preauth.envvariable; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException; +import org.springframework.security.web.authentication.preauth.EnvironmentVariableAuthenticationFilter; + +/** + * + * @author Milan Sevcik + */ +public class EnvironmentVariableAuthenticationFilterTests { + + @After + @Before + public void clearContext() { + SecurityContextHolder.clearContext(); + } + + @Test(expected = PreAuthenticatedCredentialsNotFoundException.class) + public void rejectsMissingHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + EnvironmentVariableAuthenticationFilter filter = new EnvironmentVariableAuthenticationFilter(); + + filter.doFilter(request, response, chain); + } + + @Test + public void defaultsToUsingSiteminderHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute("REMOTE_USER", "cat"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + EnvironmentVariableAuthenticationFilter filter = new EnvironmentVariableAuthenticationFilter(); + filter.setAuthenticationManager(createAuthenticationManager()); + + filter.doFilter(request, response, chain); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("cat"); + assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("N/A"); + } + + @Test + public void alternativeHeaderNameIsSupported() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute("myUsernameVariable", "wolfman"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + EnvironmentVariableAuthenticationFilter filter = new EnvironmentVariableAuthenticationFilter(); + filter.setAuthenticationManager(createAuthenticationManager()); + filter.setPrincipalEnvironmentVariable("myUsernameVariable"); + + filter.doFilter(request, response, chain); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("wolfman"); + } + + @Test + public void credentialsAreRetrievedIfHeaderNameIsSet() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + EnvironmentVariableAuthenticationFilter filter = new EnvironmentVariableAuthenticationFilter(); + filter.setAuthenticationManager(createAuthenticationManager()); + filter.setCredentialsEnvironmentVariable("myCredentialsVariable"); + request.setAttribute("REMOTE_USER", "cat"); + request.setAttribute("myCredentialsVariable", "catspassword"); + + filter.doFilter(request, response, chain); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("catspassword"); + } + + @Test + public void userIsReauthenticatedIfPrincipalChangesAndCheckForPrincipalChangesIsSet() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + EnvironmentVariableAuthenticationFilter filter = new EnvironmentVariableAuthenticationFilter(); + filter.setAuthenticationManager(createAuthenticationManager()); + filter.setCheckForPrincipalChanges(true); + request.setAttribute("REMOTE_USER", "cat"); + filter.doFilter(request, response, new MockFilterChain()); + request = new MockHttpServletRequest(); + request.setAttribute("REMOTE_USER", "dog"); + filter.doFilter(request, response, new MockFilterChain()); + Authentication dog = SecurityContextHolder.getContext().getAuthentication(); + assertThat(dog).isNotNull(); + assertThat(dog.getName()).isEqualTo("dog"); + // Make sure authentication doesn't occur every time (i.e. if the variable *doesn't* + // change) + filter.setAuthenticationManager(mock(AuthenticationManager.class)); + filter.doFilter(request, response, new MockFilterChain()); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(dog); + } + + @Test(expected = PreAuthenticatedCredentialsNotFoundException.class) + public void missingHeaderCausesException() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + EnvironmentVariableAuthenticationFilter filter = new EnvironmentVariableAuthenticationFilter(); + filter.setAuthenticationManager(createAuthenticationManager()); + + filter.doFilter(request, response, chain); + } + + @Test + public void missingHeaderIsIgnoredIfExceptionIfHeaderMissingIsFalse() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + EnvironmentVariableAuthenticationFilter filter = new EnvironmentVariableAuthenticationFilter(); + filter.setExceptionIfVariableMissing(false); + filter.setAuthenticationManager(createAuthenticationManager()); + filter.doFilter(request, response, chain); + } + + /** + * Create an authentication manager which returns the passed in object. + */ + private AuthenticationManager createAuthenticationManager() { + AuthenticationManager am = mock(AuthenticationManager.class); + when(am.authenticate(any(Authentication.class))).thenAnswer( + new Answer() { + public Authentication answer(InvocationOnMock invocation) + throws Throwable { + return (Authentication) invocation.getArguments()[0]; + } + }); + + return am; + } +}