mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-30 00:32:14 +00:00
Remove Deprecated OpenSAML 3 Support
Closes gh-10556
This commit is contained in:
parent
2a487ae7f8
commit
48e31f87e4
@ -118,14 +118,6 @@ updateDependenciesSettings {
|
||||
selection.reject("nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency");
|
||||
}
|
||||
}
|
||||
components.all { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
// Do not compare version due to multiple versions existing
|
||||
// will cause opensaml 3.x to be updated to 4.x
|
||||
if (candidate.getGroup().equals("org.opensaml")) {
|
||||
selection.reject("org.opensaml maintains two different versions, so it must be updated manually");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,8 +46,8 @@ dependencies {
|
||||
testImplementation project(path : ':spring-security-ldap', configuration : 'tests')
|
||||
testImplementation project(path : ':spring-security-oauth2-client', configuration : 'tests')
|
||||
testImplementation project(path : ':spring-security-oauth2-resource-server', configuration : 'tests')
|
||||
testImplementation project(':spring-security-saml2-service-provider')
|
||||
testImplementation project(path : ':spring-security-saml2-service-provider', configuration : 'tests')
|
||||
testImplementation project(path : ':spring-security-saml2-service-provider', configuration : 'opensaml4MainImplementation')
|
||||
testImplementation project(path : ':spring-security-web', configuration : 'tests')
|
||||
testImplementation "jakarta.inject:jakarta.inject-api"
|
||||
testImplementation "org.assertj:assertj-core"
|
||||
|
@ -19,8 +19,6 @@ package org.springframework.security.config.annotation.web.configurers.saml2;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.opensaml.core.Version;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
@ -33,7 +31,6 @@ import org.springframework.security.config.annotation.web.configurers.CsrfConfig
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
|
||||
@ -43,7 +40,6 @@ import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2A
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml3AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
@ -200,10 +196,6 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
* @since 6.0
|
||||
*/
|
||||
public Saml2LoginConfigurer<B> authenticationRequestUri(String authenticationRequestUri) {
|
||||
// OpenSAML 3 is no longer supported by spring security
|
||||
if (version().startsWith("3")) {
|
||||
return this;
|
||||
}
|
||||
Assert.state(authenticationRequestUri.contains("{registrationId}"),
|
||||
"authenticationRequestUri must contain {registrationId} path variable");
|
||||
this.authenticationRequestUri = authenticationRequestUri;
|
||||
@ -345,14 +337,11 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (bean != null) {
|
||||
return bean;
|
||||
}
|
||||
if (version().startsWith("4")) {
|
||||
OpenSaml4AuthenticationRequestResolver openSaml4AuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver(
|
||||
relyingPartyRegistrationResolver(http));
|
||||
openSaml4AuthenticationRequestResolver
|
||||
.setRequestMatcher(new AntPathRequestMatcher(this.authenticationRequestUri));
|
||||
return openSaml4AuthenticationRequestResolver;
|
||||
}
|
||||
return new OpenSaml3AuthenticationRequestResolver(relyingPartyRegistrationResolver(http));
|
||||
OpenSaml4AuthenticationRequestResolver openSaml4AuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver(
|
||||
relyingPartyRegistrationResolver(http));
|
||||
openSaml4AuthenticationRequestResolver
|
||||
.setRequestMatcher(new AntPathRequestMatcher(this.authenticationRequestUri));
|
||||
return openSaml4AuthenticationRequestResolver;
|
||||
}
|
||||
|
||||
private AuthenticationConverter getAuthenticationConverter(B http) {
|
||||
@ -370,22 +359,8 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return authenticationConverterBean;
|
||||
}
|
||||
|
||||
private String version() {
|
||||
String version = Version.getVersion();
|
||||
if (version != null) {
|
||||
return version;
|
||||
}
|
||||
return Version.class.getModule().getDescriptor().version().map(Object::toString)
|
||||
.orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
|
||||
}
|
||||
|
||||
private void registerDefaultAuthenticationProvider(B http) {
|
||||
if (version().startsWith("4")) {
|
||||
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
|
||||
}
|
||||
else {
|
||||
http.authenticationProvider(postProcess(new OpenSamlAuthenticationProvider()));
|
||||
}
|
||||
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
|
||||
}
|
||||
|
||||
private void registerDefaultCsrfOverride(B http) {
|
||||
|
@ -22,8 +22,6 @@ import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.opensaml.core.Version;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
@ -44,8 +42,6 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
|
||||
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
|
||||
@ -313,15 +309,6 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return this.context.getBean(clazz);
|
||||
}
|
||||
|
||||
private String version() {
|
||||
String version = Version.getVersion();
|
||||
if (version != null) {
|
||||
return version;
|
||||
}
|
||||
return Version.class.getModule().getDescriptor().version().map(Object::toString)
|
||||
.orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
|
||||
}
|
||||
|
||||
/**
|
||||
* A configurer for SAML 2.0 LogoutRequest components
|
||||
*/
|
||||
@ -401,10 +388,7 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
if (this.logoutRequestResolver != null) {
|
||||
return this.logoutRequestResolver;
|
||||
}
|
||||
if (version().startsWith("4")) {
|
||||
return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
return new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver);
|
||||
return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
|
||||
}
|
||||
@ -471,10 +455,7 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
private Saml2LogoutResponseResolver logoutResponseResolver(
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
if (this.logoutResponseResolver == null) {
|
||||
if (version().startsWith("4")) {
|
||||
return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
|
||||
return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
return this.logoutResponseResolver;
|
||||
}
|
||||
@ -511,12 +492,4 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
}
|
||||
|
||||
private static class NoopLogoutHandler implements LogoutHandler {
|
||||
|
||||
@Override
|
||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,9 +18,7 @@ package org.springframework.security.config.annotation.web.configurers.saml2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
@ -32,7 +30,6 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -40,18 +37,14 @@ import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
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.Customizer;
|
||||
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;
|
||||
@ -59,16 +52,12 @@ import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.Saml2Utils;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
@ -77,7 +66,6 @@ import org.springframework.security.saml2.provider.service.registration.InMemory
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
|
||||
@ -91,7 +79,6 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
|
||||
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 org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
@ -121,14 +108,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class Saml2LoginConfigurerTests {
|
||||
|
||||
private static final Converter<Assertion, Collection<? extends GrantedAuthority>> AUTHORITIES_EXTRACTOR = (
|
||||
a) -> Collections.singletonList(new SimpleGrantedAuthority("TEST"));
|
||||
|
||||
private static final GrantedAuthoritiesMapper AUTHORITIES_MAPPER = (authorities) -> Collections
|
||||
.singletonList(new SimpleGrantedAuthority("TEST CONVERTED"));
|
||||
|
||||
private static final Duration RESPONSE_TIME_VALIDATION_SKEW = Duration.ZERO;
|
||||
|
||||
private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=";
|
||||
|
||||
private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class);
|
||||
@ -197,14 +176,6 @@ public class Saml2LoginConfigurerTests {
|
||||
performSaml2Login("ROLE_AUTH_MANAGER");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saml2LoginWhenConfiguringAuthenticationDefaultsUsingCustomizerThenTheProviderIsConfigured()
|
||||
throws Exception {
|
||||
// setup application context
|
||||
this.spring.register(Saml2LoginConfigWithAuthenticationDefaultsWithPostProcessor.class).autowire();
|
||||
validateSaml2WebSsoAuthenticationFilterConfiguration();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticationRequestWhenAuthenticationRequestResolverBeanThenUses() throws Exception {
|
||||
this.spring.register(CustomAuthenticationRequestResolverBean.class).autowire();
|
||||
@ -362,22 +333,6 @@ public class Saml2LoginConfigurerTests {
|
||||
.andExpect(redirectedUrl("http://localhost/saml2/authenticate/registration-id"));
|
||||
}
|
||||
|
||||
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 OpenSaml4AuthenticationProvider).findFirst().get();
|
||||
assertThat(provider).isNotNull();
|
||||
}
|
||||
|
||||
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.setRequestURI("/login/saml2/sso/registration-id");
|
||||
@ -460,28 +415,6 @@ public class Saml2LoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
static class Saml2LoginConfigWithAuthenticationDefaultsWithPostProcessor extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
ObjectPostProcessor<OpenSamlAuthenticationProvider> processor = new ObjectPostProcessor<OpenSamlAuthenticationProvider>() {
|
||||
@Override
|
||||
public <O extends OpenSamlAuthenticationProvider> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
|
@ -154,14 +154,6 @@ Instead, such classes as `OpenSamlAuthenticationRequestFactory` and `OpenSamlAut
|
||||
|
||||
For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter delegates to `OpenSamlAuthenticationProvider`:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For backward compatibility, Spring Security will use the latest OpenSAML 3 by default.
|
||||
Note, though that OpenSAML 3 has reached it's end-of-life and updating to OpenSAML 4.x is recommended.
|
||||
For that reason, Spring Security supports both OpenSAML 3.x and 4.x.
|
||||
If you manage your OpenSAML dependency to 4.x, then Spring Security will select its OpenSAML 4.x implementations.
|
||||
====
|
||||
|
||||
.Authenticating an OpenSAML `Response`
|
||||
image:{figures}/opensamlauthenticationprovider.png[]
|
||||
|
||||
|
@ -6,6 +6,8 @@ Below are the highlights of the release.
|
||||
|
||||
== Breaking Changes
|
||||
|
||||
* https://github.com/spring-projects/spring-security/issues/10556[gh-10556] - Remove EOL OpenSaml 3 Support.
|
||||
Use the OpenSaml 4 Support instead.
|
||||
* https://github.com/spring-projects/spring-security/issues/8980[gh-8980] - Remove unsafe/deprecated `Encryptors.querableText(CharSequence,CharSequence)`.
|
||||
Instead use data storage to encrypt values.
|
||||
* https://github.com/spring-projects/spring-security/issues/11520[gh-11520] - Remember Me uses SHA256 by default
|
||||
|
@ -2,7 +2,7 @@ aspectjVersion=1.9.9.1
|
||||
springJavaformatVersion=0.0.34
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=6.0.0-M6
|
||||
openSamlVersion=3.4.6
|
||||
openSamlVersion=4.1.1
|
||||
version=6.0.0-SNAPSHOT
|
||||
kotlinVersion=1.7.10
|
||||
samplesBranch=main
|
||||
|
@ -1,44 +1,4 @@
|
||||
apply plugin: 'io.spring.convention.spring-module'
|
||||
apply plugin: 'nebula.facet'
|
||||
|
||||
facets {
|
||||
opensaml3Main {
|
||||
parentSourceSet = 'main'
|
||||
}
|
||||
opensaml4Main {
|
||||
parentSourceSet = 'main'
|
||||
}
|
||||
opensaml3Test {
|
||||
parentSourceSet = 'opensaml3Main'
|
||||
}
|
||||
opensaml4Test {
|
||||
parentSourceSet = 'opensaml4Main'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
opensaml3Test {
|
||||
compileClasspath += sourceSets.test.output
|
||||
runtimeClasspath += sourceSets.test.output
|
||||
}
|
||||
opensaml4Test {
|
||||
compileClasspath += sourceSets.test.output
|
||||
runtimeClasspath += sourceSets.test.output
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
opensaml3TestImplementation.extendsFrom testImplementation
|
||||
opensaml4TestImplementation.extendsFrom testImplementation
|
||||
opensaml4MainImplementation {
|
||||
canBeConsumed = true
|
||||
}
|
||||
}
|
||||
|
||||
compileOpensaml4MainJava {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
dependencies {
|
||||
management platform(project(":spring-security-dependencies"))
|
||||
@ -50,13 +10,6 @@ dependencies {
|
||||
api ("org.opensaml:opensaml-saml-impl") {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
}
|
||||
opensaml4MainImplementation "org.opensaml:opensaml-core:4.1.0"
|
||||
opensaml4MainImplementation ("org.opensaml:opensaml-saml-api:4.1.0") {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
}
|
||||
opensaml4MainImplementation ("org.opensaml:opensaml-saml-impl:4.1.0") {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
}
|
||||
|
||||
provided 'jakarta.servlet:jakarta.servlet-api'
|
||||
|
||||
@ -73,35 +26,3 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
}
|
||||
|
||||
project.tasks.matching { t -> t.name == "jar"}.configureEach {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
from {
|
||||
compileOpensaml3MainJava
|
||||
}
|
||||
from {
|
||||
compileOpensaml4MainJava
|
||||
}
|
||||
}
|
||||
|
||||
project.tasks.matching { t -> t.name == "sourcesJar"}.configureEach {
|
||||
from {
|
||||
sourceSets.opensaml3Main.allSource
|
||||
}
|
||||
from {
|
||||
sourceSets.opensaml4Main.allSource
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
javadoc {
|
||||
source += sourceSets.opensaml3Main.allJava + sourceSets.opensaml4Main.allJava
|
||||
}
|
||||
|
||||
opensaml3Test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
opensaml4Test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ import org.springframework.util.StringUtils;
|
||||
* @see <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38">SAML 2
|
||||
* StatusResponse</a>
|
||||
* @see <a href="https://wiki.shibboleth.net/confluence/display/OS30/Home">OpenSAML 3</a>
|
||||
* @see <a href="https://shibboleth.atlassian.net/wiki/spaces/OSAML/overview">OpenSAML</a>
|
||||
*/
|
||||
public final class OpenSaml4AuthenticationProvider implements AuthenticationProvider {
|
||||
|
@ -1,864 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.saml2.provider.service.authentication;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import net.shibboleth.utilities.java.support.xml.ParserPool;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.opensaml.core.config.ConfigurationService;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
||||
import org.opensaml.core.xml.schema.XSAny;
|
||||
import org.opensaml.core.xml.schema.XSBoolean;
|
||||
import org.opensaml.core.xml.schema.XSBooleanValue;
|
||||
import org.opensaml.core.xml.schema.XSDateTime;
|
||||
import org.opensaml.core.xml.schema.XSInteger;
|
||||
import org.opensaml.core.xml.schema.XSString;
|
||||
import org.opensaml.core.xml.schema.XSURI;
|
||||
import org.opensaml.saml.common.assertion.AssertionValidationException;
|
||||
import org.opensaml.saml.common.assertion.ValidationContext;
|
||||
import org.opensaml.saml.common.assertion.ValidationResult;
|
||||
import org.opensaml.saml.saml2.assertion.ConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
|
||||
import org.opensaml.saml.saml2.assertion.StatementValidator;
|
||||
import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
import org.opensaml.saml.saml2.core.Condition;
|
||||
import org.opensaml.saml.saml2.core.EncryptedAssertion;
|
||||
import org.opensaml.saml.saml2.core.OneTimeUse;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
||||
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
|
||||
import org.opensaml.saml.saml2.encryption.Decrypter;
|
||||
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
|
||||
import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
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.core.OpenSamlInitializationService;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link AuthenticationProvider} for SAML authentications when
|
||||
* receiving a {@code Response} object containing an {@code Assertion}. This
|
||||
* implementation uses the {@code OpenSAML 3} library.
|
||||
*
|
||||
* <p>
|
||||
* The {@link OpenSamlAuthenticationProvider} supports {@link Saml2AuthenticationToken}
|
||||
* objects that contain a SAML response in its decoded XML format
|
||||
* {@link Saml2AuthenticationToken#getSaml2Response()} along with the information about
|
||||
* the asserting party, the identity provider (IDP), as well as the relying party, the
|
||||
* service provider (SP, this application).
|
||||
* <p>
|
||||
* The {@link Saml2AuthenticationToken} will be processed into a SAML Response object. The
|
||||
* SAML response object can be signed. If the Response is signed, a signature will not be
|
||||
* required on the assertion.
|
||||
* <p>
|
||||
* While a response object can contain a list of assertion, this provider will only
|
||||
* leverage the first valid assertion for the purpose of authentication. Assertions that
|
||||
* do not pass validation will be ignored. If no valid assertions are found a
|
||||
* {@link Saml2AuthenticationException} is thrown.
|
||||
* <p>
|
||||
* This provider supports two types of encrypted SAML elements
|
||||
* <ul>
|
||||
* <li><a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=17">EncryptedAssertion</a></li>
|
||||
* <li><a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=14">EncryptedID</a></li>
|
||||
* </ul>
|
||||
* If the assertion is encrypted, then signature validation on the assertion is no longer
|
||||
* required.
|
||||
* <p>
|
||||
* This provider does not perform an X509 certificate validation on the configured
|
||||
* asserting party, IDP, verification certificates.
|
||||
*
|
||||
* @author Ryan Cassar
|
||||
* @since 5.2
|
||||
* @see <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38">SAML 2
|
||||
* StatusResponse</a>
|
||||
* @see <a href="https://wiki.shibboleth.net/confluence/display/OS30/Home">OpenSAML 3</a>
|
||||
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
|
||||
* {@code OpenSaml4AuthenticationProvider}
|
||||
*/
|
||||
public final class OpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
static {
|
||||
OpenSamlInitializationService.initialize();
|
||||
}
|
||||
|
||||
private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class);
|
||||
|
||||
private final XMLObjectProviderRegistry registry;
|
||||
|
||||
private final ResponseUnmarshaller responseUnmarshaller;
|
||||
|
||||
private final ParserPool parserPool;
|
||||
|
||||
private Converter<Assertion, Collection<? extends GrantedAuthority>> authoritiesExtractor = ((a) -> Collections
|
||||
.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = ((a) -> a);
|
||||
|
||||
private Duration responseTimeValidationSkew = Duration.ofMinutes(5);
|
||||
|
||||
private Converter<ResponseToken, Saml2ResponseValidatorResult> responseSignatureValidator = createDefaultResponseSignatureValidator();
|
||||
|
||||
private Consumer<ResponseToken> responseElementsDecrypter = createDefaultResponseElementsDecrypter();
|
||||
|
||||
private Converter<ResponseToken, Saml2ResponseValidatorResult> responseValidator = createDefaultResponseValidator();
|
||||
|
||||
private Converter<AssertionToken, Saml2ResponseValidatorResult> assertionSignatureValidator = createDefaultAssertionSignatureValidator();
|
||||
|
||||
private Consumer<AssertionToken> assertionElementsDecrypter = createDefaultAssertionElementsDecrypter();
|
||||
|
||||
private Converter<AssertionToken, Saml2ResponseValidatorResult> assertionValidator = createCompatibleAssertionValidator();
|
||||
|
||||
private Converter<ResponseToken, ? extends AbstractAuthenticationToken> responseAuthenticationConverter = createCompatibleResponseAuthenticationConverter();
|
||||
|
||||
/**
|
||||
* Creates an {@link OpenSamlAuthenticationProvider}
|
||||
*/
|
||||
public OpenSamlAuthenticationProvider() {
|
||||
this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
|
||||
this.responseUnmarshaller = (ResponseUnmarshaller) this.registry.getUnmarshallerFactory()
|
||||
.getUnmarshaller(Response.DEFAULT_ELEMENT_NAME);
|
||||
this.parserPool = this.registry.getParserPool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link Consumer} strategy to use for decrypting elements of a validated
|
||||
* {@link Response}. The default strategy decrypts all {@link EncryptedAssertion}s
|
||||
* using OpenSAML's {@link Decrypter}, adding the results to
|
||||
* {@link Response#getAssertions()}.
|
||||
*
|
||||
* You can use this method to configure the {@link Decrypter} instance like so:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* provider.setResponseElementsDecrypter((responseToken) -> {
|
||||
* DecrypterParameters parameters = new DecrypterParameters();
|
||||
* // ... set parameters as needed
|
||||
* Decrypter decrypter = new Decrypter(parameters);
|
||||
* Response response = responseToken.getResponse();
|
||||
* EncryptedAssertion encrypted = response.getEncryptedAssertions().get(0);
|
||||
* try {
|
||||
* Assertion assertion = decrypter.decrypt(encrypted);
|
||||
* response.getAssertions().add(assertion);
|
||||
* } catch (Exception e) {
|
||||
* throw new Saml2AuthenticationException(...);
|
||||
* }
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* Or, in the event that you have your own custom decryption interface, the same
|
||||
* pattern applies:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* Converter<EncryptedAssertion, Assertion> myService = ...
|
||||
* provider.setResponseDecrypter((responseToken) -> {
|
||||
* Response response = responseToken.getResponse();
|
||||
* response.getEncryptedAssertions().stream()
|
||||
* .map(service::decrypt).forEach(response.getAssertions()::add);
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* This is valuable when using an external service to perform the decryption.
|
||||
* @param responseElementsDecrypter the {@link Consumer} for decrypting response
|
||||
* elements
|
||||
* @since 5.5
|
||||
*/
|
||||
public void setResponseElementsDecrypter(Consumer<ResponseToken> responseElementsDecrypter) {
|
||||
Assert.notNull(responseElementsDecrypter, "responseElementsDecrypter cannot be null");
|
||||
this.responseElementsDecrypter = responseElementsDecrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link Converter} to use for validating each {@link Assertion} in the SAML
|
||||
* 2.0 Response.
|
||||
*
|
||||
* You can still invoke the default validator by delgating to
|
||||
* {@link #createAssertionValidator}, like so:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* provider.setAssertionValidator(assertionToken -> {
|
||||
* Saml2ResponseValidatorResult result = createDefaultAssertionValidator()
|
||||
* .convert(assertionToken)
|
||||
* return result.concat(myCustomValidator.convert(assertionToken));
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* You can also use this method to configure the provider to use a different
|
||||
* {@link ValidationContext} from the default, like so:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* provider.setAssertionValidator(
|
||||
* createDefaultAssertionValidator(assertionToken -> {
|
||||
* Map<String, Object> params = new HashMap<>();
|
||||
* params.put(CLOCK_SKEW, 2 * 60 * 1000);
|
||||
* // other parameters
|
||||
* return new ValidationContext(params);
|
||||
* }));
|
||||
* </pre>
|
||||
*
|
||||
* Consider taking a look at {@link #createValidationContext} to see how it constructs
|
||||
* a {@link ValidationContext}.
|
||||
*
|
||||
* It is not necessary to delegate to the default validator. You can safely replace it
|
||||
* entirely with your own. Note that signature verification is performed as a separate
|
||||
* step from this validator.
|
||||
*
|
||||
* This method takes precedence over {@link #setResponseTimeValidationSkew}.
|
||||
* @param assertionValidator the strategy for validating a given assertion
|
||||
* @since 5.4
|
||||
*/
|
||||
public void setAssertionValidator(Converter<AssertionToken, Saml2ResponseValidatorResult> assertionValidator) {
|
||||
Assert.notNull(assertionValidator, "assertionValidator cannot be null");
|
||||
this.assertionValidator = assertionValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link Consumer} strategy to use for decrypting elements of a validated
|
||||
* {@link Assertion}.
|
||||
*
|
||||
* You can use this method to configure the {@link Decrypter} used like so:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* provider.setResponseDecrypter((assertionToken) -> {
|
||||
* DecrypterParameters parameters = new DecrypterParameters();
|
||||
* // ... set parameters as needed
|
||||
* Decrypter decrypter = new Decrypter(parameters);
|
||||
* Assertion assertion = assertionToken.getAssertion();
|
||||
* EncryptedID encrypted = assertion.getSubject().getEncryptedID();
|
||||
* try {
|
||||
* NameID name = decrypter.decrypt(encrypted);
|
||||
* assertion.getSubject().setNameID(name);
|
||||
* } catch (Exception e) {
|
||||
* throw new Saml2AuthenticationException(...);
|
||||
* }
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* Or, in the event that you have your own custom interface, the same pattern applies:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* MyDecryptionService myService = ...
|
||||
* provider.setResponseDecrypter((responseToken) -> {
|
||||
* Assertion assertion = assertionToken.getAssertion();
|
||||
* EncryptedID encrypted = assertion.getSubject().getEncryptedID();
|
||||
* NameID name = myService.decrypt(encrypted);
|
||||
* assertion.getSubject().setNameID(name);
|
||||
* });
|
||||
* </pre>
|
||||
* @param assertionDecrypter the {@link Consumer} for decrypting assertion elements
|
||||
* @since 5.5
|
||||
*/
|
||||
public void setAssertionElementsDecrypter(Consumer<AssertionToken> assertionDecrypter) {
|
||||
Assert.notNull(assertionDecrypter, "assertionDecrypter cannot be null");
|
||||
this.assertionElementsDecrypter = assertionDecrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link Converter} to use for converting a validated {@link Response} into
|
||||
* an {@link AbstractAuthenticationToken}.
|
||||
*
|
||||
* You can delegate to the default behavior by calling
|
||||
* {@link #createDefaultResponseAuthenticationConverter()} like so:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* Converter<ResponseToken, Saml2Authentication> authenticationConverter =
|
||||
* createDefaultResponseAuthenticationConverter();
|
||||
* provider.setResponseAuthenticationConverter(responseToken -> {
|
||||
* Saml2Authentication authentication = authenticationConverter.convert(responseToken);
|
||||
* User user = myUserRepository.findByUsername(authentication.getName());
|
||||
* return new MyAuthentication(authentication, user);
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* This method takes precedence over {@link #setAuthoritiesExtractor(Converter)} and
|
||||
* {@link #setAuthoritiesMapper(GrantedAuthoritiesMapper)}.
|
||||
* @param responseAuthenticationConverter the {@link Converter} to use
|
||||
* @since 5.4
|
||||
*/
|
||||
public void setResponseAuthenticationConverter(
|
||||
Converter<ResponseToken, ? extends AbstractAuthenticationToken> responseAuthenticationConverter) {
|
||||
Assert.notNull(responseAuthenticationConverter, "responseAuthenticationConverter cannot be null");
|
||||
this.responseAuthenticationConverter = responseAuthenticationConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Converter} used for extracting assertion attributes that can be
|
||||
* mapped to authorities.
|
||||
* @param authoritiesExtractor the {@code Converter} used for mapping the assertion
|
||||
* attributes to authorities
|
||||
* @deprecated Use {@link #setResponseAuthenticationConverter(Converter)} instead
|
||||
*/
|
||||
public void setAuthoritiesExtractor(
|
||||
Converter<Assertion, Collection<? extends GrantedAuthority>> authoritiesExtractor) {
|
||||
Assert.notNull(authoritiesExtractor, "authoritiesExtractor cannot be null");
|
||||
this.authoritiesExtractor = authoritiesExtractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link GrantedAuthoritiesMapper} used for mapping assertion attributes to
|
||||
* a new set of authorities which will be associated to the
|
||||
* {@link Saml2Authentication}. Note: This implementation is only retrieving
|
||||
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the
|
||||
* user's authorities
|
||||
* @deprecated Use {@link #setResponseAuthenticationConverter(Converter)} instead
|
||||
*/
|
||||
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
|
||||
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
|
||||
this.authoritiesMapper = authoritiesMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration for how much time skew an assertion may tolerate during
|
||||
* timestamp, NotOnOrBefore and NotOnOrAfter, validation.
|
||||
* @param responseTimeValidationSkew duration for skew tolerance
|
||||
* @deprecated Use {@link #setAssertionValidator(Converter)} instead
|
||||
*/
|
||||
public void setResponseTimeValidationSkew(Duration responseTimeValidationSkew) {
|
||||
this.responseTimeValidationSkew = responseTimeValidationSkew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a default strategy for validating each SAML 2.0 Assertion and associated
|
||||
* {@link Authentication} token
|
||||
* @return the default assertion validator strategy
|
||||
* @since 5.4
|
||||
*/
|
||||
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator() {
|
||||
|
||||
return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
|
||||
(assertionToken) -> SAML20AssertionValidators.attributeValidator,
|
||||
(assertionToken) -> createValidationContext(assertionToken, (params) -> {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a default strategy for validating each SAML 2.0 Assertion and associated
|
||||
* {@link Authentication} token
|
||||
* @param contextConverter the conversion strategy to use to generate a
|
||||
* {@link ValidationContext} for each assertion being validated
|
||||
* @return the default assertion validator strategy
|
||||
* @since 5.4
|
||||
*/
|
||||
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator(
|
||||
Converter<AssertionToken, ValidationContext> contextConverter) {
|
||||
|
||||
return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
|
||||
(assertionToken) -> SAML20AssertionValidators.attributeValidator, contextConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a default strategy for converting a SAML 2.0 Response and
|
||||
* {@link Authentication} token into a {@link Saml2Authentication}
|
||||
* @return the default response authentication converter strategy
|
||||
* @since 5.4
|
||||
*/
|
||||
public static Converter<ResponseToken, Saml2Authentication> createDefaultResponseAuthenticationConverter() {
|
||||
return (responseToken) -> {
|
||||
Saml2AuthenticationToken token = responseToken.token;
|
||||
Response response = responseToken.response;
|
||||
Assertion assertion = CollectionUtils.firstElement(response.getAssertions());
|
||||
String username = assertion.getSubject().getNameID().getValue();
|
||||
Map<String, List<Object>> attributes = getAssertionAttributes(assertion);
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes);
|
||||
String registrationId = responseToken.token.getRelyingPartyRegistration().getRegistrationId();
|
||||
principal.setRelyingPartyRegistrationId(registrationId);
|
||||
return new Saml2Authentication(principal, token.getSaml2Response(),
|
||||
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param authentication the authentication request object, must be of type
|
||||
* {@link Saml2AuthenticationToken}
|
||||
* @return {@link Saml2Authentication} if the assertion is valid
|
||||
* @throws AuthenticationException if a validation exception occurs
|
||||
*/
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
try {
|
||||
Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication;
|
||||
String serializedResponse = token.getSaml2Response();
|
||||
Response response = parse(serializedResponse);
|
||||
process(token, response);
|
||||
return this.responseAuthenticationConverter.convert(new ResponseToken(response, token));
|
||||
}
|
||||
catch (Saml2AuthenticationException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw createAuthenticationException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private Response parse(String response) throws Saml2Exception, Saml2AuthenticationException {
|
||||
try {
|
||||
Document document = this.parserPool
|
||||
.parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8)));
|
||||
Element element = document.getDocumentElement();
|
||||
return (Response) this.responseUnmarshaller.unmarshall(element);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void process(Saml2AuthenticationToken token, Response response) {
|
||||
String issuer = response.getIssuer().getValue();
|
||||
logger.debug(LogMessage.format("Processing SAML response from %s", issuer));
|
||||
boolean responseSigned = response.isSigned();
|
||||
|
||||
ResponseToken responseToken = new ResponseToken(response, token);
|
||||
Saml2ResponseValidatorResult result = this.responseSignatureValidator.convert(responseToken);
|
||||
if (responseSigned) {
|
||||
this.responseElementsDecrypter.accept(responseToken);
|
||||
}
|
||||
result = result.concat(this.responseValidator.convert(responseToken));
|
||||
boolean allAssertionsSigned = true;
|
||||
for (Assertion assertion : response.getAssertions()) {
|
||||
AssertionToken assertionToken = new AssertionToken(assertion, token);
|
||||
result = result.concat(this.assertionSignatureValidator.convert(assertionToken));
|
||||
allAssertionsSigned = allAssertionsSigned && assertion.isSigned();
|
||||
if (responseSigned || assertion.isSigned()) {
|
||||
this.assertionElementsDecrypter.accept(new AssertionToken(assertion, token));
|
||||
}
|
||||
result = result.concat(this.assertionValidator.convert(assertionToken));
|
||||
}
|
||||
if (!responseSigned && !allAssertionsSigned) {
|
||||
String description = "Either the response or one of the assertions is unsigned. "
|
||||
+ "Please either sign the response or all of the assertions.";
|
||||
throw createAuthenticationException(Saml2ErrorCodes.INVALID_SIGNATURE, description, null);
|
||||
}
|
||||
Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions());
|
||||
if (!hasName(firstAssertion)) {
|
||||
Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
|
||||
"Assertion [" + firstAssertion.getID() + "] is missing a subject");
|
||||
result = result.concat(error);
|
||||
}
|
||||
|
||||
if (result.hasErrors()) {
|
||||
Collection<Saml2Error> errors = result.getErrors();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.debug("Found " + errors.size() + " validation errors in SAML response [" + response.getID()
|
||||
+ "]: " + errors);
|
||||
}
|
||||
else if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"Found " + errors.size() + " validation errors in SAML response [" + response.getID() + "]");
|
||||
}
|
||||
Saml2Error first = errors.iterator().next();
|
||||
throw createAuthenticationException(first.getErrorCode(), first.getDescription(), null);
|
||||
}
|
||||
else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Successfully processed SAML Response [" + response.getID() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseSignatureValidator() {
|
||||
return (responseToken) -> {
|
||||
Response response = responseToken.getResponse();
|
||||
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
|
||||
if (response.isSigned()) {
|
||||
return OpenSamlVerificationUtils.verifySignature(response, registration).post(response.getSignature());
|
||||
}
|
||||
return Saml2ResponseValidatorResult.success();
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<ResponseToken> createDefaultResponseElementsDecrypter() {
|
||||
return (responseToken) -> {
|
||||
Response response = responseToken.getResponse();
|
||||
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
|
||||
try {
|
||||
OpenSamlDecryptionUtils.decryptResponseElements(response, registration);
|
||||
}
|
||||
catch (Saml2Exception ex) {
|
||||
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
|
||||
return (responseToken) -> {
|
||||
Response response = responseToken.getResponse();
|
||||
Saml2AuthenticationToken token = responseToken.getToken();
|
||||
Saml2ResponseValidatorResult result = Saml2ResponseValidatorResult.success();
|
||||
String statusCode = getStatusCode(response);
|
||||
if (!StatusCode.SUCCESS.equals(statusCode)) {
|
||||
String message = String.format("Invalid status [%s] for SAML response [%s]", statusCode,
|
||||
response.getID());
|
||||
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, message));
|
||||
}
|
||||
String issuer = response.getIssuer().getValue();
|
||||
String destination = response.getDestination();
|
||||
String location = token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation();
|
||||
if (StringUtils.hasText(destination) && !destination.equals(location)) {
|
||||
String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID()
|
||||
+ "]";
|
||||
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, message));
|
||||
}
|
||||
String assertingPartyEntityId = token.getRelyingPartyRegistration().getAssertingPartyDetails()
|
||||
.getEntityId();
|
||||
if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) {
|
||||
String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID());
|
||||
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, message));
|
||||
}
|
||||
if (response.getAssertions().isEmpty()) {
|
||||
throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA,
|
||||
"No assertions found in response.", null);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
private String getStatusCode(Response response) {
|
||||
if (response.getStatus() == null) {
|
||||
return StatusCode.SUCCESS;
|
||||
}
|
||||
if (response.getStatus().getStatusCode() == null) {
|
||||
return StatusCode.SUCCESS;
|
||||
}
|
||||
return response.getStatus().getStatusCode().getValue();
|
||||
}
|
||||
|
||||
private Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionSignatureValidator() {
|
||||
return createAssertionValidator(Saml2ErrorCodes.INVALID_SIGNATURE, (assertionToken) -> {
|
||||
RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration();
|
||||
SignatureTrustEngine engine = OpenSamlVerificationUtils.trustEngine(registration);
|
||||
return SAML20AssertionValidators.createSignatureValidator(engine);
|
||||
}, (assertionToken) -> new ValidationContext(
|
||||
Collections.singletonMap(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false)));
|
||||
}
|
||||
|
||||
private Consumer<AssertionToken> createDefaultAssertionElementsDecrypter() {
|
||||
return (assertionToken) -> {
|
||||
Assertion assertion = assertionToken.getAssertion();
|
||||
RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration();
|
||||
try {
|
||||
OpenSamlDecryptionUtils.decryptAssertionElements(assertion, registration);
|
||||
}
|
||||
catch (Saml2Exception ex) {
|
||||
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Converter<AssertionToken, Saml2ResponseValidatorResult> createCompatibleAssertionValidator() {
|
||||
return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
|
||||
(assertionToken) -> SAML20AssertionValidators.attributeValidator,
|
||||
(assertionToken) -> createValidationContext(assertionToken,
|
||||
(params) -> params.put(SAML2AssertionValidationParameters.CLOCK_SKEW,
|
||||
this.responseTimeValidationSkew.toMillis())));
|
||||
}
|
||||
|
||||
private Converter<ResponseToken, Saml2Authentication> createCompatibleResponseAuthenticationConverter() {
|
||||
return (responseToken) -> {
|
||||
Response response = responseToken.response;
|
||||
Saml2AuthenticationToken token = responseToken.token;
|
||||
Assertion assertion = CollectionUtils.firstElement(response.getAssertions());
|
||||
String username = assertion.getSubject().getNameID().getValue();
|
||||
Map<String, List<Object>> attributes = getAssertionAttributes(assertion);
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes);
|
||||
String registrationId = responseToken.token.getRelyingPartyRegistration().getRegistrationId();
|
||||
principal.setRelyingPartyRegistrationId(registrationId);
|
||||
return new Saml2Authentication(principal, token.getSaml2Response(),
|
||||
this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion)));
|
||||
};
|
||||
}
|
||||
|
||||
private Collection<? extends GrantedAuthority> getAssertionAuthorities(Assertion assertion) {
|
||||
return this.authoritiesExtractor.convert(assertion);
|
||||
}
|
||||
|
||||
private boolean hasName(Assertion assertion) {
|
||||
if (assertion == null) {
|
||||
return false;
|
||||
}
|
||||
if (assertion.getSubject() == null) {
|
||||
return false;
|
||||
}
|
||||
if (assertion.getSubject().getNameID() == null) {
|
||||
return false;
|
||||
}
|
||||
return assertion.getSubject().getNameID().getValue() != null;
|
||||
}
|
||||
|
||||
private static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
|
||||
Map<String, List<Object>> attributeMap = new LinkedHashMap<>();
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
List<Object> attributeValues = new ArrayList<>();
|
||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
||||
Object attributeValue = getXmlObjectValue(xmlObject);
|
||||
if (attributeValue != null) {
|
||||
attributeValues.add(attributeValue);
|
||||
}
|
||||
}
|
||||
attributeMap.put(attribute.getName(), attributeValues);
|
||||
}
|
||||
}
|
||||
return attributeMap;
|
||||
}
|
||||
|
||||
private static Object getXmlObjectValue(XMLObject xmlObject) {
|
||||
if (xmlObject instanceof XSAny) {
|
||||
return ((XSAny) xmlObject).getTextContent();
|
||||
}
|
||||
if (xmlObject instanceof XSString) {
|
||||
return ((XSString) xmlObject).getValue();
|
||||
}
|
||||
if (xmlObject instanceof XSInteger) {
|
||||
return ((XSInteger) xmlObject).getValue();
|
||||
}
|
||||
if (xmlObject instanceof XSURI) {
|
||||
return ((XSURI) xmlObject).getValue();
|
||||
}
|
||||
if (xmlObject instanceof XSBoolean) {
|
||||
XSBooleanValue xsBooleanValue = ((XSBoolean) xmlObject).getValue();
|
||||
return (xsBooleanValue != null) ? xsBooleanValue.getValue() : null;
|
||||
}
|
||||
if (xmlObject instanceof XSDateTime) {
|
||||
return Instant.ofEpochMilli(((XSDateTime) xmlObject).getValue().getMillis());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Saml2AuthenticationException createAuthenticationException(String code, String message,
|
||||
Exception cause) {
|
||||
return new Saml2AuthenticationException(new Saml2Error(code, message), cause);
|
||||
}
|
||||
|
||||
private static Converter<AssertionToken, Saml2ResponseValidatorResult> createAssertionValidator(String errorCode,
|
||||
Converter<AssertionToken, SAML20AssertionValidator> validatorConverter,
|
||||
Converter<AssertionToken, ValidationContext> contextConverter) {
|
||||
|
||||
return (assertionToken) -> {
|
||||
Assertion assertion = assertionToken.assertion;
|
||||
SAML20AssertionValidator validator = validatorConverter.convert(assertionToken);
|
||||
ValidationContext context = contextConverter.convert(assertionToken);
|
||||
try {
|
||||
ValidationResult result = validator.validate(assertion, context);
|
||||
if (result == ValidationResult.VALID) {
|
||||
return Saml2ResponseValidatorResult.success();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
|
||||
((Response) assertion.getParent()).getID(), ex.getMessage());
|
||||
return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
|
||||
}
|
||||
String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
|
||||
((Response) assertion.getParent()).getID(), context.getValidationFailureMessage());
|
||||
return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
|
||||
};
|
||||
}
|
||||
|
||||
private static ValidationContext createValidationContext(AssertionToken assertionToken,
|
||||
Consumer<Map<String, Object>> paramsConsumer) {
|
||||
String audience = assertionToken.token.getRelyingPartyRegistration().getEntityId();
|
||||
String recipient = assertionToken.token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation();
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience));
|
||||
params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient));
|
||||
paramsConsumer.accept(params);
|
||||
return new ValidationContext(params);
|
||||
}
|
||||
|
||||
private static class SAML20AssertionValidators {
|
||||
|
||||
private static final Collection<ConditionValidator> conditions = new ArrayList<>();
|
||||
|
||||
private static final Collection<SubjectConfirmationValidator> subjects = new ArrayList<>();
|
||||
|
||||
private static final Collection<StatementValidator> statements = new ArrayList<>();
|
||||
|
||||
private static final SignaturePrevalidator validator = new SAMLSignatureProfileValidator();
|
||||
|
||||
static {
|
||||
conditions.add(new AudienceRestrictionConditionValidator());
|
||||
conditions.add(new DelegationRestrictionConditionValidator());
|
||||
conditions.add(new ConditionValidator() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public QName getServicedCondition() {
|
||||
return OneTimeUse.DEFAULT_ELEMENT_NAME;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ValidationResult validate(Condition condition, Assertion assertion, ValidationContext context) {
|
||||
// applications should validate their own OneTimeUse conditions
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
});
|
||||
subjects.add(new BearerSubjectConfirmationValidator() {
|
||||
@Override
|
||||
protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion,
|
||||
ValidationContext context) throws AssertionValidationException {
|
||||
// applications should validate their own addresses - gh-7514
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final SAML20AssertionValidator attributeValidator = new SAML20AssertionValidator(conditions,
|
||||
subjects, statements, null, null) {
|
||||
@Nonnull
|
||||
@Override
|
||||
protected ValidationResult validateSignature(Assertion token, ValidationContext context) {
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
};
|
||||
|
||||
static SAML20AssertionValidator createSignatureValidator(SignatureTrustEngine engine) {
|
||||
return new SAML20AssertionValidator(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), engine,
|
||||
validator) {
|
||||
@Nonnull
|
||||
@Override
|
||||
protected ValidationResult validateConditions(Assertion assertion, ValidationContext context) {
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
protected ValidationResult validateSubjectConfirmation(Assertion assertion, ValidationContext context) {
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
protected ValidationResult validateStatements(Assertion assertion, ValidationContext context) {
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A tuple containing an OpenSAML {@link Response} and its associated authentication
|
||||
* token.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public static class ResponseToken {
|
||||
|
||||
private final Saml2AuthenticationToken token;
|
||||
|
||||
private final Response response;
|
||||
|
||||
ResponseToken(Response response, Saml2AuthenticationToken token) {
|
||||
this.token = token;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public Response getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
public Saml2AuthenticationToken getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A tuple containing an OpenSAML {@link Assertion} and its associated authentication
|
||||
* token.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public static class AssertionToken {
|
||||
|
||||
private final Saml2AuthenticationToken token;
|
||||
|
||||
private final Assertion assertion;
|
||||
|
||||
AssertionToken(Assertion assertion, Saml2AuthenticationToken token) {
|
||||
this.token = token;
|
||||
this.assertion = assertion;
|
||||
}
|
||||
|
||||
public Assertion getAssertion() {
|
||||
return this.assertion;
|
||||
}
|
||||
|
||||
public Saml2AuthenticationToken getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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.saml2.provider.service.web.authentication;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.joda.time.DateTime;
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A strategy for resolving a SAML 2.0 Authentication Request from the
|
||||
* {@link HttpServletRequest} using OpenSAML.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.7
|
||||
* @deprecated OpenSAML 3 has reached end-of-life so this version is no longer recommended
|
||||
*/
|
||||
@Deprecated
|
||||
public final class OpenSaml3AuthenticationRequestResolver implements Saml2AuthenticationRequestResolver {
|
||||
|
||||
private final OpenSamlAuthenticationRequestResolver authnRequestResolver;
|
||||
|
||||
private Consumer<AuthnRequestContext> contextConsumer = (parameters) -> {
|
||||
};
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSaml3AuthenticationRequestResolver}
|
||||
*/
|
||||
public OpenSaml3AuthenticationRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.authnRequestResolver = new OpenSamlAuthenticationRequestResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends AbstractSaml2AuthenticationRequest> T resolve(HttpServletRequest request) {
|
||||
return this.authnRequestResolver.resolve(request, (registration, authnRequest) -> {
|
||||
authnRequest.setIssueInstant(new DateTime(this.clock.millis()));
|
||||
this.contextConsumer.accept(new AuthnRequestContext(request, registration, authnRequest));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
|
||||
* @param contextConsumer a consumer that accepts an {@link AuthnRequestContext}
|
||||
*/
|
||||
public void setAuthnRequestCustomizer(Consumer<AuthnRequestContext> contextConsumer) {
|
||||
Assert.notNull(contextConsumer, "contextConsumer cannot be null");
|
||||
this.contextConsumer = contextConsumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Clock} for generating the issued {@link DateTime}
|
||||
* @param clock the {@link Clock} to use
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public static final class AuthnRequestContext {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private final AuthnRequest authnRequest;
|
||||
|
||||
public AuthnRequestContext(HttpServletRequest request, RelyingPartyRegistration registration,
|
||||
AuthnRequest authnRequest) {
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
this.authnRequest = authnRequest;
|
||||
}
|
||||
|
||||
public HttpServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public AuthnRequest getAuthnRequest() {
|
||||
return this.authnRequest;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.joda.time.DateTime;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Requests with
|
||||
* OpenSAML 3
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
|
||||
* {@code OpenSaml4LogoutRequestResolver}
|
||||
*/
|
||||
public final class OpenSaml3LogoutRequestResolver implements Saml2LogoutRequestResolver {
|
||||
|
||||
private final OpenSamlLogoutRequestResolver logoutRequestResolver;
|
||||
|
||||
private Consumer<LogoutRequestParameters> parametersConsumer = (parameters) -> {
|
||||
};
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSaml3LogoutRequestResolver}
|
||||
*/
|
||||
public OpenSaml3LogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.logoutRequestResolver = new OpenSamlLogoutRequestResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
|
||||
return this.logoutRequestResolver.resolve(request, authentication, (registration, logoutRequest) -> {
|
||||
logoutRequest.setIssueInstant(new DateTime(this.clock.millis()));
|
||||
this.parametersConsumer
|
||||
.accept(new LogoutRequestParameters(request, registration, authentication, logoutRequest));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
|
||||
* @param parametersConsumer a consumer that accepts an
|
||||
* {@link LogoutRequestParameters}
|
||||
*/
|
||||
public void setParametersConsumer(Consumer<LogoutRequestParameters> parametersConsumer) {
|
||||
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
|
||||
this.parametersConsumer = parametersConsumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Clock} for generating the issued {@link DateTime}
|
||||
* @param clock the {@link Clock} to use
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public static final class LogoutRequestParameters {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private final Authentication authentication;
|
||||
|
||||
private final LogoutRequest logoutRequest;
|
||||
|
||||
public LogoutRequestParameters(HttpServletRequest request, RelyingPartyRegistration registration,
|
||||
Authentication authentication, LogoutRequest logoutRequest) {
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
this.authentication = authentication;
|
||||
this.logoutRequest = logoutRequest;
|
||||
}
|
||||
|
||||
public HttpServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public LogoutRequest getLogoutRequest() {
|
||||
return this.logoutRequest;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.joda.time.DateTime;
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Saml2LogoutResponseResolver} for resolving SAML 2.0 Logout Responses with
|
||||
* OpenSAML 3
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
|
||||
* {@code OpenSaml4LogoutResponseResolver}
|
||||
*/
|
||||
public final class OpenSaml3LogoutResponseResolver implements Saml2LogoutResponseResolver {
|
||||
|
||||
private final OpenSamlLogoutResponseResolver logoutResponseResolver;
|
||||
|
||||
private Consumer<LogoutResponseParameters> parametersConsumer = (parameters) -> {
|
||||
};
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSaml3LogoutResponseResolver}
|
||||
*/
|
||||
public OpenSaml3LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
|
||||
return this.logoutResponseResolver.resolve(request, authentication, (registration, logoutResponse) -> {
|
||||
logoutResponse.setIssueInstant(new DateTime(this.clock.millis()));
|
||||
this.parametersConsumer
|
||||
.accept(new LogoutResponseParameters(request, registration, authentication, logoutResponse));
|
||||
});
|
||||
}
|
||||
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutResponse}
|
||||
* @param parametersConsumer a consumer that accepts an
|
||||
* {@link LogoutResponseParameters}
|
||||
*/
|
||||
public void setParametersConsumer(Consumer<LogoutResponseParameters> parametersConsumer) {
|
||||
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
|
||||
this.parametersConsumer = parametersConsumer;
|
||||
}
|
||||
|
||||
public static final class LogoutResponseParameters {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private final Authentication authentication;
|
||||
|
||||
private final LogoutResponse logoutResponse;
|
||||
|
||||
public LogoutResponseParameters(HttpServletRequest request, RelyingPartyRegistration registration,
|
||||
Authentication authentication, LogoutResponse logoutResponse) {
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
this.authentication = authentication;
|
||||
this.logoutResponse = logoutResponse;
|
||||
}
|
||||
|
||||
public HttpServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public LogoutResponse getLogoutResponse() {
|
||||
return this.logoutResponse;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,682 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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.saml2.provider.service.authentication;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.core.xml.io.Marshaller;
|
||||
import org.opensaml.core.xml.io.MarshallingException;
|
||||
import org.opensaml.core.xml.schema.XSDateTime;
|
||||
import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder;
|
||||
import org.opensaml.saml.common.assertion.ValidationContext;
|
||||
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
import org.opensaml.saml.saml2.core.AttributeValue;
|
||||
import org.opensaml.saml.saml2.core.Conditions;
|
||||
import org.opensaml.saml.saml2.core.EncryptedAssertion;
|
||||
import org.opensaml.saml.saml2.core.EncryptedAttribute;
|
||||
import org.opensaml.saml.saml2.core.EncryptedID;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.OneTimeUse;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
||||
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
|
||||
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.ResponseToken;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSamlAuthenticationProvider}
|
||||
*
|
||||
* @author Filip Hanik
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class OpenSamlAuthenticationProviderTests {
|
||||
|
||||
private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias";
|
||||
|
||||
private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
|
||||
|
||||
private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";
|
||||
|
||||
private OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
|
||||
private Saml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("name",
|
||||
Collections.emptyMap());
|
||||
|
||||
private Saml2Authentication authentication = new Saml2Authentication(this.principal, "response",
|
||||
Collections.emptyList());
|
||||
|
||||
@Test
|
||||
public void supportsWhenSaml2AuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.provider.supports(Saml2AuthenticationToken.class))
|
||||
.withFailMessage(
|
||||
OpenSamlAuthenticationProvider.class + "should support " + Saml2AuthenticationToken.class)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenNotSaml2AuthenticationTokenThenReturnFalse() {
|
||||
assertThat(!this.provider.supports(Authentication.class))
|
||||
.withFailMessage(OpenSamlAuthenticationProvider.class + "should not support " + Authentication.class)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenUnknownDataClassThenThrowAuthenticationException() {
|
||||
Assertion assertion = (Assertion) XMLObjectProviderRegistrySupport.getBuilderFactory()
|
||||
.getBuilder(Assertion.DEFAULT_ELEMENT_NAME).buildObject(Assertion.DEFAULT_ELEMENT_NAME);
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(
|
||||
new Saml2AuthenticationToken(verifying(registration()).build(), serialize(assertion))))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenXmlErrorThenThrowAuthenticationException() {
|
||||
Saml2AuthenticationToken token = new Saml2AuthenticationToken(verifying(registration()).build(), "invalid xml");
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
|
||||
Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID);
|
||||
response.getAssertions().add(assertion());
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.INVALID_DESTINATION));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() {
|
||||
Saml2AuthenticationToken token = token();
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() {
|
||||
Response response = response();
|
||||
response.getAssertions().add(assertion());
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
assertion.getSubject().getSubjectConfirmations().get(0).getSubjectConfirmationData()
|
||||
.setNotOnOrAfter(DateTime.now().minus(Duration.standardDays(3)));
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.INVALID_ASSERTION));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenMissingSubjectThenThrowAuthenticationException() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
assertion.setSubject(null);
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.SUBJECT_NOT_FOUND));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenUsernameMissingThenThrowAuthenticationException() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
assertion.getSubject().getNameID().setValue(null);
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.SUBJECT_NOT_FOUND));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
assertion.getSubject().getSubjectConfirmations()
|
||||
.forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10"));
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
this.provider.authenticate(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAssertionContainsAttributesThenItSucceeds() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
List<AttributeStatement> attributes = attributeStatements();
|
||||
assertion.getAttributeStatements().addAll(attributes);
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
Authentication authentication = this.provider.authenticate(token);
|
||||
Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
|
||||
Map<String, Object> expected = new LinkedHashMap<>();
|
||||
expected.put("email", Arrays.asList("john.doe@example.com", "doe.john@example.com"));
|
||||
expected.put("name", Collections.singletonList("John Doe"));
|
||||
expected.put("age", Collections.singletonList(21));
|
||||
expected.put("website", Collections.singletonList("https://johndoe.com/"));
|
||||
expected.put("registered", Collections.singletonList(true));
|
||||
expected.put("role", Arrays.asList("RoleTwo"));
|
||||
Instant registeredDate = Instant.ofEpochMilli(DateTime.parse("1970-01-01T00:00:00Z").getMillis());
|
||||
expected.put("registeredDate", Collections.singletonList(registeredDate));
|
||||
assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe");
|
||||
assertThat(principal.getAttributes()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() {
|
||||
Response response = response();
|
||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
|
||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, decrypting(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() {
|
||||
Response response = response();
|
||||
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
|
||||
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
|
||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
|
||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
|
||||
this.provider.authenticate(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() {
|
||||
Response response = response();
|
||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
|
||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
|
||||
this.provider.authenticate(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
NameID nameId = assertion.getSubject().getNameID();
|
||||
EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId,
|
||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||
assertion.getSubject().setNameID(null);
|
||||
assertion.getSubject().setEncryptedID(encryptedID);
|
||||
response.getAssertions().add(assertion);
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
|
||||
this.provider.authenticate(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenEncryptedAttributeThenDecrypts() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value",
|
||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||
AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME);
|
||||
statement.getEncryptedAttributes().add(attribute);
|
||||
assertion.getAttributeStatements().add(statement);
|
||||
response.getAssertions().add(assertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
|
||||
Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token);
|
||||
Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
|
||||
assertThat(principal.getAttribute("name")).containsExactly("value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() {
|
||||
Response response = response();
|
||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
|
||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() {
|
||||
Response response = response();
|
||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
|
||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, registration()
|
||||
.decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartyPrivateCredential())));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException {
|
||||
Response response = response();
|
||||
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
|
||||
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
|
||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
|
||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
|
||||
Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token);
|
||||
// the following code will throw an exception if authentication isn't serializable
|
||||
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
|
||||
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream);
|
||||
objectOutputStream.writeObject(authentication);
|
||||
objectOutputStream.flush();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDefaultAssertionValidatorWhenAssertionThenValidates() {
|
||||
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
|
||||
Assertion assertion = response.getAssertions().get(0);
|
||||
OpenSamlAuthenticationProvider.AssertionToken assertionToken = new OpenSamlAuthenticationProvider.AssertionToken(
|
||||
assertion, token());
|
||||
assertThat(OpenSamlAuthenticationProvider.createDefaultAssertionValidator().convert(assertionToken).hasErrors())
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenDelegatingToDefaultAssertionValidatorThenUses() {
|
||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
// @formatter:off
|
||||
provider.setAssertionValidator((assertionToken) -> OpenSamlAuthenticationProvider
|
||||
.createDefaultAssertionValidator((token) -> new ValidationContext())
|
||||
.convert(assertionToken)
|
||||
.concat(new Saml2Error("wrong error", "wrong error"))
|
||||
);
|
||||
// @formatter:on
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME);
|
||||
assertion.getConditions().getConditions().add(oneTimeUse);
|
||||
response.getAssertions().add(assertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
ASSERTING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class)
|
||||
.satisfies((error) -> assertThat(error.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_ASSERTION));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenCustomAssertionValidatorThenUses() {
|
||||
Converter<OpenSamlAuthenticationProvider.AssertionToken, Saml2ResponseValidatorResult> validator = mock(
|
||||
Converter.class);
|
||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
// @formatter:off
|
||||
provider.setAssertionValidator((assertionToken) -> OpenSamlAuthenticationProvider.createDefaultAssertionValidator()
|
||||
.convert(assertionToken)
|
||||
.concat(validator.convert(assertionToken))
|
||||
);
|
||||
// @formatter:on
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
response.getAssertions().add(assertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
ASSERTING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
given(validator.convert(any(OpenSamlAuthenticationProvider.AssertionToken.class)))
|
||||
.willReturn(Saml2ResponseValidatorResult.success());
|
||||
provider.authenticate(token);
|
||||
verify(validator).convert(any(OpenSamlAuthenticationProvider.AssertionToken.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() {
|
||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
provider.setAssertionValidator((assertionToken) -> Saml2ResponseValidatorResult.success());
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.relyingPartyDecryptingCredential(),
|
||||
RELYING_PARTY_ENTITY_ID); // broken
|
||||
// signature
|
||||
response.getAssertions().add(assertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
ASSERTING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> provider.authenticate(token))
|
||||
.satisfies((error) -> assertThat(error.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidationContextCustomizedThenUsers() {
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton("blah"));
|
||||
ValidationContext context = mock(ValidationContext.class);
|
||||
given(context.getStaticParameters()).willReturn(parameters);
|
||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
provider.setAssertionValidator(
|
||||
OpenSamlAuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context));
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
response.getAssertions().add(assertion);
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
ASSERTING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class)
|
||||
.satisfies((error) -> assertThat(error).hasMessageContaining("Invalid assertion"));
|
||||
// @formatter:on
|
||||
verify(context, atLeastOnce()).getStaticParameters();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWithSHA1SignatureThenItSucceeds() throws Exception {
|
||||
Response response = response();
|
||||
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
|
||||
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID,
|
||||
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
this.provider.authenticate(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAssertionValidatorWhenNullThenIllegalArgument() {
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.provider.setAssertionValidator(null));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDefaultResponseAuthenticationConverterWhenResponseThenConverts() {
|
||||
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
ResponseToken responseToken = new ResponseToken(response, token);
|
||||
Saml2Authentication authentication = OpenSamlAuthenticationProvider
|
||||
.createDefaultResponseAuthenticationConverter().convert(responseToken);
|
||||
assertThat(authentication.getName()).isEqualTo("test@saml.user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenResponseAuthenticationConverterConfiguredThenUses() {
|
||||
Converter<ResponseToken, Saml2Authentication> authenticationConverter = mock(Converter.class);
|
||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
provider.setResponseAuthenticationConverter(authenticationConverter);
|
||||
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
provider.authenticate(token);
|
||||
verify(authenticationConverter).convert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setResponseAuthenticationConverterWhenNullThenIllegalArgument() {
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.provider.setResponseAuthenticationConverter(null));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setResponseElementsDecrypterWhenNullThenIllegalArgument() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setResponseElementsDecrypter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAssertionElementsDecrypterWhenNullThenIllegalArgument() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setAssertionElementsDecrypter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject());
|
||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
this.provider.setResponseElementsDecrypter((tuple) -> tuple.getResponse().getAssertions().add(assertion));
|
||||
Authentication authentication = this.provider.authenticate(token);
|
||||
assertThat(authentication.getName()).isEqualTo("test@saml.user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertion() {
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
EncryptedID id = new EncryptedIDBuilder().buildObject();
|
||||
id.setEncryptedData(new EncryptedDataBuilder().buildObject());
|
||||
assertion.getSubject().setEncryptedID(id);
|
||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||
RELYING_PARTY_ENTITY_ID);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
this.provider.setAssertionElementsDecrypter((tuple) -> {
|
||||
NameID name = new NameIDBuilder().buildObject();
|
||||
name.setValue("decrypted name");
|
||||
tuple.getAssertion().getSubject().setNameID(name);
|
||||
});
|
||||
Authentication authentication = this.provider.authenticate(token);
|
||||
assertThat(authentication.getName()).isEqualTo("decrypted name");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenResponseStatusIsNotSuccessThenFails() {
|
||||
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion(
|
||||
(r) -> r.setStatus(TestOpenSamlObjects.status(StatusCode.AUTHN_FAILED)));
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies(errorOf(Saml2ErrorCodes.INVALID_RESPONSE, "Invalid status"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenResponseStatusIsSuccessThenSucceeds() {
|
||||
Response response = TestOpenSamlObjects
|
||||
.signedResponseWithOneAssertion((r) -> r.setStatus(TestOpenSamlObjects.successStatus()));
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
Authentication authentication = this.provider.authenticate(token);
|
||||
assertThat(authentication.getName()).isEqualTo("test@saml.user");
|
||||
}
|
||||
|
||||
private <T extends XMLObject> T build(QName qName) {
|
||||
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
||||
}
|
||||
|
||||
private String serialize(XMLObject object) {
|
||||
try {
|
||||
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
|
||||
Element element = marshaller.marshall(object);
|
||||
return SerializeSupport.nodeToString(element);
|
||||
}
|
||||
catch (MarshallingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Consumer<Saml2AuthenticationException> errorOf(String errorCode) {
|
||||
return errorOf(errorCode, null);
|
||||
}
|
||||
|
||||
private Consumer<Saml2AuthenticationException> errorOf(String errorCode, String description) {
|
||||
return (ex) -> {
|
||||
assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(errorCode);
|
||||
if (StringUtils.hasText(description)) {
|
||||
assertThat(ex.getSaml2Error().getDescription()).contains(description);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Response response() {
|
||||
Response response = TestOpenSamlObjects.response();
|
||||
response.setIssueInstant(DateTime.now());
|
||||
return response;
|
||||
}
|
||||
|
||||
private Response response(String destination, String issuerEntityId) {
|
||||
Response response = TestOpenSamlObjects.response(destination, issuerEntityId);
|
||||
response.setIssueInstant(DateTime.now());
|
||||
return response;
|
||||
}
|
||||
|
||||
private Assertion assertion() {
|
||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
||||
assertion.setIssueInstant(DateTime.now());
|
||||
for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) {
|
||||
SubjectConfirmationData data = confirmation.getSubjectConfirmationData();
|
||||
data.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000)));
|
||||
data.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000)));
|
||||
}
|
||||
Conditions conditions = assertion.getConditions();
|
||||
conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000)));
|
||||
conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000)));
|
||||
return assertion;
|
||||
}
|
||||
|
||||
private List<AttributeStatement> attributeStatements() {
|
||||
List<AttributeStatement> attributeStatements = TestOpenSamlObjects.attributeStatements();
|
||||
AttributeBuilder attributeBuilder = new AttributeBuilder();
|
||||
Attribute registeredDateAttr = attributeBuilder.buildObject();
|
||||
registeredDateAttr.setName("registeredDate");
|
||||
XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME,
|
||||
XSDateTime.TYPE_NAME);
|
||||
registeredDate.setValue(DateTime.parse("1970-01-01T00:00:00Z"));
|
||||
registeredDateAttr.getAttributeValues().add(registeredDate);
|
||||
attributeStatements.get(0).getAttributes().add(registeredDateAttr);
|
||||
return attributeStatements;
|
||||
}
|
||||
|
||||
private Saml2AuthenticationToken token() {
|
||||
Response response = response();
|
||||
RelyingPartyRegistration registration = verifying(registration()).build();
|
||||
return new Saml2AuthenticationToken(registration, serialize(response));
|
||||
}
|
||||
|
||||
private Saml2AuthenticationToken token(Response response, RelyingPartyRegistration.Builder registration) {
|
||||
return new Saml2AuthenticationToken(registration.build(), serialize(response));
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder registration() {
|
||||
return TestRelyingPartyRegistrations.noCredentials().entityId(RELYING_PARTY_ENTITY_ID)
|
||||
.assertionConsumerServiceLocation(DESTINATION)
|
||||
.assertingPartyDetails((party) -> party.entityId(ASSERTING_PARTY_ENTITY_ID));
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
|
||||
return builder.assertingPartyDetails((party) -> party
|
||||
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) {
|
||||
return builder
|
||||
.decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential()));
|
||||
}
|
||||
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSaml3LogoutRequestResolver}
|
||||
*/
|
||||
public class OpenSaml3LogoutRequestResolverTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
@Test
|
||||
public void resolveWhenCustomParametersConsumerThenUses() {
|
||||
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid"));
|
||||
HttpServletRequest request = new MockHttpServletRequest();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceLocation("https://ap.example.com/logout"))
|
||||
.build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication);
|
||||
assertThat(logoutRequest.getId()).isEqualTo("myid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setParametersConsumerWhenNullThenIllegalArgument() {
|
||||
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
|
||||
}
|
||||
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.core.Saml2ParameterNames;
|
||||
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver.LogoutResponseParameters;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSaml3LogoutResponseResolver}
|
||||
*/
|
||||
public class OpenSaml3LogoutResponseResolverTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
@Test
|
||||
public void resolveWhenCustomParametersConsumerThenUses() {
|
||||
OpenSaml3LogoutResponseResolver logoutResponseResolver = new OpenSaml3LogoutResponseResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
Consumer<LogoutResponseParameters> parametersConsumer = mock(Consumer.class);
|
||||
logoutResponseResolver.setParametersConsumer(parametersConsumer);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
|
||||
.assertingPartyDetails(
|
||||
(party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout"))
|
||||
.build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
request.setParameter(Saml2ParameterNames.SAML_REQUEST,
|
||||
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication);
|
||||
assertThat(logoutResponse).isNotNull();
|
||||
verify(parametersConsumer).accept(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setParametersConsumerWhenNullThenIllegalArgument() {
|
||||
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
|
||||
}
|
||||
|
||||
}
|
@ -22,7 +22,6 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ -72,7 +71,7 @@ public class DefaultSaml2AuthenticatedPrincipalTests {
|
||||
@Test
|
||||
public void getAttributeWhenDistinctValuesThenReturnsValues() {
|
||||
final Boolean registered = true;
|
||||
final Instant registeredDate = Instant.ofEpochMilli(DateTime.parse("1970-01-01T00:00:00Z").getMillis());
|
||||
final Instant registeredDate = Instant.parse("1970-01-01T00:00:00Z");
|
||||
Map<String, List<Object>> attributes = new LinkedHashMap<>();
|
||||
attributes.put("registration", Arrays.asList(registered, registeredDate));
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", attributes);
|
||||
|
Loading…
x
Reference in New Issue
Block a user