mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-12 13:23:29 +00:00
Add OpenSAML 4 Support
Closes gh-9095
This commit is contained in:
parent
a015b8b000
commit
d0d0a8d958
@ -4,6 +4,10 @@ apply plugin: 'io.spring.convention.spring-module'
|
|||||||
apply plugin: 'trang'
|
apply plugin: 'trang'
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// NB: Don't add other compile time dependencies to the config module as this breaks tooling
|
// NB: Don't add other compile time dependencies to the config module as this breaks tooling
|
||||||
compile project(':spring-security-core')
|
compile project(':spring-security-core')
|
||||||
@ -14,7 +18,8 @@ dependencies {
|
|||||||
|
|
||||||
optional project(':spring-security-ldap')
|
optional project(':spring-security-ldap')
|
||||||
optional project(':spring-security-messaging')
|
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-client')
|
||||||
optional project(':spring-security-oauth2-jose')
|
optional project(':spring-security-oauth2-jose')
|
||||||
optional project(':spring-security-oauth2-resource-server')
|
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-ldap', configuration : 'tests')
|
||||||
testCompile project(path : ':spring-security-oauth2-client', 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-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 project(path : ':spring-security-web', configuration : 'tests')
|
||||||
testCompile apachedsDependencies
|
testCompile apachedsDependencies
|
||||||
testCompile powerMock2Dependencies
|
testCompile powerMock2Dependencies
|
||||||
|
@ -21,15 +21,20 @@ import java.util.Map;
|
|||||||
|
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
|
|
||||||
|
import org.opensaml.core.Version;
|
||||||
|
|
||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
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.HttpSecurityBuilder;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
|
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.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.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.OpenSamlAuthenticationProvider;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
|
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
|
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>The {@code loginProcessingUrl} is set</li>
|
||||||
* <li>A custom login page is configured, <b>or</b></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>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>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@ -256,8 +261,12 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void registerDefaultAuthenticationProvider(B http) {
|
private void registerDefaultAuthenticationProvider(B http) {
|
||||||
OpenSamlAuthenticationProvider provider = postProcess(new OpenSamlAuthenticationProvider());
|
if (Version.getVersion().startsWith("4")) {
|
||||||
http.authenticationProvider(provider);
|
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
http.authenticationProvider(postProcess(new OpenSamlAuthenticationProvider()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerDefaultCsrfOverride(B http) {
|
private void registerDefaultCsrfOverride(B http) {
|
||||||
@ -337,7 +346,10 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
|||||||
private Saml2AuthenticationRequestFactory getResolver(B http) {
|
private Saml2AuthenticationRequestFactory getResolver(B http) {
|
||||||
Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class);
|
Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class);
|
||||||
if (resolver == null) {
|
if (resolver == null) {
|
||||||
resolver = new OpenSamlAuthenticationRequestFactory();
|
if (Version.getVersion().startsWith("4")) {
|
||||||
|
return new OpenSaml4AuthenticationRequestFactory();
|
||||||
|
}
|
||||||
|
return new OpenSamlAuthenticationRequestFactory();
|
||||||
}
|
}
|
||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web.configurers.saml2;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
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.Saml2ErrorCodes;
|
||||||
import org.springframework.security.saml2.core.Saml2Utils;
|
import org.springframework.security.saml2.core.Saml2Utils;
|
||||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.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.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.Saml2Authentication;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext;
|
||||||
@ -235,11 +237,8 @@ public class Saml2LoginConfigurerTests {
|
|||||||
"authenticationManager");
|
"authenticationManager");
|
||||||
ProviderManager pm = (ProviderManager) manager;
|
ProviderManager pm = (ProviderManager) manager;
|
||||||
AuthenticationProvider provider = pm.getProviders().stream()
|
AuthenticationProvider provider = pm.getProviders().stream()
|
||||||
.filter((p) -> p instanceof OpenSamlAuthenticationProvider).findFirst().get();
|
.filter((p) -> p instanceof OpenSaml4AuthenticationProvider).findFirst().get();
|
||||||
Assert.assertSame(AUTHORITIES_EXTRACTOR, ReflectionTestUtils.getField(provider, "authoritiesExtractor"));
|
assertThat(provider).isNotNull();
|
||||||
Assert.assertSame(AUTHORITIES_MAPPER, ReflectionTestUtils.getField(provider, "authoritiesMapper"));
|
|
||||||
Assert.assertSame(RESPONSE_TIME_VALIDATION_SKEW,
|
|
||||||
ReflectionTestUtils.getField(provider, "responseTimeValidationSkew"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Saml2WebSsoAuthenticationFilter getSaml2SsoFilter(FilterChainProxy chain) {
|
private Saml2WebSsoAuthenticationFilter getSaml2SsoFilter(FilterChainProxy chain) {
|
||||||
@ -370,9 +369,10 @@ public class Saml2LoginConfigurerTests {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
Saml2AuthenticationRequestFactory authenticationRequestFactory() {
|
Saml2AuthenticationRequestFactory authenticationRequestFactory() {
|
||||||
OpenSamlAuthenticationRequestFactory authenticationRequestFactory = new OpenSamlAuthenticationRequestFactory();
|
OpenSaml4AuthenticationRequestFactory authenticationRequestFactory = new OpenSaml4AuthenticationRequestFactory();
|
||||||
authenticationRequestFactory.setAuthenticationRequestContextConverter((context) -> {
|
authenticationRequestFactory.setAuthenticationRequestContextConverter((context) -> {
|
||||||
AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest();
|
AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest();
|
||||||
|
authnRequest.setIssueInstant(Instant.now());
|
||||||
authnRequest.setForceAuthn(true);
|
authnRequest.setForceAuthn(true);
|
||||||
return authnRequest;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -24,6 +24,8 @@ import javax.servlet.ServletException;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.opensaml.core.Version;
|
||||||
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext;
|
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.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;
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult;
|
import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
import org.springframework.web.util.HtmlUtils;
|
import org.springframework.web.util.HtmlUtils;
|
||||||
@ -88,8 +91,21 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter
|
|||||||
public Saml2WebSsoAuthenticationRequestFilter(
|
public Saml2WebSsoAuthenticationRequestFilter(
|
||||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||||
this(new DefaultSaml2AuthenticationRequestContextResolver(
|
this(new DefaultSaml2AuthenticationRequestContextResolver(
|
||||||
new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository)),
|
new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository)), requestFactory());
|
||||||
new org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory());
|
}
|
||||||
|
|
||||||
|
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 javax.xml.namespace.QName;
|
||||||
|
|
||||||
import org.apache.xml.security.encryption.XMLCipherParameters;
|
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.XMLObject;
|
||||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||||
import org.opensaml.core.xml.io.MarshallingException;
|
import org.opensaml.core.xml.io.MarshallingException;
|
||||||
import org.opensaml.core.xml.schema.XSAny;
|
import org.opensaml.core.xml.schema.XSAny;
|
||||||
import org.opensaml.core.xml.schema.XSBoolean;
|
import org.opensaml.core.xml.schema.XSBoolean;
|
||||||
import org.opensaml.core.xml.schema.XSBooleanValue;
|
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.XSInteger;
|
||||||
import org.opensaml.core.xml.schema.XSString;
|
import org.opensaml.core.xml.schema.XSString;
|
||||||
import org.opensaml.core.xml.schema.XSURI;
|
import org.opensaml.core.xml.schema.XSURI;
|
||||||
import org.opensaml.core.xml.schema.impl.XSAnyBuilder;
|
import org.opensaml.core.xml.schema.impl.XSAnyBuilder;
|
||||||
import org.opensaml.core.xml.schema.impl.XSBooleanBuilder;
|
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.XSIntegerBuilder;
|
||||||
import org.opensaml.core.xml.schema.impl.XSStringBuilder;
|
import org.opensaml.core.xml.schema.impl.XSStringBuilder;
|
||||||
import org.opensaml.core.xml.schema.impl.XSURIBuilder;
|
import org.opensaml.core.xml.schema.impl.XSURIBuilder;
|
||||||
@ -114,7 +110,6 @@ public final class TestOpenSamlObjects {
|
|||||||
static Response response(String destination, String issuerEntityId) {
|
static Response response(String destination, String issuerEntityId) {
|
||||||
Response response = build(Response.DEFAULT_ELEMENT_NAME);
|
Response response = build(Response.DEFAULT_ELEMENT_NAME);
|
||||||
response.setID("R" + UUID.randomUUID().toString());
|
response.setID("R" + UUID.randomUUID().toString());
|
||||||
response.setIssueInstant(DateTime.now());
|
|
||||||
response.setVersion(SAMLVersion.VERSION_20);
|
response.setVersion(SAMLVersion.VERSION_20);
|
||||||
response.setID("_" + UUID.randomUUID().toString());
|
response.setID("_" + UUID.randomUUID().toString());
|
||||||
response.setDestination(destination);
|
response.setDestination(destination);
|
||||||
@ -141,9 +136,7 @@ public final class TestOpenSamlObjects {
|
|||||||
static Assertion assertion(String username, String issuerEntityId, String recipientEntityId, String recipientUri) {
|
static Assertion assertion(String username, String issuerEntityId, String recipientEntityId, String recipientUri) {
|
||||||
Assertion assertion = build(Assertion.DEFAULT_ELEMENT_NAME);
|
Assertion assertion = build(Assertion.DEFAULT_ELEMENT_NAME);
|
||||||
assertion.setID("A" + UUID.randomUUID().toString());
|
assertion.setID("A" + UUID.randomUUID().toString());
|
||||||
assertion.setIssueInstant(DateTime.now());
|
|
||||||
assertion.setVersion(SAMLVersion.VERSION_20);
|
assertion.setVersion(SAMLVersion.VERSION_20);
|
||||||
assertion.setIssueInstant(DateTime.now());
|
|
||||||
assertion.setIssuer(issuer(issuerEntityId));
|
assertion.setIssuer(issuer(issuerEntityId));
|
||||||
assertion.setSubject(subject(username));
|
assertion.setSubject(subject(username));
|
||||||
assertion.setConditions(conditions());
|
assertion.setConditions(conditions());
|
||||||
@ -183,16 +176,11 @@ public final class TestOpenSamlObjects {
|
|||||||
static SubjectConfirmationData subjectConfirmationData(String recipient) {
|
static SubjectConfirmationData subjectConfirmationData(String recipient) {
|
||||||
SubjectConfirmationData subject = build(SubjectConfirmationData.DEFAULT_ELEMENT_NAME);
|
SubjectConfirmationData subject = build(SubjectConfirmationData.DEFAULT_ELEMENT_NAME);
|
||||||
subject.setRecipient(recipient);
|
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;
|
return subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Conditions conditions() {
|
static Conditions conditions() {
|
||||||
Conditions conditions = build(Conditions.DEFAULT_ELEMENT_NAME);
|
return 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AuthnRequest authnRequest() {
|
public static AuthnRequest authnRequest() {
|
||||||
@ -338,13 +326,6 @@ public final class TestOpenSamlObjects {
|
|||||||
registered.setValue(new XSBooleanValue(true, false));
|
registered.setValue(new XSBooleanValue(true, false));
|
||||||
registeredAttr.getAttributeValues().add(registered);
|
registeredAttr.getAttributeValues().add(registered);
|
||||||
attrStmt2.getAttributes().add(registeredAttr);
|
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);
|
attributeStatements.add(attrStmt2);
|
||||||
return attributeStatements;
|
return attributeStatements;
|
||||||
}
|
}
|
@ -28,8 +28,10 @@ import org.springframework.mock.web.MockFilterChain;
|
|||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
import org.springframework.security.saml2.credentials.TestSaml2X509Credentials;
|
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.Saml2AuthenticationRequestFactory;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
|
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.authentication.TestSaml2AuthenticationRequestContexts;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||||
@ -68,7 +70,7 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
this.filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
|
this.filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver, this.factory);
|
||||||
this.request = new MockHttpServletRequest();
|
this.request = new MockHttpServletRequest();
|
||||||
this.response = new MockHttpServletResponse();
|
this.response = new MockHttpServletResponse();
|
||||||
this.request.setPathInfo("/saml2/authenticate/registration-id");
|
this.request.setPathInfo("/saml2/authenticate/registration-id");
|
||||||
@ -81,25 +83,48 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenNoRelayStateThenRedirectDoesNotContainParameter() throws ServletException, IOException {
|
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);
|
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
|
||||||
assertThat(this.response.getHeader("Location")).doesNotContain("RelayState=").startsWith(IDP_SSO_URL);
|
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
|
@Test
|
||||||
public void doFilterWhenRelayStateThenRedirectDoesContainParameter() throws ServletException, IOException {
|
public void doFilterWhenRelayStateThenRedirectDoesContainParameter() throws ServletException, IOException {
|
||||||
given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build());
|
Saml2AuthenticationRequestContext context = authenticationRequestContext().build();
|
||||||
this.request.setParameter("RelayState", "my-relay-state");
|
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);
|
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
|
@Test
|
||||||
public void doFilterWhenRelayStateThatRequiresEncodingThenRedirectDoesContainsEncodedParameter() throws Exception {
|
public void doFilterWhenRelayStateThatRequiresEncodingThenRedirectDoesContainsEncodedParameter() throws Exception {
|
||||||
given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build());
|
String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
|
||||||
final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
|
String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
|
||||||
final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
|
Saml2AuthenticationRequestContext context = authenticationRequestContext().relayState(relayStateValue).build();
|
||||||
this.request.setParameter("RelayState", relayStateValue);
|
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);
|
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
|
||||||
assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded)
|
assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded)
|
||||||
.startsWith(IDP_SSO_URL);
|
.startsWith(IDP_SSO_URL);
|
||||||
@ -107,34 +132,39 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenSimpleSignatureSpecifiedThenSignatureParametersAreInTheRedirectURL() throws Exception {
|
public void doFilterWhenSimpleSignatureSpecifiedThenSignatureParametersAreInTheRedirectURL() throws Exception {
|
||||||
given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build());
|
Saml2AuthenticationRequestContext context = authenticationRequestContext().build();
|
||||||
final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
|
Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).sigAlg("sigalg")
|
||||||
final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
|
.signature("signature").build();
|
||||||
this.request.setParameter("RelayState", relayStateValue);
|
given(this.resolver.resolve(any())).willReturn(context);
|
||||||
|
given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request);
|
||||||
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
|
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
|
||||||
assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded).contains("SigAlg=")
|
assertThat(this.response.getHeader("Location")).contains("SigAlg=").contains("Signature=")
|
||||||
.contains("Signature=").startsWith(IDP_SSO_URL);
|
.startsWith(IDP_SSO_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenSignatureIsDisabledThenSignatureParametersAreNotInTheRedirectURL() throws Exception {
|
public void doFilterWhenSignatureIsDisabledThenSignatureParametersAreNotInTheRedirectURL() throws Exception {
|
||||||
given(this.repository.findByRegistrationId("registration-id"))
|
Saml2AuthenticationRequestContext context = authenticationRequestContext().build();
|
||||||
.willReturn(this.rpBuilder.providerDetails((c) -> c.signAuthNRequest(false)).build());
|
Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build();
|
||||||
final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
|
given(this.resolver.resolve(any())).willReturn(context);
|
||||||
final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
|
given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request);
|
||||||
this.request.setParameter("RelayState", relayStateValue);
|
|
||||||
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
|
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
|
||||||
assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded)
|
assertThat(this.response.getHeader("Location")).doesNotContain("SigAlg=").doesNotContain("Signature=")
|
||||||
.doesNotContain("SigAlg=").doesNotContain("Signature=").startsWith(IDP_SSO_URL);
|
.startsWith(IDP_SSO_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenPostFormDataIsPresent() throws Exception {
|
public void doFilterWhenPostFormDataIsPresent() throws Exception {
|
||||||
given(this.repository.findByRegistrationId("registration-id"))
|
String relayStateValue = "https://my-relay-state.example.com?with=param&other=param&javascript{alert('1');}";
|
||||||
.willReturn(this.rpBuilder.providerDetails((c) -> c.binding(Saml2MessageBinding.POST)).build());
|
String relayStateEncoded = HtmlUtils.htmlEscape(relayStateValue);
|
||||||
final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param&javascript{alert('1');}";
|
RelyingPartyRegistration registration = this.rpBuilder
|
||||||
final String relayStateEncoded = HtmlUtils.htmlEscape(relayStateValue);
|
.assertingPartyDetails((asserting) -> asserting.singleSignOnServiceBinding(Saml2MessageBinding.POST))
|
||||||
this.request.setParameter("RelayState", relayStateValue);
|
.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);
|
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
|
||||||
assertThat(this.response.getHeader("Location")).isNull();
|
assertThat(this.response.getHeader("Location")).isNull();
|
||||||
assertThat(this.response.getContentAsString())
|
assertThat(this.response.getContentAsString())
|
||||||
@ -145,66 +175,43 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenSetAuthenticationRequestFactoryThenUses() throws Exception {
|
public void doFilterWhenSetAuthenticationRequestFactoryThenUses() throws Exception {
|
||||||
RelyingPartyRegistration relyingParty = this.rpBuilder
|
Saml2AuthenticationRequestContext context = authenticationRequestContext().build();
|
||||||
.providerDetails((c) -> c.binding(Saml2MessageBinding.POST)).build();
|
Saml2RedirectAuthenticationRequest authenticationRequest = redirectAuthenticationRequest(context).build();
|
||||||
Saml2PostAuthenticationRequest authenticationRequest = mock(Saml2PostAuthenticationRequest.class);
|
Saml2AuthenticationRequestFactory factory = mock(Saml2AuthenticationRequestFactory.class);
|
||||||
given(authenticationRequest.getAuthenticationRequestUri()).willReturn("uri");
|
given(this.resolver.resolve(any())).willReturn(context);
|
||||||
given(authenticationRequest.getRelayState()).willReturn("relay");
|
given(factory.createRedirectAuthenticationRequest(any())).willReturn(authenticationRequest);
|
||||||
given(authenticationRequest.getSamlRequest()).willReturn("saml");
|
this.filter.setAuthenticationRequestFactory(factory);
|
||||||
given(this.repository.findByRegistrationId("registration-id")).willReturn(relyingParty);
|
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
|
||||||
given(this.factory.createPostAuthenticationRequest(any())).willReturn(authenticationRequest);
|
verify(factory).createRedirectAuthenticationRequest(any());
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setRequestMatcherWhenNullThenException() {
|
public void setRequestMatcherWhenNullThenException() {
|
||||||
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
|
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver,
|
||||||
|
this.factory);
|
||||||
assertThatIllegalArgumentException().isThrownBy(() -> filter.setRedirectMatcher(null));
|
assertThatIllegalArgumentException().isThrownBy(() -> filter.setRedirectMatcher(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setAuthenticationRequestFactoryWhenNullThenException() {
|
public void setAuthenticationRequestFactoryWhenNullThenException() {
|
||||||
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
|
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver,
|
||||||
|
this.factory);
|
||||||
assertThatIllegalArgumentException().isThrownBy(() -> filter.setAuthenticationRequestFactory(null));
|
assertThatIllegalArgumentException().isThrownBy(() -> filter.setAuthenticationRequestFactory(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenRequestMatcherFailsThenSkipsFilter() throws Exception {
|
public void doFilterWhenRequestMatcherFailsThenSkipsFilter() throws Exception {
|
||||||
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository);
|
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver,
|
||||||
|
this.factory);
|
||||||
filter.setRedirectMatcher((request) -> false);
|
filter.setRedirectMatcher((request) -> false);
|
||||||
filter.doFilter(this.request, this.response, this.filterChain);
|
filter.doFilter(this.request, this.response, this.filterChain);
|
||||||
verifyNoInteractions(this.repository);
|
verifyNoInteractions(this.resolver, this.factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenRelyingPartyRegistrationNotFoundThenUnauthorized() throws Exception {
|
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);
|
filter.doFilter(this.request, this.response, this.filterChain);
|
||||||
assertThat(this.response.getStatus()).isEqualTo(401);
|
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.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.xml.namespace.QName;
|
import javax.xml.namespace.QName;
|
||||||
|
|
||||||
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
|
|
||||||
import net.shibboleth.utilities.java.support.xml.ParserPool;
|
import net.shibboleth.utilities.java.support.xml.ParserPool;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.joda.time.DateTime;
|
|
||||||
import org.opensaml.core.config.ConfigurationService;
|
import org.opensaml.core.config.ConfigurationService;
|
||||||
import org.opensaml.core.criterion.EntityIdCriterion;
|
|
||||||
import org.opensaml.core.xml.XMLObject;
|
import org.opensaml.core.xml.XMLObject;
|
||||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
||||||
import org.opensaml.core.xml.schema.XSAny;
|
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.XSInteger;
|
||||||
import org.opensaml.core.xml.schema.XSString;
|
import org.opensaml.core.xml.schema.XSString;
|
||||||
import org.opensaml.core.xml.schema.XSURI;
|
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.ValidationContext;
|
||||||
import org.opensaml.saml.common.assertion.ValidationResult;
|
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.ConditionValidator;
|
||||||
import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
|
import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
|
||||||
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
|
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.AttributeStatement;
|
||||||
import org.opensaml.saml.saml2.core.Condition;
|
import org.opensaml.saml.saml2.core.Condition;
|
||||||
import org.opensaml.saml.saml2.core.EncryptedAssertion;
|
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.OneTimeUse;
|
||||||
import org.opensaml.saml.saml2.core.Response;
|
import org.opensaml.saml.saml2.core.Response;
|
||||||
import org.opensaml.saml.saml2.core.StatusCode;
|
import org.opensaml.saml.saml2.core.StatusCode;
|
||||||
import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
||||||
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
|
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
|
||||||
import org.opensaml.saml.saml2.encryption.Decrypter;
|
import org.opensaml.saml.saml2.encryption.Decrypter;
|
||||||
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
|
|
||||||
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
|
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.SignaturePrevalidator;
|
||||||
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
||||||
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
|
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
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.Saml2Error;
|
||||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||||
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
|
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.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
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
|
* "https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38">SAML 2
|
||||||
* StatusResponse</a>
|
* StatusResponse</a>
|
||||||
* @see <a href="https://wiki.shibboleth.net/confluence/display/OS30/Home">OpenSAML 3</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 {
|
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<ResponseToken, ? extends AbstractAuthenticationToken> responseAuthenticationConverter = createCompatibleResponseAuthenticationConverter();
|
||||||
|
|
||||||
private Converter<Saml2AuthenticationToken, SignatureTrustEngine> signatureTrustEngineConverter = new SignatureTrustEngineConverter();
|
|
||||||
|
|
||||||
private Converter<Saml2AuthenticationToken, Decrypter> decrypterConverter = new DecrypterConverter();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an {@link OpenSamlAuthenticationProvider}
|
* Creates an {@link OpenSamlAuthenticationProvider}
|
||||||
*/
|
*/
|
||||||
@ -560,54 +530,24 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
|||||||
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseSignatureValidator() {
|
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseSignatureValidator() {
|
||||||
return (responseToken) -> {
|
return (responseToken) -> {
|
||||||
Response response = responseToken.getResponse();
|
Response response = responseToken.getResponse();
|
||||||
Saml2AuthenticationToken token = responseToken.getToken();
|
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
|
||||||
Collection<Saml2Error> errors = new ArrayList<>();
|
|
||||||
String issuer = response.getIssuer().getValue();
|
|
||||||
if (response.isSigned()) {
|
if (response.isSigned()) {
|
||||||
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
|
return OpenSamlVerificationUtils.verifySignature(response, registration).post(response.getSignature());
|
||||||
try {
|
|
||||||
profileValidator.validate(response.getSignature());
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
return Saml2ResponseValidatorResult.success();
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Consumer<ResponseToken> createDefaultResponseElementsDecrypter() {
|
private Consumer<ResponseToken> createDefaultResponseElementsDecrypter() {
|
||||||
return (responseToken) -> {
|
return (responseToken) -> {
|
||||||
Decrypter decrypter = this.decrypterConverter.convert(responseToken.getToken());
|
|
||||||
Response response = responseToken.getResponse();
|
Response response = responseToken.getResponse();
|
||||||
for (EncryptedAssertion encryptedAssertion : responseToken.getResponse().getEncryptedAssertions()) {
|
RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration();
|
||||||
try {
|
try {
|
||||||
Assertion assertion = decrypter.decrypt(encryptedAssertion);
|
OpenSamlDecryptionUtils.decryptResponseElements(response, registration);
|
||||||
response.getAssertions().add(assertion);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Saml2Exception ex) {
|
||||||
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
|
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,7 +596,8 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
|||||||
|
|
||||||
private Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionSignatureValidator() {
|
private Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionSignatureValidator() {
|
||||||
return createAssertionValidator(Saml2ErrorCodes.INVALID_SIGNATURE, (assertionToken) -> {
|
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);
|
return SAML20AssertionValidators.createSignatureValidator(engine);
|
||||||
}, (assertionToken) -> new ValidationContext(
|
}, (assertionToken) -> new ValidationContext(
|
||||||
Collections.singletonMap(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false)));
|
Collections.singletonMap(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false)));
|
||||||
@ -664,29 +605,12 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
|||||||
|
|
||||||
private Consumer<AssertionToken> createDefaultAssertionElementsDecrypter() {
|
private Consumer<AssertionToken> createDefaultAssertionElementsDecrypter() {
|
||||||
return (assertionToken) -> {
|
return (assertionToken) -> {
|
||||||
Decrypter decrypter = this.decrypterConverter.convert(assertionToken.getToken());
|
|
||||||
Assertion assertion = assertionToken.getAssertion();
|
Assertion assertion = assertionToken.getAssertion();
|
||||||
for (AttributeStatement statement : assertion.getAttributeStatements()) {
|
RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration();
|
||||||
for (EncryptedAttribute encryptedAttribute : statement.getEncryptedAttributes()) {
|
|
||||||
try {
|
try {
|
||||||
Attribute attribute = decrypter.decrypt(encryptedAttribute);
|
OpenSamlDecryptionUtils.decryptAssertionElements(assertion, registration);
|
||||||
statement.getAttributes().add(attribute);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Saml2Exception 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) {
|
|
||||||
throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), 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;
|
return (xsBooleanValue != null) ? xsBooleanValue.getValue() : null;
|
||||||
}
|
}
|
||||||
if (xmlObject instanceof XSDateTime) {
|
if (xmlObject instanceof XSDateTime) {
|
||||||
DateTime dateTime = ((XSDateTime) xmlObject).getValue();
|
return Instant.ofEpochMilli(((XSDateTime) xmlObject).getValue().getMillis());
|
||||||
return (dateTime != null) ? Instant.ofEpochMilli(dateTime.getMillis()) : null;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -812,27 +735,6 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
|||||||
return new ValidationContext(params);
|
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 class SAML20AssertionValidators {
|
||||||
|
|
||||||
private static final Collection<ConditionValidator> conditions = new ArrayList<>();
|
private static final Collection<ConditionValidator> conditions = new ArrayList<>();
|
||||||
@ -861,10 +763,9 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
subjects.add(new BearerSubjectConfirmationValidator() {
|
subjects.add(new BearerSubjectConfirmationValidator() {
|
||||||
@Nonnull
|
|
||||||
@Override
|
@Override
|
||||||
protected ValidationResult validateAddress(@Nonnull SubjectConfirmation confirmation,
|
protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion,
|
||||||
@Nonnull Assertion assertion, @Nonnull ValidationContext context) {
|
ValidationContext context) throws AssertionValidationException {
|
||||||
// applications should validate their own addresses - gh-7514
|
// applications should validate their own addresses - gh-7514
|
||||||
return ValidationResult.VALID;
|
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
|
* A tuple containing an OpenSAML {@link Response} and its associated authentication
|
||||||
* token.
|
* 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.config.XMLObjectProviderRegistrySupport;
|
||||||
import org.opensaml.core.xml.io.Marshaller;
|
import org.opensaml.core.xml.io.Marshaller;
|
||||||
import org.opensaml.core.xml.io.MarshallingException;
|
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.common.assertion.ValidationContext;
|
||||||
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
|
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
|
||||||
import org.opensaml.saml.saml2.core.Assertion;
|
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.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.EncryptedAssertion;
|
||||||
import org.opensaml.saml.saml2.core.EncryptedAttribute;
|
import org.opensaml.saml.saml2.core.EncryptedAttribute;
|
||||||
import org.opensaml.saml.saml2.core.EncryptedID;
|
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.OneTimeUse;
|
||||||
import org.opensaml.saml.saml2.core.Response;
|
import org.opensaml.saml.saml2.core.Response;
|
||||||
import org.opensaml.saml.saml2.core.StatusCode;
|
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.EncryptedAssertionBuilder;
|
||||||
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
|
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
|
||||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||||
@ -134,8 +142,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
|
public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
|
||||||
Response response = TestOpenSamlObjects.response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID);
|
Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID);
|
||||||
response.getAssertions().add(TestOpenSamlObjects.assertion());
|
response.getAssertions().add(assertion());
|
||||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
RELYING_PARTY_ENTITY_ID);
|
RELYING_PARTY_ENTITY_ID);
|
||||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||||
@ -154,8 +162,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() {
|
public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
response.getAssertions().add(TestOpenSamlObjects.assertion());
|
response.getAssertions().add(assertion());
|
||||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||||
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
assertThatExceptionOfType(Saml2AuthenticationException.class)
|
||||||
.isThrownBy(() -> this.provider.authenticate(token))
|
.isThrownBy(() -> this.provider.authenticate(token))
|
||||||
@ -164,8 +172,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() {
|
public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
assertion.getSubject().getSubjectConfirmations().get(0).getSubjectConfirmationData()
|
assertion.getSubject().getSubjectConfirmations().get(0).getSubjectConfirmationData()
|
||||||
.setNotOnOrAfter(DateTime.now().minus(Duration.standardDays(3)));
|
.setNotOnOrAfter(DateTime.now().minus(Duration.standardDays(3)));
|
||||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
@ -179,8 +187,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenMissingSubjectThenThrowAuthenticationException() {
|
public void authenticateWhenMissingSubjectThenThrowAuthenticationException() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
assertion.setSubject(null);
|
assertion.setSubject(null);
|
||||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
RELYING_PARTY_ENTITY_ID);
|
RELYING_PARTY_ENTITY_ID);
|
||||||
@ -193,8 +201,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenUsernameMissingThenThrowAuthenticationException() {
|
public void authenticateWhenUsernameMissingThenThrowAuthenticationException() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
assertion.getSubject().getNameID().setValue(null);
|
assertion.getSubject().getNameID().setValue(null);
|
||||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
RELYING_PARTY_ENTITY_ID);
|
RELYING_PARTY_ENTITY_ID);
|
||||||
@ -207,8 +215,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() {
|
public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
assertion.getSubject().getSubjectConfirmations()
|
assertion.getSubject().getSubjectConfirmations()
|
||||||
.forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10"));
|
.forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10"));
|
||||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
@ -220,9 +228,9 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenAssertionContainsAttributesThenItSucceeds() {
|
public void authenticateWhenAssertionContainsAttributesThenItSucceeds() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
List<AttributeStatement> attributes = TestOpenSamlObjects.attributeStatements();
|
List<AttributeStatement> attributes = attributeStatements();
|
||||||
assertion.getAttributeStatements().addAll(attributes);
|
assertion.getAttributeStatements().addAll(attributes);
|
||||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
RELYING_PARTY_ENTITY_ID);
|
RELYING_PARTY_ENTITY_ID);
|
||||||
@ -244,8 +252,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() {
|
public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(),
|
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
|
||||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
@ -258,8 +266,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() {
|
public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(),
|
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
|
||||||
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
|
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
|
||||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
|
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
|
||||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||||
@ -272,8 +280,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() {
|
public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(),
|
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
|
||||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
@ -284,8 +292,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() {
|
public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
NameID nameId = assertion.getSubject().getNameID();
|
NameID nameId = assertion.getSubject().getNameID();
|
||||||
EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId,
|
EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId,
|
||||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||||
@ -300,8 +308,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenEncryptedAttributeThenDecrypts() {
|
public void authenticateWhenEncryptedAttributeThenDecrypts() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value",
|
EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value",
|
||||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||||
AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME);
|
AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME);
|
||||||
@ -318,8 +326,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() {
|
public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(),
|
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
|
||||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
@ -332,8 +340,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() {
|
public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(),
|
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(),
|
||||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
@ -347,8 +355,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException {
|
public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(),
|
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
|
||||||
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
|
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID);
|
||||||
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
|
EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion,
|
||||||
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
TestSaml2X509Credentials.assertingPartyEncryptingCredential());
|
||||||
@ -384,8 +392,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
.concat(new Saml2Error("wrong error", "wrong error"))
|
.concat(new Saml2Error("wrong error", "wrong error"))
|
||||||
);
|
);
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME);
|
OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME);
|
||||||
assertion.getConditions().getConditions().add(oneTimeUse);
|
assertion.getConditions().getConditions().add(oneTimeUse);
|
||||||
response.getAssertions().add(assertion);
|
response.getAssertions().add(assertion);
|
||||||
@ -410,8 +418,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
.concat(validator.convert(assertionToken))
|
.concat(validator.convert(assertionToken))
|
||||||
);
|
);
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
response.getAssertions().add(assertion);
|
response.getAssertions().add(assertion);
|
||||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
ASSERTING_PARTY_ENTITY_ID);
|
ASSERTING_PARTY_ENTITY_ID);
|
||||||
@ -426,8 +434,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() {
|
public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() {
|
||||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||||
provider.setAssertionValidator((assertionToken) -> Saml2ResponseValidatorResult.success());
|
provider.setAssertionValidator((assertionToken) -> Saml2ResponseValidatorResult.success());
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.relyingPartyDecryptingCredential(),
|
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.relyingPartyDecryptingCredential(),
|
||||||
RELYING_PARTY_ENTITY_ID); // broken
|
RELYING_PARTY_ENTITY_ID); // broken
|
||||||
// signature
|
// signature
|
||||||
@ -451,8 +459,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||||
provider.setAssertionValidator(
|
provider.setAssertionValidator(
|
||||||
OpenSamlAuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context));
|
OpenSamlAuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context));
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
response.getAssertions().add(assertion);
|
response.getAssertions().add(assertion);
|
||||||
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
ASSERTING_PARTY_ENTITY_ID);
|
ASSERTING_PARTY_ENTITY_ID);
|
||||||
@ -467,8 +475,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWithSHA1SignatureThenItSucceeds() throws Exception {
|
public void authenticateWithSHA1SignatureThenItSucceeds() throws Exception {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(),
|
Assertion assertion = TestOpenSamlObjects.signed(assertion(),
|
||||||
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID,
|
TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID,
|
||||||
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
|
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
|
||||||
response.getAssertions().add(assertion);
|
response.getAssertions().add(assertion);
|
||||||
@ -525,8 +533,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() {
|
public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(),
|
||||||
RELYING_PARTY_ENTITY_ID);
|
RELYING_PARTY_ENTITY_ID);
|
||||||
response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject());
|
response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject());
|
||||||
@ -540,8 +548,8 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertion() {
|
public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertion() {
|
||||||
Response response = TestOpenSamlObjects.response();
|
Response response = response();
|
||||||
Assertion assertion = TestOpenSamlObjects.assertion();
|
Assertion assertion = assertion();
|
||||||
EncryptedID id = new EncryptedIDBuilder().buildObject();
|
EncryptedID id = new EncryptedIDBuilder().buildObject();
|
||||||
id.setEncryptedData(new EncryptedDataBuilder().buildObject());
|
id.setEncryptedData(new EncryptedDataBuilder().buildObject());
|
||||||
assertion.getSubject().setEncryptedID(id);
|
assertion.getSubject().setEncryptedID(id);
|
||||||
@ -600,13 +608,52 @@ public class OpenSamlAuthenticationProviderTests {
|
|||||||
return (ex) -> {
|
return (ex) -> {
|
||||||
assertThat(ex.getError().getErrorCode()).isEqualTo(errorCode);
|
assertThat(ex.getError().getErrorCode()).isEqualTo(errorCode);
|
||||||
if (StringUtils.hasText(description)) {
|
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 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();
|
RelyingPartyRegistration registration = verifying(registration()).build();
|
||||||
return new Saml2AuthenticationToken(registration, serialize(response));
|
return new Saml2AuthenticationToken(registration, serialize(response));
|
||||||
}
|
}
|
@ -19,6 +19,7 @@ package org.springframework.security.saml2.provider.service.authentication;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.joda.time.DateTime;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -200,8 +201,7 @@ public class OpenSamlAuthenticationRequestFactoryTests {
|
|||||||
public void createPostAuthenticationRequestWhenAuthnRequestConsumerThenUses() {
|
public void createPostAuthenticationRequestWhenAuthnRequestConsumerThenUses() {
|
||||||
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = mock(
|
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = mock(
|
||||||
Converter.class);
|
Converter.class);
|
||||||
given(authenticationRequestContextConverter.convert(this.context))
|
given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest());
|
||||||
.willReturn(TestOpenSamlObjects.authnRequest());
|
|
||||||
this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter);
|
this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter);
|
||||||
|
|
||||||
this.factory.createPostAuthenticationRequest(this.context);
|
this.factory.createPostAuthenticationRequest(this.context);
|
||||||
@ -212,8 +212,7 @@ public class OpenSamlAuthenticationRequestFactoryTests {
|
|||||||
public void createRedirectAuthenticationRequestWhenAuthnRequestConsumerThenUses() {
|
public void createRedirectAuthenticationRequestWhenAuthnRequestConsumerThenUses() {
|
||||||
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = mock(
|
Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = mock(
|
||||||
Converter.class);
|
Converter.class);
|
||||||
given(authenticationRequestContextConverter.convert(this.context))
|
given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest());
|
||||||
.willReturn(TestOpenSamlObjects.authnRequest());
|
|
||||||
this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter);
|
this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter);
|
||||||
|
|
||||||
this.factory.createRedirectAuthenticationRequest(this.context);
|
this.factory.createRedirectAuthenticationRequest(this.context);
|
||||||
@ -256,6 +255,12 @@ public class OpenSamlAuthenticationRequestFactoryTests {
|
|||||||
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
|
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AuthnRequest authnRequest() {
|
||||||
|
AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest();
|
||||||
|
authnRequest.setIssueInstant(DateTime.now());
|
||||||
|
return authnRequest;
|
||||||
|
}
|
||||||
|
|
||||||
private AuthnRequest getAuthNRequest(Saml2MessageBinding binding) {
|
private AuthnRequest getAuthNRequest(Saml2MessageBinding binding) {
|
||||||
AbstractSaml2AuthenticationRequest result = (binding == Saml2MessageBinding.REDIRECT)
|
AbstractSaml2AuthenticationRequest result = (binding == Saml2MessageBinding.REDIRECT)
|
||||||
? this.factory.createRedirectAuthenticationRequest(this.context)
|
? 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'
|
apply plugin: 'io.spring.convention.spring-module'
|
||||||
|
|
||||||
dependencies {
|
build.dependsOn(project(":saml2-service-provider-core").tasks["build"])
|
||||||
compile project(':spring-security-core')
|
build.dependsOn(project(":saml2-service-provider-opensaml3").tasks["build"])
|
||||||
compile project(':spring-security-web')
|
build.dependsOn(project(":saml2-service-provider-opensaml4").tasks["build"])
|
||||||
|
|
||||||
compile("org.opensaml:opensaml-core")
|
check.dependsOn(project(":saml2-service-provider-core").tasks["check"])
|
||||||
compile("org.opensaml:opensaml-saml-api")
|
check.dependsOn(project(":saml2-service-provider-opensaml3").tasks["check"])
|
||||||
compile("org.opensaml:opensaml-saml-impl")
|
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'
|
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||||
|
|
||||||
|
sourceCompatibility = '11'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':spring-security-config')
|
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-thymeleaf'
|
||||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
apply plugin: 'io.spring.convention.spring-sample-war'
|
apply plugin: 'io.spring.convention.spring-sample-war'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':spring-security-saml2-service-provider')
|
compile project(':saml2-service-provider-opensaml3')
|
||||||
compile project(':spring-security-config')
|
compile project(':spring-security-config')
|
||||||
compile slf4jDependencies
|
compile slf4jDependencies
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user