diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java index f5f69e9a39..ae497d1d44 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java @@ -15,19 +15,20 @@ */ package org.springframework.security.saml2.provider.service.authentication; -import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; import javax.annotation.Nonnull; import net.shibboleth.utilities.java.support.resolver.CriteriaSet; @@ -38,7 +39,6 @@ import org.opensaml.core.criterion.EntityIdCriterion; 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.schema.XSAny; import org.opensaml.core.xml.schema.XSBoolean; import org.opensaml.core.xml.schema.XSBooleanValue; @@ -53,7 +53,6 @@ import org.opensaml.saml.criterion.ProtocolCriterion; import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; import org.opensaml.saml.saml2.assertion.ConditionValidator; import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator; -import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; import org.opensaml.saml.saml2.assertion.StatementValidator; import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator; import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator; @@ -65,9 +64,9 @@ import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedID; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.Response; -import org.opensaml.saml.saml2.core.Subject; import org.opensaml.saml.saml2.core.SubjectConfirmation; import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; import org.opensaml.security.credential.Credential; import org.opensaml.security.credential.CredentialResolver; @@ -79,7 +78,11 @@ 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.DecryptionException; +import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; import org.opensaml.xmlsec.keyinfo.impl.StaticKeyInfoCredentialResolver; import org.opensaml.xmlsec.signature.support.SignaturePrevalidator; @@ -87,6 +90,7 @@ import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -96,12 +100,15 @@ import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMap import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.credentials.Saml2X509Credential; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.CLOCK_SKEW; import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.COND_VALID_AUDIENCES; +import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS; import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.SIGNATURE_REQUIRED; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.DECRYPTION_ERROR; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR; @@ -112,7 +119,6 @@ import static org.springframework.security.saml2.provider.service.authentication import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.MALFORMED_RESPONSE_DATA; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.SUBJECT_NOT_FOUND; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS; -import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.USERNAME_NOT_FOUND; import static org.springframework.util.Assert.notNull; /** @@ -155,26 +161,31 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class); - private final List conditions = Collections.singletonList(new AudienceRestrictionConditionValidator()); - private final SubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator() { - @Nonnull - @Override - protected ValidationResult validateAddress(@Nonnull SubjectConfirmation confirmation, - @Nonnull Assertion assertion, @Nonnull ValidationContext context) { - // skipping address validation - gh-7514 - return ValidationResult.VALID; - } - }; - private final List subjects = Collections.singletonList(this.subjectConfirmationValidator); - private final List statements = Collections.emptyList(); - private final SignaturePrevalidator signaturePrevalidator = new SAMLSignatureProfileValidator(); - private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); + private Converter> authoritiesExtractor = (a -> singletonList(new SimpleGrantedAuthority("ROLE_USER"))); private GrantedAuthoritiesMapper authoritiesMapper = (a -> a); private Duration responseTimeValidationSkew = Duration.ofMinutes(5); + private Function>> responseValidator + = validator(Arrays.asList(new ResponseSignatureValidator(), new ResponseValidator())); + private Function> assertionDecrypter + = new AssertionDecrypter(); + private Function>> assertionValidator + = validator(Arrays.asList(new AssertionSignatureValidator(), new AssertionValidator.Builder().build())); + private Function> principalDecrypter + = new PrincipalDecrypter(); + private Function> authenticationConverter = + token -> response -> { + Assertion assertion = CollectionUtils.firstElement(response.getAssertions()); + String username = assertion.getSubject().getNameID().getValue(); + Map> attributes = getAssertionAttributes(assertion); + return new Saml2Authentication( + new SimpleSaml2AuthenticatedPrincipal(username, attributes), token.getSaml2Response(), + this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))); + }; + /** * Sets the {@link Converter} used for extracting assertion attributes that * can be mapped to authorities. @@ -204,6 +215,12 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi */ public void setResponseTimeValidationSkew(Duration responseTimeValidationSkew) { this.responseTimeValidationSkew = responseTimeValidationSkew; + this.assertionValidator = validator(Arrays.asList( + new AssertionSignatureValidator(), + new AssertionValidator.Builder() + .validationContext(params -> params + .put(CLOCK_SKEW, responseTimeValidationSkew.toMillis())) + .build())); } /** @@ -217,14 +234,10 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication; - Response response = parse(token.getSaml2Response()); - List validAssertions = validateResponse(token, response); - Assertion assertion = validAssertions.get(0); - String username = getUsername(token, assertion); - Map> attributes = getAssertionAttributes(assertion); - return new Saml2Authentication( - new SimpleSaml2AuthenticatedPrincipal(username, attributes), token.getSaml2Response(), - this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))); + String serializedResponse = token.getSaml2Response(); + Response response = parse(serializedResponse); + process(token, response); + return this.authenticationConverter.apply(token).convert(response); } catch (Saml2AuthenticationException e) { throw e; } catch (Exception e) { @@ -259,88 +272,32 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi } - private List validateResponse(Saml2AuthenticationToken token, Response response) - throws Saml2AuthenticationException { - - List validAssertions = new ArrayList<>(); + private void process(Saml2AuthenticationToken token, Response response) { String issuer = response.getIssuer().getValue(); if (logger.isDebugEnabled()) { - logger.debug("Validating SAML response from " + issuer); + logger.debug("Processing SAML response from " + issuer); } - List assertions = new ArrayList<>(response.getAssertions()); - for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) { - Assertion assertion = decrypt(token, encryptedAssertion); - assertions.add(assertion); - } - if (assertions.isEmpty()) { - throw authException(MALFORMED_RESPONSE_DATA, "No assertions found in response."); - } + boolean responseSigned = response.isSigned(); + Map validationExceptions = validateResponse(token, response); - if (!isSigned(response, assertions)) { + List assertions = decryptAssertions(token, response); + if (!isSigned(responseSigned, assertions)) { throw authException(INVALID_SIGNATURE, "Either the response or one of the assertions is unsigned. " + "Please either sign the response or all of the assertions."); } + validationExceptions.putAll(validateAssertions(token, assertions)); - SignatureTrustEngine signatureTrustEngine = buildSignatureTrustEngine(token); - - Map validationExceptions = new HashMap<>(); - if (response.isSigned()) { - SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); - try { - profileValidator.validate(response.getSignature()); - } catch (Exception e) { - validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, - "Invalid signature for SAML Response [" + response.getID() + "]", e)); - } - - 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 (!signatureTrustEngine.validate(response.getSignature(), criteriaSet)) { - validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, - "Invalid signature for SAML Response [" + response.getID() + "]")); - } - } catch (Exception e) { - validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, - "Invalid signature for SAML Response [" + response.getID() + "]", e)); - } - } - - String destination = response.getDestination(); - if (StringUtils.hasText(destination) && !destination.equals(token.getRecipientUri())) { - String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID() + "]"; - validationExceptions.put(INVALID_DESTINATION, authException(INVALID_DESTINATION, message)); - } - - if (!StringUtils.hasText(issuer) || !issuer.equals(token.getIdpEntityId())) { - String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID()); - validationExceptions.put(INVALID_ISSUER, authException(INVALID_ISSUER, message)); - } - - SAML20AssertionValidator validator = buildSamlAssertionValidator(signatureTrustEngine); - ValidationContext context = buildValidationContext(token, response); - - if (logger.isDebugEnabled()) { - logger.debug("Validating " + assertions.size() + " assertions"); - } - for (Assertion assertion : assertions) { - if (logger.isTraceEnabled()) { - logger.trace("Validating assertion " + assertion.getID()); - } - try { - validAssertions.add(validateAssertion(assertion, validator, context)); - } catch (Exception e) { - String message = String.format("Invalid assertion [%s] for SAML response [%s]", assertion.getID(), response.getID()); - validationExceptions.put(INVALID_ASSERTION, authException(INVALID_ASSERTION, message, e)); - } + Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions()); + NameID nameId = decryptPrincipal(token, firstAssertion); + if (nameId == null || nameId.getValue() == null) { + validationExceptions.put(SUBJECT_NOT_FOUND, authException(SUBJECT_NOT_FOUND, + "Assertion [" + firstAssertion.getID() + "] is missing a subject")); } if (validationExceptions.isEmpty()) { if (logger.isDebugEnabled()) { - logger.debug("Successfully validated SAML Response [" + response.getID() + "]"); + logger.debug("Successfully processed SAML Response [" + response.getID() + "]"); } } else { if (logger.isTraceEnabled()) { @@ -354,15 +311,49 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi if (!validationExceptions.isEmpty()) { throw validationExceptions.values().iterator().next(); } - if (validAssertions.isEmpty()) { - throw authException(MALFORMED_RESPONSE_DATA, "No valid assertions found in response."); - } - - return validAssertions; } - private boolean isSigned(Response samlResponse, List assertions) { - if (samlResponse.isSigned()) { + private Map validateResponse + (Saml2AuthenticationToken token, Response response) { + + Map validationExceptions = new HashMap<>(); + validationExceptions.putAll(this.responseValidator.apply(token).convert(response)); + return validationExceptions; + } + + private List decryptAssertions + (Saml2AuthenticationToken token, Response response) { + List assertions = new ArrayList<>(); + for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) { + Assertion assertion = this.assertionDecrypter.apply(token).convert(encryptedAssertion); + assertions.add(assertion); + } + response.getAssertions().addAll(assertions); + return response.getAssertions(); + } + + private Map validateAssertions + (Saml2AuthenticationToken token, List assertions) { + if (assertions.isEmpty()) { + throw authException(MALFORMED_RESPONSE_DATA, "No assertions found in response."); + } + + Map validationExceptions = new LinkedHashMap<>(); + if (logger.isDebugEnabled()) { + logger.debug("Validating " + assertions.size() + " assertions"); + } + for (Assertion assertion : assertions) { + if (logger.isTraceEnabled()) { + logger.trace("Validating assertion " + assertion.getID()); + } + validationExceptions.putAll(this.assertionValidator.apply(token).convert(assertion)); + } + + return validationExceptions; + } + + private boolean isSigned(boolean responseSigned, List assertions) { + if (responseSigned) { return true; } @@ -375,140 +366,16 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi return true; } - private SignatureTrustEngine buildSignatureTrustEngine(Saml2AuthenticationToken token) { - Set credentials = new HashSet<>(); - for (X509Certificate key : getVerificationCertificates(token)) { - BasicX509Credential cred = new BasicX509Credential(key); - cred.setUsageType(UsageType.SIGNING); - cred.setEntityId(token.getIdpEntityId()); - credentials.add(cred); + private NameID decryptPrincipal(Saml2AuthenticationToken token, Assertion assertion) { + if (assertion.getSubject() == null) { + return null; } - CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); - return new ExplicitKeySignatureTrustEngine( - credentialsResolver, - DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver() - ); - } - - private ValidationContext buildValidationContext(Saml2AuthenticationToken token, Response response) { - Map validationParams = new HashMap<>(); - validationParams.put(SIGNATURE_REQUIRED, !response.isSigned()); - validationParams.put(CLOCK_SKEW, this.responseTimeValidationSkew.toMillis()); - validationParams.put(COND_VALID_AUDIENCES, singleton(token.getLocalSpEntityId())); - if (StringUtils.hasText(token.getRecipientUri())) { - validationParams.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, singleton(token.getRecipientUri())); + if (assertion.getSubject().getEncryptedID() == null) { + return assertion.getSubject().getNameID(); } - return new ValidationContext(validationParams); - } - - private SAML20AssertionValidator buildSamlAssertionValidator(SignatureTrustEngine signatureTrustEngine) { - return new SAML20AssertionValidator( - this.conditions, this.subjects, this.statements, signatureTrustEngine, this.signaturePrevalidator); - } - - private Assertion validateAssertion(Assertion assertion, - SAML20AssertionValidator validator, ValidationContext context) { - - ValidationResult result; - try { - result = validator.validate(assertion, context); - } catch (Exception e) { - throw new Saml2Exception("An error occurred while validation the assertion", e); - } - if (result != ValidationResult.VALID) { - throw new Saml2Exception("An error occurred while validating the assertion: " + - context.getValidationFailureMessage()); - } - return assertion; - } - - private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion) - throws Saml2AuthenticationException { - - Saml2AuthenticationException last = null; - List decryptionCredentials = getDecryptionCredentials(token); - if (decryptionCredentials.isEmpty()) { - throw authException(DECRYPTION_ERROR, "No valid decryption credentials found."); - } - for (Saml2X509Credential key : decryptionCredentials) { - Decrypter decrypter = getDecrypter(key); - try { - return decrypter.decrypt(assertion); - } - catch (DecryptionException e) { - last = authException(DECRYPTION_ERROR, e.getMessage(), e); - } - } - throw last; - } - - private Decrypter getDecrypter(Saml2X509Credential key) { - Credential credential = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); - KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(credential); - Decrypter decrypter = new Decrypter(null, resolver, this.saml.getEncryptedKeyResolver()); - decrypter.setRootInNewDocument(true); - return decrypter; - } - - private List getDecryptionCredentials(Saml2AuthenticationToken token) { - List result = new LinkedList<>(); - for (Saml2X509Credential c : token.getX509Credentials()) { - if (c.isDecryptionCredential()) { - result.add(c); - } - } - return result; - } - - private List getVerificationCertificates(Saml2AuthenticationToken token) { - List result = new LinkedList<>(); - for (Saml2X509Credential c : token.getX509Credentials()) { - if (c.isSignatureVerficationCredential()) { - result.add(c.getCertificate()); - } - } - return result; - } - - private String getUsername(Saml2AuthenticationToken token, Assertion assertion) - throws Saml2AuthenticationException { - - String username = null; - Subject subject = assertion.getSubject(); - if (subject == null) { - throw authException(SUBJECT_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a subject"); - } - if (subject.getNameID() != null) { - username = subject.getNameID().getValue(); - } - else if (subject.getEncryptedID() != null) { - NameID nameId = decrypt(token, subject.getEncryptedID()); - username = nameId.getValue(); - } - if (username == null) { - throw authException(USERNAME_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a user identifier"); - } - return username; - } - - private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) - throws Saml2AuthenticationException { - - Saml2AuthenticationException last = null; - List decryptionCredentials = getDecryptionCredentials(token); - if (decryptionCredentials.isEmpty()) { - throw authException(DECRYPTION_ERROR, "No valid decryption credentials found."); - } - for (Saml2X509Credential key : decryptionCredentials) { - Decrypter decrypter = getDecrypter(key); - try { - return (NameID) decrypter.decrypt(assertion); - } - catch (DecryptionException e) { - last = authException(DECRYPTION_ERROR, e.getMessage(), e); - } - } - throw last; + NameID nameId = this.principalDecrypter.apply(token).convert(assertion.getSubject().getEncryptedID()); + assertion.getSubject().setNameID(nameId); + return nameId; } private Map> getAssertionAttributes(Assertion assertion) { @@ -562,17 +429,344 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi return xsAny.getTextContent(); } - private Saml2Error validationError(String code, String description) { + private static Function>> + validator(Collection>>> validators) { + return token -> response -> { + Map errors = new LinkedHashMap<>(); + for (Function>> validator : validators) { + errors.putAll(validator.apply(token).convert(response)); + } + return errors; + }; + } + + private static class ResponseSignatureValidator implements + Function>> { + + private final SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); + + @Override + public Converter> apply(Saml2AuthenticationToken token) { + return response -> { + Map validationExceptions = new LinkedHashMap<>(); + String issuer = response.getIssuer().getValue(); + if (response.isSigned()) { + try { + this.profileValidator.validate(response.getSignature()); + } catch (Exception e) { + validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]: ", e)); + } + + 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 (!buildSignatureTrustEngine(token).validate(response.getSignature(), criteriaSet)) { + validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]")); + } + } catch (Exception e) { + validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]: ", e)); + } + } + + return validationExceptions; + }; + } + + private SignatureTrustEngine buildSignatureTrustEngine(Saml2AuthenticationToken token) { + Set credentials = new HashSet<>(); + for (Saml2X509Credential key : token.getX509Credentials()) { + if (!key.isSignatureVerficationCredential()) { + continue; + } + BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(token.getIdpEntityId()); + credentials.add(cred); + } + CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); + return new ExplicitKeySignatureTrustEngine( + credentialsResolver, + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver() + ); + } + } + + private static class ResponseValidator + implements Function>> { + + @Override + public Converter> apply(Saml2AuthenticationToken token) { + return response -> { + Map validationExceptions = new LinkedHashMap<>(); + + String destination = response.getDestination(); + if (StringUtils.hasText(destination) && !destination.equals(token.getRecipientUri())) { + String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID() + "]"; + validationExceptions.put(INVALID_DESTINATION, authException(INVALID_DESTINATION, message)); + } + + String issuer = response.getIssuer().getValue(); + String assertingPartyEntityId = token.getIdpEntityId(); + if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) { + String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID()); + validationExceptions.put(INVALID_ISSUER, authException(INVALID_ISSUER, message)); + } + + return validationExceptions; + }; + } + } + + private static class AssertionDecrypter + implements Function> { + private final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( + asList( + new InlineEncryptedKeyResolver(), + new EncryptedElementTypeEncryptedKeyResolver(), + new SimpleRetrievalMethodEncryptedKeyResolver() + ) + ); + + @Override + public Converter apply(Saml2AuthenticationToken token) { + return encrypted -> { + Saml2AuthenticationException last = + authException(DECRYPTION_ERROR, "No valid decryption credentials found."); + List decryptionCredentials = token.getX509Credentials(); + for (Saml2X509Credential key : decryptionCredentials) { + if (!key.isDecryptionCredential()) { + continue; + } + Decrypter decrypter = getDecrypter(key); + try { + return decrypter.decrypt(encrypted); + } catch (DecryptionException e) { + last = authException(DECRYPTION_ERROR, e.getMessage(), e); + } + } + throw last; + }; + } + + private Decrypter getDecrypter(Saml2X509Credential key) { + Credential credential = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); + KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(credential); + Decrypter decrypter = new Decrypter(null, resolver, this.encryptedKeyResolver); + decrypter.setRootInNewDocument(true); + return decrypter; + } + } + + private static class AssertionSignatureValidator + implements Function>> { + + private final SignaturePrevalidator signaturePrevalidator = new SAMLSignatureProfileValidator(); + + @Override + public Converter> apply(Saml2AuthenticationToken token) { + return assertion -> { + Map validationExceptions = new LinkedHashMap<>(); + try { + ValidationContext context = buildValidationContext(); + ValidationResult result = buildSamlAssertionValidator(token).validate(assertion, context); + if (result != ValidationResult.VALID) { + String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", + assertion.getID(), ((Response) assertion.getParent()).getID(), + context.getValidationFailureMessage()); + validationExceptions.put(INVALID_ASSERTION, authException(INVALID_ASSERTION, message)); + } + } catch (Exception e) { + String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", + assertion.getID(), ((Response) assertion.getParent()).getID(), + e.getMessage()); + validationExceptions.put(INVALID_ASSERTION, authException(INVALID_ASSERTION, message, e)); + } + return validationExceptions; + }; + } + + private ValidationContext buildValidationContext() { + Map validationParams = new HashMap<>(); + validationParams.put(SIGNATURE_REQUIRED, Boolean.FALSE); // this requirement is checked earlier + return new ValidationContext(validationParams); + } + + private SAML20AssertionValidator buildSamlAssertionValidator(Saml2AuthenticationToken token) { + SignatureTrustEngine signatureTrustEngine = buildSignatureTrustEngine(token); + return new SAML20AssertionValidator( + Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), signatureTrustEngine, signaturePrevalidator) { + @Nonnull + @Override + protected ValidationResult validateConditions(@Nonnull Assertion assertion, @Nonnull ValidationContext context) { + return ValidationResult.VALID; + } + + @Nonnull + @Override + protected ValidationResult validateSubjectConfirmation(@Nonnull Assertion assertion, @Nonnull ValidationContext context) { + return ValidationResult.VALID; + } + + @Nonnull + @Override + protected ValidationResult validateStatements(@Nonnull Assertion assertion, @Nonnull ValidationContext context) { + return ValidationResult.VALID; + } + }; + } + + private SignatureTrustEngine buildSignatureTrustEngine(Saml2AuthenticationToken token) { + Set credentials = new HashSet<>(); + for (Saml2X509Credential key : token.getX509Credentials()) { + if (!key.isSignatureVerficationCredential()) continue; + BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(token.getIdpEntityId()); + credentials.add(cred); + } + CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); + return new ExplicitKeySignatureTrustEngine( + credentialsResolver, + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver() + ); + } + } + + private static class AssertionValidator + implements Function>> { + + private final Function validationContextResolver; + private final Function assertionValidatorResolver; + + AssertionValidator(Function assertionValidatorResolver, + Function validationContextResolver) { + + this.validationContextResolver = validationContextResolver; + this.assertionValidatorResolver = assertionValidatorResolver; + } + + @Override + public Converter> apply(Saml2AuthenticationToken token) { + return assertion -> { + Map validationExceptions = new LinkedHashMap<>(); + try { + ValidationContext context = this.validationContextResolver.apply(token); + ValidationResult result = this.assertionValidatorResolver.apply(token).validate(assertion, context); + if (result != ValidationResult.VALID) { + String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", + assertion.getID(), ((Response) assertion.getParent()).getID(), + context.getValidationFailureMessage()); + validationExceptions.put(INVALID_ASSERTION, authException(INVALID_ASSERTION, message)); + } + } catch (Exception e) { + String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", + assertion.getID(), ((Response) assertion.getParent()).getID(), + e.getMessage()); + validationExceptions.put(INVALID_ASSERTION, authException(INVALID_ASSERTION, message, e)); + } + return validationExceptions; + }; + } + + private static class Builder { + private final Collection conditions = new ArrayList<>(); + private final Collection subjects = new ArrayList<>(); + private final Collection statements = new ArrayList<>(); + private final Map validationContextParameters = new HashMap<>(); + + Builder() { + this.conditions.add(new AudienceRestrictionConditionValidator()); + this.subjects.add(new BearerSubjectConfirmationValidator() { + @Nonnull + @Override + protected ValidationResult validateAddress(@Nonnull SubjectConfirmation confirmation, + @Nonnull Assertion assertion, @Nonnull ValidationContext context) { + // skipping address validation - gh-7514 + return ValidationResult.VALID; + } + }); + } + + public AssertionValidator.Builder validationContext( + Consumer> validationContextParameters) { + validationContextParameters.accept(this.validationContextParameters); + return this; + } + + public AssertionValidator build() { + return new AssertionValidator( + token -> new SAML20AssertionValidator(this.conditions, this.subjects, this.statements, null, null) { + @Nonnull + @Override + protected ValidationResult validateSignature(@Nonnull Assertion token, @Nonnull ValidationContext context) { + return ValidationResult.VALID; + } + }, + token -> { + Map params = new HashMap<>(); + params.put(CLOCK_SKEW, Duration.ofMinutes(5).toMillis()); + params.put(COND_VALID_AUDIENCES, singleton(token.getIdpEntityId())); + params.put(SC_VALID_RECIPIENTS, singleton(token.getRecipientUri())); + params.putAll(this.validationContextParameters); + return new ValidationContext(params); + }); + } + } + } + + private static class PrincipalDecrypter implements Function> { + private final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( + asList( + new InlineEncryptedKeyResolver(), + new EncryptedElementTypeEncryptedKeyResolver(), + new SimpleRetrievalMethodEncryptedKeyResolver() + ) + ); + + @Override + public Converter apply(Saml2AuthenticationToken token) { + return encrypted -> { + Saml2AuthenticationException last = + authException(DECRYPTION_ERROR, "No valid decryption credentials found."); + List decryptionCredentials = token.getX509Credentials(); + for (Saml2X509Credential key : decryptionCredentials) { + if (!key.isDecryptionCredential()) continue; + Decrypter decrypter = getDecrypter(key); + try { + return (NameID) decrypter.decrypt(encrypted); + } catch (DecryptionException e) { + last = authException(DECRYPTION_ERROR, e.getMessage(), e); + } + } + throw last; + }; + } + + private Decrypter getDecrypter(Saml2X509Credential key) { + Credential credential = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); + KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(credential); + Decrypter decrypter = new Decrypter(null, resolver, this.encryptedKeyResolver); + decrypter.setRootInNewDocument(true); + return decrypter; + } + } + + private static Saml2Error validationError(String code, String description) { return new Saml2Error(code, description); } - private Saml2AuthenticationException authException(String code, String description) + private static Saml2AuthenticationException authException(String code, String description) throws Saml2AuthenticationException { return new Saml2AuthenticationException(validationError(code, description)); } - private Saml2AuthenticationException authException(String code, String description, Exception cause) + private static Saml2AuthenticationException authException(String code, String description, Exception cause) throws Saml2AuthenticationException { return new Saml2AuthenticationException(validationError(code, description), cause); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java index 35e5a1756b..64f5946dba 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java @@ -186,7 +186,7 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenUsernameMissingThenThrowAuthenticationException() throws Exception { - this.exception.expect(authenticationMatcher(Saml2ErrorCodes.USERNAME_NOT_FOUND)); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.SUBJECT_NOT_FOUND)); Response response = response(); Assertion assertion = assertion(); diff --git a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java index c2b8abde21..5c8fe54b3e 100644 --- a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java +++ b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java @@ -16,6 +16,18 @@ package org.springframework.security.saml2.provider.service.authentication; +import java.io.ByteArrayInputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.KeyException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.UUID; +import javax.servlet.http.HttpSession; + import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.xml.BasicParserPool; import net.shibboleth.utilities.java.support.xml.SerializeSupport; @@ -47,6 +59,9 @@ import org.opensaml.xmlsec.SignatureSigningParameters; import org.opensaml.xmlsec.signature.support.SignatureConstants; import org.opensaml.xmlsec.signature.support.SignatureException; import org.opensaml.xmlsec.signature.support.SignatureSupport; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -60,20 +75,6 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import javax.servlet.http.HttpSession; -import java.io.ByteArrayInputStream; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.security.KeyException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.UUID; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.Matchers.containsString; @@ -287,9 +288,9 @@ public class Saml2LoginIntegrationTests { .andExpect(unauthenticated()) .andExpect( saml2AuthenticationExceptionMatcher( - "invalid_issuer", + "invalid_signature", containsString( - "Invalid issuer [invalid issuer] for SAML response" + "Invalid signature for SAML Response" ) ) );