diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index 717aa9e74b..b953520365 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -18,7 +18,7 @@ package org.springframework.security.config.annotation.web.configurers.saml2; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; -import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; @@ -103,6 +103,25 @@ public final class Saml2LoginConfigurer> extend private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + private AuthenticationManager authenticationManager; + + private Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter; + + /** + * Allows a configuration of a {@link AuthenticationManager} to be used during SAML 2 authentication. + * If none is specified, the system will create one inject it into the {@link Saml2WebSsoAuthenticationFilter} + * @param authenticationManager the authentication manager to be used + * @return the {@link Saml2LoginConfigurer} for further configuration + * @throws IllegalArgumentException if authenticationManager is null + * configure the default manager + * @since 5.3 + */ + public Saml2LoginConfigurer authenticationManager(AuthenticationManager authenticationManager) { + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + this.authenticationManager = authenticationManager; + return this; + } + /** * Sets the {@code RelyingPartyRegistrationRepository} of relying parties, each party representing a * service provider, SP and this host, and identity provider, IDP pair that communicate with each other. @@ -164,11 +183,11 @@ public final class Saml2LoginConfigurer> extend this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class); } - Saml2WebSsoAuthenticationFilter webSsoFilter = new Saml2WebSsoAuthenticationFilter( + saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter( this.relyingPartyRegistrationRepository, this.loginProcessingUrl ); - setAuthenticationFilter(webSsoFilter); + setAuthenticationFilter(saml2WebSsoAuthenticationFilter); super.loginProcessingUrl(this.loginProcessingUrl); if (hasText(this.loginPage)) { @@ -197,7 +216,7 @@ public final class Saml2LoginConfigurer> extend super.init(http); } } - http.authenticationProvider(getAuthenticationProvider()); + this.initDefaultLoginFilter(http); } @@ -211,11 +230,17 @@ public final class Saml2LoginConfigurer> extend public void configure(B http) throws Exception { http.addFilter(this.authenticationRequestEndpoint.build(http)); super.configure(http); + if (this.authenticationManager == null) { + registerDefaultAuthenticationProvider(http); + } + else { + saml2WebSsoAuthenticationFilter.setAuthenticationManager(this.authenticationManager); + } } - private AuthenticationProvider getAuthenticationProvider() { - AuthenticationProvider provider = new OpenSamlAuthenticationProvider(); - return postProcess(provider); + private void registerDefaultAuthenticationProvider(B http) { + OpenSamlAuthenticationProvider provider = postProcess(new OpenSamlAuthenticationProvider()); + http.authenticationProvider(provider); } private void registerDefaultCsrfOverride(B http) { @@ -313,5 +338,4 @@ public final class Saml2LoginConfigurer> extend } } - } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java new file mode 100644 index 0000000000..bd280f0e39 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -0,0 +1,262 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.config.annotation.web.configurers.saml2; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.opensaml.saml.saml2.core.Assertion; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.convert.converter.Converter; +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.authentication.AuthenticationProvider; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; + +import java.io.IOException; +import java.time.Duration; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import javax.servlet.ServletException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.codehaus.groovy.runtime.InvokerHelper.asList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.security.config.annotation.web.configurers.saml2.TestRelyingPartyRegistrations.saml2AuthenticationConfiguration; + +/** + * Tests for different Java configuration for {@link Saml2LoginConfigurer} + */ +public class Saml2LoginConfigurerTests { + + private static final Converter> + AUTHORITIES_EXTRACTOR = a -> asList(new SimpleGrantedAuthority("TEST")); + private static final GrantedAuthoritiesMapper AUTHORITIES_MAPPER = + authorities -> asList(new SimpleGrantedAuthority("TEST CONVERTED")); + private static final Duration RESPONSE_TIME_VALIDATION_SKEW = Duration.ZERO; + + @Autowired + private ConfigurableApplicationContext context; + + @Autowired + private FilterChainProxy springSecurityFilterChain; + + @Autowired + private RelyingPartyRegistrationRepository repository; + + @Autowired + SecurityContextRepository securityContextRepository; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired(required = false) + MockMvc mvc; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private MockFilterChain filterChain; + + @Before + public void setup() { + this.request = new MockHttpServletRequest("POST", ""); + this.request.setServletPath("/login/saml2/sso/test-rp"); + this.response = new MockHttpServletResponse(); + this.filterChain = new MockFilterChain(); + } + + @After + public void cleanup() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void saml2LoginWhenConfiguringAuthenticationManagerThenTheManagerIsUsed() throws Exception { + // setup application context + this.spring.register(Saml2LoginConfigWithCustomAuthenticationManager.class).autowire(); + performSaml2Login("ROLE_AUTH_MANAGER"); + } + + @Test + public void saml2LoginWhenConfiguringAuthenticationDefaultsUsingCustomizerThenTheProviderIsConfigured() throws Exception { + // setup application context + this.spring.register(Saml2LoginConfigWithAuthenticationDefaultsWithPostProcessor.class).autowire(); + validateSaml2WebSsoAuthenticationFilterConfiguration(); + } + + private void validateSaml2WebSsoAuthenticationFilterConfiguration() { + // get the OpenSamlAuthenticationProvider + Saml2WebSsoAuthenticationFilter filter = getSaml2SsoFilter(this.springSecurityFilterChain); + AuthenticationManager manager = + (AuthenticationManager) ReflectionTestUtils.getField(filter, "authenticationManager"); + ProviderManager pm = (ProviderManager) manager; + AuthenticationProvider provider = pm.getProviders() + .stream() + .filter(p -> p instanceof OpenSamlAuthenticationProvider) + .findFirst() + .get(); + Assert.assertSame(AUTHORITIES_EXTRACTOR, ReflectionTestUtils.getField(provider, "authoritiesExtractor")); + Assert.assertSame(AUTHORITIES_MAPPER, ReflectionTestUtils.getField(provider, "authoritiesMapper")); + Assert.assertSame(RESPONSE_TIME_VALIDATION_SKEW, ReflectionTestUtils.getField(provider, "responseTimeValidationSkew")); + } + + private Saml2WebSsoAuthenticationFilter getSaml2SsoFilter(FilterChainProxy chain) { + return (Saml2WebSsoAuthenticationFilter) chain.getFilters("/login/saml2/sso/test") + .stream() + .filter(f -> f instanceof Saml2WebSsoAuthenticationFilter) + .findFirst() + .get(); + } + + private void performSaml2Login(String expected) throws IOException, ServletException { + // setup authentication parameters + this.request.setParameter( + "SAMLResponse", + Base64.getEncoder().encodeToString( + "saml2-xml-response-object".getBytes() + ) + ); + + + // perform test + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + // assertions + Authentication authentication = this.securityContextRepository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)) + .getAuthentication(); + Assert.assertNotNull("Expected a valid authentication object.", authentication); + assertThat(authentication.getAuthorities()).hasSize(1); + assertThat(authentication.getAuthorities()).first() + .isInstanceOf(SimpleGrantedAuthority.class).hasToString(expected); + } + + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LoginConfigWithCustomAuthenticationManager extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.saml2Login() + .authenticationManager( + getAuthenticationManagerMock("ROLE_AUTH_MANAGER") + ); + super.configure(http); + } + } + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LoginConfigWithAuthenticationDefaultsWithPostProcessor extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + ObjectPostProcessor processor + = new ObjectPostProcessor() { + @Override + public O postProcess(O provider) { + provider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW); + provider.setAuthoritiesMapper(AUTHORITIES_MAPPER); + provider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR); + return provider; + } + }; + + http.saml2Login() + .addObjectPostProcessor(processor) + ; + super.configure(http); + } + } + + private static AuthenticationManager getAuthenticationManagerMock(String role) { + return new AuthenticationManager() { + + @Override + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + if (!supports(authentication.getClass())) { + throw new AuthenticationServiceException("not supported"); + } + return new Saml2Authentication( + () -> "auth principal", + "saml2 response", + Collections.singletonList( + new SimpleGrantedAuthority(role) + ) + ); + } + + public boolean supports(Class authentication) { + return authentication.isAssignableFrom(Saml2AuthenticationToken.class); + } + }; + } + + static class Saml2LoginConfigBeans { + + @Bean + SecurityContextRepository securityContextRepository() { + return new HttpSessionSecurityContextRepository(); + } + + @Bean + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { + RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class); + when(repository.findByRegistrationId(anyString())).thenReturn( + saml2AuthenticationConfiguration() + ); + return repository; + } + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestRelyingPartyRegistrations.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestRelyingPartyRegistrations.java new file mode 100644 index 0000000000..a9103f27e7 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestRelyingPartyRegistrations.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.config.annotation.web.configurers.saml2; + +import org.springframework.security.saml2.credentials.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; + +import static org.springframework.security.config.annotation.web.configurers.saml2.TestSaml2Credentials.signingCredential; +import static org.springframework.security.config.annotation.web.configurers.saml2.TestSaml2Credentials.verificationCertificate; + +/** + * Preconfigured test data for {@link RelyingPartyRegistration} objects + */ +public class TestRelyingPartyRegistrations { + + static RelyingPartyRegistration saml2AuthenticationConfiguration() { + //remote IDP entity ID + String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"; + //remote WebSSO Endpoint - Where to Send AuthNRequests to + String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"; + //local registration ID + String registrationId = "simplesamlphp"; + //local entity ID - autogenerated based on URL + String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; + //local signing (and decryption key) + Saml2X509Credential signingCredential = signingCredential(); + //IDP certificate for verification of incoming messages + Saml2X509Credential idpVerificationCertificate = verificationCertificate(); + String acsUrlTemplate = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; + return RelyingPartyRegistration.withRegistrationId(registrationId) + .remoteIdpEntityId(idpEntityId) + .idpWebSsoUrl(webSsoEndpoint) + .credentials(c -> c.add(signingCredential)) + .credentials(c -> c.add(idpVerificationCertificate)) + .localEntityIdTemplate(localEntityIdTemplate) + .assertionConsumerServiceUrlTemplate(acsUrlTemplate) + .build(); + } + + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestSaml2Credentials.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestSaml2Credentials.java new file mode 100644 index 0000000000..4a6922f4df --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestSaml2Credentials.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.config.annotation.web.configurers.saml2; + +import org.springframework.security.converter.RsaKeyConverters; +import org.springframework.security.saml2.credentials.Saml2X509Credential; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION; + +/** + * Preconfigured SAML credentials for SAML integration tests. + */ +public class TestSaml2Credentials { + + static Saml2X509Credential verificationCertificate() { + String certificate = "-----BEGIN CERTIFICATE-----\n" + + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD\n" + + "VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD\n" + + "VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX\n" + + "c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw\n" + + "aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ\n" + + "BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa\n" + + "BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD\n" + + "DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr\n" + + "QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62\n" + + "E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz\n" + + "2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW\n" + + "RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ\n" + + "nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5\n" + + "cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph\n" + + "iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5\n" + + "ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD\n" + + "AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO\n" + + "nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v\n" + + "ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu\n" + + "xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z\n" + + "V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3\n" + + "lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + "-----END CERTIFICATE-----"; + return new Saml2X509Credential( + x509Certificate(certificate), + VERIFICATION + ); + } + + static X509Certificate x509Certificate(String source) { + try { + final CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate( + new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)) + ); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + static Saml2X509Credential signingCredential() { + String key = "-----BEGIN PRIVATE KEY-----\n" + + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" + + "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" + + "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" + + "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" + + "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" + + "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" + + "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" + + "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" + + "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" + + "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" + + "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" + + "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" + + "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" + + "INrtuLp4YHbgk1mi\n" + + "-----END PRIVATE KEY-----"; + String certificate = "-----BEGIN CERTIFICATE-----\n" + + "MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" + + "VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" + + "A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" + + "DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" + + "MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" + + "MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" + + "TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" + + "vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" + + "+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" + + "y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" + + "XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" + + "qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" + + "RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" + + "-----END CERTIFICATE-----"; + PrivateKey pk = RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(key.getBytes())); + X509Certificate cert = x509Certificate(certificate); + return new Saml2X509Credential(pk, cert, SIGNING, DECRYPTION); + } +} diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc index 3dad68482c..0c308a50e2 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc @@ -223,6 +223,96 @@ public interface Saml2AuthenticationRequestFactory { } ---- +=== Customizing Authentication Logic + +By default Spring Security configures the `OpenSamlAuthenticationProvider` +to validate and parse the SAML 2 response and assertions that are received. +This provider has three configuration options + +1. An authorities extractor - extract group information from the assertion +2. An authorities mapper - map extracted group information to internal authorities +3. Response time validation duration - the built in tolerances for timestamp validation +should be used when there may be a time synchronization issue. + +One customization strategy is to use an `ObjectPostProcessor`, which allows you to modify the +objects created by the implementation. Another option is to override the authentication +manager for the filter that intercepts the SAMLResponse. + +==== OpenSamlAuthenticationProvider ObjectPostProcessor + +[source,java] +---- +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + ObjectPostProcessor processor = new ObjectPostProcessor<>() { + @Override + public O postProcess(O provider) { + provider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW); + provider.setAuthoritiesMapper(AUTHORITIES_MAPPER); + provider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR); + return provider; + } + }; + + http + .saml2Login() + .addObjectPostProcessor(processor) + ; + super.configure(http); + } +} +---- + +==== Configure OpenSamlAuthenticationProvider as an Authentication Manager +We can leverage the same method, `authenticationManager`, to override and customize the default +`OpenSamlAuthenticationProvider`. + +[source,java] +---- +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + OpenSamlAuthenticationProvider authProvider = new OpenSamlAuthenticationProvider(); + authProvider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW); + authProvider.setAuthoritiesMapper(AUTHORITIES_MAPPER); + authProvider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR); + http + .saml2Login() + .authenticationManager(new ProviderManager(asList(authProvider))) + ; + super.configure(http); + } +} +---- + +==== Custom Authentication Manager +The authentication manager for the security filter can also be overwritten, using your own +custom `AuthenticationManager` implementation. +This authentication manager should expect a `Saml2AuthenticationToken` object +containing the SAML 2 Response XML data. + +[source,java] +---- +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...); + http + .saml2Login() + .authenticationManager(authenticationManager) + ; + super.configure(http); + } +} +---- + [[samllogin-sample-boot]] === Spring Boot 2.x Sample diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java index 900f3cd820..a45af3e78b 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java @@ -15,17 +15,6 @@ */ package org.springframework.security.saml2.provider.service.authentication; -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.credentials.Saml2X509Credential; -import org.springframework.util.Assert; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.opensaml.saml.common.SignableSAMLObject; @@ -61,6 +50,16 @@ import org.opensaml.xmlsec.signature.support.SignaturePrevalidator; import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; import org.opensaml.xmlsec.signature.support.SignatureValidator; import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.credentials.Saml2X509Credential; +import org.springframework.util.Assert; import java.security.cert.X509Certificate; import java.time.Duration; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java index 9c0d519de3..150d3568a0 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java @@ -43,13 +43,23 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce private final RequestMatcher matcher; private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + /** + * Creates a {@code Saml2WebSsoAuthenticationFilter} authentication filter that is configured + * to use the {@link #DEFAULT_FILTER_PROCESSES_URI} processing URL + * @param relyingPartyRegistrationRepository - repository of configured SAML 2 entities. Required. + */ public Saml2WebSsoAuthenticationFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { this(relyingPartyRegistrationRepository, DEFAULT_FILTER_PROCESSES_URI); } + /** + * Creates a {@code Saml2WebSsoAuthenticationFilter} authentication filter + * @param relyingPartyRegistrationRepository - repository of configured SAML 2 entities. Required. + * @param filterProcessesUrl the processing URL, must contain a {registrationId} variable. Required. + */ public Saml2WebSsoAuthenticationFilter( - RelyingPartyRegistrationRepository relyingPartyRegistrationRepository, - String filterProcessesUrl) { + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository, + String filterProcessesUrl) { super(filterProcessesUrl); Assert.notNull(relyingPartyRegistrationRepository, "relyingPartyRegistrationRepository cannot be null"); Assert.hasText(filterProcessesUrl, "filterProcessesUrl must contain a URL pattern");