parent
a015b8b000
commit
d0d0a8d958
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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'
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -57,4 +57,5 @@ public enum Saml2MessageBinding {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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')
|
||||
}
|
|
@ -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,54 +530,24 @@ 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());
|
||||
return OpenSamlVerificationUtils.verifySignature(response, registration).post(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 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()) {
|
||||
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
|
||||
try {
|
||||
Assertion assertion = decrypter.decrypt(encryptedAssertion);
|
||||
response.getAssertions().add(assertion);
|
||||
OpenSamlDecryptionUtils.decryptResponseElements(response, registration);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
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()) {
|
||||
RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration();
|
||||
try {
|
||||
Attribute attribute = decrypter.decrypt(encryptedAttribute);
|
||||
statement.getAttributes().add(attribute);
|
||||
OpenSamlDecryptionUtils.decryptAssertionElements(assertion, registration);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), 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) {
|
||||
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.
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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)
|
|
@ -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')
|
||||
}
|
|
@ -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<EncryptedAssertion, Assertion> myService = ...
|
||||
* provider.setResponseDecrypter((responseToken) -> {
|
||||
* Response response = responseToken.getResponse();
|
||||
* response.getEncryptedAssertions().stream()
|
||||
* .map(service::decrypt).forEach(response.getAssertions()::add);
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* This is valuable when using an external service to perform the decryption.
|
||||
* @param responseElementsDecrypter the {@link Consumer} for decrypting response
|
||||
* elements
|
||||
* @since 5.5
|
||||
*/
|
||||
public void setResponseElementsDecrypter(Consumer<ResponseToken> responseElementsDecrypter) {
|
||||
Assert.notNull(responseElementsDecrypter, "responseElementsDecrypter cannot be null");
|
||||
this.responseElementsDecrypter = responseElementsDecrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link Converter} to use for validating each {@link Assertion} in the SAML
|
||||
* 2.0 Response.
|
||||
*
|
||||
* You can still invoke the default validator by delgating to
|
||||
* {@link #createAssertionValidator}, like so:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* provider.setAssertionValidator(assertionToken -> {
|
||||
* Saml2ResponseValidatorResult result = createDefaultAssertionValidator()
|
||||
* .convert(assertionToken)
|
||||
* return result.concat(myCustomValidator.convert(assertionToken));
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* You can also use this method to configure the provider to use a different
|
||||
* {@link ValidationContext} from the default, like so:
|
||||
*
|
||||
* <pre>
|
||||
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
* provider.setAssertionValidator(
|
||||
* createDefaultAssertionValidator(assertionToken -> {
|
||||
* Map<String, Object> params = new HashMap<>();
|
||||
* params.put(CLOCK_SKEW, 2 * 60 * 1000);
|
||||
* // other parameters
|
||||
* return new ValidationContext(params);
|
||||
* }));
|
||||
* </pre>
|
||||
*
|
||||
* Consider taking a look at {@link #createValidationContext} to see how it constructs
|
||||
* a {@link ValidationContext}.
|
||||
*
|
||||
* It is not necessary to delegate to the default validator. You can safely replace it
|
||||
* entirely with your own. Note that signature verification is performed as a separate
|
||||
* step from this validator.
|
||||
* @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<ResponseToken, Saml2Authentication> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue