Remove OpenSaml4 Components

Issue gh-17707
This commit is contained in:
Josh Cummings 2025-08-14 18:01:02 -06:00
parent 2258699f5d
commit 5506c487de
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
48 changed files with 68 additions and 9856 deletions

View File

@ -35,18 +35,15 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; 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.OpenSaml5AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; 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.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.OpenSaml4AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.OpenSaml5AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.OpenSaml5AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; 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.Saml2AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter; import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter; import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
@ -381,10 +378,8 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
return openSamlAuthenticationRequestResolver; return openSamlAuthenticationRequestResolver;
} }
else { else {
OpenSaml4AuthenticationRequestResolver openSamlAuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver( throw new IllegalArgumentException(
relyingPartyRegistrationRepository(http)); "Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
openSamlAuthenticationRequestResolver.setRequestMatcher(getAuthenticationRequestMatcher());
return openSamlAuthenticationRequestResolver;
} }
} }
@ -429,15 +424,8 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
converter.setRequestMatcher(getLoginProcessingEndpoint()); converter.setRequestMatcher(getLoginProcessingEndpoint());
return converter; return converter;
} }
authenticationConverterBean = getBeanOrNull(http, OpenSaml4AuthenticationTokenConverter.class); throw new IllegalArgumentException(
if (authenticationConverterBean != null) { "Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
return authenticationConverterBean;
}
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(
this.relyingPartyRegistrationRepository);
converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
converter.setRequestMatcher(getLoginProcessingEndpoint());
return converter;
} }
private void registerDefaultAuthenticationProvider(B http) { private void registerDefaultAuthenticationProvider(B http) {
@ -448,10 +436,8 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
} }
} }
else { else {
OpenSaml4AuthenticationProvider provider = getBeanOrNull(http, OpenSaml4AuthenticationProvider.class); throw new IllegalArgumentException(
if (provider == null) { "Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
}
} }
} }

View File

@ -35,8 +35,6 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; 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.Saml2Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutResponseValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutRequestValidator; import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutRequestValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutResponseValidator; import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutResponseValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
@ -44,9 +42,6 @@ import org.springframework.security.saml2.provider.service.authentication.logout
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; 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.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestValidatorParametersResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutRequestValidatorParametersResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutRequestValidatorParametersResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutResponseResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutResponseResolver;
@ -250,10 +245,8 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
parameters.setRequestMatcher(requestMatcher); parameters.setRequestMatcher(requestMatcher);
return parameters; return parameters;
} }
OpenSaml4LogoutRequestValidatorParametersResolver parameters = new OpenSaml4LogoutRequestValidatorParametersResolver( throw new IllegalArgumentException(
registrations); "Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
parameters.setRequestMatcher(requestMatcher);
return parameters;
} }
private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter( private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter(
@ -384,7 +377,8 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
if (USE_OPENSAML_5) { if (USE_OPENSAML_5) {
return new OpenSaml5LogoutRequestValidator(); return new OpenSaml5LogoutRequestValidator();
} }
return new OpenSaml4LogoutRequestValidator(); throw new IllegalArgumentException(
"Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
} }
private Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) { private Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
@ -394,7 +388,8 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
if (USE_OPENSAML_5) { if (USE_OPENSAML_5) {
return new OpenSaml5LogoutRequestResolver(registrations); return new OpenSaml5LogoutRequestResolver(registrations);
} }
return new OpenSaml4LogoutRequestResolver(registrations); throw new IllegalArgumentException(
"Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
} }
} }
@ -454,7 +449,8 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
if (USE_OPENSAML_5) { if (USE_OPENSAML_5) {
return new OpenSaml5LogoutResponseValidator(); return new OpenSaml5LogoutResponseValidator();
} }
return new OpenSaml4LogoutResponseValidator(); throw new IllegalArgumentException(
"Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
} }
private Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) { private Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
@ -464,7 +460,8 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
if (USE_OPENSAML_5) { if (USE_OPENSAML_5) {
return new OpenSaml5LogoutResponseResolver(registrations); return new OpenSaml5LogoutResponseResolver(registrations);
} }
return new OpenSaml4LogoutResponseResolver(registrations); throw new IllegalArgumentException(
"Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
} }
} }

View File

@ -24,7 +24,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver;
import org.springframework.security.saml2.provider.service.metadata.OpenSaml5MetadataResolver; import org.springframework.security.saml2.provider.service.metadata.OpenSaml5MetadataResolver;
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
@ -113,10 +112,8 @@ public class Saml2MetadataConfigurer<H extends HttpSecurityBuilder<H>>
metadata.setRequestMatcher(getRequestMatcherBuilder().matcher(metadataUrl)); metadata.setRequestMatcher(getRequestMatcherBuilder().matcher(metadataUrl));
return metadata; return metadata;
} }
RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(registrations, throw new IllegalArgumentException(
new OpenSaml4MetadataResolver()); "Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
metadata.setRequestMatcher(getRequestMatcherBuilder().matcher(metadataUrl));
return metadata;
}; };
return this; return this;
} }
@ -156,7 +153,8 @@ public class Saml2MetadataConfigurer<H extends HttpSecurityBuilder<H>>
if (USE_OPENSAML_5) { if (USE_OPENSAML_5) {
return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml5MetadataResolver()); return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml5MetadataResolver());
} }
return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver()); throw new IllegalArgumentException(
"Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
} }
private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) { private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) {

View File

@ -24,13 +24,11 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -90,16 +88,16 @@ final class Saml2LoginBeanDefinitionParserUtils {
.addConstructorArgValue(defaultRelyingPartyRegistrationResolver) .addConstructorArgValue(defaultRelyingPartyRegistrationResolver)
.getBeanDefinition(); .getBeanDefinition();
} }
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4AuthenticationRequestResolver.class) throw new IllegalArgumentException(
.addConstructorArgValue(defaultRelyingPartyRegistrationResolver) "Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
.getBeanDefinition();
} }
static BeanDefinition createAuthenticationProvider() { static BeanDefinition createAuthenticationProvider() {
if (USE_OPENSAML_5) { if (USE_OPENSAML_5) {
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5AuthenticationProvider.class).getBeanDefinition(); return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5AuthenticationProvider.class).getBeanDefinition();
} }
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4AuthenticationProvider.class).getBeanDefinition(); throw new IllegalArgumentException(
"Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
} }
static BeanMetadataElement getAuthenticationConverter(Element element) { static BeanMetadataElement getAuthenticationConverter(Element element) {

View File

@ -22,14 +22,10 @@ import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutResponseValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutRequestValidator; import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutRequestValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutResponseValidator; import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutResponseValidator;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
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.OpenSaml5LogoutRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutResponseResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutResponseResolver;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -76,9 +72,8 @@ final class Saml2LogoutBeanDefinitionParserUtils {
.addConstructorArgValue(registrations) .addConstructorArgValue(registrations)
.getBeanDefinition(); .getBeanDefinition();
} }
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4LogoutResponseResolver.class) throw new IllegalArgumentException(
.addConstructorArgValue(registrations) "Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
.getBeanDefinition();
} }
static BeanMetadataElement getLogoutRequestValidator(Element element) { static BeanMetadataElement getLogoutRequestValidator(Element element) {
@ -89,7 +84,8 @@ final class Saml2LogoutBeanDefinitionParserUtils {
if (USE_OPENSAML_5) { if (USE_OPENSAML_5) {
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5LogoutRequestValidator.class).getBeanDefinition(); return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5LogoutRequestValidator.class).getBeanDefinition();
} }
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4LogoutRequestValidator.class).getBeanDefinition(); throw new IllegalArgumentException(
"Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
} }
static BeanMetadataElement getLogoutResponseValidator(Element element) { static BeanMetadataElement getLogoutResponseValidator(Element element) {
@ -100,7 +96,8 @@ final class Saml2LogoutBeanDefinitionParserUtils {
if (USE_OPENSAML_5) { if (USE_OPENSAML_5) {
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5LogoutResponseValidator.class).getBeanDefinition(); return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5LogoutResponseValidator.class).getBeanDefinition();
} }
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4LogoutResponseValidator.class).getBeanDefinition(); throw new IllegalArgumentException(
"Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
} }
static BeanMetadataElement getLogoutRequestRepository(Element element) { static BeanMetadataElement getLogoutRequestRepository(Element element) {
@ -121,9 +118,8 @@ final class Saml2LogoutBeanDefinitionParserUtils {
.addConstructorArgValue(registrations) .addConstructorArgValue(registrations)
.getBeanDefinition(); .getBeanDefinition();
} }
return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4LogoutRequestResolver.class) throw new IllegalArgumentException(
.addConstructorArgValue(registrations) "Spring Security does not support OpenSAML " + Version.getVersion() + ". Please use OpenSAML 5");
.getBeanDefinition();
} }
} }

View File

@ -32,7 +32,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.opensaml.core.Version;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Marshaller; import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Assertion;
@ -69,7 +68,6 @@ import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2Utils; import org.springframework.security.saml2.core.Saml2Utils;
import org.springframework.security.saml2.core.TestSaml2X509Credentials; import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; 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.OpenSaml5AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; 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.Saml2Authentication;
@ -84,7 +82,6 @@ import org.springframework.security.saml2.provider.service.web.DefaultRelyingPar
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; 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.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver;
import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterChainProxy;
@ -139,8 +136,6 @@ public class Saml2LoginConfigurerTests {
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
.build(); .build();
private static final boolean USE_OPENSAML_5 = Version.getVersion().startsWith("5");
private static String SIGNED_RESPONSE; private static String SIGNED_RESPONSE;
private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class); private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class);
@ -550,13 +545,7 @@ public class Saml2LoginConfigurerTests {
RelyingPartyRegistrationRepository registrations) { RelyingPartyRegistrationRepository registrations) {
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver( RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(
registrations); registrations);
if (USE_OPENSAML_5) { OpenSaml5AuthenticationRequestResolver delegate = new OpenSaml5AuthenticationRequestResolver(
OpenSaml5AuthenticationRequestResolver delegate = new OpenSaml5AuthenticationRequestResolver(
registrationResolver);
delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true));
return delegate;
}
OpenSaml4AuthenticationRequestResolver delegate = new OpenSaml4AuthenticationRequestResolver(
registrationResolver); registrationResolver);
delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true));
return delegate; return delegate;
@ -589,13 +578,7 @@ public class Saml2LoginConfigurerTests {
RelyingPartyRegistrationRepository registrations) { RelyingPartyRegistrationRepository registrations) {
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver( RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(
registrations); registrations);
if (USE_OPENSAML_5) { OpenSaml5AuthenticationRequestResolver delegate = new OpenSaml5AuthenticationRequestResolver(
OpenSaml5AuthenticationRequestResolver delegate = new OpenSaml5AuthenticationRequestResolver(
registrationResolver);
delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true));
return delegate;
}
OpenSaml4AuthenticationRequestResolver delegate = new OpenSaml4AuthenticationRequestResolver(
registrationResolver); registrationResolver);
delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true));
return delegate; return delegate;
@ -773,8 +756,7 @@ public class Saml2LoginConfigurerTests {
@Import(Saml2LoginConfigBeans.class) @Import(Saml2LoginConfigBeans.class)
static class CustomAuthenticationProviderConfig { static class CustomAuthenticationProviderConfig {
private final AuthenticationProvider provider = spy( private final AuthenticationProvider provider = spy(new OpenSaml5AuthenticationProvider());
USE_OPENSAML_5 ? new OpenSaml5AuthenticationProvider() : new OpenSaml4AuthenticationProvider());
@Bean @Bean
SecurityFilterChain web(HttpSecurity http) throws Exception { SecurityFilterChain web(HttpSecurity http) throws Exception {

View File

@ -30,7 +30,7 @@ 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.EnableWebSecurity;
import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver; import org.springframework.security.saml2.provider.service.metadata.OpenSaml5MetadataResolver;
import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver; import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse;
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
@ -159,7 +159,7 @@ public class Saml2MetadataConfigurerTests {
// should ignore // should ignore
@Bean @Bean
Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) { Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) {
return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver()); return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml5MetadataResolver());
} }
} }

View File

@ -20,7 +20,7 @@ import java.nio.charset.StandardCharsets;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import net.shibboleth.utilities.java.support.xml.SerializeSupport; import net.shibboleth.shared.xml.SerializeSupport;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;

View File

@ -37,7 +37,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -279,8 +279,8 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
public void parseWhenRelayStateResolverThenUses() { public void parseWhenRelayStateResolverThenUses() {
this.spring.configLocations(xml("RelayStateResolver")).autowire(); this.spring.configLocations(xml("RelayStateResolver")).autowire();
Converter<HttpServletRequest, String> relayStateResolver = this.spring.getContext().getBean(Converter.class); Converter<HttpServletRequest, String> relayStateResolver = this.spring.getContext().getBean(Converter.class);
OpenSaml4AuthenticationRequestResolver authenticationRequestResolver = this.spring.getContext() OpenSaml5AuthenticationRequestResolver authenticationRequestResolver = this.spring.getContext()
.getBean(OpenSaml4AuthenticationRequestResolver.class); .getBean(OpenSaml5AuthenticationRequestResolver.class);
MockHttpServletRequest request = get("/saml2/authenticate/one").build(); MockHttpServletRequest request = get("/saml2/authenticate/one").build();
authenticationRequestResolver.resolve(request); authenticationRequestResolver.resolve(request);
verify(relayStateResolver).convert(request); verify(relayStateResolver).convert(request);

View File

@ -47,7 +47,7 @@
</asserting-party> </asserting-party>
</relying-party-registrations> </relying-party-registrations>
<b:bean class="org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver"> <b:bean class="org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver">
<b:constructor-arg ref="registrations"/> <b:constructor-arg ref="registrations"/>
<b:property name="relayStateResolver" ref="relayStateResolver"/> <b:property name="relayStateResolver" ref="relayStateResolver"/>
</b:bean> </b:bean>

View File

@ -61,8 +61,8 @@ dependencies {
api libs.org.hibernate.orm.hibernate.core api libs.org.hibernate.orm.hibernate.core
api libs.org.hsqldb api libs.org.hsqldb
api libs.org.apereo.cas.client.cas.client.core api libs.org.apereo.cas.client.cas.client.core
api libs.org.opensaml.opensaml.saml.api api libs.org.opensaml.opensaml5.saml.api
api libs.org.opensaml.opensaml.saml.impl api libs.org.opensaml.opensaml5.saml.impl
api libs.org.python.jython api libs.org.python.jython
api libs.org.seleniumhq.selenium.htmlunit.driver api libs.org.seleniumhq.selenium.htmlunit.driver
api libs.org.seleniumhq.selenium.selenium.java api libs.org.seleniumhq.selenium.selenium.java

View File

@ -1,76 +0,0 @@
= OpenSAML Support
Spring Security provides an API for implementing SAML 2.0 features, and it also provides a default implementation using OpenSAML.
Because Spring Security supports more than one version of OpenSAML at the same time, the components use the following naming convention:
* Any component that is usable across all supported versions is named `OpenSamlXXX`.
* Any component that targets OpenSAML 4.x is named `OpenSaml4XXX`
* Any component that targets OpenSAML 5.x is named `OpenSaml5XXX`
`spring-security-config` selects between these implementations by default by discovering which version your application is currently using.
For example, if you are using OpenSAML 4, Spring Security will use the `OpenSaml4XXX` components.
== Selecting OpenSAML 4
Spring Security depends on OpenSAML 4 by default, so you need do nothing to begin using it other than importing the `spring-security-saml` dependency.
== Selecting OpenSAML 5
To use OpenSAML, you should override the `opensaml` dependencies as follows:
[tabs]
======
Maven::
+
[source,maven,role="primary"]
----
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
<exclusions>
<exclusion>
<groupId>org.opensaml</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-api</artifactId>
<version>5.1.2</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-impl</artifactId>
<version>5.1.2</version>
</dependency>
</dependencies>
----
Gradle::
+
[source,gradle,role="secondary"]
----
dependencies {
constraints {
implementation "org.opensaml:opensaml-core-api:5.1.2"
implementation "org.opensaml:opensaml-core-impl:5.1.2"
implementation "org.opensaml:opensaml-saml-api:5.1.2"
implementation "org.opensaml:opensaml-saml-impl:5.1.2"
}
// ...
implementation ('org.springframework.security:spring-security-saml2-service-provider') {
exclude group: "org.opensaml", module: "opensaml-core"
}
// ...
}
----
======
[NOTE]
The exclusion is necessary because OpenSAML 5 splits `opensaml-core` into `opensaml-core-api` and `opensaml-core-impl`

View File

@ -11,7 +11,6 @@ org-eclipse-jetty = "11.0.25"
org-jetbrains-kotlin = "2.2.0" org-jetbrains-kotlin = "2.2.0"
org-jetbrains-kotlinx = "1.10.2" org-jetbrains-kotlinx = "1.10.2"
org-mockito = "5.17.0" org-mockito = "5.17.0"
org-opensaml = "4.3.2"
org-opensaml5 = "5.1.2" org-opensaml5 = "5.1.2"
org-springframework = "7.0.0-M7" org-springframework = "7.0.0-M7"
@ -70,8 +69,6 @@ org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" } org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
org-junit-junit-bom = "org.junit:junit-bom:5.12.2" org-junit-junit-bom = "org.junit:junit-bom:5.12.2"
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" } org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
org-opensaml-opensaml-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml" }
org-opensaml-opensaml-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml" }
org-opensaml-opensaml5-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml5" } org-opensaml-opensaml5-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml5" }
org-opensaml-opensaml5-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml5" } org-opensaml-opensaml5-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml5" }
org-python-jython = { module = "org.python:jython", version = "2.5.3" } org-python-jython = { module = "org.python:jython", version = "2.5.3" }

View File

@ -6,25 +6,12 @@ configurations {
} }
sourceSets { sourceSets {
opensaml4Main {
java {
compileClasspath += main.output
srcDir 'src/opensaml4Main/java'
}
}
opensaml5Main { opensaml5Main {
java { java {
compileClasspath = main.output + configurations.opensamlFiveMain compileClasspath = main.output + configurations.opensamlFiveMain
srcDir 'src/opensaml5Main/java' srcDir 'src/opensaml5Main/java'
} }
} }
opensaml4Test {
java {
compileClasspath += main.output + test.output + opensaml4Main.output + test.compileClasspath
runtimeClasspath += main.output + test.output + opensaml4Main.output + test.runtimeClasspath
srcDir 'src/opensaml4Test/java'
}
}
opensaml5Test { opensaml5Test {
java { java {
compileClasspath = main.output + test.output + opensaml5Main.output + configurations.opensamlFiveTest compileClasspath = main.output + test.output + opensaml5Main.output + configurations.opensamlFiveTest
@ -99,7 +86,7 @@ dependencies {
opensamlFiveMain (libs.org.opensaml.opensaml5.saml.api) { opensamlFiveMain (libs.org.opensaml.opensaml5.saml.api) {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
opensamlFiveMain (libs.org.opensaml.opensaml5.saml.impl) { opensamlFiveMain (libs.org.opensaml.opensaml5.saml.api) {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
@ -125,31 +112,22 @@ dependencies {
jar { jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from sourceSets.opensaml4Main.output
from sourceSets.opensaml5Main.output from sourceSets.opensaml5Main.output
} }
sourcesJar { sourcesJar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from sourceSets.opensaml4Main.allJava
from sourceSets.opensaml5Main.allJava from sourceSets.opensaml5Main.allJava
} }
testJar { testJar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from sourceSets.opensaml4Test.output
from sourceSets.opensaml5Test.output from sourceSets.opensaml5Test.output
} }
javadoc { javadoc {
classpath += configurations.opensamlFiveMain classpath += configurations.opensamlFiveMain
source = sourceSets.main.allJava + sourceSets.opensaml4Main.allJava + sourceSets.opensaml5Main.allJava source = sourceSets.main.allJava + sourceSets.opensaml5Main.allJava
}
tasks.register("opensaml4Test", Test) {
useJUnitPlatform()
testClassesDirs = sourceSets.opensaml4Test.output.classesDirs
classpath = sourceSets.opensaml4Test.runtimeClasspath
} }
tasks.register("opensaml5Test", Test) { tasks.register("opensaml5Test", Test) {
@ -159,6 +137,5 @@ tasks.register("opensaml5Test", Test) {
} }
tasks.named("test") { tasks.named("test") {
dependsOn opensaml4Test
dependsOn opensaml5Test dependsOn opensaml5Test
} }

View File

@ -253,13 +253,6 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
(params) -> params.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(5))); (params) -> params.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(5)));
} }
static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator(
Converter<AssertionToken, ValidationContext> contextConverter) {
return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
(assertionToken) -> SAML20AssertionValidators.attributeValidator, contextConverter);
}
static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidatorWithParameters( static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidatorWithParameters(
Consumer<Map<String, Object>> validationContextParameters) { Consumer<Map<String, Object>> validationContextParameters) {
return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION, return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
@ -523,7 +516,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message)); return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
} }
String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(), String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
((Response) assertion.getParent()).getID(), context.getValidationFailureMessage()); ((Response) assertion.getParent()).getID(), context.getValidationFailureMessages());
return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message)); return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
}; };
} }

