Add OpenSAML 4 Support

Closes gh-9095
This commit is contained in:
Josh Cummings 2020-12-07 12:28:19 -07:00
parent a015b8b000
commit d0d0a8d958
85 changed files with 3138 additions and 615 deletions

View File

@ -4,6 +4,10 @@ apply plugin: 'io.spring.convention.spring-module'
apply plugin: 'trang'
apply plugin: 'kotlin'
repositories {
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
// NB: Don't add other compile time dependencies to the config module as this breaks tooling
compile project(':spring-security-core')
@ -14,7 +18,8 @@ dependencies {
optional project(':spring-security-ldap')
optional project(':spring-security-messaging')
optional project(':spring-security-saml2-service-provider')
optional project(':saml2-service-provider-opensaml3')
optional project(':saml2-service-provider-opensaml4')
optional project(':spring-security-oauth2-client')
optional project(':spring-security-oauth2-jose')
optional project(':spring-security-oauth2-resource-server')
@ -42,7 +47,8 @@ dependencies {
testCompile project(path : ':spring-security-ldap', configuration : 'tests')
testCompile project(path : ':spring-security-oauth2-client', configuration : 'tests')
testCompile project(path : ':spring-security-oauth2-resource-server', configuration : 'tests')
testCompile project(path : ':spring-security-saml2-service-provider', configuration : 'tests')
testCompile project(path : ':saml2-service-provider-core', configuration : 'tests')
testCompile project(path : ':saml2-service-provider-opensaml4', configuration : 'tests')
testCompile project(path : ':spring-security-web', configuration : 'tests')
testCompile apachedsDependencies
testCompile powerMock2Dependencies

View File

@ -21,15 +21,20 @@ import java.util.Map;
import javax.servlet.Filter;
import org.opensaml.core.Version;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
@ -190,7 +195,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
* <li>The {@code loginProcessingUrl} is set</li>
* <li>A custom login page is configured, <b>or</b></li>
* <li>A default login page with all SAML 2.0 Identity Providers is configured</li>
* <li>An {@link OpenSamlAuthenticationProvider} is configured</li>
* <li>An {@link AuthenticationProvider} is configured</li>
* </ul>
*/
@Override
@ -256,8 +261,12 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
}
private void registerDefaultAuthenticationProvider(B http) {
OpenSamlAuthenticationProvider provider = postProcess(new OpenSamlAuthenticationProvider());
http.authenticationProvider(provider);
if (Version.getVersion().startsWith("4")) {
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
}
else {
http.authenticationProvider(postProcess(new OpenSamlAuthenticationProvider()));
}
}
private void registerDefaultCsrfOverride(B http) {
@ -337,7 +346,10 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
private Saml2AuthenticationRequestFactory getResolver(B http) {
Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class);
if (resolver == null) {
resolver = new OpenSamlAuthenticationRequestFactory();
if (Version.getVersion().startsWith("4")) {
return new OpenSaml4AuthenticationRequestFactory();
}
return new OpenSamlAuthenticationRequestFactory();
}
return resolver;
}

View File

@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web.configurers.saml2;
import java.io.IOException;
import java.net.URLDecoder;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
@ -61,8 +62,9 @@ import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMap
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2Utils;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext;
@ -235,11 +237,8 @@ public class Saml2LoginConfigurerTests {
"authenticationManager");
ProviderManager pm = (ProviderManager) manager;
AuthenticationProvider provider = pm.getProviders().stream()
.filter((p) -> p instanceof OpenSamlAuthenticationProvider).findFirst().get();
Assert.assertSame(AUTHORITIES_EXTRACTOR, ReflectionTestUtils.getField(provider, "authoritiesExtractor"));
Assert.assertSame(AUTHORITIES_MAPPER, ReflectionTestUtils.getField(provider, "authoritiesMapper"));
Assert.assertSame(RESPONSE_TIME_VALIDATION_SKEW,
ReflectionTestUtils.getField(provider, "responseTimeValidationSkew"));
.filter((p) -> p instanceof OpenSaml4AuthenticationProvider).findFirst().get();
assertThat(provider).isNotNull();
}
private Saml2WebSsoAuthenticationFilter getSaml2SsoFilter(FilterChainProxy chain) {
@ -370,9 +369,10 @@ public class Saml2LoginConfigurerTests {
@Bean
Saml2AuthenticationRequestFactory authenticationRequestFactory() {
OpenSamlAuthenticationRequestFactory authenticationRequestFactory = new OpenSamlAuthenticationRequestFactory();
OpenSaml4AuthenticationRequestFactory authenticationRequestFactory = new OpenSaml4AuthenticationRequestFactory();
authenticationRequestFactory.setAuthenticationRequestContextConverter((context) -> {
AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest();
authnRequest.setIssueInstant(Instant.now());
authnRequest.setForceAuthn(true);
return authnRequest;
});

View File

@ -0,0 +1,53 @@
buildscript {
repositories {
maven { url 'https://repo.spring.io/plugins-release' }
}
dependencies {
classpath 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE'
}
}
plugins {
id 'java-library'
id 'io.spring.convention.repository'
id 'io.spring.convention.springdependencymangement'
id 'io.spring.convention.dependency-set'
id 'io.spring.convention.checkstyle'
id 'io.spring.convention.tests-configuration'
id 'io.spring.convention.integration-test'
id 'propdeps'
}
configurations {
classesOnlyElements {
canBeConsumed = true
canBeResolved = false
}
}
artifacts {
classesOnlyElements(compileJava.destinationDir)
}
repositories {
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
constraints {
management("org.opensaml:opensaml-core:3.+")
management("org.opensaml:opensaml-saml-api:3.+")
management("org.opensaml:opensaml-saml-impl:3.+")
}
compile project(':spring-security-core')
compile project(':spring-security-web')
provided("org.opensaml:opensaml-core")
provided("org.opensaml:opensaml-saml-api")
provided("org.opensaml:opensaml-saml-impl")
provided 'javax.servlet:javax.servlet-api'
testCompile 'com.squareup.okhttp3:mockwebserver'
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2002-2020 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.core;
import java.time.Clock;
import java.time.Instant;
import java.util.UUID;
import org.joda.time.DateTime;
import org.opensaml.core.xml.XMLObjectBuilder;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
/**
* A {@link AuthnRequestBuilder} that gives each {@link AuthnRequest} some reasonable
* defaults.
*
* @author Josh Cummings
* @since 5.5
*/
public final class SpringSecurityAuthnRequestBuilder extends AuthnRequestBuilder {
private final XMLObjectBuilder<AuthnRequest> builder;
private Clock clock = Clock.systemUTC();
SpringSecurityAuthnRequestBuilder(XMLObjectBuilder<AuthnRequest> builder) {
this.builder = builder;
}
/** {@inheritDoc} */
@Override
public AuthnRequest buildObject(final String namespaceURI, final String localName, final String namespacePrefix) {
AuthnRequest authnRequest = this.builder.buildObject(namespaceURI, localName, namespacePrefix);
setDefaults(authnRequest);
return authnRequest;
}
/**
* Use this {@link Clock} with {@link Instant#now()} for generating timestamps
* @param clock
*/
public void setClock(Clock clock) {
this.clock = clock;
}
private void setDefaults(AuthnRequest authnRequest) {
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
}
if (authnRequest.getIssueInstant() == null) {
authnRequest.setIssueInstant(new DateTime(this.clock.millis()));
}
if (authnRequest.isForceAuthn() == null) {
authnRequest.setForceAuthn(Boolean.FALSE);
}
if (authnRequest.isPassive() == null) {
authnRequest.setIsPassive(Boolean.FALSE);
}
if (authnRequest.getProtocolBinding() == null) {
authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
}
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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.EncryptedAssertion;
import org.opensaml.saml.saml2.core.EncryptedAttribute;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
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.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
/**
* Utility methods for decrypting SAML components with OpenSAML
*
* For internal use only.
*
* @author Josh Cummings
*/
final class OpenSamlDecryptionUtils {
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
static void decryptResponseElements(Response response, RelyingPartyRegistration registration) {
Decrypter decrypter = decrypter(registration);
for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) {
try {
Assertion assertion = decrypter.decrypt(encryptedAssertion);
response.getAssertions().add(assertion);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
}
static void decryptAssertionElements(Assertion assertion, RelyingPartyRegistration registration) {
Decrypter decrypter = decrypter(registration);
for (AttributeStatement statement : assertion.getAttributeStatements()) {
for (EncryptedAttribute encryptedAttribute : statement.getEncryptedAttributes()) {
try {
Attribute attribute = decrypter.decrypt(encryptedAttribute);
statement.getAttributes().add(attribute);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
}
if (assertion.getSubject() == null) {
return;
}
if (assertion.getSubject().getEncryptedID() == null) {
return;
}
try {
assertion.getSubject().setNameID((NameID) decrypter.decrypt(assertion.getSubject().getEncryptedID()));
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private static Decrypter decrypter(RelyingPartyRegistration registration) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : registration.getDecryptionX509Credentials()) {
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;
}
private OpenSamlDecryptionUtils() {
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
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.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.w3c.dom.Element;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* Utility methods for signing SAML components with OpenSAML
*
* For internal use only.
*
* @author Josh Cummings
*/
final class OpenSamlSigningUtils {
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);
}
}
static <O extends SignableXMLObject> O sign(O object, RelyingPartyRegistration relyingPartyRegistration) {
SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
try {
SignatureSupport.signObject(object, parameters);
return object;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
static QueryParametersPartial sign(RelyingPartyRegistration registration) {
return new QueryParametersPartial(registration);
}
private static SignatureSigningParameters resolveSigningParameters(
RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
CriteriaSet criteria = new CriteriaSet();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(algorithms);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
criteria.add(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 static List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setEntityId(relyingPartyRegistration.getEntityId());
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
static class QueryParametersPartial {
final RelyingPartyRegistration registration;
final Map<String, String> components = new LinkedHashMap<>();
QueryParametersPartial(RelyingPartyRegistration registration) {
this.registration = registration;
}
QueryParametersPartial param(String key, String value) {
this.components.put(key, value);
return this;
}
Map<String, String> parameters() {
SignatureSigningParameters parameters = resolveSigningParameters(this.registration);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put("SigAlg", 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("Signature", b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
}
private OpenSamlSigningUtils() {
}
}

View File

@ -0,0 +1,217 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
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.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.web.util.UriUtils;
/**
* Utility methods for verifying SAML component signatures with OpenSAML
*
* For internal use only.
*
* @author Josh Cummings
*/
final class OpenSamlVerificationUtils {
static VerifierPartial verifySignature(StatusResponseType object, RelyingPartyRegistration registration) {
return new VerifierPartial(object, registration);
}
static VerifierPartial verifySignature(RequestAbstractType object, RelyingPartyRegistration registration) {
return new VerifierPartial(object, registration);
}
static SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) {
Set<Credential> credentials = new HashSet<>();
Collection<Saml2X509Credential> keys = registration.getAssertingPartyDetails().getVerificationX509Credentials();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(registration.getAssertingPartyDetails().getEntityId());
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
static class VerifierPartial {
private final String id;
private final CriteriaSet criteria;
private final SignatureTrustEngine trustEngine;
VerifierPartial(StatusResponseType object, RelyingPartyRegistration registration) {
this.id = object.getID();
this.criteria = verificationCriteria(object.getIssuer());
this.trustEngine = trustEngine(registration);
}
VerifierPartial(RequestAbstractType object, RelyingPartyRegistration registration) {
this.id = object.getID();
this.criteria = verificationCriteria(object.getIssuer());
this.trustEngine = trustEngine(registration);
}
Saml2ResponseValidatorResult redirect(HttpServletRequest request, String objectParameterName) {
RedirectSignature signature = new RedirectSignature(request, objectParameterName);
if (signature.getAlgorithm() == null) {
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + this.id + "]"));
}
if (!signature.hasSignature()) {
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + this.id + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = signature.getAlgorithm();
try {
if (!this.trustEngine.validate(signature.getSignature(), signature.getContent(), algorithmUri,
this.criteria, null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]: "));
}
return Saml2ResponseValidatorResult.failure(errors);
}
Saml2ResponseValidatorResult post(Signature signature) {
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 [" + this.id + "]: "));
}
try {
if (!this.trustEngine.validate(signature, this.criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]: "));
}
return Saml2ResponseValidatorResult.failure(errors);
}
private CriteriaSet verificationCriteria(Issuer issuer) {
CriteriaSet criteria = new CriteriaSet();
criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())));
criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)));
criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
return criteria;
}
private static class RedirectSignature {
private final HttpServletRequest request;
private final String objectParameterName;
RedirectSignature(HttpServletRequest request, String objectParameterName) {
this.request = request;
this.objectParameterName = objectParameterName;
}
String getAlgorithm() {
return this.request.getParameter("SigAlg");
}
byte[] getContent() {
if (this.request.getParameter("RelayState") != null) {
return String.format("%s=%s&RelayState=%s&SigAlg=%s", this.objectParameterName,
UriUtils.encode(this.request.getParameter(this.objectParameterName),
StandardCharsets.ISO_8859_1),
UriUtils.encode(this.request.getParameter("RelayState"), StandardCharsets.ISO_8859_1),
UriUtils.encode(getAlgorithm(), StandardCharsets.ISO_8859_1))
.getBytes(StandardCharsets.UTF_8);
}
else {
return String
.format("%s=%s&SigAlg=%s", this.objectParameterName,
UriUtils.encode(this.request.getParameter(this.objectParameterName),
StandardCharsets.ISO_8859_1),
UriUtils.encode(getAlgorithm(), StandardCharsets.ISO_8859_1))
.getBytes(StandardCharsets.UTF_8);
}
}
byte[] getSignature() {
return Saml2Utils.samlDecode(this.request.getParameter("Signature"));
}
boolean hasSignature() {
return this.request.getParameter("Signature") != null;
}
}
}
private OpenSamlVerificationUtils() {
}
}

View File

@ -24,6 +24,8 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.opensaml.core.Version;
import org.springframework.http.MediaType;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
@ -39,6 +41,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.HtmlUtils;
@ -88,8 +91,21 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter
public Saml2WebSsoAuthenticationRequestFilter(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
this(new DefaultSaml2AuthenticationRequestContextResolver(
new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository)),
new org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory());
new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository)), requestFactory());
}
private static Saml2AuthenticationRequestFactory requestFactory() {
String opensamlClassName = "org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory";
if (Version.getVersion().startsWith("4")) {
opensamlClassName = "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory";
}
try {
return (Saml2AuthenticationRequestFactory) ClassUtils.forName(opensamlClassName, null)
.getDeclaredConstructor().newInstance();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
/**

View File

@ -28,21 +28,17 @@ import javax.crypto.spec.SecretKeySpec;
import javax.xml.namespace.QName;
import org.apache.xml.security.encryption.XMLCipherParameters;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.schema.XSAny;
import org.opensaml.core.xml.schema.XSBoolean;
import org.opensaml.core.xml.schema.XSBooleanValue;
import org.opensaml.core.xml.schema.XSDateTime;
import org.opensaml.core.xml.schema.XSInteger;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.core.xml.schema.XSURI;
import org.opensaml.core.xml.schema.impl.XSAnyBuilder;
import org.opensaml.core.xml.schema.impl.XSBooleanBuilder;
import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder;
import org.opensaml.core.xml.schema.impl.XSIntegerBuilder;
import org.opensaml.core.xml.schema.impl.XSStringBuilder;
import org.opensaml.core.xml.schema.impl.XSURIBuilder;
@ -114,7 +110,6 @@ public final class TestOpenSamlObjects {
static Response response(String destination, String issuerEntityId) {
Response response = build(Response.DEFAULT_ELEMENT_NAME);
response.setID("R" + UUID.randomUUID().toString());
response.setIssueInstant(DateTime.now());
response.setVersion(SAMLVersion.VERSION_20);
response.setID("_" + UUID.randomUUID().toString());
response.setDestination(destination);
@ -141,9 +136,7 @@ public final class TestOpenSamlObjects {
static Assertion assertion(String username, String issuerEntityId, String recipientEntityId, String recipientUri) {
Assertion assertion = build(Assertion.DEFAULT_ELEMENT_NAME);
assertion.setID("A" + UUID.randomUUID().toString());
assertion.setIssueInstant(DateTime.now());
assertion.setVersion(SAMLVersion.VERSION_20);
assertion.setIssueInstant(DateTime.now());
assertion.setIssuer(issuer(issuerEntityId));
assertion.setSubject(subject(username));
assertion.setConditions(conditions());
@ -183,16 +176,11 @@ public final class TestOpenSamlObjects {
static SubjectConfirmationData subjectConfirmationData(String recipient) {
SubjectConfirmationData subject = build(SubjectConfirmationData.DEFAULT_ELEMENT_NAME);
subject.setRecipient(recipient);
subject.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000)));
subject.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000)));
return subject;
}
static Conditions conditions() {
Conditions conditions = build(Conditions.DEFAULT_ELEMENT_NAME);
conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000)));
conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000)));
return conditions;
return build(Conditions.DEFAULT_ELEMENT_NAME);
}
public static AuthnRequest authnRequest() {
@ -338,13 +326,6 @@ public final class TestOpenSamlObjects {
registered.setValue(new XSBooleanValue(true, false));
registeredAttr.getAttributeValues().add(registered);
attrStmt2.getAttributes().add(registeredAttr);
Attribute registeredDateAttr = attributeBuilder.buildObject();
registeredDateAttr.setName("registeredDate");
XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME,
XSDateTime.TYPE_NAME);
registeredDate.setValue(DateTime.parse("1970-01-01T00:00:00Z"));
registeredDateAttr.getAttributeValues().add(registeredDate);
attrStmt2.getAttributes().add(registeredDateAttr);
attributeStatements.add(attrStmt2);
return attributeStatements;
}

View File

@ -28,8 +28,10 @@ import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.saml2.credentials.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
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.authentication.TestSaml2AuthenticationRequestContexts;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@ -68,7 +70,7 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
@Before
public void setup() {
this.filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
this.filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver, this.factory);
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.request.setPathInfo("/saml2/authenticate/registration-id");
@ -81,25 +83,48 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
@Test
public void doFilterWhenNoRelayStateThenRedirectDoesNotContainParameter() throws ServletException, IOException {
given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build());
Saml2AuthenticationRequestContext context = authenticationRequestContext().relayState(null).build();
Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build();
given(this.resolver.resolve(any())).willReturn(context);
given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request);
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getHeader("Location")).doesNotContain("RelayState=").startsWith(IDP_SSO_URL);
}
private static Saml2AuthenticationRequestContext.Builder authenticationRequestContext() {
return TestSaml2AuthenticationRequestContexts.authenticationRequestContext();
}
private static Saml2RedirectAuthenticationRequest.Builder redirectAuthenticationRequest(
Saml2AuthenticationRequestContext context) {
return Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context).samlRequest("request")
.authenticationRequestUri(IDP_SSO_URL);
}
private static Saml2PostAuthenticationRequest.Builder postAuthenticationRequest(
Saml2AuthenticationRequestContext context) {
return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context).samlRequest("request")
.authenticationRequestUri(IDP_SSO_URL);
}
@Test
public void doFilterWhenRelayStateThenRedirectDoesContainParameter() throws ServletException, IOException {
given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build());
this.request.setParameter("RelayState", "my-relay-state");
Saml2AuthenticationRequestContext context = authenticationRequestContext().build();
Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build();
given(this.resolver.resolve(any())).willReturn(context);
given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request);
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getHeader("Location")).contains("RelayState=my-relay-state").startsWith(IDP_SSO_URL);
assertThat(this.response.getHeader("Location")).contains("RelayState=relayState").startsWith(IDP_SSO_URL);
}
@Test
public void doFilterWhenRelayStateThatRequiresEncodingThenRedirectDoesContainsEncodedParameter() throws Exception {
given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build());
final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
this.request.setParameter("RelayState", relayStateValue);
String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
Saml2AuthenticationRequestContext context = authenticationRequestContext().relayState(relayStateValue).build();
Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build();
given(this.resolver.resolve(any())).willReturn(context);
given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request);
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded)
.startsWith(IDP_SSO_URL);
@ -107,34 +132,39 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
@Test
public void doFilterWhenSimpleSignatureSpecifiedThenSignatureParametersAreInTheRedirectURL() throws Exception {
given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build());
final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
this.request.setParameter("RelayState", relayStateValue);
Saml2AuthenticationRequestContext context = authenticationRequestContext().build();
Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).sigAlg("sigalg")
.signature("signature").build();
given(this.resolver.resolve(any())).willReturn(context);
given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request);
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded).contains("SigAlg=")
.contains("Signature=").startsWith(IDP_SSO_URL);
assertThat(this.response.getHeader("Location")).contains("SigAlg=").contains("Signature=")
.startsWith(IDP_SSO_URL);
}
@Test
public void doFilterWhenSignatureIsDisabledThenSignatureParametersAreNotInTheRedirectURL() throws Exception {
given(this.repository.findByRegistrationId("registration-id"))
.willReturn(this.rpBuilder.providerDetails((c) -> c.signAuthNRequest(false)).build());
final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
this.request.setParameter("RelayState", relayStateValue);
Saml2AuthenticationRequestContext context = authenticationRequestContext().build();
Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build();
given(this.resolver.resolve(any())).willReturn(context);
given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request);
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded)
.doesNotContain("SigAlg=").doesNotContain("Signature=").startsWith(IDP_SSO_URL);
assertThat(this.response.getHeader("Location")).doesNotContain("SigAlg=").doesNotContain("Signature=")
.startsWith(IDP_SSO_URL);
}
@Test
public void doFilterWhenPostFormDataIsPresent() throws Exception {
given(this.repository.findByRegistrationId("registration-id"))
.willReturn(this.rpBuilder.providerDetails((c) -> c.binding(Saml2MessageBinding.POST)).build());
final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param&javascript{alert('1');}";
final String relayStateEncoded = HtmlUtils.htmlEscape(relayStateValue);
this.request.setParameter("RelayState", relayStateValue);
String relayStateValue = "https://my-relay-state.example.com?with=param&other=param&javascript{alert('1');}";
String relayStateEncoded = HtmlUtils.htmlEscape(relayStateValue);
RelyingPartyRegistration registration = this.rpBuilder
.assertingPartyDetails((asserting) -> asserting.singleSignOnServiceBinding(Saml2MessageBinding.POST))
.build();
Saml2AuthenticationRequestContext context = authenticationRequestContext().relayState(relayStateValue)
.relyingPartyRegistration(registration).build();
Saml2PostAuthenticationRequest request = postAuthenticationRequest(context).build();
given(this.resolver.resolve(any())).willReturn(context);
given(this.factory.createPostAuthenticationRequest(any())).willReturn(request);
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getHeader("Location")).isNull();
assertThat(this.response.getContentAsString())
@ -145,66 +175,43 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
@Test
public void doFilterWhenSetAuthenticationRequestFactoryThenUses() throws Exception {
RelyingPartyRegistration relyingParty = this.rpBuilder
.providerDetails((c) -> c.binding(Saml2MessageBinding.POST)).build();
Saml2PostAuthenticationRequest authenticationRequest = mock(Saml2PostAuthenticationRequest.class);
given(authenticationRequest.getAuthenticationRequestUri()).willReturn("uri");
given(authenticationRequest.getRelayState()).willReturn("relay");
given(authenticationRequest.getSamlRequest()).willReturn("saml");
given(this.repository.findByRegistrationId("registration-id")).willReturn(relyingParty);
given(this.factory.createPostAuthenticationRequest(any())).willReturn(authenticationRequest);
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
filter.setAuthenticationRequestFactory(this.factory);
filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getContentAsString()).contains("<form action=\"uri\" method=\"post\">")
.contains("<input type=\"hidden\" name=\"SAMLRequest\" value=\"saml\"")
.contains("<input type=\"hidden\" name=\"RelayState\" value=\"relay\"");
verify(this.factory).createPostAuthenticationRequest(any());
}
@Test
public void doFilterWhenCustomAuthenticationRequestFactoryThenUses() throws Exception {
RelyingPartyRegistration relyingParty = this.rpBuilder
.providerDetails((c) -> c.binding(Saml2MessageBinding.POST)).build();
Saml2PostAuthenticationRequest authenticationRequest = mock(Saml2PostAuthenticationRequest.class);
given(authenticationRequest.getAuthenticationRequestUri()).willReturn("uri");
given(authenticationRequest.getRelayState()).willReturn("relay");
given(authenticationRequest.getSamlRequest()).willReturn("saml");
given(this.resolver.resolve(this.request)).willReturn(TestSaml2AuthenticationRequestContexts
.authenticationRequestContext().relyingPartyRegistration(relyingParty).build());
given(this.factory.createPostAuthenticationRequest(any())).willReturn(authenticationRequest);
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver,
this.factory);
filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getContentAsString()).contains("<form action=\"uri\" method=\"post\">")
.contains("<input type=\"hidden\" name=\"SAMLRequest\" value=\"saml\"")
.contains("<input type=\"hidden\" name=\"RelayState\" value=\"relay\"");
verify(this.factory).createPostAuthenticationRequest(any());
Saml2AuthenticationRequestContext context = authenticationRequestContext().build();
Saml2RedirectAuthenticationRequest authenticationRequest = redirectAuthenticationRequest(context).build();
Saml2AuthenticationRequestFactory factory = mock(Saml2AuthenticationRequestFactory.class);
given(this.resolver.resolve(any())).willReturn(context);
given(factory.createRedirectAuthenticationRequest(any())).willReturn(authenticationRequest);
this.filter.setAuthenticationRequestFactory(factory);
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
verify(factory).createRedirectAuthenticationRequest(any());
}
@Test
public void setRequestMatcherWhenNullThenException() {
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver,
this.factory);
assertThatIllegalArgumentException().isThrownBy(() -> filter.setRedirectMatcher(null));
}
@Test
public void setAuthenticationRequestFactoryWhenNullThenException() {
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver,
this.factory);
assertThatIllegalArgumentException().isThrownBy(() -> filter.setAuthenticationRequestFactory(null));
}
@Test
public void doFilterWhenRequestMatcherFailsThenSkipsFilter() throws Exception {
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver,
this.factory);
filter.setRedirectMatcher((request) -> false);
filter.doFilter(this.request, this.response, this.filterChain);
verifyNoInteractions(this.repository);
verifyNoInteractions(this.resolver, this.factory);
}
@Test
public void doFilterWhenRelyingPartyRegistrationNotFoundThenUnauthorized() throws Exception {
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver,
this.factory);
filter.doFilter(this.request, this.response, this.filterChain);
assertThat(this.response.getStatus()).isEqualTo(401);
}

View File

@ -0,0 +1,53 @@
buildscript {
repositories {
maven { url 'https://repo.spring.io/plugins-release' }
}
dependencies {
classpath 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE'
}
}
plugins {
id 'java-library'
id 'io.spring.convention.repository'
id 'io.spring.convention.springdependencymangement'
id 'io.spring.convention.dependency-set'
id 'io.spring.convention.checkstyle'
id 'io.spring.convention.tests-configuration'
id 'io.spring.convention.integration-test'
id 'propdeps'
}
configurations {
classesOnlyElements {
canBeConsumed = true
canBeResolved = false
}
}
artifacts {
classesOnlyElements(compileJava.destinationDir)
}
repositories {
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
constraints {
management("org.opensaml:opensaml-core:3.+")
management("org.opensaml:opensaml-saml-api:3.+")
management("org.opensaml:opensaml-saml-impl:3.+")
}
compile project(':saml2-service-provider-core')
compile("org.opensaml:opensaml-core")
compile("org.opensaml:opensaml-saml-api")
compile("org.opensaml:opensaml-saml-impl")
provided 'javax.servlet:javax.servlet-api'
testCompile 'com.squareup.okhttp3:mockwebserver'
testCompile project(path : ':saml2-service-provider-core', configuration : 'tests')
}

View File

@ -21,27 +21,21 @@ import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.schema.XSAny;
@ -51,11 +45,9 @@ import org.opensaml.core.xml.schema.XSDateTime;
import org.opensaml.core.xml.schema.XSInteger;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.core.xml.schema.XSURI;
import org.opensaml.saml.common.assertion.AssertionValidationException;
import org.opensaml.saml.common.assertion.ValidationContext;
import org.opensaml.saml.common.assertion.ValidationResult;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
import org.opensaml.saml.saml2.assertion.ConditionValidator;
import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
@ -69,35 +61,15 @@ 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.NameID;
import org.opensaml.saml.saml2.core.OneTimeUse;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
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.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
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.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver;
import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
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;
@ -115,7 +87,7 @@ import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@ -165,6 +137,8 @@ import org.springframework.util.StringUtils;
* "https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38">SAML 2
* StatusResponse</a>
* @see <a href="https://wiki.shibboleth.net/confluence/display/OS30/Home">OpenSAML 3</a>
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
* {@link OpenSaml4AuthenticationProvider}
*/
public final class OpenSamlAuthenticationProvider implements AuthenticationProvider {
@ -201,10 +175,6 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
private Converter<ResponseToken, ? extends AbstractAuthenticationToken> responseAuthenticationConverter = createCompatibleResponseAuthenticationConverter();
private Converter<Saml2AuthenticationToken, SignatureTrustEngine> signatureTrustEngineConverter = new SignatureTrustEngineConverter();
private Converter<Saml2AuthenticationToken, Decrypter> decrypterConverter = new DecrypterConverter();
/**
* Creates an {@link OpenSamlAuthenticationProvider}
*/
@ -560,53 +530,23 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseSignatureValidator() {
return (responseToken) -> {
Response response = responseToken.getResponse();
Saml2AuthenticationToken token = responseToken.getToken();
Collection<Saml2Error> errors = new ArrayList<>();
String issuer = response.getIssuer().getValue();
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
if (response.isSigned()) {
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(response.getSignature());
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for SAML Response [" + response.getID() + "]: "));
}
try {
CriteriaSet criteriaSet = new CriteriaSet();
criteriaSet.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer)));
criteriaSet.add(new EvaluableProtocolRoleDescriptorCriterion(
new ProtocolCriterion(SAMLConstants.SAML20P_NS)));
criteriaSet.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
if (!this.signatureTrustEngineConverter.convert(token).validate(response.getSignature(),
criteriaSet)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for SAML Response [" + response.getID() + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for SAML Response [" + response.getID() + "]: "));
}
return OpenSamlVerificationUtils.verifySignature(response, registration).post(response.getSignature());
}
return Saml2ResponseValidatorResult.failure(errors);
return Saml2ResponseValidatorResult.success();
};
}
private Consumer<ResponseToken> createDefaultResponseElementsDecrypter() {
return (responseToken) -> {
Decrypter decrypter = this.decrypterConverter.convert(responseToken.getToken());
Response response = responseToken.getResponse();
for (EncryptedAssertion encryptedAssertion : responseToken.getResponse().getEncryptedAssertions()) {
try {
Assertion assertion = decrypter.decrypt(encryptedAssertion);
response.getAssertions().add(assertion);
}
catch (Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
}
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
try {
OpenSamlDecryptionUtils.decryptResponseElements(response, registration);
}
catch (Saml2Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
}
};
}
@ -656,7 +596,8 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
private Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionSignatureValidator() {
return createAssertionValidator(Saml2ErrorCodes.INVALID_SIGNATURE, (assertionToken) -> {
SignatureTrustEngine engine = this.signatureTrustEngineConverter.convert(assertionToken.token);
RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration();
SignatureTrustEngine engine = OpenSamlVerificationUtils.trustEngine(registration);
return SAML20AssertionValidators.createSignatureValidator(engine);
}, (assertionToken) -> new ValidationContext(
Collections.singletonMap(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false)));
@ -664,29 +605,12 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
private Consumer<AssertionToken> createDefaultAssertionElementsDecrypter() {
return (assertionToken) -> {
Decrypter decrypter = this.decrypterConverter.convert(assertionToken.getToken());
Assertion assertion = assertionToken.getAssertion();
for (AttributeStatement statement : assertion.getAttributeStatements()) {
for (EncryptedAttribute encryptedAttribute : statement.getEncryptedAttributes()) {
try {
Attribute attribute = decrypter.decrypt(encryptedAttribute);
statement.getAttributes().add(attribute);
}
catch (Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
}
}
}
if (assertion.getSubject() == null) {
return;
}
if (assertion.getSubject().getEncryptedID() == null) {
return;
}
RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration();
try {
assertion.getSubject().setNameID((NameID) decrypter.decrypt(assertion.getSubject().getEncryptedID()));
OpenSamlDecryptionUtils.decryptAssertionElements(assertion, registration);
}
catch (Exception ex) {
catch (Saml2Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
}
};
@ -765,8 +689,7 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
return (xsBooleanValue != null) ? xsBooleanValue.getValue() : null;
}
if (xmlObject instanceof XSDateTime) {
DateTime dateTime = ((XSDateTime) xmlObject).getValue();
return (dateTime != null) ? Instant.ofEpochMilli(dateTime.getMillis()) : null;
return Instant.ofEpochMilli(((XSDateTime) xmlObject).getValue().getMillis());
}
return null;
}
@ -812,27 +735,6 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
return new ValidationContext(params);
}
private static class SignatureTrustEngineConverter
implements Converter<Saml2AuthenticationToken, SignatureTrustEngine> {
@Override
public SignatureTrustEngine convert(Saml2AuthenticationToken token) {
Set<Credential> credentials = new HashSet<>();
Collection<Saml2X509Credential> keys = token.getRelyingPartyRegistration().getAssertingPartyDetails()
.getVerificationX509Credentials();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(token.getRelyingPartyRegistration().getAssertingPartyDetails().getEntityId());
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
}
private static class SAML20AssertionValidators {
private static final Collection<ConditionValidator> conditions = new ArrayList<>();
@ -861,10 +763,9 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
}
});
subjects.add(new BearerSubjectConfirmationValidator() {
@Nonnull
@Override
protected ValidationResult validateAddress(@Nonnull SubjectConfirmation confirmation,
@Nonnull Assertion assertion, @Nonnull ValidationContext context) {
protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion,
ValidationContext context) throws AssertionValidationException {
// applications should validate their own addresses - gh-7514
return ValidationResult.VALID;
}
@ -906,27 +807,6 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
}
private static class DecrypterConverter implements Converter<Saml2AuthenticationToken, Decrypter> {
private final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
@Override
public Decrypter convert(Saml2AuthenticationToken token) {
Collection<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential key : token.getRelyingPartyRegistration().getDecryptionX509Credentials()) {
Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
credentials.add(cred);
}
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
Decrypter decrypter = new Decrypter(null, resolver, this.encryptedKeyResolver);
decrypter.setRootInNewDocument(true);
return decrypter;
}
}
/**
* A tuple containing an OpenSAML {@link Response} and its associated authentication
* token.

View File

@ -0,0 +1,203 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import org.joda.time.DateTime;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlSigningUtils.QueryParametersPartial;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link Saml2AuthenticationRequestFactory} that generates, signs, and serializes a
* SAML 2.0 AuthnRequest using OpenSAML 3
*
* @author Filip Hanik
* @author Josh Cummings
* @since 5.2
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
* {@link OpenSaml4AuthenticationRequestFactory}
*/
public class OpenSamlAuthenticationRequestFactory implements Saml2AuthenticationRequestFactory {
static {
OpenSamlInitializationService.initialize();
}
private AuthnRequestBuilder authnRequestBuilder;
private IssuerBuilder issuerBuilder;
private Clock clock = Clock.systemUTC();
private Converter<Saml2AuthenticationRequestContext, Saml2MessageBinding> protocolBindingResolver = (context) -> {
if (context == null) {
return Saml2MessageBinding.POST;
}
return context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding();
};
private Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter;
/**
* Creates an {@link OpenSamlAuthenticationRequestFactory}
*/
public OpenSamlAuthenticationRequestFactory() {
this.authenticationRequestContextConverter = this::createAuthnRequest;
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory()
.getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME);
this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
}
@Override
@Deprecated
public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
Saml2MessageBinding binding = this.protocolBindingResolver.convert(null);
RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("noId")
.assertionConsumerServiceBinding(binding)
.assertionConsumerServiceLocation(request.getAssertionConsumerServiceUrl())
.entityId(request.getIssuer()).remoteIdpEntityId("noIssuer").idpWebSsoUrl("noUrl")
.credentials((credentials) -> credentials.addAll(request.getCredentials())).build();
Saml2AuthenticationRequestContext context = Saml2AuthenticationRequestContext.builder()
.relyingPartyRegistration(registration).issuer(request.getIssuer())
.assertionConsumerServiceUrl(request.getAssertionConsumerServiceUrl()).build();
AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context);
return OpenSamlSigningUtils.serialize(OpenSamlSigningUtils.sign(authnRequest, registration));
}
@Override
public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) {
AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context);
RelyingPartyRegistration registration = context.getRelyingPartyRegistration();
if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) {
OpenSamlSigningUtils.sign(authnRequest, registration);
}
String xml = OpenSamlSigningUtils.serialize(authnRequest);
return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context)
.samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build();
}
@Override
public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest(
Saml2AuthenticationRequestContext context) {
AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context);
RelyingPartyRegistration registration = context.getRelyingPartyRegistration();
String xml = OpenSamlSigningUtils.serialize(authnRequest);
Saml2RedirectAuthenticationRequest.Builder result = Saml2RedirectAuthenticationRequest
.withAuthenticationRequestContext(context);
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
result.samlRequest(deflatedAndEncoded).relayState(context.getRelayState());
if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) {
QueryParametersPartial partial = OpenSamlSigningUtils.sign(registration).param("SAMLRequest",
deflatedAndEncoded);
if (StringUtils.hasText(context.getRelayState())) {
partial.param("RelayState", context.getRelayState());
}
Map<String, String> parameters = partial.parameters();
return result.sigAlg(parameters.get("SigAlg")).signature(parameters.get("Signature")).build();
}
return result.build();
}
private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext context) {
String issuer = context.getIssuer();
String destination = context.getDestination();
String assertionConsumerServiceUrl = context.getAssertionConsumerServiceUrl();
Saml2MessageBinding protocolBinding = this.protocolBindingResolver.convert(context);
AuthnRequest auth = this.authnRequestBuilder.buildObject();
if (auth.getID() == null) {
auth.setID("ARQ" + UUID.randomUUID().toString().substring(1));
}
if (auth.getIssueInstant() == null) {
auth.setIssueInstant(new DateTime(this.clock.millis()));
}
if (auth.isForceAuthn() == null) {
auth.setForceAuthn(Boolean.FALSE);
}
if (auth.isPassive() == null) {
auth.setIsPassive(Boolean.FALSE);
}
if (auth.getProtocolBinding() == null) {
auth.setProtocolBinding(protocolBinding.getUrn());
}
Issuer iss = this.issuerBuilder.buildObject();
iss.setValue(issuer);
auth.setIssuer(iss);
auth.setDestination(destination);
auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl);
return auth;
}
/**
* Set the {@link AuthnRequest} post-processor resolver
* @param authenticationRequestContextConverter
* @since 5.4
*/
public void setAuthenticationRequestContextConverter(
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter) {
Assert.notNull(authenticationRequestContextConverter, "authenticationRequestContextConverter cannot be null");
this.authenticationRequestContextConverter = authenticationRequestContextConverter;
}
/**
* ' Use this {@link Clock} with {@link Instant#now()} for generating timestamps
* @param clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
/**
* Sets the {@code protocolBinding} to use when generating authentication requests.
* Acceptable values are {@link SAMLConstants#SAML2_POST_BINDING_URI} and
* {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI} The IDP will be reading this value
* in the {@code AuthNRequest} to determine how to send the Response/Assertion to the
* ACS URL, assertion consumer service URL.
* @param protocolBinding either {@link SAMLConstants#SAML2_POST_BINDING_URI} or
* {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI}
* @throws IllegalArgumentException if the protocolBinding is not valid
* @deprecated Use
* {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.Builder#assertionConsumerServiceBinding(Saml2MessageBinding)}
* instead
*/
@Deprecated
public void setProtocolBinding(String protocolBinding) {
Saml2MessageBinding binding = Saml2MessageBinding.from(protocolBinding);
Assert.notNull(binding, "Invalid protocol binding: " + protocolBinding);
this.protocolBindingResolver = (context) -> binding;
}
}

View File

@ -38,10 +38,15 @@ import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.schema.XSDateTime;
import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder;
import org.opensaml.saml.common.assertion.ValidationContext;
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AttributeValue;
import org.opensaml.saml.saml2.core.Conditions;
import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.EncryptedAttribute;
import org.opensaml.saml.saml2.core.EncryptedID;
@ -49,6 +54,9 @@ import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.OneTimeUse;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder;
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
@ -134,8 +142,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
Response response = TestOpenSamlObjects.response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID);
response.getAssertions().add(TestOpenSamlObjects.assertion());
Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID);
response.getAssertions().add(assertion());
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, verifying(registration()));
@ -154,8 +162,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() {
Response response = TestOpenSamlObjects.response();
response.getAssertions().add(TestOpenSamlObjects.assertion());
Response response = response();
response.getAssertions().add(assertion());
Saml2AuthenticationToken token = token(response, verifying(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
@ -164,8 +172,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
assertion.getSubject().getSubjectConfirmations().get(0).getSubjectConfirmationData()
.setNotOnOrAfter(DateTime.now().minus(Duration.standardDays(3)));
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
@ -179,8 +187,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenMissingSubjectThenThrowAuthenticationException() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
assertion.setSubject(null);
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
@ -193,8 +201,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenUsernameMissingThenThrowAuthenticationException() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
assertion.getSubject().getNameID().setValue(null);
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
@ -207,8 +215,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
assertion.getSubject().getSubjectConfirmations()
.forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10"));
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
@ -220,9 +228,9 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenAssertionContainsAttributesThenItSucceeds() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
List<AttributeStatement> attributes = TestOpenSamlObjects.attributeStatements();
Response response = response();
Assertion assertion = assertion();
List<AttributeStatement> attributes = attributeStatements();
assertion.getAttributeStatements().addAll(attributes);
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
@ -244,8 +252,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() {
Response response = TestOpenSamlObjects.response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(),
Response response = response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
@ -258,8 +266,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(),
Response response = response();
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
@ -272,8 +280,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() {
Response response = TestOpenSamlObjects.response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(),
Response response = response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
@ -284,8 +292,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
NameID nameId = assertion.getSubject().getNameID();
EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId,
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
@ -300,8 +308,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenEncryptedAttributeThenDecrypts() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value",
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME);
@ -318,8 +326,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() {
Response response = TestOpenSamlObjects.response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(),
Response response = response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
@ -332,8 +340,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() {
Response response = TestOpenSamlObjects.response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(),
Response response = response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
@ -347,8 +355,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(),
Response response = response();
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
@ -384,8 +392,8 @@ public class OpenSamlAuthenticationProviderTests {
.concat(new Saml2Error("wrong error", "wrong error"))
);
// @formatter:on
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME);
assertion.getConditions().getConditions().add(oneTimeUse);
response.getAssertions().add(assertion);
@ -410,8 +418,8 @@ public class OpenSamlAuthenticationProviderTests {
.concat(validator.convert(assertionToken))
);
// @formatter:on
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
response.getAssertions().add(assertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
ASSERTING_PARTY_ENTITY_ID);
@ -426,8 +434,8 @@ public class OpenSamlAuthenticationProviderTests {
public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() {
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
provider.setAssertionValidator((assertionToken) -> Saml2ResponseValidatorResult.success());
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.relyingPartyDecryptingCredential(),
RELYING_PARTY_ENTITY_ID); // broken
// signature
@ -451,8 +459,8 @@ public class OpenSamlAuthenticationProviderTests {
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
provider.setAssertionValidator(
OpenSamlAuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context));
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
response.getAssertions().add(assertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
ASSERTING_PARTY_ENTITY_ID);
@ -467,8 +475,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWithSHA1SignatureThenItSucceeds() throws Exception {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(),
Response response = response();
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID,
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
response.getAssertions().add(assertion);
@ -525,8 +533,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject());
@ -540,8 +548,8 @@ public class OpenSamlAuthenticationProviderTests {
@Test
public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertion() {
Response response = TestOpenSamlObjects.response();
Assertion assertion = TestOpenSamlObjects.assertion();
Response response = response();
Assertion assertion = assertion();
EncryptedID id = new EncryptedIDBuilder().buildObject();
id.setEncryptedData(new EncryptedDataBuilder().buildObject());
assertion.getSubject().setEncryptedID(id);
@ -600,13 +608,52 @@ public class OpenSamlAuthenticationProviderTests {
return (ex) -> {
assertThat(ex.getError().getErrorCode()).isEqualTo(errorCode);
if (StringUtils.hasText(description)) {
assertThat(ex.getError().getDescription()).isEqualTo(description);
assertThat(ex.getError().getDescription()).contains(description);
}
};
}
private Saml2AuthenticationToken token() {
private Response response() {
Response response = TestOpenSamlObjects.response();
response.setIssueInstant(DateTime.now());
return response;
}
private Response response(String destination, String issuerEntityId) {
Response response = TestOpenSamlObjects.response(destination, issuerEntityId);
response.setIssueInstant(DateTime.now());
return response;
}
private Assertion assertion() {
Assertion assertion = TestOpenSamlObjects.assertion();
assertion.setIssueInstant(DateTime.now());
for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) {
SubjectConfirmationData data = confirmation.getSubjectConfirmationData();
data.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000)));
data.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000)));
}
Conditions conditions = assertion.getConditions();
conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000)));
conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000)));
return assertion;
}
private List<AttributeStatement> attributeStatements() {
List<AttributeStatement> attributeStatements = TestOpenSamlObjects.attributeStatements();
AttributeBuilder attributeBuilder = new AttributeBuilder();
Attribute registeredDateAttr = attributeBuilder.buildObject();
registeredDateAttr.setName("registeredDate");
XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME,
XSDateTime.TYPE_NAME);
registeredDate.setValue(DateTime.parse("1970-01-01T00:00:00Z"));
registeredDateAttr.getAttributeValues().add(registeredDate);
attributeStatements.get(0).getAttributes().add(registeredDateAttr);
return attributeStatements;
}
private Saml2AuthenticationToken token() {
Response response = response();
RelyingPartyRegistration registration = verifying(registration()).build();
return new Saml2AuthenticationToken(registration, serialize(response));
}