View File

@ -20,7 +20,6 @@ import java.io.InputStream;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import org.opensaml.core.Version;
import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Unmarshaller; import org.opensaml.core.xml.io.Unmarshaller;
@ -43,19 +42,7 @@ final class OpenSamlMetadataUtils {
} }
static OpenSamlDeserializer resolveDeserializer() { static OpenSamlDeserializer resolveDeserializer() {
if (Version.getVersion().startsWith("4")) { return new OpenSaml5Deserializer();
return new OpenSaml4Deserializer();
}
String opensaml5 = "org.springframework.security.saml2.provider.service.registration.OpenSaml5Template";
try {
Class<?> template = Class.forName(opensaml5);
OpenSamlOperations operations = (OpenSamlOperations) template.getDeclaredConstructor().newInstance();
return operations::deserialize;
}
catch (Exception ex) {
throw new IllegalStateException(
"Application appears to be using OpenSAML 5, but Spring's OpenSAML 5 support is not on the classpath");
}
} }
private OpenSamlMetadataUtils() { private OpenSamlMetadataUtils() {
@ -79,7 +66,7 @@ final class OpenSamlMetadataUtils {
} }
private static class OpenSaml4Deserializer implements OpenSamlDeserializer { private static class OpenSaml5Deserializer implements OpenSamlDeserializer {
@Override @Override
public XMLObject deserialize(InputStream serialized) { public XMLObject deserialize(InputStream serialized) {

View File

@ -1,607 +0,0 @@
/*
* Copyright 2004-present 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.internal;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
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.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.ext.saml2delrestrict.Delegate;
import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.EncryptedAttribute;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.DecryptionException;
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* For internal use only. Subject to breaking changes at any time.
*/
final class OpenSaml4Template implements OpenSamlOperations {
private static final Log logger = LogFactory.getLog(OpenSaml4Template.class);
@Override
public <T extends XMLObject> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
}
return (T) builder.buildObject(elementName);
}
@Override
public <T extends XMLObject> T deserialize(String serialized) {
return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
}
@Override
public <T extends XMLObject> T deserialize(InputStream serialized) {
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized);
Element element = document.getDocumentElement();
UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
Unmarshaller unmarshaller = factory.getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
return (T) unmarshaller.unmarshall(element);
}
catch (Saml2Exception ex) {
throw ex;
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize payload", ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(XMLObject object) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
try {
return serialize(marshaller.marshall(object));
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(Element element) {
return new OpenSaml4SerializationConfigurer(element);
}
@Override
public OpenSaml4SignatureConfigurer withSigningKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4SignatureConfigurer(credentials);
}
@Override
public OpenSaml4VerificationConfigurer withVerificationKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4VerificationConfigurer(credentials);
}
@Override
public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4DecryptionConfigurer(credentials);
}
OpenSaml4Template() {
}
static final class OpenSaml4SerializationConfigurer
implements SerializationConfigurer<OpenSaml4SerializationConfigurer> {
private final Element element;
boolean pretty;
OpenSaml4SerializationConfigurer(Element element) {
this.element = element;
}
@Override
public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) {
this.pretty = pretty;
return this;
}
@Override
public String serialize() {
if (this.pretty) {
return SerializeSupport.prettyPrintXML(this.element);
}
return SerializeSupport.nodeToString(this.element);
}
}
static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer<OpenSaml4SignatureConfigurer> {
private final Collection<Saml2X509Credential> credentials;
private final Map<String, String> components = new LinkedHashMap<>();
private List<String> algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
OpenSaml4SignatureConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public OpenSaml4SignatureConfigurer algorithms(List<String> algs) {
this.algs = algs;
return this;
}
@Override
public <O extends SignableXMLObject> O sign(O object) {
SignatureSigningParameters parameters = resolveSigningParameters();
try {
SignatureSupport.signObject(object, parameters);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
return object;
}
@Override
public Map<String, String> sign(Map<String, String> params) {
SignatureSigningParameters parameters = resolveSigningParameters();
this.components.putAll(params);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
private SignatureSigningParameters resolveSigningParameters() {
List<Credential> credentials = resolveSigningCredentials();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(this.algs);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
namedManager.setUseDefaultManager(true);
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
// Generator for X509Credentials
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
x509Factory.setEmitEntityCertificate(true);
x509Factory.setEmitEntityCertificateChain(true);
defaultManager.registerFactory(x509Factory);
return namedManager;
}
private List<Credential> resolveSigningCredentials() {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : this.credentials) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}
static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer {
private final Collection<Saml2X509Credential> credentials;
private String entityId;
OpenSaml4VerificationConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public VerificationConfigurer entityId(String entityId) {
this.entityId = entityId;
return this;
}
private SignatureTrustEngine trustEngine(Collection<Saml2X509Credential> keys) {
Set<Credential> credentials = new HashSet<>();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(this.entityId);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private CriteriaSet verificationCriteria(Issuer issuer) {
return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())),
new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)),
new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
}
@Override
public Collection<Saml2Error> verify(SignableXMLObject signable) {
if (signable instanceof StatusResponseType response) {
return verifySignature(response.getID(), response.getIssuer(), response.getSignature());
}
if (signable instanceof RequestAbstractType request) {
return verifySignature(request.getID(), request.getIssuer(), request.getSignature());
}
if (signable instanceof Assertion assertion) {
return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature());
}
throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName());
}
private Collection<Saml2Error> verifySignature(String id, Issuer issuer, Signature signature) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(issuer);
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
try {
if (!trustEngine.validate(signature, criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
return errors;
}
@Override
public Collection<Saml2Error> verify(RedirectParameters parameters) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(parameters.getIssuer());
if (parameters.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + parameters.getId() + "]"));
}
if (!parameters.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + parameters.getId() + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = parameters.getAlgorithm();
try {
if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria,
null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]: "));
}
return errors;
}
}
static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
private final Decrypter decrypter;
OpenSaml4DecryptionConfigurer(Collection<Saml2X509Credential> decryptionCredentials) {
this.decrypter = decrypter(decryptionCredentials);
}
private static Decrypter decrypter(Collection<Saml2X509Credential> decryptionCredentials) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : decryptionCredentials) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
@Override
public void decrypt(XMLObject object) {
if (object instanceof Response response) {
decryptResponse(response);
return;
}
if (object instanceof Assertion assertion) {
decryptAssertion(assertion);
}
if (object instanceof LogoutRequest request) {
decryptLogoutRequest(request);
}
}
/*
* The methods that follow are adapted from OpenSAML's {@link DecryptAssertions},
* {@link DecryptNameIDs}, and {@link DecryptAttributes}.
*
* <p>The reason that these OpenSAML classes are not used directly is because they
* reference {@link javax.servlet.http.HttpServletRequest} which is a lower
* Servlet API version than what Spring Security SAML uses.
*
* If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then
* this arrangement can be revisited.
*/
private void decryptResponse(Response response) {
Collection<Assertion> decrypteds = new ArrayList<>();
int count = 0;
int size = response.getEncryptedAssertions().size();
for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) {
logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size,
response.getID()));
try {
Assertion decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
count++;
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
response.getAssertions().addAll(decrypteds);
// Re-marshall the response so that any ID attributes within the decrypted
// Assertions
// will have their ID-ness re-established at the DOM level.
if (!decrypteds.isEmpty()) {
try {
XMLObjectSupport.marshall(response);
}
catch (final MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}
private void decryptAssertion(Assertion assertion) {
for (AttributeStatement statement : assertion.getAttributeStatements()) {
decryptAttributes(statement);
}
decryptSubject(assertion.getSubject());
if (assertion.getConditions() != null) {
for (Condition c : assertion.getConditions().getConditions()) {
if (!(c instanceof DelegationRestrictionType delegation)) {
continue;
}
for (Delegate d : delegation.getDelegates()) {
if (d.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID());
if (decrypted != null) {
d.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
}
private void decryptAttributes(AttributeStatement statement) {
Collection<Attribute> decrypteds = new ArrayList<>();
for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) {
try {
Attribute decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
statement.getAttributes().addAll(decrypteds);
}
private void decryptSubject(Subject subject) {
if (subject != null) {
if (subject.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID());
if (decrypted != null) {
subject.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
if (sc.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID());
if (decrypted != null) {
sc.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
private void decryptLogoutRequest(LogoutRequest request) {
if (request.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID());
if (decrypted != null) {
request.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}

View File

@ -1,425 +0,0 @@
/*
* Copyright 2004-present 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.time.Duration;
import java.util.Map;
import java.util.function.Consumer;
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.EncryptedAssertion;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.springframework.core.convert.converter.Converter;
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.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.util.Assert;
/**
* Implementation of {@link AuthenticationProvider} for SAML authentications when
* receiving a {@code Response} object containing an {@code Assertion}. This
* implementation uses the {@code OpenSAML 4} library.
*
* <p>
* The {@link OpenSaml4AuthenticationProvider} 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 Josh Cummings
* @since 5.5
* @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://shibboleth.atlassian.net/wiki/spaces/OSAML/overview">OpenSAML</a>
*/
public final class OpenSaml4AuthenticationProvider implements AuthenticationProvider {
private final BaseOpenSamlAuthenticationProvider delegate;
/**
* Creates an {@link OpenSaml4AuthenticationProvider}
*/
public OpenSaml4AuthenticationProvider() {
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml4Template());
this.delegate.setValidateResponseAfterAssertions(false);
}
/**
* 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) -&gt; {
* 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&lt;EncryptedAssertion, Assertion&gt; myService = ...
* provider.setResponseDecrypter((responseToken) -&gt; {
* 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.delegate
.setResponseElementsDecrypter((token) -> responseElementsDecrypter.accept(new ResponseToken(token)));
}
/**
* Set the {@link Converter} to use for validating the SAML 2.0 Response.
*
* You can still invoke the default validator by delegating to
* {@link #createDefaultResponseValidator()}, like so:
*
* <pre>
* OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
* provider.setResponseValidator(responseToken -&gt; {
* Saml2ResponseValidatorResult result = createDefaultResponseValidator()
* .convert(responseToken)
* return result.concat(myCustomValidator.convert(responseToken));
* });
* </pre>
* @param responseValidator the {@link Converter} to use
* @since 5.6
*/
public void setResponseValidator(Converter<ResponseToken, Saml2ResponseValidatorResult> responseValidator) {
Assert.notNull(responseValidator, "responseValidator cannot be null");
this.delegate.setResponseValidator((token) -> responseValidator.convert(new ResponseToken(token)));
}
/**
* 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 -&gt; {
* 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 -&gt; {
* Map&lt;String, Object&gt; params = new HashMap&lt;&gt;();
* 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.
* @param assertionValidator the validator to use
* @since 5.4
*/
public void setAssertionValidator(Converter<AssertionToken, Saml2ResponseValidatorResult> assertionValidator) {
Assert.notNull(assertionValidator, "assertionValidator cannot be null");
this.delegate.setAssertionValidator((token) -> assertionValidator.convert(new AssertionToken(token)));
}
/**
* 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) -&gt; {
* 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) -&gt; {
* 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.delegate.setAssertionElementsDecrypter((token) -> assertionDecrypter.accept(new AssertionToken(token)));
}
/**
* 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&lt;ResponseToken, Saml2Authentication&gt; authenticationConverter =
* createDefaultResponseAuthenticationConverter();
* provider.setResponseAuthenticationConverter(responseToken -&gt; {
* Saml2Authentication authentication = authenticationConverter.convert(responseToken);
* User user = myUserRepository.findByUsername(authentication.getName());
* return new MyAuthentication(authentication, user);
* });
* </pre>
* @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.delegate.setResponseAuthenticationConverter(
(token) -> responseAuthenticationConverter.convert(new ResponseToken(token)));
}
/**
* Construct a default strategy for validating the SAML 2.0 Response
* @return the default response validator strategy
* @since 5.6
*/
public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
Converter<BaseOpenSamlAuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> delegate = BaseOpenSamlAuthenticationProvider
.createDefaultResponseValidator();
return (token) -> delegate
.convert(new BaseOpenSamlAuthenticationProvider.ResponseToken(token.getResponse(), token.getToken()));
}
/**
* Construct a default strategy for validating each SAML 2.0 Assertion and associated
* {@link Authentication} token
* @return the default assertion validator strategy
*/
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator() {
return createDefaultAssertionValidatorWithParameters(
(params) -> params.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(5)));
}
/**
* 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
* @deprecated Use {@link #createDefaultAssertionValidatorWithParameters} instead
*/
@Deprecated
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator(
Converter<AssertionToken, ValidationContext> contextConverter) {
Converter<BaseOpenSamlAuthenticationProvider.AssertionToken, ValidationContext> contextDelegate = (
token) -> contextConverter.convert(new AssertionToken(token.getAssertion(), token.getToken()));
Converter<BaseOpenSamlAuthenticationProvider.AssertionToken, Saml2ResponseValidatorResult> delegate = BaseOpenSamlAuthenticationProvider
.createDefaultAssertionValidator(contextDelegate);
return (token) -> delegate
.convert(new BaseOpenSamlAuthenticationProvider.AssertionToken(token.getAssertion(), token.getToken()));
}
/**
* Construct a default strategy for validating each SAML 2.0 Assertion and associated
* {@link Authentication} token
* @param validationContextParameters a consumer for editing the values passed to the
* {@link ValidationContext} for each assertion being validated
* @return the default assertion validator strategy
* @since 5.8
*/
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidatorWithParameters(
Consumer<Map<String, Object>> validationContextParameters) {
Converter<BaseOpenSamlAuthenticationProvider.AssertionToken, Saml2ResponseValidatorResult> delegate = BaseOpenSamlAuthenticationProvider
.createDefaultAssertionValidatorWithParameters(validationContextParameters);
return (token) -> delegate
.convert(new BaseOpenSamlAuthenticationProvider.AssertionToken(token.getAssertion(), token.getToken()));
}
/**
* 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
*/
public static Converter<ResponseToken, Saml2Authentication> createDefaultResponseAuthenticationConverter() {
Converter<BaseOpenSamlAuthenticationProvider.ResponseToken, Saml2Authentication> delegate = BaseOpenSamlAuthenticationProvider
.createDefaultResponseAuthenticationConverter();
return (token) -> delegate
.convert(new BaseOpenSamlAuthenticationProvider.ResponseToken(token.getResponse(), token.getToken()));
}
/**
* @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 {
return this.delegate.authenticate(authentication);
}
@Override
public boolean supports(Class<?> authentication) {
return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* 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;
}
ResponseToken(BaseOpenSamlAuthenticationProvider.ResponseToken token) {
this.token = token.getToken();
this.response = token.getResponse();
}
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;
}
AssertionToken(BaseOpenSamlAuthenticationProvider.AssertionToken token) {
this.token = token.getToken();
this.assertion = token.getAssertion();
}
public Assertion getAssertion() {
return this.assertion;
}
public Saml2AuthenticationToken getToken() {
return this.token;
}
}
}

View File

@ -1,607 +0,0 @@
/*
* Copyright 2004-present 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.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
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.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.ext.saml2delrestrict.Delegate;
import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.EncryptedAttribute;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.DecryptionException;
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* For internal use only. Subject to breaking changes at any time.
*/
final class OpenSaml4Template implements OpenSamlOperations {
private static final Log logger = LogFactory.getLog(OpenSaml4Template.class);
@Override
public <T extends XMLObject> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
}
return (T) builder.buildObject(elementName);
}
@Override
public <T extends XMLObject> T deserialize(String serialized) {
return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
}
@Override
public <T extends XMLObject> T deserialize(InputStream serialized) {
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized);
Element element = document.getDocumentElement();
UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
Unmarshaller unmarshaller = factory.getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
return (T) unmarshaller.unmarshall(element);
}
catch (Saml2Exception ex) {
throw ex;
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize payload", ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(XMLObject object) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
try {
return serialize(marshaller.marshall(object));
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(Element element) {
return new OpenSaml4SerializationConfigurer(element);
}
@Override
public OpenSaml4SignatureConfigurer withSigningKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4SignatureConfigurer(credentials);
}
@Override
public OpenSaml4VerificationConfigurer withVerificationKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4VerificationConfigurer(credentials);
}
@Override
public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4DecryptionConfigurer(credentials);
}
OpenSaml4Template() {
}
static final class OpenSaml4SerializationConfigurer
implements SerializationConfigurer<OpenSaml4SerializationConfigurer> {
private final Element element;
boolean pretty;
OpenSaml4SerializationConfigurer(Element element) {
this.element = element;
}
@Override
public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) {
this.pretty = pretty;
return this;
}
@Override
public String serialize() {
if (this.pretty) {
return SerializeSupport.prettyPrintXML(this.element);
}
return SerializeSupport.nodeToString(this.element);
}
}
static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer<OpenSaml4SignatureConfigurer> {
private final Collection<Saml2X509Credential> credentials;
private final Map<String, String> components = new LinkedHashMap<>();
private List<String> algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
OpenSaml4SignatureConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public OpenSaml4SignatureConfigurer algorithms(List<String> algs) {
this.algs = algs;
return this;
}
@Override
public <O extends SignableXMLObject> O sign(O object) {
SignatureSigningParameters parameters = resolveSigningParameters();
try {
SignatureSupport.signObject(object, parameters);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
return object;
}
@Override
public Map<String, String> sign(Map<String, String> params) {
SignatureSigningParameters parameters = resolveSigningParameters();
this.components.putAll(params);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
private SignatureSigningParameters resolveSigningParameters() {
List<Credential> credentials = resolveSigningCredentials();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(this.algs);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
namedManager.setUseDefaultManager(true);
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
// Generator for X509Credentials
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
x509Factory.setEmitEntityCertificate(true);
x509Factory.setEmitEntityCertificateChain(true);
defaultManager.registerFactory(x509Factory);
return namedManager;
}
private List<Credential> resolveSigningCredentials() {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : this.credentials) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}
static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer {
private final Collection<Saml2X509Credential> credentials;
private String entityId;
OpenSaml4VerificationConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public VerificationConfigurer entityId(String entityId) {
this.entityId = entityId;
return this;
}
private SignatureTrustEngine trustEngine(Collection<Saml2X509Credential> keys) {
Set<Credential> credentials = new HashSet<>();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(this.entityId);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private CriteriaSet verificationCriteria(Issuer issuer) {
return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())),
new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)),
new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
}
@Override
public Collection<Saml2Error> verify(SignableXMLObject signable) {
if (signable instanceof StatusResponseType response) {
return verifySignature(response.getID(), response.getIssuer(), response.getSignature());
}
if (signable instanceof RequestAbstractType request) {
return verifySignature(request.getID(), request.getIssuer(), request.getSignature());
}
if (signable instanceof Assertion assertion) {
return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature());
}
throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName());
}
private Collection<Saml2Error> verifySignature(String id, Issuer issuer, Signature signature) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(issuer);
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
try {
if (!trustEngine.validate(signature, criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
return errors;
}
@Override
public Collection<Saml2Error> verify(RedirectParameters parameters) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(parameters.getIssuer());
if (parameters.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + parameters.getId() + "]"));
}
if (!parameters.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + parameters.getId() + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = parameters.getAlgorithm();
try {
if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria,
null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]: "));
}
return errors;
}
}
static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
private final Decrypter decrypter;
OpenSaml4DecryptionConfigurer(Collection<Saml2X509Credential> decryptionCredentials) {
this.decrypter = decrypter(decryptionCredentials);
}
private static Decrypter decrypter(Collection<Saml2X509Credential> decryptionCredentials) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : decryptionCredentials) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
@Override
public void decrypt(XMLObject object) {
if (object instanceof Response response) {
decryptResponse(response);
return;
}
if (object instanceof Assertion assertion) {
decryptAssertion(assertion);
}
if (object instanceof LogoutRequest request) {
decryptLogoutRequest(request);
}
}
/*
* The methods that follow are adapted from OpenSAML's {@link DecryptAssertions},
* {@link DecryptNameIDs}, and {@link DecryptAttributes}.
*
* <p>The reason that these OpenSAML classes are not used directly is because they
* reference {@link javax.servlet.http.HttpServletRequest} which is a lower
* Servlet API version than what Spring Security SAML uses.
*
* If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then
* this arrangement can be revisited.
*/
private void decryptResponse(Response response) {
Collection<Assertion> decrypteds = new ArrayList<>();
int count = 0;
int size = response.getEncryptedAssertions().size();
for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) {
logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size,
response.getID()));
try {
Assertion decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
count++;
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
response.getAssertions().addAll(decrypteds);
// Re-marshall the response so that any ID attributes within the decrypted
// Assertions
// will have their ID-ness re-established at the DOM level.
if (!decrypteds.isEmpty()) {
try {
XMLObjectSupport.marshall(response);
}
catch (final MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}
private void decryptAssertion(Assertion assertion) {
for (AttributeStatement statement : assertion.getAttributeStatements()) {
decryptAttributes(statement);
}
decryptSubject(assertion.getSubject());
if (assertion.getConditions() != null) {
for (Condition c : assertion.getConditions().getConditions()) {
if (!(c instanceof DelegationRestrictionType delegation)) {
continue;
}
for (Delegate d : delegation.getDelegates()) {
if (d.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID());
if (decrypted != null) {
d.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
}
private void decryptAttributes(AttributeStatement statement) {
Collection<Attribute> decrypteds = new ArrayList<>();
for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) {
try {
Attribute decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
statement.getAttributes().addAll(decrypteds);
}
private void decryptSubject(Subject subject) {
if (subject != null) {
if (subject.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID());
if (decrypted != null) {
subject.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
if (sc.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID());
if (decrypted != null) {
sc.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
private void decryptLogoutRequest(LogoutRequest request) {
if (request.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID());
if (decrypted != null) {
request.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}

View File

@ -1,30 +0,0 @@
/*
* Copyright 2004-present 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.logout;
public final class OpenSaml4LogoutRequestValidator implements Saml2LogoutRequestValidator {
@SuppressWarnings("deprecation")
private final Saml2LogoutRequestValidator delegate = new BaseOpenSamlLogoutRequestValidator(
new OpenSaml4Template());
@Override
public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters) {
return this.delegate.validate(parameters);
}
}

View File

@ -1,30 +0,0 @@
/*
* Copyright 2004-present 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.logout;
public final class OpenSaml4LogoutResponseValidator implements Saml2LogoutResponseValidator {
@SuppressWarnings("deprecation")
private final Saml2LogoutResponseValidator delegate = new BaseOpenSamlLogoutResponseValidator(
new OpenSaml4Template());
@Override
public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters) {
return this.delegate.validate(parameters);
}
}

View File

@ -1,607 +0,0 @@
/*
* Copyright 2004-present 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.logout;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
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.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.ext.saml2delrestrict.Delegate;
import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.EncryptedAttribute;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.DecryptionException;
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* For internal use only. Subject to breaking changes at any time.
*/
final class OpenSaml4Template implements OpenSamlOperations {
private static final Log logger = LogFactory.getLog(OpenSaml4Template.class);
@Override
public <T extends XMLObject> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
}
return (T) builder.buildObject(elementName);
}
@Override
public <T extends XMLObject> T deserialize(String serialized) {
return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
}
@Override
public <T extends XMLObject> T deserialize(InputStream serialized) {
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized);
Element element = document.getDocumentElement();
UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
Unmarshaller unmarshaller = factory.getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
return (T) unmarshaller.unmarshall(element);
}
catch (Saml2Exception ex) {
throw ex;
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize payload", ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(XMLObject object) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
try {
return serialize(marshaller.marshall(object));
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(Element element) {
return new OpenSaml4SerializationConfigurer(element);
}
@Override
public OpenSaml4SignatureConfigurer withSigningKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4SignatureConfigurer(credentials);
}
@Override
public OpenSaml4VerificationConfigurer withVerificationKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4VerificationConfigurer(credentials);
}
@Override
public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4DecryptionConfigurer(credentials);
}
OpenSaml4Template() {
}
static final class OpenSaml4SerializationConfigurer
implements SerializationConfigurer<OpenSaml4SerializationConfigurer> {
private final Element element;
boolean pretty;
OpenSaml4SerializationConfigurer(Element element) {
this.element = element;
}
@Override
public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) {
this.pretty = pretty;
return this;
}
@Override
public String serialize() {
if (this.pretty) {
return SerializeSupport.prettyPrintXML(this.element);
}
return SerializeSupport.nodeToString(this.element);
}
}
static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer<OpenSaml4SignatureConfigurer> {
private final Collection<Saml2X509Credential> credentials;
private final Map<String, String> components = new LinkedHashMap<>();
private List<String> algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
OpenSaml4SignatureConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public OpenSaml4SignatureConfigurer algorithms(List<String> algs) {
this.algs = algs;
return this;
}
@Override
public <O extends SignableXMLObject> O sign(O object) {
SignatureSigningParameters parameters = resolveSigningParameters();
try {
SignatureSupport.signObject(object, parameters);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
return object;
}
@Override
public Map<String, String> sign(Map<String, String> params) {
SignatureSigningParameters parameters = resolveSigningParameters();
this.components.putAll(params);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
private SignatureSigningParameters resolveSigningParameters() {
List<Credential> credentials = resolveSigningCredentials();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(this.algs);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
namedManager.setUseDefaultManager(true);
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
// Generator for X509Credentials
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
x509Factory.setEmitEntityCertificate(true);
x509Factory.setEmitEntityCertificateChain(true);
defaultManager.registerFactory(x509Factory);
return namedManager;
}
private List<Credential> resolveSigningCredentials() {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : this.credentials) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}
static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer {
private final Collection<Saml2X509Credential> credentials;
private String entityId;
OpenSaml4VerificationConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public VerificationConfigurer entityId(String entityId) {
this.entityId = entityId;
return this;
}
private SignatureTrustEngine trustEngine(Collection<Saml2X509Credential> keys) {
Set<Credential> credentials = new HashSet<>();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(this.entityId);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private CriteriaSet verificationCriteria(Issuer issuer) {
return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())),
new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)),
new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
}
@Override
public Collection<Saml2Error> verify(SignableXMLObject signable) {
if (signable instanceof StatusResponseType response) {
return verifySignature(response.getID(), response.getIssuer(), response.getSignature());
}
if (signable instanceof RequestAbstractType request) {
return verifySignature(request.getID(), request.getIssuer(), request.getSignature());
}
if (signable instanceof Assertion assertion) {
return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature());
}
throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName());
}
private Collection<Saml2Error> verifySignature(String id, Issuer issuer, Signature signature) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(issuer);
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
try {
if (!trustEngine.validate(signature, criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
return errors;
}
@Override
public Collection<Saml2Error> verify(RedirectParameters parameters) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(parameters.getIssuer());
if (parameters.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + parameters.getId() + "]"));
}
if (!parameters.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + parameters.getId() + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = parameters.getAlgorithm();
try {
if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria,
null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]: "));
}
return errors;
}
}
static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
private final Decrypter decrypter;
OpenSaml4DecryptionConfigurer(Collection<Saml2X509Credential> decryptionCredentials) {
this.decrypter = decrypter(decryptionCredentials);
}
private static Decrypter decrypter(Collection<Saml2X509Credential> decryptionCredentials) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : decryptionCredentials) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
@Override
public void decrypt(XMLObject object) {
if (object instanceof Response response) {
decryptResponse(response);
return;
}
if (object instanceof Assertion assertion) {
decryptAssertion(assertion);
}
if (object instanceof LogoutRequest request) {
decryptLogoutRequest(request);
}
}
/*
* The methods that follow are adapted from OpenSAML's {@link DecryptAssertions},
* {@link DecryptNameIDs}, and {@link DecryptAttributes}.
*
* <p>The reason that these OpenSAML classes are not used directly is because they
* reference {@link javax.servlet.http.HttpServletRequest} which is a lower
* Servlet API version than what Spring Security SAML uses.
*
* If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then
* this arrangement can be revisited.
*/
private void decryptResponse(Response response) {
Collection<Assertion> decrypteds = new ArrayList<>();
int count = 0;
int size = response.getEncryptedAssertions().size();
for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) {
logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size,
response.getID()));
try {
Assertion decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
count++;
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
response.getAssertions().addAll(decrypteds);
// Re-marshall the response so that any ID attributes within the decrypted
// Assertions
// will have their ID-ness re-established at the DOM level.
if (!decrypteds.isEmpty()) {
try {
XMLObjectSupport.marshall(response);
}
catch (final MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}
private void decryptAssertion(Assertion assertion) {
for (AttributeStatement statement : assertion.getAttributeStatements()) {
decryptAttributes(statement);
}
decryptSubject(assertion.getSubject());
if (assertion.getConditions() != null) {
for (Condition c : assertion.getConditions().getConditions()) {
if (!(c instanceof DelegationRestrictionType delegation)) {
continue;
}
for (Delegate d : delegation.getDelegates()) {
if (d.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID());
if (decrypted != null) {
d.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
}
private void decryptAttributes(AttributeStatement statement) {
Collection<Attribute> decrypteds = new ArrayList<>();
for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) {
try {
Attribute decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
statement.getAttributes().addAll(decrypteds);
}
private void decryptSubject(Subject subject) {
if (subject != null) {
if (subject.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID());
if (decrypted != null) {
subject.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
if (sc.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID());
if (decrypted != null) {
sc.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
private void decryptLogoutRequest(LogoutRequest request) {
if (request.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID());
if (decrypted != null) {
request.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}

View File

@ -1,196 +0,0 @@
/*
* Copyright 2004-present 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.logout;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterOutputStream;
import org.springframework.security.saml2.Saml2Exception;
/**
* Utility methods for working with serialized SAML messages.
*
* For internal use only.
*
* @author Josh Cummings
*/
final class Saml2Utils {
private Saml2Utils() {
}
static String samlEncode(byte[] b) {
return Base64.getEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {
return Base64.getMimeDecoder().decode(s);
}
static byte[] samlDeflate(String s) {
try {
ByteArrayOutputStream b = new ByteArrayOutputStream();
DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true));
deflater.write(s.getBytes(StandardCharsets.UTF_8));
deflater.finish();
return b.toByteArray();
}
catch (IOException ex) {
throw new Saml2Exception("Unable to deflate string", ex);
}
}
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
catch (IOException ex) {
throw new Saml2Exception("Unable to inflate string", ex);
}
}
static EncodingConfigurer withDecoded(String decoded) {
return new EncodingConfigurer(decoded);
}
static DecodingConfigurer withEncoded(String encoded) {
return new DecodingConfigurer(encoded);
}
static final class EncodingConfigurer {
private final String decoded;
private boolean deflate;
private EncodingConfigurer(String decoded) {
this.decoded = decoded;
}
EncodingConfigurer deflate(boolean deflate) {
this.deflate = deflate;
return this;
}
String encode() {
byte[] bytes = (this.deflate) ? Saml2Utils.samlDeflate(this.decoded)
: this.decoded.getBytes(StandardCharsets.UTF_8);
return Saml2Utils.samlEncode(bytes);
}
}
static final class DecodingConfigurer {
private static final Base64Checker BASE_64_CHECKER = new Base64Checker();
private final String encoded;
private boolean inflate;
private boolean requireBase64;
private DecodingConfigurer(String encoded) {
this.encoded = encoded;
}
DecodingConfigurer inflate(boolean inflate) {
this.inflate = inflate;
return this;
}
DecodingConfigurer requireBase64(boolean requireBase64) {
this.requireBase64 = requireBase64;
return this;
}
String decode() {
if (this.requireBase64) {
BASE_64_CHECKER.checkAcceptable(this.encoded);
}
byte[] bytes = Saml2Utils.samlDecode(this.encoded);
return (this.inflate) ? Saml2Utils.samlInflate(bytes) : new String(bytes, StandardCharsets.UTF_8);
}
static class Base64Checker {
private static final int[] values = genValueMapping();
Base64Checker() {
}
private static int[] genValueMapping() {
byte[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
.getBytes(StandardCharsets.ISO_8859_1);
int[] values = new int[256];
Arrays.fill(values, -1);
for (int i = 0; i < alphabet.length; i++) {
values[alphabet[i] & 0xff] = i;
}
return values;
}
boolean isAcceptable(String s) {
int goodChars = 0;
int lastGoodCharVal = -1;
// count number of characters from Base64 alphabet
for (int i = 0; i < s.length(); i++) {
int val = values[0xff & s.charAt(i)];
if (val != -1) {
lastGoodCharVal = val;
goodChars++;
}
}
// in cases of an incomplete final chunk, ensure the unused bits are zero
switch (goodChars % 4) {
case 0:
return true;
case 2:
return (lastGoodCharVal & 0b1111) == 0;
case 3:
return (lastGoodCharVal & 0b11) == 0;
default:
return false;
}
}
void checkAcceptable(String ins) {
if (!isAcceptable(ins)) {
throw new IllegalArgumentException("Failed to decode SAMLResponse");
}
}
}
}
}

View File

@ -1,117 +0,0 @@
/*
* Copyright 2004-present 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.metadata;
import java.util.function.Consumer;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
/**
* Resolves the SAML 2.0 Relying Party Metadata for a given
* {@link RelyingPartyRegistration} using the OpenSAML API.
*
* @author Jakub Kubrynski
* @author Josh Cummings
* @since 5.4
*/
public final class OpenSaml4MetadataResolver implements Saml2MetadataResolver {
static {
OpenSamlInitializationService.initialize();
}
private final BaseOpenSamlMetadataResolver delegate;
public OpenSaml4MetadataResolver() {
this.delegate = new BaseOpenSamlMetadataResolver(new OpenSaml4Template());
}
@Override
public String resolve(RelyingPartyRegistration relyingPartyRegistration) {
return this.delegate.resolve(relyingPartyRegistration);
}
public String resolve(Iterable<RelyingPartyRegistration> relyingPartyRegistrations) {
return this.delegate.resolve(relyingPartyRegistrations);
}
/**
* Set a {@link Consumer} for modifying the OpenSAML {@link EntityDescriptor}
* @param entityDescriptorCustomizer a consumer that accepts an
* {@link EntityDescriptorParameters}
* @since 5.7
*/
public void setEntityDescriptorCustomizer(Consumer<EntityDescriptorParameters> entityDescriptorCustomizer) {
this.delegate.setEntityDescriptorCustomizer(
(parameters) -> entityDescriptorCustomizer.accept(new EntityDescriptorParameters(parameters)));
}
/**
* Configure whether to pretty-print the metadata XML. This can be helpful when
* signing the metadata payload.
*
* @since 6.2
**/
public void setUsePrettyPrint(boolean usePrettyPrint) {
this.delegate.setUsePrettyPrint(usePrettyPrint);
}
/**
* Configure whether to sign the metadata, defaults to {@code false}.
*
* @since 6.4
*/
public void setSignMetadata(boolean signMetadata) {
this.delegate.setSignMetadata(signMetadata);
}
/**
* A tuple containing an OpenSAML {@link EntityDescriptor} and its associated
* {@link RelyingPartyRegistration}
*
* @since 5.7
*/
public static final class EntityDescriptorParameters {
private final EntityDescriptor entityDescriptor;
private final RelyingPartyRegistration registration;
public EntityDescriptorParameters(EntityDescriptor entityDescriptor, RelyingPartyRegistration registration) {
this.entityDescriptor = entityDescriptor;
this.registration = registration;
}
EntityDescriptorParameters(BaseOpenSamlMetadataResolver.EntityDescriptorParameters parameters) {
this.entityDescriptor = parameters.getEntityDescriptor();
this.registration = parameters.getRelyingPartyRegistration();
}
public EntityDescriptor getEntityDescriptor() {
return this.entityDescriptor;
}
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
}
}

View File

@ -1,607 +0,0 @@
/*
* Copyright 2004-present 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.metadata;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
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.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.ext.saml2delrestrict.Delegate;
import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.EncryptedAttribute;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.DecryptionException;
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* For internal use only. Subject to breaking changes at any time.
*/
final class OpenSaml4Template implements OpenSamlOperations {
private static final Log logger = LogFactory.getLog(OpenSaml4Template.class);
@Override
public <T extends XMLObject> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
}
return (T) builder.buildObject(elementName);
}
@Override
public <T extends XMLObject> T deserialize(String serialized) {
return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
}
@Override
public <T extends XMLObject> T deserialize(InputStream serialized) {
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized);
Element element = document.getDocumentElement();
UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
Unmarshaller unmarshaller = factory.getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
return (T) unmarshaller.unmarshall(element);
}
catch (Saml2Exception ex) {
throw ex;
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize payload", ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(XMLObject object) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
try {
return serialize(marshaller.marshall(object));
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(Element element) {
return new OpenSaml4SerializationConfigurer(element);
}
@Override
public OpenSaml4SignatureConfigurer withSigningKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4SignatureConfigurer(credentials);
}
@Override
public OpenSaml4VerificationConfigurer withVerificationKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4VerificationConfigurer(credentials);
}
@Override
public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4DecryptionConfigurer(credentials);
}
OpenSaml4Template() {
}
static final class OpenSaml4SerializationConfigurer
implements SerializationConfigurer<OpenSaml4SerializationConfigurer> {
private final Element element;
boolean pretty;
OpenSaml4SerializationConfigurer(Element element) {
this.element = element;
}
@Override
public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) {
this.pretty = pretty;
return this;
}
@Override
public String serialize() {
if (this.pretty) {
return SerializeSupport.prettyPrintXML(this.element);
}
return SerializeSupport.nodeToString(this.element);
}
}
static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer<OpenSaml4SignatureConfigurer> {
private final Collection<Saml2X509Credential> credentials;
private final Map<String, String> components = new LinkedHashMap<>();
private List<String> algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
OpenSaml4SignatureConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public OpenSaml4SignatureConfigurer algorithms(List<String> algs) {
this.algs = algs;
return this;
}
@Override
public <O extends SignableXMLObject> O sign(O object) {
SignatureSigningParameters parameters = resolveSigningParameters();
try {
SignatureSupport.signObject(object, parameters);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
return object;
}
@Override
public Map<String, String> sign(Map<String, String> params) {
SignatureSigningParameters parameters = resolveSigningParameters();
this.components.putAll(params);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
private SignatureSigningParameters resolveSigningParameters() {
List<Credential> credentials = resolveSigningCredentials();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(this.algs);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
namedManager.setUseDefaultManager(true);
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
// Generator for X509Credentials
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
x509Factory.setEmitEntityCertificate(true);
x509Factory.setEmitEntityCertificateChain(true);
defaultManager.registerFactory(x509Factory);
return namedManager;
}
private List<Credential> resolveSigningCredentials() {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : this.credentials) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}
static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer {
private final Collection<Saml2X509Credential> credentials;
private String entityId;
OpenSaml4VerificationConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public VerificationConfigurer entityId(String entityId) {
this.entityId = entityId;
return this;
}
private SignatureTrustEngine trustEngine(Collection<Saml2X509Credential> keys) {
Set<Credential> credentials = new HashSet<>();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(this.entityId);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private CriteriaSet verificationCriteria(Issuer issuer) {
return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())),
new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)),
new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
}
@Override
public Collection<Saml2Error> verify(SignableXMLObject signable) {
if (signable instanceof StatusResponseType response) {
return verifySignature(response.getID(), response.getIssuer(), response.getSignature());
}
if (signable instanceof RequestAbstractType request) {
return verifySignature(request.getID(), request.getIssuer(), request.getSignature());
}
if (signable instanceof Assertion assertion) {
return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature());
}
throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName());
}
private Collection<Saml2Error> verifySignature(String id, Issuer issuer, Signature signature) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(issuer);
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
try {
if (!trustEngine.validate(signature, criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
return errors;
}
@Override
public Collection<Saml2Error> verify(RedirectParameters parameters) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(parameters.getIssuer());
if (parameters.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + parameters.getId() + "]"));
}
if (!parameters.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + parameters.getId() + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = parameters.getAlgorithm();
try {
if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria,
null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]: "));
}
return errors;
}
}
static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
private final Decrypter decrypter;
OpenSaml4DecryptionConfigurer(Collection<Saml2X509Credential> decryptionCredentials) {
this.decrypter = decrypter(decryptionCredentials);
}
private static Decrypter decrypter(Collection<Saml2X509Credential> decryptionCredentials) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : decryptionCredentials) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
@Override
public void decrypt(XMLObject object) {
if (object instanceof Response response) {
decryptResponse(response);
return;
}
if (object instanceof Assertion assertion) {
decryptAssertion(assertion);
}
if (object instanceof LogoutRequest request) {
decryptLogoutRequest(request);
}
}
/*
* The methods that follow are adapted from OpenSAML's {@link DecryptAssertions},
* {@link DecryptNameIDs}, and {@link DecryptAttributes}.
*
* <p>The reason that these OpenSAML classes are not used directly is because they
* reference {@link javax.servlet.http.HttpServletRequest} which is a lower
* Servlet API version than what Spring Security SAML uses.
*
* If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then
* this arrangement can be revisited.
*/
private void decryptResponse(Response response) {
Collection<Assertion> decrypteds = new ArrayList<>();
int count = 0;
int size = response.getEncryptedAssertions().size();
for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) {
logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size,
response.getID()));
try {
Assertion decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
count++;
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
response.getAssertions().addAll(decrypteds);
// Re-marshall the response so that any ID attributes within the decrypted
// Assertions
// will have their ID-ness re-established at the DOM level.
if (!decrypteds.isEmpty()) {
try {
XMLObjectSupport.marshall(response);
}
catch (final MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}
private void decryptAssertion(Assertion assertion) {
for (AttributeStatement statement : assertion.getAttributeStatements()) {
decryptAttributes(statement);
}
decryptSubject(assertion.getSubject());
if (assertion.getConditions() != null) {
for (Condition c : assertion.getConditions().getConditions()) {
if (!(c instanceof DelegationRestrictionType delegation)) {
continue;
}
for (Delegate d : delegation.getDelegates()) {
if (d.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID());
if (decrypted != null) {
d.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
}
private void decryptAttributes(AttributeStatement statement) {
Collection<Attribute> decrypteds = new ArrayList<>();
for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) {
try {
Attribute decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
statement.getAttributes().addAll(decrypteds);
}
private void decryptSubject(Subject subject) {
if (subject != null) {
if (subject.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID());
if (decrypted != null) {
subject.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
if (sc.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID());
if (decrypted != null) {
sc.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
private void decryptLogoutRequest(LogoutRequest request) {
if (request.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID());
if (decrypted != null) {
request.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}

View File

@ -1,320 +0,0 @@
/*
* Copyright 2004-present 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.registration;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.function.Consumer;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.criterion.EntityRoleCriterion;
import org.opensaml.saml.metadata.IterableMetadataSource;
import org.opensaml.saml.metadata.resolver.MetadataResolver;
import org.opensaml.saml.metadata.resolver.filter.impl.SignatureValidationFilter;
import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver;
import org.opensaml.saml.metadata.resolver.index.impl.RoleMetadataIndex;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.provider.service.registration.BaseOpenSamlAssertingPartyMetadataRepository.MetadataResolverAdapter;
import org.springframework.util.Assert;
/**
* An implementation of {@link AssertingPartyMetadataRepository} that uses a
* {@link MetadataResolver} to retrieve {@link AssertingPartyMetadata} instances.
*
* <p>
* The {@link MetadataResolver} constructed in {@link #withTrustedMetadataLocation}
* provides expiry-aware refreshing.
*
* @author Josh Cummings
* @since 6.4
* @see AssertingPartyMetadataRepository
* @see RelyingPartyRegistrations
*/
public final class OpenSaml4AssertingPartyMetadataRepository implements AssertingPartyMetadataRepository {
static {
OpenSamlInitializationService.initialize();
}
private final BaseOpenSamlAssertingPartyMetadataRepository delegate;
/**
* Construct an {@link OpenSaml4AssertingPartyMetadataRepository} using the provided
* {@link MetadataResolver}.
*
* <p>
* The {@link MetadataResolver} should either be of type
* {@link IterableMetadataSource} or it should have a {@link RoleMetadataIndex}
* configured.
* @param metadataResolver the {@link MetadataResolver} to use
*/
public OpenSaml4AssertingPartyMetadataRepository(MetadataResolver metadataResolver) {
Assert.notNull(metadataResolver, "metadataResolver cannot be null");
this.delegate = new BaseOpenSamlAssertingPartyMetadataRepository(
new CriteriaSetResolverWrapper(metadataResolver));
}
/**
* {@inheritDoc}
*/
@Override
@NonNull
public Iterator<AssertingPartyMetadata> iterator() {
return this.delegate.iterator();
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public AssertingPartyMetadata findByEntityId(String entityId) {
return this.delegate.findByEntityId(entityId);
}
/**
* Use this trusted {@code metadataLocation} to retrieve refreshable, expiry-aware
* SAML 2.0 Asserting Party (IDP) metadata.
*
* <p>
* Valid locations can be classpath- or file-based or they can be HTTPS endpoints.
* Some valid endpoints might include:
*
* <pre>
* metadataLocation = "classpath:asserting-party-metadata.xml";
* metadataLocation = "file:asserting-party-metadata.xml";
* metadataLocation = "https://ap.example.org/metadata";
* </pre>
*
* <p>
* Resolution of location is attempted immediately. To defer, wrap in
* {@link CachingRelyingPartyRegistrationRepository}.
* @param metadataLocation the classpath- or file-based locations or HTTPS endpoints
* of the asserting party metadata file
* @return the {@link MetadataLocationRepositoryBuilder} for further configuration
*/
public static MetadataLocationRepositoryBuilder withTrustedMetadataLocation(String metadataLocation) {
return new MetadataLocationRepositoryBuilder(metadataLocation, true);
}
/**
* Use this {@code metadataLocation} to retrieve refreshable, expiry-aware SAML 2.0
* Asserting Party (IDP) metadata. Verification credentials are required.
*
* <p>
* Valid locations can be classpath- or file-based or they can be remote endpoints.
* Some valid endpoints might include:
*
* <pre>
* metadataLocation = "classpath:asserting-party-metadata.xml";
* metadataLocation = "file:asserting-party-metadata.xml";
* metadataLocation = "https://ap.example.org/metadata";
* </pre>
*
* <p>
* Resolution of location is attempted immediately. To defer, wrap in
* {@link CachingRelyingPartyRegistrationRepository}.
* @param metadataLocation the classpath- or file-based locations or remote endpoints
* of the asserting party metadata file
* @return the {@link MetadataLocationRepositoryBuilder} for further configuration
*/
public static MetadataLocationRepositoryBuilder withMetadataLocation(String metadataLocation) {
return new MetadataLocationRepositoryBuilder(metadataLocation, false);
}
/**
* A builder class for configuring {@link OpenSaml4AssertingPartyMetadataRepository}
* for a specific metadata location.
*
* @author Josh Cummings
*/
public static final class MetadataLocationRepositoryBuilder {
private final String metadataLocation;
private final boolean requireVerificationCredentials;
private final Collection<Credential> verificationCredentials = new ArrayList<>();
private ResourceLoader resourceLoader = new DefaultResourceLoader();
MetadataLocationRepositoryBuilder(String metadataLocation, boolean trusted) {
this.metadataLocation = metadataLocation;
this.requireVerificationCredentials = !trusted;
}
public MetadataLocationRepositoryBuilder verificationCredentials(Consumer<Collection<Credential>> credentials) {
credentials.accept(this.verificationCredentials);
return this;
}
public MetadataLocationRepositoryBuilder resourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
return this;
}
public OpenSaml4AssertingPartyMetadataRepository build() {
return new OpenSaml4AssertingPartyMetadataRepository(metadataResolver());
}
private MetadataResolver metadataResolver() {
ResourceBackedMetadataResolver metadataResolver = resourceBackedMetadataResolver();
boolean missingCredentials = this.requireVerificationCredentials && this.verificationCredentials.isEmpty();
Assert.isTrue(!missingCredentials, "Verification credentials are required");
return initialize(metadataResolver);
}
private ResourceBackedMetadataResolver resourceBackedMetadataResolver() {
Resource resource = this.resourceLoader.getResource(this.metadataLocation);
try {
ResourceBackedMetadataResolver metadataResolver = new ResourceBackedMetadataResolver(
new SpringResource(resource));
if (this.verificationCredentials.isEmpty()) {
return metadataResolver;
}
SignatureTrustEngine engine = new ExplicitKeySignatureTrustEngine(
new CollectionCredentialResolver(this.verificationCredentials),
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
SignatureValidationFilter filter = new SignatureValidationFilter(engine);
filter.setRequireSignedRoot(true);
metadataResolver.setMetadataFilter(filter);
return metadataResolver;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private MetadataResolver initialize(ResourceBackedMetadataResolver metadataResolver) {
metadataResolver.setParserPool(XMLObjectProviderRegistrySupport.getParserPool());
return BaseOpenSamlAssertingPartyMetadataRepository.initialize(metadataResolver);
}
private static final class SpringResource implements net.shibboleth.utilities.java.support.resource.Resource {
private final Resource resource;
SpringResource(Resource resource) {
this.resource = resource;
}
@Override
public boolean exists() {
return this.resource.exists();
}
@Override
public boolean isReadable() {
return this.resource.isReadable();
}
@Override
public boolean isOpen() {
return this.resource.isOpen();
}
@Override
public URL getURL() throws IOException {
return this.resource.getURL();
}
@Override
public URI getURI() throws IOException {
return this.resource.getURI();
}
@Override
public File getFile() throws IOException {
return this.resource.getFile();
}
@NonNull
@Override
public InputStream getInputStream() throws IOException {
return this.resource.getInputStream();
}
@Override
public long contentLength() throws IOException {
return this.resource.contentLength();
}
@Override
public long lastModified() throws IOException {
return this.resource.lastModified();
}
@Override
public net.shibboleth.utilities.java.support.resource.Resource createRelativeResource(String relativePath)
throws IOException {
return new SpringResource(this.resource.createRelative(relativePath));
}
@Override
public String getFilename() {
return this.resource.getFilename();
}
@Override
public String getDescription() {
return this.resource.getDescription();
}
}
}
private static final class CriteriaSetResolverWrapper extends MetadataResolverAdapter {
CriteriaSetResolverWrapper(MetadataResolver metadataResolver) {
super(metadataResolver);
}
@Override
EntityDescriptor resolveSingle(EntityIdCriterion entityId) throws Exception {
return super.metadataResolver.resolveSingle(new CriteriaSet(entityId));
}
@Override
Iterable<EntityDescriptor> resolve(EntityRoleCriterion role) throws Exception {
return super.metadataResolver.resolve(new CriteriaSet(role));
}
}
}

View File

@ -1,607 +0,0 @@
/*
* Copyright 2004-present 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.registration;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
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.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.ext.saml2delrestrict.Delegate;
import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.EncryptedAttribute;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.DecryptionException;
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* For internal use only. Subject to breaking changes at any time.
*/
final class OpenSaml4Template implements OpenSamlOperations {
private static final Log logger = LogFactory.getLog(OpenSaml4Template.class);
@Override
public <T extends XMLObject> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
}
return (T) builder.buildObject(elementName);
}
@Override
public <T extends XMLObject> T deserialize(String serialized) {
return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
}
@Override
public <T extends XMLObject> T deserialize(InputStream serialized) {
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized);
Element element = document.getDocumentElement();
UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
Unmarshaller unmarshaller = factory.getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
return (T) unmarshaller.unmarshall(element);
}
catch (Saml2Exception ex) {
throw ex;
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize payload", ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(XMLObject object) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
try {
return serialize(marshaller.marshall(object));
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(Element element) {
return new OpenSaml4SerializationConfigurer(element);
}
@Override
public OpenSaml4SignatureConfigurer withSigningKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4SignatureConfigurer(credentials);
}
@Override
public OpenSaml4VerificationConfigurer withVerificationKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4VerificationConfigurer(credentials);
}
@Override
public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4DecryptionConfigurer(credentials);
}
OpenSaml4Template() {
}
static final class OpenSaml4SerializationConfigurer
implements SerializationConfigurer<OpenSaml4SerializationConfigurer> {
private final Element element;
boolean pretty;
OpenSaml4SerializationConfigurer(Element element) {
this.element = element;
}
@Override
public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) {
this.pretty = pretty;
return this;
}
@Override
public String serialize() {
if (this.pretty) {
return SerializeSupport.prettyPrintXML(this.element);
}
return SerializeSupport.nodeToString(this.element);
}
}
static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer<OpenSaml4SignatureConfigurer> {
private final Collection<Saml2X509Credential> credentials;
private final Map<String, String> components = new LinkedHashMap<>();
private List<String> algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
OpenSaml4SignatureConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public OpenSaml4SignatureConfigurer algorithms(List<String> algs) {
this.algs = algs;
return this;
}
@Override
public <O extends SignableXMLObject> O sign(O object) {
SignatureSigningParameters parameters = resolveSigningParameters();
try {
SignatureSupport.signObject(object, parameters);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
return object;
}
@Override
public Map<String, String> sign(Map<String, String> params) {
SignatureSigningParameters parameters = resolveSigningParameters();
this.components.putAll(params);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
private SignatureSigningParameters resolveSigningParameters() {
List<Credential> credentials = resolveSigningCredentials();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(this.algs);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
namedManager.setUseDefaultManager(true);
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
// Generator for X509Credentials
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
x509Factory.setEmitEntityCertificate(true);
x509Factory.setEmitEntityCertificateChain(true);
defaultManager.registerFactory(x509Factory);
return namedManager;
}
private List<Credential> resolveSigningCredentials() {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : this.credentials) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}
static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer {
private final Collection<Saml2X509Credential> credentials;
private String entityId;
OpenSaml4VerificationConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public VerificationConfigurer entityId(String entityId) {
this.entityId = entityId;
return this;
}
private SignatureTrustEngine trustEngine(Collection<Saml2X509Credential> keys) {
Set<Credential> credentials = new HashSet<>();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(this.entityId);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private CriteriaSet verificationCriteria(Issuer issuer) {
return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())),
new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)),
new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
}
@Override
public Collection<Saml2Error> verify(SignableXMLObject signable) {
if (signable instanceof StatusResponseType response) {
return verifySignature(response.getID(), response.getIssuer(), response.getSignature());
}
if (signable instanceof RequestAbstractType request) {
return verifySignature(request.getID(), request.getIssuer(), request.getSignature());
}
if (signable instanceof Assertion assertion) {
return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature());
}
throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName());
}
private Collection<Saml2Error> verifySignature(String id, Issuer issuer, Signature signature) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(issuer);
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
try {
if (!trustEngine.validate(signature, criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
return errors;
}
@Override
public Collection<Saml2Error> verify(RedirectParameters parameters) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(parameters.getIssuer());
if (parameters.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + parameters.getId() + "]"));
}
if (!parameters.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + parameters.getId() + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = parameters.getAlgorithm();
try {
if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria,
null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]: "));
}
return errors;
}
}
static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
private final Decrypter decrypter;
OpenSaml4DecryptionConfigurer(Collection<Saml2X509Credential> decryptionCredentials) {
this.decrypter = decrypter(decryptionCredentials);
}
private static Decrypter decrypter(Collection<Saml2X509Credential> decryptionCredentials) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : decryptionCredentials) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
@Override
public void decrypt(XMLObject object) {
if (object instanceof Response response) {
decryptResponse(response);
return;
}
if (object instanceof Assertion assertion) {
decryptAssertion(assertion);
}
if (object instanceof LogoutRequest request) {
decryptLogoutRequest(request);
}
}
/*
* The methods that follow are adapted from OpenSAML's {@link DecryptAssertions},
* {@link DecryptNameIDs}, and {@link DecryptAttributes}.
*
* <p>The reason that these OpenSAML classes are not used directly is because they
* reference {@link javax.servlet.http.HttpServletRequest} which is a lower
* Servlet API version than what Spring Security SAML uses.
*
* If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then
* this arrangement can be revisited.
*/
private void decryptResponse(Response response) {
Collection<Assertion> decrypteds = new ArrayList<>();
int count = 0;
int size = response.getEncryptedAssertions().size();
for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) {
logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size,
response.getID()));
try {
Assertion decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
count++;
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
response.getAssertions().addAll(decrypteds);
// Re-marshall the response so that any ID attributes within the decrypted
// Assertions
// will have their ID-ness re-established at the DOM level.
if (!decrypteds.isEmpty()) {
try {
XMLObjectSupport.marshall(response);
}
catch (final MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}
private void decryptAssertion(Assertion assertion) {
for (AttributeStatement statement : assertion.getAttributeStatements()) {
decryptAttributes(statement);
}
decryptSubject(assertion.getSubject());
if (assertion.getConditions() != null) {
for (Condition c : assertion.getConditions().getConditions()) {
if (!(c instanceof DelegationRestrictionType delegation)) {
continue;
}
for (Delegate d : delegation.getDelegates()) {
if (d.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID());
if (decrypted != null) {
d.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
}
private void decryptAttributes(AttributeStatement statement) {
Collection<Attribute> decrypteds = new ArrayList<>();
for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) {
try {
Attribute decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
statement.getAttributes().addAll(decrypteds);
}
private void decryptSubject(Subject subject) {
if (subject != null) {
if (subject.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID());
if (decrypted != null) {
subject.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
if (sc.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID());
if (decrypted != null) {
sc.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
private void decryptLogoutRequest(LogoutRequest request) {
if (request.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID());
if (decrypted != null) {
request.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}

View File

@ -1,114 +0,0 @@
/*
* Copyright 2004-present 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;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationConverter} that generates a {@link Saml2AuthenticationToken}
* appropriate for authenticated a SAML 2.0 Assertion against an
* {@link org.springframework.security.authentication.AuthenticationManager}.
*
* @author Josh Cummings
* @since 6.1
*/
public final class OpenSaml4AuthenticationTokenConverter implements AuthenticationConverter {
private final BaseOpenSamlAuthenticationTokenConverter delegate;
/**
* Constructs a {@link OpenSaml4AuthenticationTokenConverter} given a repository for
* {@link RelyingPartyRegistration}s
* @param registrations the repository for {@link RelyingPartyRegistration}s
* {@link RelyingPartyRegistration}s
*/
public OpenSaml4AuthenticationTokenConverter(RelyingPartyRegistrationRepository registrations) {
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
this.delegate = new BaseOpenSamlAuthenticationTokenConverter(registrations, new OpenSaml4Template());
}
/**
* Resolve an authentication request from the given {@link HttpServletRequest}.
*
* <p>
* First uses the configured {@link RequestMatcher} to deduce whether an
* authentication request is being made and optionally for which
* {@code registrationId}.
*
* <p>
* If there is an associated {@code <saml2:AuthnRequest>}, then the
* {@code registrationId} is looked up and used.
*
* <p>
* If a {@code registrationId} is found in the request, then it is looked up and used.
* In that case, if none is found a {@link Saml2AuthenticationException} is thrown.
*
* <p>
* Finally, if no {@code registrationId} is found in the request, then the code
* attempts to resolve the {@link RelyingPartyRegistration} from the SAML Response's
* Issuer.
* @param request the HTTP request
* @return the {@link Saml2AuthenticationToken} authentication request
* @throws Saml2AuthenticationException if the {@link RequestMatcher} specifies a
* non-existent {@code registrationId}
*/
@Override
public Saml2AuthenticationToken convert(HttpServletRequest request) {
return this.delegate.convert(request);
}
/**
* Use the given {@link Saml2AuthenticationRequestRepository} to load authentication
* request.
* @param authenticationRequestRepository the
* {@link Saml2AuthenticationRequestRepository} to use
*/
public void setAuthenticationRequestRepository(
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository) {
Assert.notNull(authenticationRequestRepository, "authenticationRequestRepository cannot be null");
this.delegate.setAuthenticationRequestRepository(authenticationRequestRepository);
}
/**
* Use the given {@link RequestMatcher} to match the request.
* @param requestMatcher the {@link RequestMatcher} to use
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.delegate.setRequestMatcher(requestMatcher);
}
/**
* Use the given {@code shouldConvertGetRequests} to convert {@code GET} requests.
* Default is {@code true}.
* @param shouldConvertGetRequests the {@code shouldConvertGetRequests} to use
* @since 7.0
*/
public void setShouldConvertGetRequests(boolean shouldConvertGetRequests) {
this.delegate.setShouldConvertGetRequests(shouldConvertGetRequests);
}
}

View File

@ -1,607 +0,0 @@
/*
* Copyright 2004-present 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;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
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.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.ext.saml2delrestrict.Delegate;
import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.EncryptedAttribute;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.DecryptionException;
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* For internal use only. Subject to breaking changes at any time.
*/
final class OpenSaml4Template implements OpenSamlOperations {
private static final Log logger = LogFactory.getLog(OpenSaml4Template.class);
@Override
public <T extends XMLObject> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
}
return (T) builder.buildObject(elementName);
}
@Override
public <T extends XMLObject> T deserialize(String serialized) {
return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
}
@Override
public <T extends XMLObject> T deserialize(InputStream serialized) {
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized);
Element element = document.getDocumentElement();
UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
Unmarshaller unmarshaller = factory.getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
return (T) unmarshaller.unmarshall(element);
}
catch (Saml2Exception ex) {
throw ex;
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize payload", ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(XMLObject object) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
try {
return serialize(marshaller.marshall(object));
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(Element element) {
return new OpenSaml4SerializationConfigurer(element);
}
@Override
public OpenSaml4SignatureConfigurer withSigningKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4SignatureConfigurer(credentials);
}
@Override
public OpenSaml4VerificationConfigurer withVerificationKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4VerificationConfigurer(credentials);
}
@Override
public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4DecryptionConfigurer(credentials);
}
OpenSaml4Template() {
}
static final class OpenSaml4SerializationConfigurer
implements SerializationConfigurer<OpenSaml4SerializationConfigurer> {
private final Element element;
boolean pretty;
OpenSaml4SerializationConfigurer(Element element) {
this.element = element;
}
@Override
public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) {
this.pretty = pretty;
return this;
}
@Override
public String serialize() {
if (this.pretty) {
return SerializeSupport.prettyPrintXML(this.element);
}
return SerializeSupport.nodeToString(this.element);
}
}
static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer<OpenSaml4SignatureConfigurer> {
private final Collection<Saml2X509Credential> credentials;
private final Map<String, String> components = new LinkedHashMap<>();
private List<String> algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
OpenSaml4SignatureConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public OpenSaml4SignatureConfigurer algorithms(List<String> algs) {
this.algs = algs;
return this;
}
@Override
public <O extends SignableXMLObject> O sign(O object) {
SignatureSigningParameters parameters = resolveSigningParameters();
try {
SignatureSupport.signObject(object, parameters);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
return object;
}
@Override
public Map<String, String> sign(Map<String, String> params) {
SignatureSigningParameters parameters = resolveSigningParameters();
this.components.putAll(params);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
private SignatureSigningParameters resolveSigningParameters() {
List<Credential> credentials = resolveSigningCredentials();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(this.algs);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
namedManager.setUseDefaultManager(true);
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
// Generator for X509Credentials
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
x509Factory.setEmitEntityCertificate(true);
x509Factory.setEmitEntityCertificateChain(true);
defaultManager.registerFactory(x509Factory);
return namedManager;
}
private List<Credential> resolveSigningCredentials() {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : this.credentials) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}
static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer {
private final Collection<Saml2X509Credential> credentials;
private String entityId;
OpenSaml4VerificationConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public VerificationConfigurer entityId(String entityId) {
this.entityId = entityId;
return this;
}
private SignatureTrustEngine trustEngine(Collection<Saml2X509Credential> keys) {
Set<Credential> credentials = new HashSet<>();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(this.entityId);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private CriteriaSet verificationCriteria(Issuer issuer) {
return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())),
new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)),
new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
}
@Override
public Collection<Saml2Error> verify(SignableXMLObject signable) {
if (signable instanceof StatusResponseType response) {
return verifySignature(response.getID(), response.getIssuer(), response.getSignature());
}
if (signable instanceof RequestAbstractType request) {
return verifySignature(request.getID(), request.getIssuer(), request.getSignature());
}
if (signable instanceof Assertion assertion) {
return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature());
}
throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName());
}
private Collection<Saml2Error> verifySignature(String id, Issuer issuer, Signature signature) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(issuer);
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
try {
if (!trustEngine.validate(signature, criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
return errors;
}
@Override
public Collection<Saml2Error> verify(RedirectParameters parameters) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(parameters.getIssuer());
if (parameters.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + parameters.getId() + "]"));
}
if (!parameters.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + parameters.getId() + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = parameters.getAlgorithm();
try {
if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria,
null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]: "));
}
return errors;
}
}
static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
private final Decrypter decrypter;
OpenSaml4DecryptionConfigurer(Collection<Saml2X509Credential> decryptionCredentials) {
this.decrypter = decrypter(decryptionCredentials);
}
private static Decrypter decrypter(Collection<Saml2X509Credential> decryptionCredentials) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : decryptionCredentials) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
@Override
public void decrypt(XMLObject object) {
if (object instanceof Response response) {
decryptResponse(response);
return;
}
if (object instanceof Assertion assertion) {
decryptAssertion(assertion);
}
if (object instanceof LogoutRequest request) {
decryptLogoutRequest(request);
}
}
/*
* The methods that follow are adapted from OpenSAML's {@link DecryptAssertions},
* {@link DecryptNameIDs}, and {@link DecryptAttributes}.
*
* <p>The reason that these OpenSAML classes are not used directly is because they
* reference {@link javax.servlet.http.HttpServletRequest} which is a lower
* Servlet API version than what Spring Security SAML uses.
*
* If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then
* this arrangement can be revisited.
*/
private void decryptResponse(Response response) {
Collection<Assertion> decrypteds = new ArrayList<>();
int count = 0;
int size = response.getEncryptedAssertions().size();
for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) {
logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size,
response.getID()));
try {
Assertion decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
count++;
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
response.getAssertions().addAll(decrypteds);
// Re-marshall the response so that any ID attributes within the decrypted
// Assertions
// will have their ID-ness re-established at the DOM level.
if (!decrypteds.isEmpty()) {
try {
XMLObjectSupport.marshall(response);
}
catch (final MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}
private void decryptAssertion(Assertion assertion) {
for (AttributeStatement statement : assertion.getAttributeStatements()) {
decryptAttributes(statement);
}
decryptSubject(assertion.getSubject());
if (assertion.getConditions() != null) {
for (Condition c : assertion.getConditions().getConditions()) {
if (!(c instanceof DelegationRestrictionType delegation)) {
continue;
}
for (Delegate d : delegation.getDelegates()) {
if (d.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID());
if (decrypted != null) {
d.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
}
private void decryptAttributes(AttributeStatement statement) {
Collection<Attribute> decrypteds = new ArrayList<>();
for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) {
try {
Attribute decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
statement.getAttributes().addAll(decrypteds);
}
private void decryptSubject(Subject subject) {
if (subject != null) {
if (subject.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID());
if (decrypted != null) {
subject.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
if (sc.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID());
if (decrypted != null) {
sc.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
private void decryptLogoutRequest(LogoutRequest request) {
if (request.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID());
if (decrypted != null) {
request.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}

View File

@ -1,144 +0,0 @@
/*
* Copyright 2004-present 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.time.Instant;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.core.convert.converter.Converter;
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.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.util.matcher.RequestMatcher;
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
*/
public final class OpenSaml4AuthenticationRequestResolver implements Saml2AuthenticationRequestResolver {
private final BaseOpenSamlAuthenticationRequestResolver delegate;
/**
* Construct an {@link OpenSaml4AuthenticationRequestResolver}
* @param registrations a repository for relying and asserting party configuration
* @since 6.1
*/
public OpenSaml4AuthenticationRequestResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new BaseOpenSamlAuthenticationRequestResolver((request, id) -> {
if (id == null) {
return null;
}
return registrations.findByRegistrationId(id);
}, new OpenSaml4Template());
}
/**
* Construct a {@link OpenSaml4AuthenticationRequestResolver}
*/
public OpenSaml4AuthenticationRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.delegate = new BaseOpenSamlAuthenticationRequestResolver(relyingPartyRegistrationResolver,
new OpenSaml4Template());
}
@Override
public <T extends AbstractSaml2AuthenticationRequest> T resolve(HttpServletRequest request) {
return this.delegate.resolve(request);
}
/**
* Set a {@link Consumer} for modifying the OpenSAML {@link AuthnRequest}
* @param contextConsumer a consumer that accepts an {@link AuthnRequestContext}
*/
public void setAuthnRequestCustomizer(Consumer<AuthnRequestContext> contextConsumer) {
Assert.notNull(contextConsumer, "contextConsumer cannot be null");
this.delegate.setParametersConsumer(
(parameters) -> contextConsumer.accept(new AuthnRequestContext(parameters.getRequest(),
parameters.getRelyingPartyRegistration(), parameters.getAuthnRequest())));
}
/**
* Set the {@link RequestMatcher} to use for setting the
* {@link BaseOpenSamlAuthenticationRequestResolver#setRequestMatcher(RequestMatcher)}
* (RequestMatcher)}
* @param requestMatcher the {@link RequestMatcher} to identify authentication
* requests.
* @since 5.8
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.delegate.setRequestMatcher(requestMatcher);
}
/**
* Use this {@link Clock} for generating the issued {@link Instant}
* @param clock the {@link Clock} to use
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.delegate.setClock(clock);
}
/**
* Use this {@link Converter} to compute the RelayState
* @param relayStateResolver the {@link Converter} to use
* @since 5.8
*/
public void setRelayStateResolver(Converter<HttpServletRequest, String> relayStateResolver) {
Assert.notNull(relayStateResolver, "relayStateResolver cannot be null");
this.delegate.setRelayStateResolver(relayStateResolver);
}
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;
}
}
}

View File

@ -1,607 +0,0 @@
/*
* Copyright 2004-present 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.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
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.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.ext.saml2delrestrict.Delegate;
import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.EncryptedAttribute;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.DecryptionException;
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* For internal use only. Subject to breaking changes at any time.
*/
final class OpenSaml4Template implements OpenSamlOperations {
private static final Log logger = LogFactory.getLog(OpenSaml4Template.class);
@Override
public <T extends XMLObject> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
}
return (T) builder.buildObject(elementName);
}
@Override
public <T extends XMLObject> T deserialize(String serialized) {
return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
}
@Override
public <T extends XMLObject> T deserialize(InputStream serialized) {
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized);
Element element = document.getDocumentElement();
UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
Unmarshaller unmarshaller = factory.getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
return (T) unmarshaller.unmarshall(element);
}
catch (Saml2Exception ex) {
throw ex;
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize payload", ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(XMLObject object) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
try {
return serialize(marshaller.marshall(object));
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(Element element) {
return new OpenSaml4SerializationConfigurer(element);
}
@Override
public OpenSaml4SignatureConfigurer withSigningKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4SignatureConfigurer(credentials);
}
@Override
public OpenSaml4VerificationConfigurer withVerificationKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4VerificationConfigurer(credentials);
}
@Override
public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4DecryptionConfigurer(credentials);
}
OpenSaml4Template() {
}
static final class OpenSaml4SerializationConfigurer
implements SerializationConfigurer<OpenSaml4SerializationConfigurer> {
private final Element element;
boolean pretty;
OpenSaml4SerializationConfigurer(Element element) {
this.element = element;
}
@Override
public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) {
this.pretty = pretty;
return this;
}
@Override
public String serialize() {
if (this.pretty) {
return SerializeSupport.prettyPrintXML(this.element);
}
return SerializeSupport.nodeToString(this.element);
}
}
static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer<OpenSaml4SignatureConfigurer> {
private final Collection<Saml2X509Credential> credentials;
private final Map<String, String> components = new LinkedHashMap<>();
private List<String> algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
OpenSaml4SignatureConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public OpenSaml4SignatureConfigurer algorithms(List<String> algs) {
this.algs = algs;
return this;
}
@Override
public <O extends SignableXMLObject> O sign(O object) {
SignatureSigningParameters parameters = resolveSigningParameters();
try {
SignatureSupport.signObject(object, parameters);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
return object;
}
@Override
public Map<String, String> sign(Map<String, String> params) {
SignatureSigningParameters parameters = resolveSigningParameters();
this.components.putAll(params);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
private SignatureSigningParameters resolveSigningParameters() {
List<Credential> credentials = resolveSigningCredentials();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(this.algs);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
namedManager.setUseDefaultManager(true);
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
// Generator for X509Credentials
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
x509Factory.setEmitEntityCertificate(true);
x509Factory.setEmitEntityCertificateChain(true);
defaultManager.registerFactory(x509Factory);
return namedManager;
}
private List<Credential> resolveSigningCredentials() {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : this.credentials) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}
static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer {
private final Collection<Saml2X509Credential> credentials;
private String entityId;
OpenSaml4VerificationConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public VerificationConfigurer entityId(String entityId) {
this.entityId = entityId;
return this;
}
private SignatureTrustEngine trustEngine(Collection<Saml2X509Credential> keys) {
Set<Credential> credentials = new HashSet<>();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(this.entityId);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private CriteriaSet verificationCriteria(Issuer issuer) {
return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())),
new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)),
new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
}
@Override
public Collection<Saml2Error> verify(SignableXMLObject signable) {
if (signable instanceof StatusResponseType response) {
return verifySignature(response.getID(), response.getIssuer(), response.getSignature());
}
if (signable instanceof RequestAbstractType request) {
return verifySignature(request.getID(), request.getIssuer(), request.getSignature());
}
if (signable instanceof Assertion assertion) {
return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature());
}
throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName());
}
private Collection<Saml2Error> verifySignature(String id, Issuer issuer, Signature signature) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(issuer);
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
try {
if (!trustEngine.validate(signature, criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
return errors;
}
@Override
public Collection<Saml2Error> verify(RedirectParameters parameters) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(parameters.getIssuer());
if (parameters.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + parameters.getId() + "]"));
}
if (!parameters.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + parameters.getId() + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = parameters.getAlgorithm();
try {
if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria,
null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]: "));
}
return errors;
}
}
static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
private final Decrypter decrypter;
OpenSaml4DecryptionConfigurer(Collection<Saml2X509Credential> decryptionCredentials) {
this.decrypter = decrypter(decryptionCredentials);
}
private static Decrypter decrypter(Collection<Saml2X509Credential> decryptionCredentials) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : decryptionCredentials) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
@Override
public void decrypt(XMLObject object) {
if (object instanceof Response response) {
decryptResponse(response);
return;
}
if (object instanceof Assertion assertion) {
decryptAssertion(assertion);
}
if (object instanceof LogoutRequest request) {
decryptLogoutRequest(request);
}
}
/*
* The methods that follow are adapted from OpenSAML's {@link DecryptAssertions},
* {@link DecryptNameIDs}, and {@link DecryptAttributes}.
*
* <p>The reason that these OpenSAML classes are not used directly is because they
* reference {@link javax.servlet.http.HttpServletRequest} which is a lower
* Servlet API version than what Spring Security SAML uses.
*
* If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then
* this arrangement can be revisited.
*/
private void decryptResponse(Response response) {
Collection<Assertion> decrypteds = new ArrayList<>();
int count = 0;
int size = response.getEncryptedAssertions().size();
for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) {
logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size,
response.getID()));
try {
Assertion decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
count++;
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
response.getAssertions().addAll(decrypteds);
// Re-marshall the response so that any ID attributes within the decrypted
// Assertions
// will have their ID-ness re-established at the DOM level.
if (!decrypteds.isEmpty()) {
try {
XMLObjectSupport.marshall(response);
}
catch (final MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}
private void decryptAssertion(Assertion assertion) {
for (AttributeStatement statement : assertion.getAttributeStatements()) {
decryptAttributes(statement);
}
decryptSubject(assertion.getSubject());
if (assertion.getConditions() != null) {
for (Condition c : assertion.getConditions().getConditions()) {
if (!(c instanceof DelegationRestrictionType delegation)) {
continue;
}
for (Delegate d : delegation.getDelegates()) {
if (d.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID());
if (decrypted != null) {
d.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
}
private void decryptAttributes(AttributeStatement statement) {
Collection<Attribute> decrypteds = new ArrayList<>();
for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) {
try {
Attribute decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
statement.getAttributes().addAll(decrypteds);
}
private void decryptSubject(Subject subject) {
if (subject != null) {
if (subject.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID());
if (decrypted != null) {
subject.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
if (sc.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID());
if (decrypted != null) {
sc.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
private void decryptLogoutRequest(LogoutRequest request) {
if (request.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID());
if (decrypted != null) {
request.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}

View File

@ -1,142 +0,0 @@
/*
* Copyright 2004-present 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.time.Instant;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.core.convert.converter.Converter;
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.RelyingPartyRegistrationRepository;
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 4
*
* @author Josh Cummings
* @author Gerhard Haege
* @since 5.6
*/
public final class OpenSaml4LogoutRequestResolver implements Saml2LogoutRequestResolver {
private final BaseOpenSamlLogoutRequestResolver delegate;
public OpenSaml4LogoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
this((request, id) -> {
if (id == null) {
return null;
}
return registrations.findByRegistrationId(id);
});
}
/**
* Construct a {@link OpenSaml4LogoutRequestResolver}
*/
public OpenSaml4LogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.delegate = new BaseOpenSamlLogoutRequestResolver(relyingPartyRegistrationResolver,
new OpenSaml4Template());
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
return this.delegate.resolve(request, authentication);
}
/**
* 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.delegate
.setParametersConsumer((parameters) -> parametersConsumer.accept(new LogoutRequestParameters(parameters)));
}
/**
* Use this {@link Clock} for determining the issued {@link Instant}
* @param clock the {@link Clock} to use
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.delegate.setClock(clock);
}
/**
* Use this {@link Converter} to compute the RelayState
* @param relayStateResolver the {@link Converter} to use
* @since 6.1
*/
public void setRelayStateResolver(Converter<HttpServletRequest, String> relayStateResolver) {
Assert.notNull(relayStateResolver, "relayStateResolver cannot be null");
this.delegate.setRelayStateResolver(relayStateResolver);
}
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;
}
LogoutRequestParameters(BaseOpenSamlLogoutRequestResolver.LogoutRequestParameters parameters) {
this(parameters.getRequest(), parameters.getRelyingPartyRegistration(), parameters.getAuthentication(),
parameters.getLogoutRequest());
}
public HttpServletRequest getRequest() {
return this.request;
}
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
public Authentication getAuthentication() {
return this.authentication;
}
public LogoutRequest getLogoutRequest() {
return this.logoutRequest;
}
}
}

View File

@ -1,100 +0,0 @@
/*
* Copyright 2004-present 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.springframework.security.core.Authentication;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* An OpenSAML-based implementation of
* {@link Saml2LogoutRequestValidatorParametersResolver}
*/
public final class OpenSaml4LogoutRequestValidatorParametersResolver
implements Saml2LogoutRequestValidatorParametersResolver {
static {
OpenSamlInitializationService.initialize();
}
private final BaseOpenSamlLogoutRequestValidatorParametersResolver delegate;
/**
* Constructs a {@link OpenSaml4LogoutRequestValidatorParametersResolver}
*/
public OpenSaml4LogoutRequestValidatorParametersResolver(RelyingPartyRegistrationRepository registrations) {
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
this.delegate = new BaseOpenSamlLogoutRequestValidatorParametersResolver(new OpenSaml4Template(),
registrations);
}
/**
* Construct the parameters necessary for validating an asserting party's
* {@code <saml2:LogoutRequest>} based on the given {@link HttpServletRequest}
*
* <p>
* Uses the configured {@link RequestMatcher} to identify the processing request,
* including looking for any indicated {@code registrationId}.
*
* <p>
* If a {@code registrationId} is found in the request, it will attempt to use that,
* erroring if no {@link RelyingPartyRegistration} is found.
*
* <p>
* If no {@code registrationId} is found in the request, it will look for a currently
* logged-in user and use the associated {@code registrationId}.
*
* <p>
* In the event that neither the URL nor any logged in user could determine a
* {@code registrationId}, this code then will try and derive a
* {@link RelyingPartyRegistration} given the {@code <saml2:LogoutRequest>}'s
* {@code Issuer} value.
* @param request the HTTP request
* @return a {@link Saml2LogoutRequestValidatorParameters} instance, or {@code null}
* if one could not be resolved
* @throws Saml2AuthenticationException if the {@link RequestMatcher} specifies a
* non-existent {@code registrationId}
*/
@Override
public Saml2LogoutRequestValidatorParameters resolve(HttpServletRequest request, Authentication authentication) {
return this.delegate.resolve(request, authentication);
}
/**
* The request matcher to use to identify a request to process a
* {@code <saml2:LogoutRequest>}. By default, checks for {@code /logout/saml2/slo} and
* {@code /logout/saml2/slo/{registrationId}}.
*
* <p>
* Generally speaking, the URL does not need to have a {@code registrationId} in it
* since either it can be looked up from the active logged in user or it can be
* derived through the {@code Issuer} in the {@code <saml2:LogoutRequest>}.
* @param requestMatcher the {@link RequestMatcher} to use
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.delegate.setRequestMatcher(requestMatcher);
}
}

View File

@ -1,140 +0,0 @@
/*
* Copyright 2004-present 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.time.Instant;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
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.RelyingPartyRegistrationRepository;
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 4
*
* @author Josh Cummings
* @since 5.6
*/
public final class OpenSaml4LogoutResponseResolver implements Saml2LogoutResponseResolver {
private final BaseOpenSamlLogoutResponseResolver delegate;
public OpenSaml4LogoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new BaseOpenSamlLogoutResponseResolver(registrations, (request, id) -> {
if (id == null) {
return null;
}
return registrations.findByRegistrationId(id);
}, new OpenSaml4Template());
}
/**
* Construct a {@link OpenSaml4LogoutResponseResolver}
*/
public OpenSaml4LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.delegate = new BaseOpenSamlLogoutResponseResolver(null, relyingPartyRegistrationResolver,
new OpenSaml4Template());
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
return this.delegate.resolve(request, authentication);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
Saml2AuthenticationException exception) {
return this.delegate.resolve(request, authentication, exception);
}
/**
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
* @param parametersConsumer a consumer that accepts an
* {@link OpenSaml4LogoutRequestResolver.LogoutRequestParameters}
*/
public void setParametersConsumer(Consumer<LogoutResponseParameters> parametersConsumer) {
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
this.delegate
.setParametersConsumer((parameters) -> parametersConsumer.accept(new LogoutResponseParameters(parameters)));
}
/**
* Use this {@link Clock} for determining the issued {@link Instant}
* @param clock the {@link Clock} to use
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.delegate.setClock(clock);
}
public static final class LogoutResponseParameters {
private final HttpServletRequest request;
private final RelyingPartyRegistration registration;
private final Authentication authentication;
private final LogoutRequest logoutRequest;
public LogoutResponseParameters(HttpServletRequest request, RelyingPartyRegistration registration,
Authentication authentication, LogoutRequest logoutRequest) {
this.request = request;
this.registration = registration;
this.authentication = authentication;
this.logoutRequest = logoutRequest;
}
LogoutResponseParameters(BaseOpenSamlLogoutResponseResolver.LogoutResponseParameters parameters) {
this(parameters.getRequest(), parameters.getRelyingPartyRegistration(), parameters.getAuthentication(),
parameters.getLogoutRequest());
}
public HttpServletRequest getRequest() {
return this.request;
}
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
public Authentication getAuthentication() {
return this.authentication;
}
public LogoutRequest getLogoutRequest() {
return this.logoutRequest;
}
}
}

View File

@ -1,607 +0,0 @@
/*
* Copyright 2004-present 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.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilder;
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.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.ext.saml2delrestrict.Delegate;
import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.EncryptedAttribute;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.DecryptionException;
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* For internal use only. Subject to breaking changes at any time.
*/
final class OpenSaml4Template implements OpenSamlOperations {
private static final Log logger = LogFactory.getLog(OpenSaml4Template.class);
@Override
public <T extends XMLObject> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
}
return (T) builder.buildObject(elementName);
}
@Override
public <T extends XMLObject> T deserialize(String serialized) {
return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
}
@Override
public <T extends XMLObject> T deserialize(InputStream serialized) {
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized);
Element element = document.getDocumentElement();
UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
Unmarshaller unmarshaller = factory.getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
return (T) unmarshaller.unmarshall(element);
}
catch (Saml2Exception ex) {
throw ex;
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize payload", ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(XMLObject object) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
try {
return serialize(marshaller.marshall(object));
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
@Override
public OpenSaml4SerializationConfigurer serialize(Element element) {
return new OpenSaml4SerializationConfigurer(element);
}
@Override
public OpenSaml4SignatureConfigurer withSigningKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4SignatureConfigurer(credentials);
}
@Override
public OpenSaml4VerificationConfigurer withVerificationKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4VerificationConfigurer(credentials);
}
@Override
public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection<Saml2X509Credential> credentials) {
return new OpenSaml4DecryptionConfigurer(credentials);
}
OpenSaml4Template() {
}
static final class OpenSaml4SerializationConfigurer
implements SerializationConfigurer<OpenSaml4SerializationConfigurer> {
private final Element element;
boolean pretty;
OpenSaml4SerializationConfigurer(Element element) {
this.element = element;
}
@Override
public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) {
this.pretty = pretty;
return this;
}
@Override
public String serialize() {
if (this.pretty) {
return SerializeSupport.prettyPrintXML(this.element);
}
return SerializeSupport.nodeToString(this.element);
}
}
static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer<OpenSaml4SignatureConfigurer> {
private final Collection<Saml2X509Credential> credentials;
private final Map<String, String> components = new LinkedHashMap<>();
private List<String> algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
OpenSaml4SignatureConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public OpenSaml4SignatureConfigurer algorithms(List<String> algs) {
this.algs = algs;
return this;
}
@Override
public <O extends SignableXMLObject> O sign(O object) {
SignatureSigningParameters parameters = resolveSigningParameters();
try {
SignatureSupport.signObject(object, parameters);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
return object;
}
@Override
public Map<String, String> sign(Map<String, String> params) {
SignatureSigningParameters parameters = resolveSigningParameters();
this.components.putAll(params);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
private SignatureSigningParameters resolveSigningParameters() {
List<Credential> credentials = resolveSigningCredentials();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(this.algs);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
namedManager.setUseDefaultManager(true);
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
// Generator for X509Credentials
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
x509Factory.setEmitEntityCertificate(true);
x509Factory.setEmitEntityCertificateChain(true);
defaultManager.registerFactory(x509Factory);
return namedManager;
}
private List<Credential> resolveSigningCredentials() {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : this.credentials) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}
static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer {
private final Collection<Saml2X509Credential> credentials;
private String entityId;
OpenSaml4VerificationConfigurer(Collection<Saml2X509Credential> credentials) {
this.credentials = credentials;
}
@Override
public VerificationConfigurer entityId(String entityId) {
this.entityId = entityId;
return this;
}
private SignatureTrustEngine trustEngine(Collection<Saml2X509Credential> keys) {
Set<Credential> credentials = new HashSet<>();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(this.entityId);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private CriteriaSet verificationCriteria(Issuer issuer) {
return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())),
new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)),
new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
}
@Override
public Collection<Saml2Error> verify(SignableXMLObject signable) {
if (signable instanceof StatusResponseType response) {
return verifySignature(response.getID(), response.getIssuer(), response.getSignature());
}
if (signable instanceof RequestAbstractType request) {
return verifySignature(request.getID(), request.getIssuer(), request.getSignature());
}
if (signable instanceof Assertion assertion) {
return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature());
}
throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName());
}
private Collection<Saml2Error> verifySignature(String id, Issuer issuer, Signature signature) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(issuer);
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
try {
if (!trustEngine.validate(signature, criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + id + "]: "));
}
return errors;
}
@Override
public Collection<Saml2Error> verify(RedirectParameters parameters) {
SignatureTrustEngine trustEngine = trustEngine(this.credentials);
CriteriaSet criteria = verificationCriteria(parameters.getIssuer());
if (parameters.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + parameters.getId() + "]"));
}
if (!parameters.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + parameters.getId() + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = parameters.getAlgorithm();
try {
if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria,
null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + parameters.getId() + "]: "));
}
return errors;
}
}
static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
private final Decrypter decrypter;
OpenSaml4DecryptionConfigurer(Collection<Saml2X509Credential> decryptionCredentials) {
this.decrypter = decrypter(decryptionCredentials);
}
private static Decrypter decrypter(Collection<Saml2X509Credential> decryptionCredentials) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : decryptionCredentials) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
@Override
public void decrypt(XMLObject object) {
if (object instanceof Response response) {
decryptResponse(response);
return;
}
if (object instanceof Assertion assertion) {
decryptAssertion(assertion);
}
if (object instanceof LogoutRequest request) {
decryptLogoutRequest(request);
}
}
/*
* The methods that follow are adapted from OpenSAML's {@link DecryptAssertions},
* {@link DecryptNameIDs}, and {@link DecryptAttributes}.
*
* <p>The reason that these OpenSAML classes are not used directly is because they
* reference {@link javax.servlet.http.HttpServletRequest} which is a lower
* Servlet API version than what Spring Security SAML uses.
*
* If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then
* this arrangement can be revisited.
*/
private void decryptResponse(Response response) {
Collection<Assertion> decrypteds = new ArrayList<>();
int count = 0;
int size = response.getEncryptedAssertions().size();
for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) {
logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size,
response.getID()));
try {
Assertion decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
count++;
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
response.getAssertions().addAll(decrypteds);
// Re-marshall the response so that any ID attributes within the decrypted
// Assertions
// will have their ID-ness re-established at the DOM level.
if (!decrypteds.isEmpty()) {
try {
XMLObjectSupport.marshall(response);
}
catch (final MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}
private void decryptAssertion(Assertion assertion) {
for (AttributeStatement statement : assertion.getAttributeStatements()) {
decryptAttributes(statement);
}
decryptSubject(assertion.getSubject());
if (assertion.getConditions() != null) {
for (Condition c : assertion.getConditions().getConditions()) {
if (!(c instanceof DelegationRestrictionType delegation)) {
continue;
}
for (Delegate d : delegation.getDelegates()) {
if (d.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID());
if (decrypted != null) {
d.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
}
private void decryptAttributes(AttributeStatement statement) {
Collection<Attribute> decrypteds = new ArrayList<>();
for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) {
try {
Attribute decrypted = this.decrypter.decrypt(encrypted);
if (decrypted != null) {
decrypteds.add(decrypted);
}
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
statement.getAttributes().addAll(decrypteds);
}
private void decryptSubject(Subject subject) {
if (subject != null) {
if (subject.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID());
if (decrypted != null) {
subject.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
if (sc.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID());
if (decrypted != null) {
sc.setNameID(decrypted);
}
}
catch (final DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}
private void decryptLogoutRequest(LogoutRequest request) {
if (request.getEncryptedID() != null) {
try {
NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID());
if (decrypted != null) {
request.setNameID(decrypted);
}
}
catch (DecryptionException ex) {
throw new Saml2Exception(ex);
}
}
}
}
}

View File

@ -1,213 +0,0 @@
/*
* Copyright 2004-present 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.util.Collections;
import java.util.List;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.xml.ElementSupport;
import org.opensaml.core.xml.AbstractXMLObject;
import org.opensaml.core.xml.AbstractXMLObjectBuilder;
import org.opensaml.core.xml.ElementExtensibleXMLObject;
import org.opensaml.core.xml.Namespace;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.AbstractXMLObjectMarshaller;
import org.opensaml.core.xml.io.AbstractXMLObjectUnmarshaller;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.core.xml.schema.XSAny;
import org.opensaml.core.xml.schema.impl.XSAnyBuilder;
import org.opensaml.core.xml.util.IndexedXMLObjectChildrenList;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AttributeValue;
import org.w3c.dom.Element;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
public final class TestCustomOpenSaml4Objects {
static {
OpenSamlInitializationService.initialize();
XMLObjectProviderRegistrySupport.getMarshallerFactory()
.registerMarshaller(CustomOpenSamlObject.TYPE_NAME,
new TestCustomOpenSaml4Objects.CustomSamlObjectMarshaller());
XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.registerUnmarshaller(CustomOpenSamlObject.TYPE_NAME,
new TestCustomOpenSaml4Objects.CustomSamlObjectUnmarshaller());
}
public static CustomOpenSamlObject instance() {
CustomOpenSamlObject samlObject = new TestCustomOpenSaml4Objects.CustomSamlObjectBuilder()
.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, CustomOpenSamlObject.TYPE_NAME);
XSAny street = new XSAnyBuilder().buildObject(CustomOpenSamlObject.CUSTOM_NS, "Street",
CustomOpenSamlObject.TYPE_CUSTOM_PREFIX);
street.setTextContent("Test Street");
samlObject.getUnknownXMLObjects().add(street);
XSAny streetNumber = new XSAnyBuilder().buildObject(CustomOpenSamlObject.CUSTOM_NS, "Number",
CustomOpenSamlObject.TYPE_CUSTOM_PREFIX);
streetNumber.setTextContent("1");
samlObject.getUnknownXMLObjects().add(streetNumber);
XSAny zip = new XSAnyBuilder().buildObject(CustomOpenSamlObject.CUSTOM_NS, "ZIP",
CustomOpenSamlObject.TYPE_CUSTOM_PREFIX);
zip.setTextContent("11111");
samlObject.getUnknownXMLObjects().add(zip);
XSAny city = new XSAnyBuilder().buildObject(CustomOpenSamlObject.CUSTOM_NS, "City",
CustomOpenSamlObject.TYPE_CUSTOM_PREFIX);
city.setTextContent("Test City");
samlObject.getUnknownXMLObjects().add(city);
return samlObject;
}
private TestCustomOpenSaml4Objects() {
}
public interface CustomOpenSamlObject extends ElementExtensibleXMLObject {
String TYPE_LOCAL_NAME = "CustomType";
String TYPE_CUSTOM_PREFIX = "custom";
String CUSTOM_NS = "https://custom.com/schema/custom";
/** QName of the CustomType type. */
QName TYPE_NAME = new QName(CUSTOM_NS, TYPE_LOCAL_NAME, TYPE_CUSTOM_PREFIX);
String getStreet();
String getStreetNumber();
String getZIP();
String getCity();
}
public static class CustomOpenSamlObjectImpl extends AbstractXMLObject implements CustomOpenSamlObject {
@NonNull
private IndexedXMLObjectChildrenList<XMLObject> unknownXMLObjects;
/**
* Constructor.
* @param namespaceURI the namespace the element is in
* @param elementLocalName the local name of the XML element this Object
* represents
* @param namespacePrefix the prefix for the given namespace
*/
protected CustomOpenSamlObjectImpl(@Nullable String namespaceURI, @NonNull String elementLocalName,
@Nullable String namespacePrefix) {
super(namespaceURI, elementLocalName, namespacePrefix);
super.getNamespaceManager().registerNamespaceDeclaration(new Namespace(CUSTOM_NS, TYPE_CUSTOM_PREFIX));
this.unknownXMLObjects = new IndexedXMLObjectChildrenList<>(this);
}
@NonNull
@Override
public List<XMLObject> getUnknownXMLObjects() {
return this.unknownXMLObjects;
}
@NonNull
@Override
public List<XMLObject> getUnknownXMLObjects(@NonNull QName typeOrName) {
return (List<XMLObject>) this.unknownXMLObjects.subList(typeOrName);
}
@Nullable
@Override
public List<XMLObject> getOrderedChildren() {
return Collections.unmodifiableList(this.unknownXMLObjects);
}
@Override
public String getStreet() {
return ((XSAny) getOrderedChildren().get(0)).getTextContent();
}
@Override
public String getStreetNumber() {
return ((XSAny) getOrderedChildren().get(1)).getTextContent();
}
@Override
public String getZIP() {
return ((XSAny) getOrderedChildren().get(2)).getTextContent();
}
@Override
public String getCity() {
return ((XSAny) getOrderedChildren().get(3)).getTextContent();
}
}
public static class CustomSamlObjectBuilder extends AbstractXMLObjectBuilder<CustomOpenSamlObject> {
@NonNull
@Override
public CustomOpenSamlObject buildObject(@Nullable String namespaceURI, @NonNull String localName,
@Nullable String namespacePrefix) {
return new CustomOpenSamlObjectImpl(namespaceURI, localName, namespacePrefix);
}
}
public static class CustomSamlObjectMarshaller extends AbstractXMLObjectMarshaller {
public CustomSamlObjectMarshaller() {
super();
}
@Override
protected void marshallElementContent(@NonNull XMLObject xmlObject, @NonNull Element domElement) {
final CustomOpenSamlObject customSamlObject = (CustomOpenSamlObject) xmlObject;
for (XMLObject object : customSamlObject.getOrderedChildren()) {
ElementSupport.appendChildElement(domElement, object.getDOM());
}
}
}
public static class CustomSamlObjectUnmarshaller extends AbstractXMLObjectUnmarshaller {
public CustomSamlObjectUnmarshaller() {
super();
}
@Override
protected void processChildElement(@NonNull XMLObject parentXMLObject, @NonNull XMLObject childXMLObject)
throws UnmarshallingException {
final CustomOpenSamlObject customSamlObject = (CustomOpenSamlObject) parentXMLObject;
customSamlObject.getUnknownXMLObjects().add(childXMLObject);
}
@NonNull
@Override
protected XMLObject buildXMLObject(@NonNull Element domElement) {
return new CustomOpenSamlObjectImpl(SAMLConstants.SAML20_NS, AttributeValue.DEFAULT_ELEMENT_LOCAL_NAME,
CustomOpenSamlObject.TYPE_CUSTOM_PREFIX);
}
}
}

View File

@ -1,223 +0,0 @@
/*
* Copyright 2004-present 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.logout;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.SignatureConfigurer;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OpenSaml4LogoutRequestValidator}
*
* @author Josh Cummings
*/
public class OpenSaml4LogoutRequestValidatorTests {
private final OpenSamlOperations saml = new OpenSaml4Template();
private final OpenSaml4LogoutRequestValidator validator = new OpenSaml4LogoutRequestValidator();
@Test
public void handleWhenPostBindingThenValidates() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.validator.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
@Test
public void handleWhenNameIdIsEncryptedIdPostThenValidates() {
RelyingPartyRegistration registration = decrypting(encrypting(registration())).build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequestNameIdInEncryptedId(registration);
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.validator.validate(parameters);
assertThat(result.hasErrors()).withFailMessage(() -> result.getErrors().toString()).isFalse();
}
@Test
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
RelyingPartyRegistration registration = registration()
.assertingPartyMetadata((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
.build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
Saml2LogoutRequest request = redirect(logoutRequest, registration,
this.saml.withSigningKeys(registration.getSigningX509Credentials()));
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.validator.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
@Test
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.getIssuer().setValue("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.validator.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE);
}
@Test
public void handleWhenMismatchedUserThenInvalidRequestError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.getNameID().setValue("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.validator.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_REQUEST);
}
@Test
public void handleWhenMissingUserThenSubjectNotFoundError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.setNameID(null);
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.validator.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.SUBJECT_NOT_FOUND);
}
@Test
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.setDestination("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.validator.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
}
// gh-10923
@Test
public void handleWhenLogoutResponseHasLineBreaksThenHandles() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
sign(logoutRequest, registration);
String encoded = new StringBuffer(
Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8)))
.insert(10, "\r\n")
.toString();
Saml2LogoutRequest request = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(encoded)
.build();
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.validator.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
.assertingPartyMetadata((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
}
private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) {
return builder
.decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential()));
}
private RelyingPartyRegistration.Builder encrypting(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyMetadata((party) -> party
.encryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartyEncryptingCredential())));
}
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyMetadata((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
}
private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) {
return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()));
}
private Authentication authentication(RelyingPartyRegistration registration) {
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
return new Saml2Authentication(principal, "response", new ArrayList<>());
}
private Saml2LogoutRequest post(LogoutRequest logoutRequest, RelyingPartyRegistration registration) {
return Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8)))
.build();
}
private Saml2LogoutRequest redirect(LogoutRequest logoutRequest, RelyingPartyRegistration registration,
SignatureConfigurer configurer) {
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutRequest)));
Map<String, String> parameters = configurer.sign(Map.of(Saml2ParameterNames.SAML_REQUEST, serialized));
return Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(serialized)
.parameters((params) -> params.putAll(parameters))
.build();
}
private void sign(LogoutRequest logoutRequest, RelyingPartyRegistration registration) {
TestOpenSamlObjects.signed(logoutRequest, registration.getSigningX509Credentials().iterator().next(),
registration.getAssertingPartyMetadata().getEntityId());
}
private String serialize(XMLObject object) {
return this.saml.serialize(object).serialize();
}
}

View File

@ -1,190 +0,0 @@
/*
* Copyright 2004-present 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.logout;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.SignatureConfigurer;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OpenSaml4LogoutResponseValidator}
*
* @author Josh Cummings
*/
public class OpenSaml4LogoutResponseValidatorTests {
private final OpenSamlOperations saml = new OpenSaml4Template();
private final OpenSaml4LogoutResponseValidator manager = new OpenSaml4LogoutResponseValidator();
@Test
public void handleWhenAuthenticatedThenHandles() {
RelyingPartyRegistration registration = signing(verifying(registration())).build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
@Test
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
RelyingPartyRegistration registration = signing(verifying(registration()))
.assertingPartyMetadata((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
.build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
Saml2LogoutResponse response = redirect(logoutResponse, registration,
this.saml.withSigningKeys(registration.getSigningX509Credentials()));
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
@Test
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.getIssuer().setValue("wrong");
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE);
}
@Test
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.setDestination("wrong");
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
}
@Test
public void handleWhenStatusNotSuccessThenInvalidResponseError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.getStatus().getStatusCode().setValue(StatusCode.UNKNOWN_PRINCIPAL);
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE);
}
// gh-10923
@Test
public void handleWhenLogoutResponseHasLineBreaksThenHandles() {
RelyingPartyRegistration registration = signing(verifying(registration())).build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
sign(logoutResponse, registration);
String encoded = new StringBuilder(
Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8)))
.insert(10, "\r\n")
.toString();
Saml2LogoutResponse response = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(encoded)
.build();
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
.assertingPartyMetadata((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
}
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyMetadata((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
}
private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) {
return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()));
}
private Saml2LogoutResponse post(LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
return Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8)))
.build();
}
private Saml2LogoutResponse redirect(LogoutResponse logoutResponse, RelyingPartyRegistration registration,
SignatureConfigurer<?> configurer) {
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutResponse)));
Map<String, String> parameters = configurer.sign(Map.of(Saml2ParameterNames.SAML_RESPONSE, serialized));
return Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(serialized)
.parameters((params) -> params.putAll(parameters))
.build();
}
private void sign(LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
TestOpenSamlObjects.signed(logoutResponse, registration.getSigningX509Credentials().iterator().next(),
registration.getAssertingPartyMetadata().getEntityId());
}
private String serialize(XMLObject object) {
return this.saml.serialize(object).serialize();
}
}

View File

@ -1,185 +0,0 @@
/*
* Copyright 2004-present 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.metadata;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OpenSaml4MetadataResolver}
*/
public class OpenSaml4MetadataResolverTests {
@Test
public void resolveWhenRelyingPartyThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
@Test
public void resolveWhenRelyingPartyAndSignMetadataSetThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver();
OpenSaml4MetadataResolver.setSignMetadata(true);
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"")
.contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"")
.contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#")
.contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
.contains("Reference URI=\"\"")
.contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature")
.contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"")
.contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"")
.contains("DigestValue")
.contains("SignatureValue");
}
@Test
public void resolveWhenRelyingPartyNoCredentialsThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
.assertingPartyMetadata((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
.build();
OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.doesNotContain("<md:KeyDescriptor use=\"signing\">")
.doesNotContain("<md:KeyDescriptor use=\"encryption\">")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
@Test
public void resolveWhenRelyingPartyNameIDFormatThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.nameIdFormat("format")
.build();
OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:NameIDFormat>format</md:NameIDFormat>");
}
@Test
public void resolveWhenRelyingPartyNoLogoutThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.singleLogoutServiceLocation(null)
.nameIdFormat("format")
.build();
OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).doesNotContain("ResponseLocation");
}
@Test
public void resolveWhenEntityDescriptorCustomizerThenUses() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.entityId("originalEntityId")
.build();
OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver();
OpenSaml4MetadataResolver.setEntityDescriptorCustomizer(
(parameters) -> parameters.getEntityDescriptor().setEntityID("overriddenEntityId"));
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor").contains("entityID=\"overriddenEntityId\"");
}
@Test
public void resolveIterableWhenRelyingPartiesThenMetadataMatches() {
RelyingPartyRegistration one = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
RelyingPartyRegistration two = TestRelyingPartyRegistrations.full()
.entityId("two")
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(List.of(one, two));
assertThat(metadata).contains("<md:EntitiesDescriptor")
.contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.contains("entityID=\"two\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
@Test
public void resolveIterableWhenRelyingPartiesAndSignMetadataSetThenMetadataMatches() {
RelyingPartyRegistration one = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
RelyingPartyRegistration two = TestRelyingPartyRegistrations.full()
.entityId("two")
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver();
OpenSaml4MetadataResolver.setSignMetadata(true);
String metadata = OpenSaml4MetadataResolver.resolve(List.of(one, two));
assertThat(metadata).contains("<md:EntitiesDescriptor")
.contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.contains("entityID=\"two\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"")
.contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"")
.contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#")
.contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
.contains("Reference URI=\"\"")
.contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature")
.contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"")
.contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"")
.contains("DigestValue")
.contains("SignatureValue");
}
}

View File

@ -1,377 +0,0 @@
/*
* Copyright 2004-present 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.registration;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
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.saml.metadata.IterableMetadataSource;
import org.opensaml.saml.metadata.resolver.MetadataResolver;
import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver;
import org.opensaml.saml.metadata.resolver.index.impl.RoleMetadataIndex;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.security.credential.Credential;
import org.w3c.dom.Element;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
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.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.withSettings;
/**
* Tests for {@link BaseOpenSamlAssertingPartyMetadataRepository}
*/
public class OpenSaml4AssertingPartyMetadataRepositoryTests {
private static MetadataDispatcher dispatcher = new MetadataDispatcher()
.addResponse("/entity.xml", readFile("test-metadata.xml"))
.addResponse("/entities.xml", readFile("test-entitiesdescriptor.xml"));
private static MockWebServer web = new MockWebServer();
private static String readFile(String fileName) {
try {
ClassPathResource resource = new ClassPathResource(fileName);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
return reader.lines().collect(Collectors.joining());
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
@BeforeAll
public static void start() throws Exception {
web.setDispatcher(dispatcher);
web.start();
}
@AfterAll
public static void shutdown() throws Exception {
web.shutdown();
}
@Test
public void withMetadataUrlLocationWhenResolvableThenFindByEntityIdReturns() throws Exception {
AssertingPartyMetadataRepository parties = OpenSaml4AssertingPartyMetadataRepository
.withTrustedMetadataLocation(web.url("/entity.xml").toString())
.build();
AssertingPartyMetadata party = parties.findByEntityId("https://idp.example.com/idp/shibboleth");
assertThat(party.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
assertThat(party.getSingleSignOnServiceLocation())
.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
assertThat(party.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(party.getVerificationX509Credentials()).hasSize(1);
assertThat(party.getEncryptionX509Credentials()).hasSize(1);
}
@Test
public void withMetadataUrlLocationnWhenResolvableThenIteratorReturns() throws Exception {
List<AssertingPartyMetadata> parties = new ArrayList<>();
OpenSaml4AssertingPartyMetadataRepository.withTrustedMetadataLocation(web.url("/entities.xml").toString())
.build()
.iterator()
.forEachRemaining(parties::add);
assertThat(parties).hasSize(2);
assertThat(parties).extracting(AssertingPartyMetadata::getEntityId)
.contains("https://ap.example.org/idp/shibboleth", "https://idp.example.com/idp/shibboleth");
}
@Test
public void withMetadataUrlLocationWhenUnresolvableThenThrowsSaml2Exception() throws Exception {
try (MockWebServer server = new MockWebServer()) {
String url = server.url("/").toString();
server.shutdown();
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> OpenSaml4AssertingPartyMetadataRepository.withTrustedMetadataLocation(url).build());
}
}
@Test
public void withMetadataUrlLocationWhenMalformedResponseThenSaml2Exception() throws Exception {
dispatcher.addResponse("/malformed", "malformed");
String url = web.url("/malformed").toString();
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> OpenSaml4AssertingPartyMetadataRepository.withTrustedMetadataLocation(url).build());
}
@Test
public void fromMetadataFileLocationWhenResolvableThenFindByEntityIdReturns() {
File file = new File("src/test/resources/test-metadata.xml");
AssertingPartyMetadata party = OpenSaml4AssertingPartyMetadataRepository
.withTrustedMetadataLocation("file:" + file.getAbsolutePath())
.build()
.findByEntityId("https://idp.example.com/idp/shibboleth");
assertThat(party.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
assertThat(party.getSingleSignOnServiceLocation())
.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
assertThat(party.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(party.getVerificationX509Credentials()).hasSize(1);
assertThat(party.getEncryptionX509Credentials()).hasSize(1);
}
@Test
public void fromMetadataFileLocationWhenResolvableThenIteratorReturns() {
File file = new File("src/test/resources/test-entitiesdescriptor.xml");
Collection<AssertingPartyMetadata> parties = new ArrayList<>();
OpenSaml4AssertingPartyMetadataRepository.withTrustedMetadataLocation("file:" + file.getAbsolutePath())
.build()
.iterator()
.forEachRemaining(parties::add);
assertThat(parties).hasSize(2);
assertThat(parties).extracting(AssertingPartyMetadata::getEntityId)
.contains("https://idp.example.com/idp/shibboleth", "https://ap.example.org/idp/shibboleth");
}
@Test
public void withMetadataFileLocationWhenNotFoundThenSaml2Exception() {
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(
() -> OpenSaml4AssertingPartyMetadataRepository.withTrustedMetadataLocation("file:path").build());
}
@Test
public void fromMetadataClasspathLocationWhenResolvableThenFindByEntityIdReturns() {
AssertingPartyMetadata party = OpenSaml4AssertingPartyMetadataRepository
.withTrustedMetadataLocation("classpath:test-entitiesdescriptor.xml")
.build()
.findByEntityId("https://ap.example.org/idp/shibboleth");
assertThat(party.getEntityId()).isEqualTo("https://ap.example.org/idp/shibboleth");
assertThat(party.getSingleSignOnServiceLocation())
.isEqualTo("https://ap.example.org/idp/profile/SAML2/POST/SSO");
assertThat(party.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(party.getVerificationX509Credentials()).hasSize(1);
assertThat(party.getEncryptionX509Credentials()).hasSize(1);
}
@Test
public void fromMetadataClasspathLocationWhenResolvableThenIteratorReturns() {
Collection<AssertingPartyMetadata> parties = new ArrayList<>();
OpenSaml4AssertingPartyMetadataRepository.withTrustedMetadataLocation("classpath:test-entitiesdescriptor.xml")
.build()
.iterator()
.forEachRemaining(parties::add);
assertThat(parties).hasSize(2);
assertThat(parties).extracting(AssertingPartyMetadata::getEntityId)
.contains("https://idp.example.com/idp/shibboleth", "https://ap.example.org/idp/shibboleth");
}
@Test
public void withMetadataClasspathLocationWhenNotFoundThenSaml2Exception() {
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(
() -> OpenSaml4AssertingPartyMetadataRepository.withTrustedMetadataLocation("classpath:path").build());
}
@Test
public void withTrustedMetadataLocationWhenMatchingCredentialsThenVerifiesSignature() throws IOException {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration);
TestOpenSamlObjects.signed(descriptor, TestSaml2X509Credentials.assertingPartySigningCredential(),
descriptor.getEntityID());
String serialized = serialize(descriptor);
Credential credential = TestOpenSamlObjects
.getSigningCredential(TestSaml2X509Credentials.relyingPartyVerifyingCredential(), descriptor.getEntityID());
String endpoint = "/" + UUID.randomUUID().toString();
dispatcher.addResponse(endpoint, serialized);
AssertingPartyMetadataRepository parties = OpenSaml4AssertingPartyMetadataRepository
.withTrustedMetadataLocation(web.url(endpoint).toString())
.verificationCredentials((c) -> c.add(credential))
.build();
assertThat(parties.findByEntityId(registration.getAssertingPartyMetadata().getEntityId())).isNotNull();
}
@Test
public void withTrustedMetadataLocationWhenMismatchingCredentialsThenSaml2Exception() throws IOException {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration);
TestOpenSamlObjects.signed(descriptor, TestSaml2X509Credentials.relyingPartySigningCredential(),
descriptor.getEntityID());
String serialized = serialize(descriptor);
Credential credential = TestOpenSamlObjects
.getSigningCredential(TestSaml2X509Credentials.relyingPartyVerifyingCredential(), descriptor.getEntityID());
String endpoint = "/" + UUID.randomUUID().toString();
dispatcher.addResponse(endpoint, serialized);
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> OpenSaml4AssertingPartyMetadataRepository
.withTrustedMetadataLocation(web.url(endpoint).toString())
.verificationCredentials((c) -> c.add(credential))
.build());
}
@Test
public void withTrustedMetadataLocationWhenNoCredentialsThenSkipsVerifySignature() throws IOException {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration);
TestOpenSamlObjects.signed(descriptor, TestSaml2X509Credentials.assertingPartySigningCredential(),
descriptor.getEntityID());
String serialized = serialize(descriptor);
String endpoint = "/" + UUID.randomUUID().toString();
dispatcher.addResponse(endpoint, serialized);
AssertingPartyMetadataRepository parties = OpenSaml4AssertingPartyMetadataRepository
.withTrustedMetadataLocation(web.url(endpoint).toString())
.build();
assertThat(parties.findByEntityId(registration.getAssertingPartyMetadata().getEntityId())).isNotNull();
}
@Test
public void withTrustedMetadataLocationWhenCustomResourceLoaderThenUses() {
ResourceLoader resourceLoader = mock(ResourceLoader.class);
given(resourceLoader.getResource(any())).willReturn(new ClassPathResource("test-metadata.xml"));
AssertingPartyMetadata party = OpenSaml4AssertingPartyMetadataRepository
.withTrustedMetadataLocation("classpath:wrong")
.resourceLoader(resourceLoader)
.build()
.iterator()
.next();
assertThat(party.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
assertThat(party.getSingleSignOnServiceLocation())
.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
assertThat(party.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(party.getVerificationX509Credentials()).hasSize(1);
assertThat(party.getEncryptionX509Credentials()).hasSize(1);
verify(resourceLoader).getResource(any());
}
@Test
public void constructorWhenNoIndexAndNoIteratorThenException() {
MetadataResolver resolver = mock(MetadataResolver.class);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> new OpenSaml4AssertingPartyMetadataRepository(resolver));
}
@Test
public void constructorWhenIterableResolverThenUses() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration);
MetadataResolver resolver = mock(MetadataResolver.class,
withSettings().extraInterfaces(IterableMetadataSource.class));
given(((IterableMetadataSource) resolver).iterator()).willReturn(List.of(descriptor).iterator());
AssertingPartyMetadataRepository parties = new OpenSaml4AssertingPartyMetadataRepository(resolver);
parties.iterator()
.forEachRemaining((p) -> assertThat(p.getEntityId())
.isEqualTo(registration.getAssertingPartyMetadata().getEntityId()));
verify(((IterableMetadataSource) resolver)).iterator();
}
@Test
public void constructorWhenIndexedResolverThenUses() throws Exception {
FilesystemMetadataResolver resolver = new FilesystemMetadataResolver(
new ClassPathResource("test-metadata.xml").getFile());
resolver.setIndexes(Set.of(new RoleMetadataIndex()));
resolver.setId("id");
resolver.setParserPool(XMLObjectProviderRegistrySupport.getParserPool());
resolver.initialize();
MetadataResolver spied = spy(resolver);
AssertingPartyMetadataRepository parties = new OpenSaml4AssertingPartyMetadataRepository(spied);
parties.iterator()
.forEachRemaining((p) -> assertThat(p.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth"));
verify(spied).resolve(any());
}
@Test
public void withMetadataLocationWhenNoCredentialsThenException() {
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(
() -> OpenSaml4AssertingPartyMetadataRepository.withMetadataLocation("classpath:test-metadata.xml")
.build());
}
@Test
public void withMetadataLocationWhenMatchingCredentialsThenVerifiesSignature() throws IOException {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration);
TestOpenSamlObjects.signed(descriptor, TestSaml2X509Credentials.assertingPartySigningCredential(),
descriptor.getEntityID());
String serialized = serialize(descriptor);
Credential credential = TestOpenSamlObjects
.getSigningCredential(TestSaml2X509Credentials.relyingPartyVerifyingCredential(), descriptor.getEntityID());
String endpoint = "/" + UUID.randomUUID().toString();
dispatcher.addResponse(endpoint, serialized);
AssertingPartyMetadataRepository parties = OpenSaml4AssertingPartyMetadataRepository
.withMetadataLocation(web.url(endpoint).toString())
.verificationCredentials((c) -> c.add(credential))
.build();
assertThat(parties.findByEntityId(registration.getAssertingPartyMetadata().getEntityId())).isNotNull();
}
private static 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 static final class MetadataDispatcher extends Dispatcher {
private final MockResponse head = new MockResponse();
private final Map<String, MockResponse> responses = new ConcurrentHashMap<>();
private MetadataDispatcher() {
}
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
if ("HEAD".equals(request.getMethod())) {
return this.head;
}
return this.responses.get(request.getPath());
}
private MetadataDispatcher addResponse(String path, String body) {
this.responses.put(path, new MockResponse().setBody(body).setResponseCode(200));
return this;
}
}
}

View File

@ -1,243 +0,0 @@
/*
* Copyright 2004-present 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;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.common.SignableSAMLObject;
import org.opensaml.saml.saml2.core.Response;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ParameterNames;
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.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
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.web.servlet.TestMockHttpServletRequests;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.UriUtils;
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.mock;
/**
* Tests for {@link OpenSaml4AuthenticationTokenConverter}
*/
@ExtendWith(MockitoExtension.class)
public final class OpenSaml4AuthenticationTokenConverterTests {
@Mock
RelyingPartyRegistrationRepository registrations;
private final OpenSamlOperations saml = new OpenSaml4Template();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
@Test
public void convertWhenSamlResponseThenToken() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
request.setParameter(Saml2ParameterNames.SAML_RESPONSE,
Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8)));
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo("response");
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
.isEqualTo(this.registration.getRegistrationId());
}
@Test
public void convertWhenSamlResponseInvalidBase64ThenSaml2AuthenticationException() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "invalid");
assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> converter.convert(request))
.withCauseInstanceOf(IllegalArgumentException.class)
.satisfies(
(ex) -> assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE))
.satisfies(
(ex) -> assertThat(ex.getSaml2Error().getDescription()).isEqualTo("Failed to decode SAMLResponse"));
}
@Test
public void convertWhenNoSamlResponseThenNull() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
assertThat(converter.convert(request)).isNull();
}
@Test
public void convertWhenNoMatchingRequestThenNull() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "ignored");
assertThat(converter.convert(request)).isNull();
}
@Test
public void convertWhenNoRelyingPartyRegistrationThenNull() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
String response = Saml2Utils.samlEncode(serialize(signed(response())).getBytes(StandardCharsets.UTF_8));
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, response);
assertThat(converter.convert(request)).isNull();
}
@Test
public void convertWhenGetRequestThenInflates() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = get("/login/saml2/sso/" + this.registration.getRegistrationId());
byte[] deflated = Saml2Utils.samlDeflate("response");
String encoded = Saml2Utils.samlEncode(deflated);
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded);
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo("response");
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
.isEqualTo(this.registration.getRegistrationId());
}
@Test
public void convertWhenGetRequestInvalidDeflatedThenSaml2AuthenticationException() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = get("/login/saml2/sso/" + this.registration.getRegistrationId());
byte[] invalidDeflated = "invalid".getBytes();
String encoded = Saml2Utils.samlEncode(invalidDeflated);
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded);
assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> converter.convert(request))
.withRootCauseInstanceOf(IOException.class)
.satisfies(
(ex) -> assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE))
.satisfies((ex) -> assertThat(ex.getSaml2Error().getDescription()).isEqualTo("Unable to inflate string"));
}
@Test
public void convertWhenUsingSamlUtilsBase64ThenXmlIsValid() throws Exception {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, getSsoCircleEncodedXml());
Saml2AuthenticationToken token = converter.convert(request);
validateSsoCircleXml(token.getSaml2Response());
}
@Test
public void convertWhenSavedAuthenticationRequestThenToken() {
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository = mock(
Saml2AuthenticationRequestRepository.class);
AbstractSaml2AuthenticationRequest authenticationRequest = mock(AbstractSaml2AuthenticationRequest.class);
given(authenticationRequest.getRelyingPartyRegistrationId()).willReturn(this.registration.getRegistrationId());
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
converter.setAuthenticationRequestRepository(authenticationRequestRepository);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
given(authenticationRequestRepository.loadAuthenticationRequest(any(HttpServletRequest.class)))
.willReturn(authenticationRequest);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
request.setParameter(Saml2ParameterNames.SAML_RESPONSE,
Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8)));
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo("response");
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
.isEqualTo(this.registration.getRegistrationId());
assertThat(token.getAuthenticationRequest()).isEqualTo(authenticationRequest);
}
@Test
public void convertWhenMatchingNoRegistrationIdThenLooksUpByAssertingEntityId() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
String response = serialize(signed(response()));
String encoded = Saml2Utils.samlEncode(response.getBytes(StandardCharsets.UTF_8));
given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID))
.willReturn(this.registration);
MockHttpServletRequest request = post("/login/saml2/sso");
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded);
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo(response);
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
.isEqualTo(this.registration.getRegistrationId());
}
@Test
public void constructorWhenResolverIsNullThenIllegalArgument() {
assertThatIllegalArgumentException().isThrownBy(() -> new Saml2AuthenticationTokenConverter(null));
}
@Test
public void setAuthenticationRequestRepositoryWhenNullThenIllegalArgument() {
OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter(this.registrations);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> converter.setAuthenticationRequestRepository(null));
}
private void validateSsoCircleXml(String xml) {
assertThat(xml).contains("InResponseTo=\"ARQ9a73ead-7dcf-45a8-89eb-26f3c9900c36\"")
.contains(" ID=\"s246d157446618e90e43fb79bdd4d9e9e19cf2c7c4\"")
.contains("<saml:Issuer>https://idp.ssocircle.com</saml:Issuer>");
}
private String getSsoCircleEncodedXml() throws IOException {
ClassPathResource resource = new ClassPathResource("saml2-response-sso-circle.encoded");
String response = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
return UriUtils.decode(response, StandardCharsets.UTF_8);
}
private MockHttpServletRequest post(String uri) {
return TestMockHttpServletRequests.post(uri).build();
}
private MockHttpServletRequest get(String uri) {
return TestMockHttpServletRequests.get(uri).build();
}
private <T extends SignableSAMLObject> T signed(T toSign) {
TestOpenSamlObjects.signed(toSign, TestSaml2X509Credentials.assertingPartySigningCredential(),
TestOpenSamlObjects.RELYING_PARTY_ENTITY_ID);
return toSign;
}
private Response response() {
Response response = TestOpenSamlObjects.response();
response.setIssueInstant(Instant.now());
return response;
}
private String serialize(XMLObject object) {
return this.saml.serialize(object).serialize();
}
}

View File

@ -1,109 +0,0 @@
/*
* Copyright 2004-present 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 jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.servlet.TestMockHttpServletRequests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.pathPattern;
public class OpenSaml4AuthenticationRequestResolverTests {
MockHttpServletRequest request;
RelyingPartyRegistration registration;
@BeforeEach
void setup() {
this.request = givenRequest("/saml2/authenticate/registration-id");
this.registration = TestRelyingPartyRegistrations.full().build();
}
@Test
void resolveWhenRedirectThenSaml2RedirectAuthenticationRequest() {
RelyingPartyRegistrationResolver relyingParties = mock(RelyingPartyRegistrationResolver.class);
given(relyingParties.resolve(any(), any())).willReturn(this.registration);
OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingParties);
Saml2RedirectAuthenticationRequest authnRequest = resolver.resolve(this.request);
assertThat(authnRequest.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
assertThat(authnRequest.getAuthenticationRequestUri())
.isEqualTo(this.registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation());
}
@Test
void resolveWhenPostThenSaml2PostAuthenticationRequest() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
.assertingPartyMetadata((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST))
.build();
RelyingPartyRegistrationResolver relyingParties = mock(RelyingPartyRegistrationResolver.class);
given(relyingParties.resolve(any(), any())).willReturn(registration);
OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingParties);
Saml2PostAuthenticationRequest authnRequest = resolver.resolve(this.request);
assertThat(authnRequest.getBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(authnRequest.getAuthenticationRequestUri())
.isEqualTo(this.registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation());
}
@Test
void resolveWhenCustomRelayStateThenUses() {
RelyingPartyRegistrationResolver relyingParties = mock(RelyingPartyRegistrationResolver.class);
given(relyingParties.resolve(any(), any())).willReturn(this.registration);
Converter<HttpServletRequest, String> relayState = mock(Converter.class);
given(relayState.convert(any())).willReturn("state");
OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingParties);
resolver.setRelayStateResolver(relayState);
Saml2RedirectAuthenticationRequest authnRequest = resolver.resolve(this.request);
assertThat(authnRequest.getRelayState()).isEqualTo("state");
verify(relayState).convert(any());
}
@Test
void resolveWhenCustomAuthenticationUrlTHenUses() {
RelyingPartyRegistrationResolver relyingParties = mock(RelyingPartyRegistrationResolver.class);
given(relyingParties.resolve(any(), any())).willReturn(this.registration);
OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingParties);
resolver.setRequestMatcher(pathPattern("/custom/authentication/{registrationId}"));
Saml2RedirectAuthenticationRequest authnRequest = resolver
.resolve(givenRequest("/custom/authentication/registration-id"));
assertThat(authnRequest.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
assertThat(authnRequest.getAuthenticationRequestUri())
.isEqualTo(this.registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation());
}
private MockHttpServletRequest givenRequest(String path) {
return TestMockHttpServletRequests.get(path).build();
}
}

View File

@ -1,93 +0,0 @@
/*
* Copyright 2004-present 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.util.UUID;
import javax.xml.namespace.QName;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.common.SAMLVersion;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.xmlsec.signature.Signature;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test open SAML signatures
*/
public class OpenSaml4SigningUtilsTests {
static {
OpenSamlInitializationService.initialize();
}
private final OpenSamlOperations saml = new OpenSaml4Template();
private RelyingPartyRegistration registration;
@BeforeEach
public void setup() {
this.registration = RelyingPartyRegistration.withRegistrationId("saml-idp")
.entityId("https://some.idp.example.com/entity-id")
.signingX509Credentials((c) -> {
c.add(TestSaml2X509Credentials.relyingPartySigningCredential());
c.add(TestSaml2X509Credentials.assertingPartySigningCredential());
})
.assertingPartyMetadata((c) -> c.entityId("https://some.idp.example.com/entity-id")
.singleSignOnServiceLocation("https://some.idp.example.com/service-location"))
.build();
}
@Test
public void whenSigningAnObjectThenKeyInfoIsPartOfTheSignature() {
Response response = response("destination", "issuer");
this.saml.withSigningKeys(this.registration.getSigningX509Credentials()).sign(response);
Signature signature = response.getSignature();
assertThat(signature).isNotNull();
assertThat(signature.getKeyInfo()).isNotNull();
}
Response response(String destination, String issuerEntityId) {
Response response = build(Response.DEFAULT_ELEMENT_NAME);
response.setID("R" + UUID.randomUUID());
response.setVersion(SAMLVersion.VERSION_20);
response.setID("_" + UUID.randomUUID());
response.setDestination(destination);
response.setIssuer(issuer(issuerEntityId));
return response;
}
Issuer issuer(String entityId) {
Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME);
issuer.setValue(entityId);
return issuer;
}
<T extends XMLObject> T build(QName qName) {
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
}
}

View File

@ -1,94 +0,0 @@
/*
* Copyright 2004-present 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.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
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;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link OpenSaml4LogoutRequestResolver}
*/
public class OpenSaml4LogoutRequestResolverTests {
RelyingPartyRegistration registration;
RelyingPartyRegistrationResolver registrationResolver;
OpenSaml4LogoutRequestResolver logoutRequestResolver;
@BeforeEach
public void setup() {
this.registration = TestRelyingPartyRegistrations.full().build();
this.registrationResolver = mock(RelyingPartyRegistrationResolver.class);
this.logoutRequestResolver = new OpenSaml4LogoutRequestResolver(this.registrationResolver);
}
@Test
public void resolveWhenCustomParametersConsumerThenUses() {
this.logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid"));
given(this.registrationResolver.resolve(any(), any())).willReturn(this.registration);
Saml2LogoutRequest logoutRequest = this.logoutRequestResolver.resolve(givenRequest(), givenAuthentication());
assertThat(logoutRequest.getId()).isEqualTo("myid");
}
@Test
public void setParametersConsumerWhenNullThenIllegalArgument() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.logoutRequestResolver.setParametersConsumer(null));
}
@Test
public void resolveWhenCustomRelayStateThenUses() {
given(this.registrationResolver.resolve(any(), any())).willReturn(this.registration);
Converter<HttpServletRequest, String> relayState = mock(Converter.class);
given(relayState.convert(any())).willReturn("any-state");
this.logoutRequestResolver.setRelayStateResolver(relayState);
Saml2LogoutRequest logoutRequest = this.logoutRequestResolver.resolve(givenRequest(), givenAuthentication());
assertThat(logoutRequest.getRelayState()).isEqualTo("any-state");
verify(relayState).convert(any());
}
private static Authentication givenAuthentication() {
return new TestingAuthenticationToken("user", "password");
}
private MockHttpServletRequest givenRequest() {
return new MockHttpServletRequest();
}
}

View File

@ -1,150 +0,0 @@
/*
* Copyright 2004-present 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.nio.charset.StandardCharsets;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensaml.core.xml.XMLObject;
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.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.TestSaml2Authentications;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters;
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.web.servlet.TestMockHttpServletRequests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
public final class OpenSaml4LogoutRequestValidatorParametersResolverTests {
@Mock
RelyingPartyRegistrationRepository registrations;
private final OpenSamlOperations saml = new OpenSaml4Template();
private RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
private OpenSaml4LogoutRequestValidatorParametersResolver resolver;
@BeforeEach
void setup() {
this.resolver = new OpenSaml4LogoutRequestValidatorParametersResolver(this.registrations);
}
@Test
void saml2LogoutRegistrationIdResolveWhenMatchesThenParameters() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = post("/logout/saml2/slo/" + registrationId);
Authentication authentication = new TestingAuthenticationToken("user", "pass");
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, authentication);
assertThat(parameters.getAuthentication()).isEqualTo(authentication);
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request");
}
@Test
void saml2LogoutRegistrationIdWhenUnauthenticatedThenParameters() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = post("/logout/saml2/slo/" + registrationId);
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null);
assertThat(parameters.getAuthentication()).isNull();
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request");
}
@Test
void saml2LogoutResolveWhenAuthenticatedThenParameters() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = post("/logout/saml2/slo");
Authentication authentication = TestSaml2Authentications.authentication();
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, authentication);
assertThat(parameters.getAuthentication()).isEqualTo(authentication);
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request");
}
@Test
void saml2LogoutResolveWhenUnauthenticatedThenParameters() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = post("/logout/saml2/slo");
String logoutRequest = serialize(TestOpenSamlObjects.logoutRequest());
String encoded = Saml2Utils.samlEncode(logoutRequest.getBytes(StandardCharsets.UTF_8));
request.setParameter(Saml2ParameterNames.SAML_REQUEST, encoded);
given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID))
.willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null);
assertThat(parameters.getAuthentication()).isNull();
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo(encoded);
}
@Test
void saml2LogoutResolveWhenUnauthenticatedGetRequestThenInflates() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = get("/logout/saml2/slo");
String logoutRequest = serialize(TestOpenSamlObjects.logoutRequest());
String encoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(logoutRequest));
request.setParameter(Saml2ParameterNames.SAML_REQUEST, encoded);
given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID))
.willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null);
assertThat(parameters.getAuthentication()).isNull();
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo(encoded);
}
@Test
void saml2LogoutRegistrationIdResolveWhenNoMatchingRegistrationIdThenSaml2Exception() {
MockHttpServletRequest request = post("/logout/saml2/slo/id");
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.resolver.resolve(request, null));
}
private MockHttpServletRequest post(String uri) {
return TestMockHttpServletRequests.post(uri).build();
}
private MockHttpServletRequest get(String uri) {
return TestMockHttpServletRequests.get(uri).build();
}
private String serialize(XMLObject object) {
return this.saml.serialize(object).serialize();
}
}

View File

@ -1,120 +0,0 @@
/*
* Copyright 2004-present 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 java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.StatusCode;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
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.Saml2MessageBinding;
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.OpenSaml4LogoutResponseResolver.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 OpenSaml4LogoutResponseResolver}
*/
public class OpenSaml4LogoutResponseResolverTests {
private final OpenSamlOperations saml = new OpenSaml4Template();
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
@Test
public void resolveWhenCustomParametersConsumerThenUses() {
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(
this.relyingPartyRegistrationResolver);
Consumer<LogoutResponseParameters> parametersConsumer = mock(Consumer.class);
logoutResponseResolver.setParametersConsumer(parametersConsumer);
MockHttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
.assertingPartyMetadata(
(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(this.saml.serialize(logoutRequest).serialize().getBytes()));
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication);
assertThat(logoutResponse).isNotNull();
verify(parametersConsumer).accept(any());
}
@ParameterizedTest
@MethodSource("provideAuthExceptionAndExpectedSamlStatusCode")
public void resolveWithAuthException(Saml2AuthenticationException exception, String expectedStatusCode) {
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(
this.relyingPartyRegistrationResolver);
MockHttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
.assertingPartyMetadata(
(party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout")
.singleLogoutServiceBinding(Saml2MessageBinding.POST))
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setParameter(Saml2ParameterNames.SAML_REQUEST,
Saml2Utils.samlEncode(this.saml.serialize(logoutRequest).serialize().getBytes()));
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication, exception);
assertThat(logoutResponse).isNotNull();
assertThat(new String(Saml2Utils.samlDecode(logoutResponse.getSamlResponse()))).contains(expectedStatusCode);
}
@Test
public void setParametersConsumerWhenNullThenIllegalArgument() {
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
this.relyingPartyRegistrationResolver);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
}
private static Stream<Arguments> provideAuthExceptionAndExpectedSamlStatusCode() {
return Stream.of(
Arguments.of(new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, "")),
StatusCode.REQUEST_DENIED),
Arguments.of(new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, "")),
StatusCode.REQUESTER)
);
}
}

View File

@ -37,8 +37,8 @@ public class OpenSaml5MetadataResolverTests {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build(); .build();
OpenSaml5MetadataResolver OpenSaml4MetadataResolver = new OpenSaml5MetadataResolver(); OpenSaml5MetadataResolver metadataResolver = new OpenSaml5MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); String metadata = metadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor") assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"") .contains("entityID=\"rp-entity-id\"")
.contains("<md:KeyDescriptor use=\"signing\">") .contains("<md:KeyDescriptor use=\"signing\">")
@ -54,9 +54,9 @@ public class OpenSaml5MetadataResolverTests {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build(); .build();
OpenSaml5MetadataResolver OpenSaml4MetadataResolver = new OpenSaml5MetadataResolver(); OpenSaml5MetadataResolver metadataResolver = new OpenSaml5MetadataResolver();
OpenSaml4MetadataResolver.setSignMetadata(true); metadataResolver.setSignMetadata(true);
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); String metadata = metadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor") assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"") .contains("entityID=\"rp-entity-id\"")
.contains("<md:KeyDescriptor use=\"signing\">") .contains("<md:KeyDescriptor use=\"signing\">")
@ -82,8 +82,8 @@ public class OpenSaml5MetadataResolverTests {
.assertingPartyMetadata((party) -> party .assertingPartyMetadata((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
.build(); .build();
OpenSaml5MetadataResolver OpenSaml4MetadataResolver = new OpenSaml5MetadataResolver(); OpenSaml5MetadataResolver metadataResolver = new OpenSaml5MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); String metadata = metadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor") assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"") .contains("entityID=\"rp-entity-id\"")
.doesNotContain("<md:KeyDescriptor use=\"signing\">") .doesNotContain("<md:KeyDescriptor use=\"signing\">")
@ -98,8 +98,8 @@ public class OpenSaml5MetadataResolverTests {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.nameIdFormat("format") .nameIdFormat("format")
.build(); .build();
OpenSaml5MetadataResolver OpenSaml4MetadataResolver = new OpenSaml5MetadataResolver(); OpenSaml5MetadataResolver metadataResolver = new OpenSaml5MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); String metadata = metadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:NameIDFormat>format</md:NameIDFormat>"); assertThat(metadata).contains("<md:NameIDFormat>format</md:NameIDFormat>");
} }
@ -109,8 +109,8 @@ public class OpenSaml5MetadataResolverTests {
.singleLogoutServiceLocation(null) .singleLogoutServiceLocation(null)
.nameIdFormat("format") .nameIdFormat("format")
.build(); .build();
OpenSaml5MetadataResolver OpenSaml4MetadataResolver = new OpenSaml5MetadataResolver(); OpenSaml5MetadataResolver metadataResolver = new OpenSaml5MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); String metadata = metadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).doesNotContain("ResponseLocation"); assertThat(metadata).doesNotContain("ResponseLocation");
} }
@ -119,10 +119,10 @@ public class OpenSaml5MetadataResolverTests {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.entityId("originalEntityId") .entityId("originalEntityId")
.build(); .build();
OpenSaml5MetadataResolver OpenSaml4MetadataResolver = new OpenSaml5MetadataResolver(); OpenSaml5MetadataResolver metadataResolver = new OpenSaml5MetadataResolver();
OpenSaml4MetadataResolver.setEntityDescriptorCustomizer( metadataResolver.setEntityDescriptorCustomizer(
(parameters) -> parameters.getEntityDescriptor().setEntityID("overriddenEntityId")); (parameters) -> parameters.getEntityDescriptor().setEntityID("overriddenEntityId"));
String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); String metadata = metadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor").contains("entityID=\"overriddenEntityId\""); assertThat(metadata).contains("<md:EntityDescriptor").contains("entityID=\"overriddenEntityId\"");
} }
@ -135,8 +135,8 @@ public class OpenSaml5MetadataResolverTests {
.entityId("two") .entityId("two")
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build(); .build();
OpenSaml5MetadataResolver OpenSaml4MetadataResolver = new OpenSaml5MetadataResolver(); OpenSaml5MetadataResolver metadataResolver = new OpenSaml5MetadataResolver();
String metadata = OpenSaml4MetadataResolver.resolve(List.of(one, two)); String metadata = metadataResolver.resolve(List.of(one, two));
assertThat(metadata).contains("<md:EntitiesDescriptor") assertThat(metadata).contains("<md:EntitiesDescriptor")
.contains("<md:EntityDescriptor") .contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"") .contains("entityID=\"rp-entity-id\"")
@ -158,9 +158,9 @@ public class OpenSaml5MetadataResolverTests {
.entityId("two") .entityId("two")
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build(); .build();
OpenSaml5MetadataResolver OpenSaml5MetadataResolver = new OpenSaml5MetadataResolver(); OpenSaml5MetadataResolver metadataResolver = new OpenSaml5MetadataResolver();
OpenSaml5MetadataResolver.setSignMetadata(true); metadataResolver.setSignMetadata(true);
String metadata = OpenSaml5MetadataResolver.resolve(List.of(one, two)); String metadata = metadataResolver.resolve(List.of(one, two));
assertThat(metadata).contains("<md:EntitiesDescriptor") assertThat(metadata).contains("<md:EntitiesDescriptor")
.contains("<md:EntityDescriptor") .contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"") .contains("entityID=\"rp-entity-id\"")