View File

@ -19,6 +19,7 @@ package org.springframework.security.saml2.provider.service.authentication;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import org.joda.time.DateTime;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@ -200,8 +201,7 @@ public class OpenSamlAuthenticationRequestFactoryTests {
public void createPostAuthenticationRequestWhenAuthnRequestConsumerThenUses() {
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = mock(
Converter.class);
given(authenticationRequestContextConverter.convert(this.context))
.willReturn(TestOpenSamlObjects.authnRequest());
given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest());
this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter);
this.factory.createPostAuthenticationRequest(this.context);
@ -212,8 +212,7 @@ public class OpenSamlAuthenticationRequestFactoryTests {
public void createRedirectAuthenticationRequestWhenAuthnRequestConsumerThenUses() {
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = mock(
Converter.class);
given(authenticationRequestContextConverter.convert(this.context))
.willReturn(TestOpenSamlObjects.authnRequest());
given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest());
this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter);
this.factory.createRedirectAuthenticationRequest(this.context);
@ -256,6 +255,12 @@ public class OpenSamlAuthenticationRequestFactoryTests {
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
}
private AuthnRequest authnRequest() {
AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest();
authnRequest.setIssueInstant(DateTime.now());
return authnRequest;
}
private AuthnRequest getAuthNRequest(Saml2MessageBinding binding) {
AbstractSaml2AuthenticationRequest result = (binding == Saml2MessageBinding.REDIRECT)
? this.factory.createRedirectAuthenticationRequest(this.context)

View File

@ -0,0 +1,58 @@
buildscript {
repositories {
maven { url 'https://repo.spring.io/plugins-release' }
}
dependencies {
classpath 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE'
}
}
plugins {
id 'java-library'
id 'io.spring.convention.repository'
id 'io.spring.convention.springdependencymangement'
id 'io.spring.convention.dependency-set'
id 'io.spring.convention.checkstyle'
id 'io.spring.convention.tests-configuration'
id 'io.spring.convention.integration-test'
id 'propdeps'
}
configurations {
classesOnlyElements {
canBeConsumed = true
canBeResolved = false
attributes {
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 11)
}
}
}
artifacts {
classesOnlyElements(compileJava.destinationDir)
}
sourceCompatibility = '11'
repositories {
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
constraints {
management("org.opensaml:opensaml-core:4.+")
management("org.opensaml:opensaml-saml-api:4.+")
management("org.opensaml:opensaml-saml-impl:4.+")
}
compile project(':saml2-service-provider-core')
compile("org.opensaml:opensaml-core")
compile("org.opensaml:opensaml-saml-api")
compile("org.opensaml:opensaml-saml-impl")
provided 'javax.servlet:javax.servlet-api'
testCompile 'com.squareup.okhttp3:mockwebserver'
testCompile project(path : ':saml2-service-provider-core', configuration : 'tests')
}

View File

@ -0,0 +1,770 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.schema.XSAny;
import org.opensaml.core.xml.schema.XSBoolean;
import org.opensaml.core.xml.schema.XSBooleanValue;
import org.opensaml.core.xml.schema.XSDateTime;
import org.opensaml.core.xml.schema.XSInteger;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.core.xml.schema.XSURI;
import org.opensaml.saml.common.assertion.ValidationContext;
import org.opensaml.saml.common.assertion.ValidationResult;
import org.opensaml.saml.saml2.assertion.ConditionValidator;
import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
import org.opensaml.saml.saml2.assertion.StatementValidator;
import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator;
import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator;
import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator;
import org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.Condition;
import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.OneTimeUse;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link AuthenticationProvider} for SAML authentications when
* receiving a {@code Response} object containing an {@code Assertion}. This
* implementation uses the {@code OpenSAML 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>
* <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>
* <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>
* <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>
* <p>
* This provider does not perform an X509 certificate validation on the configured
* asserting party, IDP, verification certificates.
* </p>
*
* @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://wiki.shibboleth.net/confluence/display/OS30/Home">OpenSAML 3</a>
*/
public final class OpenSaml4AuthenticationProvider implements AuthenticationProvider {
static {
OpenSamlInitializationService.initialize();
}
private final Log logger = LogFactory.getLog(this.getClass());
private final ResponseUnmarshaller responseUnmarshaller;
private final ParserPool parserPool;
private final Converter<ResponseToken, Saml2ResponseValidatorResult> responseSignatureValidator = createDefaultResponseSignatureValidator();
private Consumer<ResponseToken> responseElementsDecrypter = createDefaultResponseElementsDecrypter();
private final Converter<ResponseToken, Saml2ResponseValidatorResult> responseValidator = createDefaultResponseValidator();
private final Converter<AssertionToken, Saml2ResponseValidatorResult> assertionSignatureValidator = createDefaultAssertionSignatureValidator();
private Consumer<AssertionToken> assertionElementsDecrypter = createDefaultAssertionElementsDecrypter();
private Converter<AssertionToken, Saml2ResponseValidatorResult> assertionValidator = createDefaultAssertionValidator();
private Converter<ResponseToken, ? extends AbstractAuthenticationToken> responseAuthenticationConverter = createDefaultResponseAuthenticationConverter();
/**
* Creates an {@link OpenSaml4AuthenticationProvider}
*/
public OpenSaml4AuthenticationProvider() {
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.responseUnmarshaller = (ResponseUnmarshaller) registry.getUnmarshallerFactory()
.getUnmarshaller(Response.DEFAULT_ELEMENT_NAME);
this.parserPool = registry.getParserPool();
}
/**
* Set the {@link Consumer} strategy to use for decrypting elements of a validated
* {@link Response}. The default strategy decrypts all {@link EncryptedAssertion}s
* using OpenSAML's {@link Decrypter}, adding the results to
* {@link Response#getAssertions()}.
*
* You can use this method to configure the {@link Decrypter} instance like so:
*
* <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* provider.setResponseElementsDecrypter((responseToken) -> {
* DecrypterParameters parameters = new DecrypterParameters();
* // ... set parameters as needed
* Decrypter decrypter = new Decrypter(parameters);
* Response response = responseToken.getResponse();
* EncryptedAssertion encrypted = response.getEncryptedAssertions().get(0);
* try {
* Assertion assertion = decrypter.decrypt(encrypted);
* response.getAssertions().add(assertion);
* } catch (Exception e) {
* throw new Saml2AuthenticationException(...);
* }
* });
* </pre>
*
* Or, in the event that you have your own custom decryption interface, the same
* pattern applies:
*
* <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* Converter&lt;EncryptedAssertion, Assertion&gt; myService = ...
* provider.setResponseDecrypter((responseToken) -> {
* Response response = responseToken.getResponse();
* response.getEncryptedAssertions().stream()
* .map(service::decrypt).forEach(response.getAssertions()::add);
* });
* </pre>
*
* This is valuable when using an external service to perform the decryption.
* @param responseElementsDecrypter the {@link Consumer} for decrypting response
* elements
* @since 5.5
*/
public void setResponseElementsDecrypter(Consumer<ResponseToken> responseElementsDecrypter) {
Assert.notNull(responseElementsDecrypter, "responseElementsDecrypter cannot be null");
this.responseElementsDecrypter = responseElementsDecrypter;
}
/**
* Set the {@link Converter} to use for validating each {@link Assertion} in the SAML
* 2.0 Response.
*
* You can still invoke the default validator by delgating to
* {@link #createAssertionValidator}, like so:
*
* <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* provider.setAssertionValidator(assertionToken -> {
* Saml2ResponseValidatorResult result = createDefaultAssertionValidator()
* .convert(assertionToken)
* return result.concat(myCustomValidator.convert(assertionToken));
* });
* </pre>
*
* You can also use this method to configure the provider to use a different
* {@link ValidationContext} from the default, like so:
*
* <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* provider.setAssertionValidator(
* createDefaultAssertionValidator(assertionToken -> {
* Map&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.assertionValidator = assertionValidator;
}
/**
* Set the {@link Consumer} strategy to use for decrypting elements of a validated
* {@link Assertion}.
*
* You can use this method to configure the {@link Decrypter} used like so:
*
* <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* provider.setResponseDecrypter((assertionToken) -> {
* DecrypterParameters parameters = new DecrypterParameters();
* // ... set parameters as needed
* Decrypter decrypter = new Decrypter(parameters);
* Assertion assertion = assertionToken.getAssertion();
* EncryptedID encrypted = assertion.getSubject().getEncryptedID();
* try {
* NameID name = decrypter.decrypt(encrypted);
* assertion.getSubject().setNameID(name);
* } catch (Exception e) {
* throw new Saml2AuthenticationException(...);
* }
* });
* </pre>
*
* Or, in the event that you have your own custom interface, the same pattern applies:
*
* <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* MyDecryptionService myService = ...
* provider.setResponseDecrypter((responseToken) -> {
* Assertion assertion = assertionToken.getAssertion();
* EncryptedID encrypted = assertion.getSubject().getEncryptedID();
* NameID name = myService.decrypt(encrypted);
* assertion.getSubject().setNameID(name);
* });
* </pre>
* @param assertionDecrypter the {@link Consumer} for decrypting assertion elements
* @since 5.5
*/
public void setAssertionElementsDecrypter(Consumer<AssertionToken> assertionDecrypter) {
Assert.notNull(assertionDecrypter, "assertionDecrypter cannot be null");
this.assertionElementsDecrypter = assertionDecrypter;
}
/**
* Set the {@link Converter} to use for converting a validated {@link Response} into
* an {@link AbstractAuthenticationToken}.
*
* You can delegate to the default behavior by calling
* {@link #createDefaultResponseAuthenticationConverter()} like so:
*
* <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* Converter&lt;ResponseToken, Saml2Authentication&gt; authenticationConverter =
* createDefaultResponseAuthenticationConverter();
* provider.setResponseAuthenticationConverter(responseToken -> {
* 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.responseAuthenticationConverter = responseAuthenticationConverter;
}
/**
* 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 createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
(assertionToken) -> SAML20AssertionValidators.attributeValidator,
(assertionToken) -> createValidationContext(assertionToken, (params) -> params
.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(5).toMillis())));
}
/**
* 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
*/
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator(
Converter<AssertionToken, ValidationContext> contextConverter) {
return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
(assertionToken) -> SAML20AssertionValidators.attributeValidator, contextConverter);
}
/**
* Construct a default strategy for converting a SAML 2.0 Response and
* {@link Authentication} token into a {@link Saml2Authentication}
* @return the default response authentication converter strategy
*/
public static Converter<ResponseToken, Saml2Authentication> createDefaultResponseAuthenticationConverter() {
return (responseToken) -> {
Response response = responseToken.response;
Saml2AuthenticationToken token = responseToken.token;
Assertion assertion = CollectionUtils.firstElement(response.getAssertions());
String username = assertion.getSubject().getNameID().getValue();
Map<String, List<Object>> attributes = getAssertionAttributes(assertion);
return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal(username, attributes),
token.getSaml2Response(), AuthorityUtils.createAuthorityList("ROLE_USER"));
};
}
/**
* @param authentication the authentication request object, must be of type
* {@link Saml2AuthenticationToken}
* @return {@link Saml2Authentication} if the assertion is valid
* @throws AuthenticationException if a validation exception occurs
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication;
String serializedResponse = token.getSaml2Response();
Response response = parse(serializedResponse);
process(token, response);
return this.responseAuthenticationConverter.convert(new ResponseToken(response, token));
}
catch (Saml2AuthenticationException ex) {
throw ex;
}
catch (Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, ex.getMessage(), ex);
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication);
}
private Response parse(String response) throws Saml2Exception, Saml2AuthenticationException {
try {
Document document = this.parserPool
.parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8)));
Element element = document.getDocumentElement();
return (Response) this.responseUnmarshaller.unmarshall(element);
}
catch (Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, ex.getMessage(), ex);
}
}
private void process(Saml2AuthenticationToken token, Response response) {
String issuer = response.getIssuer().getValue();
this.logger.debug(LogMessage.format("Processing SAML response from %s", issuer));
boolean responseSigned = response.isSigned();
ResponseToken responseToken = new ResponseToken(response, token);
Saml2ResponseValidatorResult result = this.responseSignatureValidator.convert(responseToken);
if (responseSigned) {
this.responseElementsDecrypter.accept(responseToken);
}
result = result.concat(this.responseValidator.convert(responseToken));
boolean allAssertionsSigned = true;
for (Assertion assertion : response.getAssertions()) {
AssertionToken assertionToken = new AssertionToken(assertion, token);
result = result.concat(this.assertionSignatureValidator.convert(assertionToken));
allAssertionsSigned = allAssertionsSigned && assertion.isSigned();
if (responseSigned || assertion.isSigned()) {
this.assertionElementsDecrypter.accept(new AssertionToken(assertion, token));
}
result = result.concat(this.assertionValidator.convert(assertionToken));
}
if (!responseSigned && !allAssertionsSigned) {
String description = "Either the response or one of the assertions is unsigned. "
+ "Please either sign the response or all of the assertions.";
throw createAuthenticationException(Saml2ErrorCodes.INVALID_SIGNATURE, description, null);
}
Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions());
if (!hasName(firstAssertion)) {
Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
"Assertion [" + firstAssertion.getID() + "] is missing a subject");
result = result.concat(error);
}
if (result.hasErrors()) {
Collection<Saml2Error> errors = result.getErrors();
if (this.logger.isTraceEnabled()) {
this.logger.debug("Found " + errors.size() + " validation errors in SAML response [" + response.getID()
+ "]: " + errors);
}
else if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Found " + errors.size() + " validation errors in SAML response [" + response.getID() + "]");
}
Saml2Error first = errors.iterator().next();
throw createAuthenticationException(first.getErrorCode(), first.getDescription(), null);
}
else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Successfully processed SAML Response [" + response.getID() + "]");
}
}
}
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseSignatureValidator() {
return (responseToken) -> {
Response response = responseToken.getResponse();
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
if (response.isSigned()) {
return OpenSamlVerificationUtils.verifySignature(response, registration).post(response.getSignature());
}
return Saml2ResponseValidatorResult.success();
};
}
private Consumer<ResponseToken> createDefaultResponseElementsDecrypter() {
return (responseToken) -> {
Response response = responseToken.getResponse();
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
try {
OpenSamlDecryptionUtils.decryptResponseElements(response, registration);
}
catch (Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
}
};
}
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
return (responseToken) -> {
Response response = responseToken.getResponse();
Saml2AuthenticationToken token = responseToken.getToken();
Saml2ResponseValidatorResult result = Saml2ResponseValidatorResult.success();
String issuer = response.getIssuer().getValue();
String destination = response.getDestination();
String location = token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation();
if (StringUtils.hasText(destination) && !destination.equals(location)) {
String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID()
+ "]";
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, message));
}
String assertingPartyEntityId = token.getRelyingPartyRegistration().getAssertingPartyDetails()
.getEntityId();
if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) {
String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID());
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, message));
}
if (response.getAssertions().isEmpty()) {
throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA,
"No assertions found in response.", null);
}
return result;
};
}
private Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionSignatureValidator() {
return createAssertionValidator(Saml2ErrorCodes.INVALID_SIGNATURE, (assertionToken) -> {
RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration();
SignatureTrustEngine engine = OpenSamlVerificationUtils.trustEngine(registration);
return SAML20AssertionValidators.createSignatureValidator(engine);
}, (assertionToken) -> new ValidationContext(
Collections.singletonMap(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false)));
}
private Consumer<AssertionToken> createDefaultAssertionElementsDecrypter() {
return (assertionToken) -> {
Assertion assertion = assertionToken.getAssertion();
RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration();
try {
OpenSamlDecryptionUtils.decryptAssertionElements(assertion, registration);
}
catch (Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
}
};
}
private boolean hasName(Assertion assertion) {
if (assertion == null) {
return false;
}
if (assertion.getSubject() == null) {
return false;
}
if (assertion.getSubject().getNameID() == null) {
return false;
}
return assertion.getSubject().getNameID().getValue() != null;
}
private static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
Map<String, List<Object>> attributeMap = new LinkedHashMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
List<Object> attributeValues = new ArrayList<>();
for (XMLObject xmlObject : attribute.getAttributeValues()) {
Object attributeValue = getXmlObjectValue(xmlObject);
if (attributeValue != null) {
attributeValues.add(attributeValue);
}
}
attributeMap.put(attribute.getName(), attributeValues);
}
}
return attributeMap;
}
private static Object getXmlObjectValue(XMLObject xmlObject) {
if (xmlObject instanceof XSAny) {
return ((XSAny) xmlObject).getTextContent();
}
if (xmlObject instanceof XSString) {
return ((XSString) xmlObject).getValue();
}
if (xmlObject instanceof XSInteger) {
return ((XSInteger) xmlObject).getValue();
}
if (xmlObject instanceof XSURI) {
return ((XSURI) xmlObject).getURI();
}
if (xmlObject instanceof XSBoolean) {
XSBooleanValue xsBooleanValue = ((XSBoolean) xmlObject).getValue();
return (xsBooleanValue != null) ? xsBooleanValue.getValue() : null;
}
if (xmlObject instanceof XSDateTime) {
return ((XSDateTime) xmlObject).getValue();
}
return null;
}
private static Saml2AuthenticationException createAuthenticationException(String code, String message,
Exception cause) {
return new Saml2AuthenticationException(new Saml2Error(code, message), cause);
}
private static Converter<AssertionToken, Saml2ResponseValidatorResult> createAssertionValidator(String errorCode,
Converter<AssertionToken, SAML20AssertionValidator> validatorConverter,
Converter<AssertionToken, ValidationContext> contextConverter) {
return (assertionToken) -> {
Assertion assertion = assertionToken.assertion;
SAML20AssertionValidator validator = validatorConverter.convert(assertionToken);
ValidationContext context = contextConverter.convert(assertionToken);
try {
ValidationResult result = validator.validate(assertion, context);
if (result == ValidationResult.VALID) {
return Saml2ResponseValidatorResult.success();
}
}
catch (Exception ex) {
String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
((Response) assertion.getParent()).getID(), ex.getMessage());
return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
}
String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
((Response) assertion.getParent()).getID(), context.getValidationFailureMessage());
return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
};
}
private static ValidationContext createValidationContext(AssertionToken assertionToken,
Consumer<Map<String, Object>> paramsConsumer) {
String audience = assertionToken.token.getRelyingPartyRegistration().getEntityId();
String recipient = assertionToken.token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation();
Map<String, Object> params = new HashMap<>();
params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience));
params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient));
paramsConsumer.accept(params);
return new ValidationContext(params);
}
private static class SAML20AssertionValidators {
private static final Collection<ConditionValidator> conditions = new ArrayList<>();
private static final Collection<SubjectConfirmationValidator> subjects = new ArrayList<>();
private static final Collection<StatementValidator> statements = new ArrayList<>();
private static final SignaturePrevalidator validator = new SAMLSignatureProfileValidator();
static {
conditions.add(new AudienceRestrictionConditionValidator());
conditions.add(new DelegationRestrictionConditionValidator());
conditions.add(new ConditionValidator() {
@Nonnull
@Override
public QName getServicedCondition() {
return OneTimeUse.DEFAULT_ELEMENT_NAME;
}
@Nonnull
@Override
public ValidationResult validate(Condition condition, Assertion assertion, ValidationContext context) {
// applications should validate their own OneTimeUse conditions
return ValidationResult.VALID;
}
});
subjects.add(new BearerSubjectConfirmationValidator() {
@Override
protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion,
ValidationContext context, boolean required) {
// applications should validate their own addresses - gh-7514
return ValidationResult.VALID;
}
@Override
protected ValidationResult validateInResponseTo(SubjectConfirmation confirmation, Assertion assertion,
ValidationContext context, boolean required) {
// applications should validate their own in response to
return ValidationResult.VALID;
}
});
}
private static final SAML20AssertionValidator attributeValidator = new SAML20AssertionValidator(conditions,
subjects, statements, null, null, null) {
@Nonnull
@Override
protected ValidationResult validateSignature(Assertion token, ValidationContext context) {
return ValidationResult.VALID;
}
};
static SAML20AssertionValidator createSignatureValidator(SignatureTrustEngine engine) {
return new SAML20AssertionValidator(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), null, engine,
validator) {
@Nonnull
@Override
protected ValidationResult validateConditions(Assertion assertion, ValidationContext context) {
return ValidationResult.VALID;
}
@Nonnull
@Override
protected ValidationResult validateSubjectConfirmation(Assertion assertion, ValidationContext context) {
return ValidationResult.VALID;
}
@Nonnull
@Override
protected ValidationResult validateStatements(Assertion assertion, ValidationContext context) {
return ValidationResult.VALID;
}
};
}
}
/**
* A tuple containing an OpenSAML {@link Response} and its associated authentication
* token.
*
* @since 5.4
*/
public static class ResponseToken {
private final Saml2AuthenticationToken token;
private final Response response;
ResponseToken(Response response, Saml2AuthenticationToken token) {
this.token = token;
this.response = response;
}
public Response getResponse() {
return this.response;
}
public Saml2AuthenticationToken getToken() {
return this.token;
}
}
/**
* A tuple containing an OpenSAML {@link Assertion} and its associated authentication
* token.
*
* @since 5.4
*/
public static class AssertionToken {
private final Saml2AuthenticationToken token;
private final Assertion assertion;
AssertionToken(Assertion assertion, Saml2AuthenticationToken token) {
this.token = token;
this.assertion = assertion;
}
public Assertion getAssertion() {
return this.assertion;
}
public Saml2AuthenticationToken getToken() {
return this.token;
}
}
}

View File

@ -0,0 +1,180 @@
/*
* Copyright 2002-2020 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.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlSigningUtils.QueryParametersPartial;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link Saml2AuthenticationRequestFactory} that generates, signs, and serializes a
* SAML 2.0 AuthnRequest using OpenSAML 4
*
* @author Josh Cummings
* @since 5.5
*/
public final class OpenSaml4AuthenticationRequestFactory implements Saml2AuthenticationRequestFactory {
static {
OpenSamlInitializationService.initialize();
}
private final AuthnRequestBuilder authnRequestBuilder;
private final IssuerBuilder issuerBuilder;
private Clock clock = Clock.systemUTC();
private Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter;
/**
* Creates an {@link OpenSaml4AuthenticationRequestFactory}
*/
public OpenSaml4AuthenticationRequestFactory() {
this.authenticationRequestContextConverter = this::createAuthnRequest;
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory()
.getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME);
this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
}
/**
* {@inheritDoc}
*/
@Override
@Deprecated
public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("noId")
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
.assertionConsumerServiceLocation(request.getAssertionConsumerServiceUrl())
.entityId(request.getIssuer()).remoteIdpEntityId("noIssuer").idpWebSsoUrl("noUrl")
.credentials((credentials) -> credentials.addAll(request.getCredentials())).build();
Saml2AuthenticationRequestContext context = Saml2AuthenticationRequestContext.builder()
.relyingPartyRegistration(registration).issuer(request.getIssuer())
.assertionConsumerServiceUrl(request.getAssertionConsumerServiceUrl()).build();
AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context);
return OpenSamlSigningUtils.serialize(OpenSamlSigningUtils.sign(authnRequest, registration));
}
/**
* {@inheritDoc}
*/
@Override
public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) {
AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context);
RelyingPartyRegistration registration = context.getRelyingPartyRegistration();
if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) {
OpenSamlSigningUtils.sign(authnRequest, registration);
}
String xml = OpenSamlSigningUtils.serialize(authnRequest);
return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context)
.samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build();
}
/**
* {@inheritDoc}
*/
@Override
public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest(
Saml2AuthenticationRequestContext context) {
AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context);
RelyingPartyRegistration registration = context.getRelyingPartyRegistration();
String xml = OpenSamlSigningUtils.serialize(authnRequest);
Saml2RedirectAuthenticationRequest.Builder result = Saml2RedirectAuthenticationRequest
.withAuthenticationRequestContext(context);
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
result.samlRequest(deflatedAndEncoded).relayState(context.getRelayState());
if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) {
QueryParametersPartial partial = OpenSamlSigningUtils.sign(registration).param("SAMLRequest",
deflatedAndEncoded);
if (StringUtils.hasText(context.getRelayState())) {
partial.param("RelayState", context.getRelayState());
}
Map<String, String> parameters = partial.parameters();
return result.sigAlg(parameters.get("SigAlg")).signature(parameters.get("Signature")).build();
}
return result.build();
}
private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext context) {
String issuer = context.getIssuer();
String destination = context.getDestination();
String assertionConsumerServiceUrl = context.getAssertionConsumerServiceUrl();
String protocolBinding = context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding().getUrn();
AuthnRequest auth = this.authnRequestBuilder.buildObject();
if (auth.getID() == null) {
auth.setID("ARQ" + UUID.randomUUID().toString().substring(1));
}
if (auth.getIssueInstant() == null) {
auth.setIssueInstant(Instant.now(this.clock));
}
if (auth.isForceAuthn() == null) {
auth.setForceAuthn(Boolean.FALSE);
}
if (auth.isPassive() == null) {
auth.setIsPassive(Boolean.FALSE);
}
if (auth.getProtocolBinding() == null) {
auth.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
}
auth.setProtocolBinding(protocolBinding);
Issuer iss = this.issuerBuilder.buildObject();
iss.setValue(issuer);
auth.setIssuer(iss);
auth.setDestination(destination);
auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl);
return auth;
}
/**
* Set the strategy for building an {@link AuthnRequest} from a given context
* @param authenticationRequestContextConverter the conversion strategy to use
*/
public void setAuthenticationRequestContextConverter(
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter) {
Assert.notNull(authenticationRequestContextConverter, "authenticationRequestContextConverter cannot be null");
this.authenticationRequestContextConverter = authenticationRequestContextConverter;
}
/**
* Use this {@link Clock} with {@link Instant#now()} for generating timestamps
* @param clock the {@link Clock} to use
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}

View File

@ -0,0 +1,661 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.junit.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.schema.XSDateTime;
import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder;
import org.opensaml.saml.common.assertion.ValidationContext;
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AttributeValue;
import org.opensaml.saml.saml2.core.Conditions;
import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.EncryptedAttribute;
import org.opensaml.saml.saml2.core.EncryptedID;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.OneTimeUse;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder;
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.w3c.dom.Element;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link OpenSaml4AuthenticationProvider}
*
* @author Filip Hanik
* @author Josh Cummings
*/
public class OpenSaml4AuthenticationProviderTests {
private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias";
private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";
private OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
private Saml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("name",
Collections.emptyMap());
private Saml2Authentication authentication = new Saml2Authentication(this.principal, "response",
Collections.emptyList());
@Test
public void supportsWhenSaml2AuthenticationTokenThenReturnTrue() {
assertThat(this.provider.supports(Saml2AuthenticationToken.class))
.withFailMessage(
OpenSaml4AuthenticationProvider.class + "should support " + Saml2AuthenticationToken.class)
.isTrue();
}
@Test
public void supportsWhenNotSaml2AuthenticationTokenThenReturnFalse() {
assertThat(!this.provider.supports(Authentication.class))
.withFailMessage(OpenSaml4AuthenticationProvider.class + "should not support " + Authentication.class)
.isTrue();
}
@Test
public void authenticateWhenUnknownDataClassThenThrowAuthenticationException() {
Assertion assertion = (Assertion) XMLObjectProviderRegistrySupport.getBuilderFactory()
.getBuilder(Assertion.DEFAULT_ELEMENT_NAME).buildObject(Assertion.DEFAULT_ELEMENT_NAME);
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(
new Saml2AuthenticationToken(verifying(registration()).build(), serialize(assertion))))
.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA));
}
@Test
public void authenticateWhenXmlErrorThenThrowAuthenticationException() {
Saml2AuthenticationToken token = new Saml2AuthenticationToken(verifying(registration()).build(), "invalid xml");
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA));
}
@Test
public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID);
response.getAssertions().add(assertion());
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, verifying(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.INVALID_DESTINATION));
}
@Test
public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() {
Saml2AuthenticationToken token = token();
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response."));
}
@Test
public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() {
Response response = response();
response.getAssertions().add(assertion());
Saml2AuthenticationToken token = token(response, verifying(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE));
}
@Test
public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() {
Response response = response();
Assertion assertion = assertion();
assertion.getSubject().getSubjectConfirmations().get(0).getSubjectConfirmationData()
.setNotOnOrAfter(Instant.now().minus(Duration.ofDays(3)));
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
response.getAssertions().add(assertion);
Saml2AuthenticationToken token = token(response, verifying(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.INVALID_ASSERTION));
}
@Test
public void authenticateWhenMissingSubjectThenThrowAuthenticationException() {
Response response = response();
Assertion assertion = assertion();
assertion.setSubject(null);
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
response.getAssertions().add(assertion);
Saml2AuthenticationToken token = token(response, verifying(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.SUBJECT_NOT_FOUND));
}
@Test
public void authenticateWhenUsernameMissingThenThrowAuthenticationException() {
Response response = response();
Assertion assertion = assertion();
assertion.getSubject().getNameID().setValue(null);
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
response.getAssertions().add(assertion);
Saml2AuthenticationToken token = token(response, verifying(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.SUBJECT_NOT_FOUND));
}
@Test
public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() {
Response response = response();
Assertion assertion = assertion();
assertion.getSubject().getSubjectConfirmations()
.forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10"));
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
response.getAssertions().add(assertion);
Saml2AuthenticationToken token = token(response, verifying(registration()));
this.provider.authenticate(token);
}
@Test
public void authenticateWhenAssertionContainsAttributesThenItSucceeds() {
Response response = response();
Assertion assertion = assertion();
List<AttributeStatement> attributes = attributeStatements();
assertion.getAttributeStatements().addAll(attributes);
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
response.getAssertions().add(assertion);
Saml2AuthenticationToken token = token(response, verifying(registration()));
Authentication authentication = this.provider.authenticate(token);
Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("email", Arrays.asList("john.doe@example.com", "doe.john@example.com"));
expected.put("name", Collections.singletonList("John Doe"));
expected.put("age", Collections.singletonList(21));
expected.put("website", Collections.singletonList("https://johndoe.com/"));
expected.put("registered", Collections.singletonList(true));
Instant registeredDate = Instant.parse("1970-01-01T00:00:00Z");
expected.put("registeredDate", Collections.singletonList(registeredDate));
assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe");
assertThat(principal.getAttributes()).isEqualTo(expected);
}
@Test
public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() {
Response response = response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, decrypting(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE));
}
@Test
public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() {
Response response = response();
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
this.provider.authenticate(token);
}
@Test
public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() {
Response response = response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
this.provider.authenticate(token);
}
@Test
public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() {
Response response = response();
Assertion assertion = assertion();
NameID nameId = assertion.getSubject().getNameID();
EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId,
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
assertion.getSubject().setNameID(null);
assertion.getSubject().setEncryptedID(encryptedID);
response.getAssertions().add(assertion);
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
this.provider.authenticate(token);
}
@Test
public void authenticateWhenEncryptedAttributeThenDecrypts() {
Response response = response();
Assertion assertion = assertion();
EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value",
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME);
statement.getEncryptedAttributes().add(attribute);
assertion.getAttributeStatements().add(statement);
response.getAssertions().add(assertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token);
Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
assertThat(principal.getAttribute("name")).containsExactly("value");
}
@Test
public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() {
Response response = response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, verifying(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData"));
}
@Test
public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() {
Response response = response();
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, registration()
.decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartyPrivateCredential())));
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.provider.authenticate(token))
.satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData"));
}
@Test
public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException {
Response response = response();
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
response.getEncryptedAssertions().add(encryptedAssertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, decrypting(verifying(registration())));
Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token);
// the following code will throw an exception if authentication isn't serializable
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream);
objectOutputStream.writeObject(authentication);
objectOutputStream.flush();
}
@Test
public void createDefaultAssertionValidatorWhenAssertionThenValidates() {
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
Assertion assertion = response.getAssertions().get(0);
OpenSaml4AuthenticationProvider.AssertionToken assertionToken = new OpenSaml4AuthenticationProvider.AssertionToken(
assertion, token());
assertThat(
OpenSaml4AuthenticationProvider.createDefaultAssertionValidator().convert(assertionToken).hasErrors())
.isFalse();
}
@Test
public void authenticateWhenDelegatingToDefaultAssertionValidatorThenUses() {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
// @formatter:off
provider.setAssertionValidator((assertionToken) -> OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator((token) -> new ValidationContext())
.convert(assertionToken)
.concat(new Saml2Error("wrong error", "wrong error"))
);
// @formatter:on
Response response = response();
Assertion assertion = assertion();
OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME);
assertion.getConditions().getConditions().add(oneTimeUse);
response.getAssertions().add(assertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
ASSERTING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, verifying(registration()));
// @formatter:off
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class)
.satisfies((error) -> assertThat(error.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_ASSERTION));
// @formatter:on
}
@Test
public void authenticateWhenCustomAssertionValidatorThenUses() {
Converter<OpenSaml4AuthenticationProvider.AssertionToken, Saml2ResponseValidatorResult> validator = mock(
Converter.class);
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
// @formatter:off
provider.setAssertionValidator((assertionToken) -> OpenSaml4AuthenticationProvider.createDefaultAssertionValidator()
.convert(assertionToken)
.concat(validator.convert(assertionToken))
);
// @formatter:on
Response response = response();
Assertion assertion = assertion();
response.getAssertions().add(assertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
ASSERTING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, verifying(registration()));
given(validator.convert(any(OpenSaml4AuthenticationProvider.AssertionToken.class)))
.willReturn(Saml2ResponseValidatorResult.success());
provider.authenticate(token);
verify(validator).convert(any(OpenSaml4AuthenticationProvider.AssertionToken.class));
}
@Test
public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setAssertionValidator((assertionToken) -> Saml2ResponseValidatorResult.success());
Response response = response();
Assertion assertion = assertion();
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.relyingPartyDecryptingCredential(),
RELYING_PARTY_ENTITY_ID); // broken
// signature
response.getAssertions().add(assertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
ASSERTING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, verifying(registration()));
// @formatter:off
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> provider.authenticate(token))
.satisfies((error) -> assertThat(error.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE));
// @formatter:on
}
@Test
public void authenticateWhenValidationContextCustomizedThenUsers() {
Map<String, Object> parameters = new HashMap<>();
parameters.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton("blah"));
ValidationContext context = mock(ValidationContext.class);
given(context.getStaticParameters()).willReturn(parameters);
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setAssertionValidator(
OpenSaml4AuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context));
Response response = response();
Assertion assertion = assertion();
response.getAssertions().add(assertion);
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
ASSERTING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, verifying(registration()));
// @formatter:off
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class)
.satisfies((error) -> assertThat(error).hasMessageContaining("Invalid assertion"));
// @formatter:on
verify(context, atLeastOnce()).getStaticParameters();
}
@Test
public void authenticateWithSHA1SignatureThenItSucceeds() throws Exception {
Response response = response();
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID,
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
response.getAssertions().add(assertion);
Saml2AuthenticationToken token = token(response, verifying(registration()));
this.provider.authenticate(token);
}
@Test
public void setAssertionValidatorWhenNullThenIllegalArgument() {
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.provider.setAssertionValidator(null));
// @formatter:on
}
@Test
public void createDefaultResponseAuthenticationConverterWhenResponseThenConverts() {
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
Saml2AuthenticationToken token = token(response, verifying(registration()));
ResponseToken responseToken = new ResponseToken(response, token);
Saml2Authentication authentication = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter().convert(responseToken);
assertThat(authentication.getName()).isEqualTo("test@saml.user");
}
@Test
public void authenticateWhenResponseAuthenticationConverterConfiguredThenUses() {
Converter<ResponseToken, Saml2Authentication> authenticationConverter = mock(Converter.class);
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter(authenticationConverter);
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
Saml2AuthenticationToken token = token(response, verifying(registration()));
provider.authenticate(token);
verify(authenticationConverter).convert(any());
}
@Test
public void setResponseAuthenticationConverterWhenNullThenIllegalArgument() {
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.provider.setResponseAuthenticationConverter(null));
// @formatter:on
}
@Test
public void setResponseElementsDecrypterWhenNullThenIllegalArgument() {
assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setResponseElementsDecrypter(null));
}
@Test
public void setAssertionElementsDecrypterWhenNullThenIllegalArgument() {
assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setAssertionElementsDecrypter(null));
}
@Test
public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() {
Response response = response();
Assertion assertion = assertion();
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject());
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
Saml2AuthenticationToken token = token(response, verifying(registration()));
this.provider.setResponseElementsDecrypter((tuple) -> tuple.getResponse().getAssertions().add(assertion));
Authentication authentication = this.provider.authenticate(token);
assertThat(authentication.getName()).isEqualTo("test@saml.user");
}
@Test
public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertion() {
Response response = response();
Assertion assertion = assertion();
EncryptedID id = new EncryptedIDBuilder().buildObject();
id.setEncryptedData(new EncryptedDataBuilder().buildObject());
assertion.getSubject().setEncryptedID(id);
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
RELYING_PARTY_ENTITY_ID);
response.getAssertions().add(assertion);
Saml2AuthenticationToken token = token(response, verifying(registration()));
this.provider.setAssertionElementsDecrypter((tuple) -> {
NameID name = new NameIDBuilder().buildObject();
name.setValue("decrypted name");
tuple.getAssertion().getSubject().setNameID(name);
});
Authentication authentication = this.provider.authenticate(token);
assertThat(authentication.getName()).isEqualTo("decrypted name");
}
private <T extends XMLObject> T build(QName qName) {
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
}
private String serialize(XMLObject object) {
try {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
Element element = marshaller.marshall(object);
return SerializeSupport.nodeToString(element);
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
private Consumer<Saml2AuthenticationException> errorOf(String errorCode) {
return errorOf(errorCode, null);
}
private Consumer<Saml2AuthenticationException> errorOf(String errorCode, String description) {
return (ex) -> {
assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(errorCode);
if (StringUtils.hasText(description)) {
assertThat(ex.getSaml2Error().getDescription()).contains(description);
}
};
}
private Response response() {
Response response = TestOpenSamlObjects.response();
response.setIssueInstant(Instant.now());
return response;
}
private Response response(String destination, String issuerEntityId) {
Response response = TestOpenSamlObjects.response(destination, issuerEntityId);
response.setIssueInstant(Instant.now());
return response;
}
private Assertion assertion() {
Assertion assertion = TestOpenSamlObjects.assertion();
assertion.setIssueInstant(Instant.now());
for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) {
SubjectConfirmationData data = confirmation.getSubjectConfirmationData();
data.setNotBefore(Instant.now().minus(Duration.ofMillis(5 * 60 * 1000)));
data.setNotOnOrAfter(Instant.now().plus(Duration.ofMillis(5 * 60 * 1000)));
}
Conditions conditions = assertion.getConditions();
conditions.setNotBefore(Instant.now().minus(Duration.ofMillis(5 * 60 * 1000)));
conditions.setNotOnOrAfter(Instant.now().plus(Duration.ofMillis(5 * 60 * 1000)));
return assertion;
}
private List<AttributeStatement> attributeStatements() {
List<AttributeStatement> attributeStatements = TestOpenSamlObjects.attributeStatements();
AttributeBuilder attributeBuilder = new AttributeBuilder();
Attribute registeredDateAttr = attributeBuilder.buildObject();
registeredDateAttr.setName("registeredDate");
XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME,
XSDateTime.TYPE_NAME);
registeredDate.setValue(Instant.parse("1970-01-01T00:00:00Z"));
registeredDateAttr.getAttributeValues().add(registeredDate);
attributeStatements.iterator().next().getAttributes().add(registeredDateAttr);
return attributeStatements;
}
private Saml2AuthenticationToken token() {
Response response = response();
RelyingPartyRegistration registration = verifying(registration()).build();
return new Saml2AuthenticationToken(registration, serialize(response));
}
private Saml2AuthenticationToken token(Response response, RelyingPartyRegistration.Builder registration) {
return new Saml2AuthenticationToken(registration.build(), serialize(response));
}
private RelyingPartyRegistration.Builder registration() {
return TestRelyingPartyRegistrations.noCredentials().entityId(RELYING_PARTY_ENTITY_ID)
.assertionConsumerServiceLocation(DESTINATION)
.assertingPartyDetails((party) -> party.entityId(ASSERTING_PARTY_ENTITY_ID));
}
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyDetails((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
}
private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) {
return builder
.decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential()));
}
}

View File

@ -0,0 +1,274 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.impl.AuthnRequestUnmarshaller;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.credentials.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;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link OpenSaml4AuthenticationRequestFactory}
*/
public class OpenSaml4AuthenticationRequestFactoryTests {
private OpenSaml4AuthenticationRequestFactory factory;
private Saml2AuthenticationRequestContext.Builder contextBuilder;
private Saml2AuthenticationRequestContext context;
private RelyingPartyRegistration.Builder relyingPartyRegistrationBuilder;
private RelyingPartyRegistration relyingPartyRegistration;
private AuthnRequestUnmarshaller unmarshaller;
@Before
public void setUp() {
this.relyingPartyRegistrationBuilder = RelyingPartyRegistration.withRegistrationId("id")
.assertionConsumerServiceLocation("template")
.providerDetails((c) -> c.webSsoUrl("https://destination/sso"))
.providerDetails((c) -> c.entityId("remote-entity-id")).localEntityIdTemplate("local-entity-id")
.credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartySigningCredential()));
this.relyingPartyRegistration = this.relyingPartyRegistrationBuilder.build();
this.contextBuilder = Saml2AuthenticationRequestContext.builder().issuer("https://issuer")
.relyingPartyRegistration(this.relyingPartyRegistration)
.assertionConsumerServiceUrl("https://issuer/sso");
this.context = this.contextBuilder.build();
this.factory = new OpenSaml4AuthenticationRequestFactory();
this.unmarshaller = (AuthnRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.getUnmarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME);
}
@Test
public void createAuthenticationRequestWhenInvokingDeprecatedMethodThenReturnsXML() {
Saml2AuthenticationRequest request = Saml2AuthenticationRequest.withAuthenticationRequestContext(this.context)
.build();
String result = this.factory.createAuthenticationRequest(request);
assertThat(result.replace("\n", ""))
.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?><saml2p:AuthnRequest");
}
@Test
public void createRedirectAuthenticationRequestWhenUsingContextThenAllValuesAreSet() {
this.context = this.contextBuilder.relayState("Relay State Value").build();
Saml2RedirectAuthenticationRequest result = this.factory.createRedirectAuthenticationRequest(this.context);
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isEqualTo("Relay State Value");
assertThat(result.getSigAlg()).isNotEmpty();
assertThat(result.getSignature()).isNotEmpty();
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
}
@Test
public void createRedirectAuthenticationRequestWhenNotSignRequestThenNoSignatureIsPresent() {
this.context = this.contextBuilder.relayState("Relay State Value")
.relyingPartyRegistration(
RelyingPartyRegistration.withRelyingPartyRegistration(this.relyingPartyRegistration)
.providerDetails((c) -> c.signAuthNRequest(false)).build())
.build();
Saml2RedirectAuthenticationRequest result = this.factory.createRedirectAuthenticationRequest(this.context);
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isEqualTo("Relay State Value");
assertThat(result.getSigAlg()).isNull();
assertThat(result.getSignature()).isNull();
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
}
@Test
public void createRedirectAuthenticationRequestWhenSignRequestThenSignatureIsPresent() {
this.context = this.contextBuilder.relayState("Relay State Value")
.relyingPartyRegistration(this.relyingPartyRegistration).build();
Saml2RedirectAuthenticationRequest request = this.factory.createRedirectAuthenticationRequest(this.context);
assertThat(request.getRelayState()).isEqualTo("Relay State Value");
assertThat(request.getSigAlg()).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
assertThat(request.getSignature()).isNotNull();
}
@Test
public void createRedirectAuthenticationRequestWhenSignRequestThenCredentialIsRequired() {
Saml2X509Credential credential = org.springframework.security.saml2.core.TestSaml2X509Credentials
.relyingPartyVerifyingCredential();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials()
.assertingPartyDetails((party) -> party.verificationX509Credentials((c) -> c.add(credential))).build();
this.context = this.contextBuilder.relayState("Relay State Value").relyingPartyRegistration(registration)
.build();
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.factory.createPostAuthenticationRequest(this.context));
}
@Test
public void createPostAuthenticationRequestWhenNotSignRequestThenNoSignatureIsPresent() {
this.context = this.contextBuilder.relayState("Relay State Value")
.relyingPartyRegistration(
RelyingPartyRegistration.withRelyingPartyRegistration(this.relyingPartyRegistration)
.providerDetails((c) -> c.signAuthNRequest(false)).build())
.build();
Saml2PostAuthenticationRequest result = this.factory.createPostAuthenticationRequest(this.context);
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isEqualTo("Relay State Value");
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(new String(Saml2Utils.samlDecode(result.getSamlRequest()), StandardCharsets.UTF_8))
.doesNotContain("ds:Signature");
}
@Test
public void createPostAuthenticationRequestWhenSignRequestThenSignatureIsPresent() {
this.context = this.contextBuilder.relayState("Relay State Value")
.relyingPartyRegistration(
RelyingPartyRegistration.withRelyingPartyRegistration(this.relyingPartyRegistration).build())
.build();
Saml2PostAuthenticationRequest result = this.factory.createPostAuthenticationRequest(this.context);
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isEqualTo("Relay State Value");
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(new String(Saml2Utils.samlDecode(result.getSamlRequest()), StandardCharsets.UTF_8))
.contains("ds:Signature");
}
@Test
public void createPostAuthenticationRequestWhenSignRequestThenCredentialIsRequired() {
Saml2X509Credential credential = org.springframework.security.saml2.core.TestSaml2X509Credentials
.relyingPartyVerifyingCredential();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials()
.assertingPartyDetails((party) -> party.verificationX509Credentials((c) -> c.add(credential))).build();
this.context = this.contextBuilder.relayState("Relay State Value").relyingPartyRegistration(registration)
.build();
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.factory.createPostAuthenticationRequest(this.context));
}
@Test
public void createAuthenticationRequestWhenDefaultThenReturnsPostBinding() {
AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST);
Assert.assertEquals(SAMLConstants.SAML2_POST_BINDING_URI, authn.getProtocolBinding());
}
@Test
public void createPostAuthenticationRequestWhenAuthnRequestConsumerThenUses() {
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = mock(
Converter.class);
given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest());
this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter);
this.factory.createPostAuthenticationRequest(this.context);
verify(authenticationRequestContextConverter).convert(this.context);
}
@Test
public void createRedirectAuthenticationRequestWhenAuthnRequestConsumerThenUses() {
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = mock(
Converter.class);
given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest());
this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter);
this.factory.createRedirectAuthenticationRequest(this.context);
verify(authenticationRequestContextConverter).convert(this.context);
}
@Test
public void setAuthenticationRequestContextConverterWhenNullThenException() {
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.factory.setAuthenticationRequestContextConverter(null));
// @formatter:on
}
@Test
public void createPostAuthenticationRequestWhenAssertionConsumerServiceBindingThenUses() {
RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationBuilder
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT).build();
Saml2AuthenticationRequestContext context = this.contextBuilder
.relyingPartyRegistration(relyingPartyRegistration).build();
Saml2PostAuthenticationRequest request = this.factory.createPostAuthenticationRequest(context);
String samlRequest = request.getSamlRequest();
String inflated = new String(Saml2Utils.samlDecode(samlRequest));
assertThat(inflated).contains("ProtocolBinding=\"" + SAMLConstants.SAML2_REDIRECT_BINDING_URI + "\"");
}
@Test
public void createRedirectAuthenticationRequestWhenSHA1SignRequestThenSignatureIsPresent() {
RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationBuilder
.assertingPartyDetails(
(a) -> a.signingAlgorithms((algs) -> algs.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1)))
.build();
Saml2AuthenticationRequestContext context = this.contextBuilder.relayState("Relay State Value")
.relyingPartyRegistration(relyingPartyRegistration).build();
Saml2RedirectAuthenticationRequest result = this.factory.createRedirectAuthenticationRequest(context);
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isEqualTo("Relay State Value");
assertThat(result.getSigAlg()).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
assertThat(result.getSignature()).isNotNull();
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
}
private AuthnRequest authnRequest() {
AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest();
authnRequest.setIssueInstant(Instant.now());
return authnRequest;
}
private AuthnRequest getAuthNRequest(Saml2MessageBinding binding) {
AbstractSaml2AuthenticationRequest result = (binding == Saml2MessageBinding.REDIRECT)
? this.factory.createRedirectAuthenticationRequest(this.context)
: this.factory.createPostAuthenticationRequest(this.context);
String samlRequest = result.getSamlRequest();
assertThat(samlRequest).isNotEmpty();
if (result.getBinding() == Saml2MessageBinding.REDIRECT) {
samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest));
}
else {
samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8);
}
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool()
.parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8)));
Element element = document.getDocumentElement();
return (AuthnRequest) this.unmarshaller.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
}

View File

@ -1,14 +1,48 @@
apply plugin: 'io.spring.convention.spring-module'
dependencies {
compile project(':spring-security-core')
compile project(':spring-security-web')
build.dependsOn(project(":saml2-service-provider-core").tasks["build"])
build.dependsOn(project(":saml2-service-provider-opensaml3").tasks["build"])
build.dependsOn(project(":saml2-service-provider-opensaml4").tasks["build"])
compile("org.opensaml:opensaml-core")
compile("org.opensaml:opensaml-saml-api")
compile("org.opensaml:opensaml-saml-impl")
check.dependsOn(project(":saml2-service-provider-core").tasks["check"])
check.dependsOn(project(":saml2-service-provider-opensaml3").tasks["check"])
check.dependsOn(project(":saml2-service-provider-opensaml4").tasks["check"])
provided 'javax.servlet:javax.servlet-api'
test.dependsOn(project(":saml2-service-provider-core").tasks["test"])
test.dependsOn(project(":saml2-service-provider-opensaml3").tasks["test"])
test.dependsOn(project(":saml2-service-provider-opensaml4").tasks["test"])
testCompile 'com.squareup.okhttp3:mockwebserver'
clean.dependsOn(project(":saml2-service-provider-core").tasks["clean"])
clean.dependsOn(project(":saml2-service-provider-opensaml3").tasks["clean"])
clean.dependsOn(project(":saml2-service-provider-opensaml4").tasks["clean"])
format.dependsOn(project(":saml2-service-provider-core").tasks["format"])
format.dependsOn(project(":saml2-service-provider-opensaml3").tasks["format"])
format.dependsOn(project(":saml2-service-provider-opensaml4").tasks["format"])
configurations {
core {
canBeConsumed = false
canBeResolved = true
}
opensaml3 {
canBeConsumed = false
canBeResolved = true
}
opensaml4 {
canBeConsumed = false
canBeResolved = true
}
}
dependencies {
core(project(path: ":saml2-service-provider-core", configuration: 'classesOnlyElements'))
opensaml3(project(path: ":saml2-service-provider-opensaml3", configuration: 'classesOnlyElements'))
opensaml4(project(path: ":saml2-service-provider-opensaml4", configuration: 'classesOnlyElements'))
}
jar {
from configurations.core
from configurations.opensaml3
from configurations.opensaml4
}

View File

@ -1,311 +0,0 @@
/*
* Copyright 2002-2020 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.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.joda.time.DateTime;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml.saml2.core.impl.AuthnRequestMarshaller;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.w3c.dom.Element;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest.Builder;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* @since 5.2
*/
public class OpenSamlAuthenticationRequestFactory implements Saml2AuthenticationRequestFactory {
static {
OpenSamlInitializationService.initialize();
}
private Clock clock = Clock.systemUTC();
private AuthnRequestMarshaller marshaller;
private AuthnRequestBuilder authnRequestBuilder;
private IssuerBuilder issuerBuilder;
private Converter<Saml2AuthenticationRequestContext, String> protocolBindingResolver = (context) -> {
if (context == null) {
return SAMLConstants.SAML2_POST_BINDING_URI;
}
return context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding().getUrn();
};
private Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = this::createAuthnRequest;
/**
* Creates an {@link OpenSamlAuthenticationRequestFactory}
*/
public OpenSamlAuthenticationRequestFactory() {
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.marshaller = (AuthnRequestMarshaller) registry.getMarshallerFactory()
.getMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME);
this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory()
.getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME);
this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
}
@Override
@Deprecated
public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
AuthnRequest authnRequest = createAuthnRequest(request.getIssuer(), request.getDestination(),
request.getAssertionConsumerServiceUrl(), this.protocolBindingResolver.convert(null));
for (org.springframework.security.saml2.credentials.Saml2X509Credential credential : request.getCredentials()) {
if (credential.isSigningCredential()) {
X509Certificate certificate = credential.getCertificate();
PrivateKey privateKey = credential.getPrivateKey();
BasicCredential cred = CredentialSupport.getSimpleCredential(certificate, privateKey);
cred.setEntityId(request.getIssuer());
cred.setUsageType(UsageType.SIGNING);
SignatureSigningParameters parameters = new SignatureSigningParameters();
parameters.setSigningCredential(cred);
parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256);
parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
return serialize(sign(authnRequest, parameters));
}
}
throw new IllegalArgumentException("No signing credential provided");
}
@Override
public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) {
AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context);
String xml = context.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned()
? serialize(sign(authnRequest, context.getRelyingPartyRegistration())) : serialize(authnRequest);
return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context)
.samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build();
}
@Override
public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest(
Saml2AuthenticationRequestContext context) {
AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context);
String xml = serialize(authnRequest);
Builder result = Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context);
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
result.samlRequest(deflatedAndEncoded).relayState(context.getRelayState());
if (context.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned()) {
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("SAMLRequest", deflatedAndEncoded);
if (StringUtils.hasText(context.getRelayState())) {
parameters.put("RelayState", context.getRelayState());
}
sign(parameters, context.getRelyingPartyRegistration());
return result.sigAlg(parameters.get("SigAlg")).signature(parameters.get("Signature")).build();
}
return result.build();
}
private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext context) {
return createAuthnRequest(context.getIssuer(), context.getDestination(),
context.getAssertionConsumerServiceUrl(), this.protocolBindingResolver.convert(context));
}
private AuthnRequest createAuthnRequest(String issuer, String destination, String assertionConsumerServiceUrl,
String protocolBinding) {
AuthnRequest auth = this.authnRequestBuilder.buildObject();
auth.setID("ARQ" + UUID.randomUUID().toString().substring(1));
auth.setIssueInstant(new DateTime(this.clock.millis()));
auth.setForceAuthn(Boolean.FALSE);
auth.setIsPassive(Boolean.FALSE);
auth.setProtocolBinding(protocolBinding);
Issuer iss = this.issuerBuilder.buildObject();
iss.setValue(issuer);
auth.setIssuer(iss);
auth.setDestination(destination);
auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl);
return auth;
}
/**
* Set the {@link AuthnRequest} post-processor resolver
* @param authenticationRequestContextConverter
* @since 5.4
*/
public void setAuthenticationRequestContextConverter(
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter) {
Assert.notNull(authenticationRequestContextConverter, "authenticationRequestContextConverter cannot be null");
this.authenticationRequestContextConverter = authenticationRequestContextConverter;
}
/**
* ' Use this {@link Clock} with {@link Instant#now()} for generating timestamps
* @param clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
/**
* Sets the {@code protocolBinding} to use when generating authentication requests.
* Acceptable values are {@link SAMLConstants#SAML2_POST_BINDING_URI} and
* {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI} The IDP will be reading this value
* in the {@code AuthNRequest} to determine how to send the Response/Assertion to the
* ACS URL, assertion consumer service URL.
* @param protocolBinding either {@link SAMLConstants#SAML2_POST_BINDING_URI} or
* {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI}
* @throws IllegalArgumentException if the protocolBinding is not valid
* @deprecated Use
* {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.Builder#assertionConsumerServiceBinding(Saml2MessageBinding)}
* instead
*/
@Deprecated
public void setProtocolBinding(String protocolBinding) {
boolean isAllowedBinding = SAMLConstants.SAML2_POST_BINDING_URI.equals(protocolBinding)
|| SAMLConstants.SAML2_REDIRECT_BINDING_URI.equals(protocolBinding);
if (!isAllowedBinding) {
throw new IllegalArgumentException("Invalid protocol binding: " + protocolBinding);
}
this.protocolBindingResolver = (context) -> protocolBinding;
}
private AuthnRequest sign(AuthnRequest authnRequest, RelyingPartyRegistration relyingPartyRegistration) {
SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
return sign(authnRequest, parameters);
}
private AuthnRequest sign(AuthnRequest authnRequest, SignatureSigningParameters parameters) {
try {
SignatureSupport.signObject(authnRequest, parameters);
return authnRequest;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private void sign(Map<String, String> components, RelyingPartyRegistration relyingPartyRegistration) {
SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
sign(components, parameters);
}
private void sign(Map<String, String> components, SignatureSigningParameters parameters) {
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
components.put("SigAlg", algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : 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);
components.put("Signature", b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
}
private String serialize(AuthnRequest authnRequest) {
try {
Element element = this.marshaller.marshall(authnRequest);
return SerializeSupport.nodeToString(element);
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
private SignatureSigningParameters resolveSigningParameters(RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
CriteriaSet criteria = new CriteriaSet();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(algorithms);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
criteria.add(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 List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setEntityId(relyingPartyRegistration.getEntityId());
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
}

View File

@ -16,9 +16,15 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
sourceCompatibility = '11'
repositories {
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
compile project(':spring-security-config')
compile project(':spring-security-saml2-service-provider')
compile project(':saml2-service-provider-opensaml4')
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

View File

@ -1,7 +1,11 @@
apply plugin: 'io.spring.convention.spring-sample-war'
repositories {
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
compile project(':spring-security-saml2-service-provider')
compile project(':saml2-service-provider-opensaml3')
compile project(':spring-security-config')
compile slf4jDependencies