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 a67d36e98d6..185da0398bf 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 @@ -16,16 +16,12 @@ package org.springframework.security.saml2.provider.service.authentication; import org.springframework.core.convert.converter.Converter; -import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.credentials.Saml2X509Credential; import org.springframework.util.Assert; @@ -79,18 +75,59 @@ import static java.lang.String.format; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; +import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.DECRYPTION_ERROR; +import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_DESTINATION; +import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_ISSUER; +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; import static org.springframework.util.StringUtils.hasText; /** + * Implementation of {@link AuthenticationProvider} for SAML authentications when receiving a + * {@code Response} object containing an {@code Assertion}. This implementation uses + * the {@code OpenSAML 3} library. + * + *

+ * The {@link OpenSamlAuthenticationProvider} 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). + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * This provider supports two types of encrypted SAML elements + *

+ * If the assertion is encrypted, then signature validation on the assertion is no longer required. + *

+ *

+ * This provider does not perform an X509 certificate validation on the configured asserting party, IDP, verification + * certificates. + *

* @since 5.2 + * @see SAML 2 StatusResponse + * @see OpenSAML 3 */ public final class OpenSamlAuthenticationProvider implements AuthenticationProvider { private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class); private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); - private Converter> authoritiesExtractor = (a -> singletonList(new SimpleGrantedAuthority("ROLE_USER"))); + private Converter> authoritiesExtractor = + (a -> singletonList(new SimpleGrantedAuthority("ROLE_USER"))); private GrantedAuthoritiesMapper authoritiesMapper = (a -> a); private Duration responseTimeValidationSkew = Duration.ofMinutes(5); @@ -136,22 +173,17 @@ public void setResponseTimeValidationSkew(Duration responseTimeValidationSkew) { public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication; - String xml = token.getSaml2Response(); - Response samlResponse = getSaml2Response(xml); - + Response samlResponse = getSaml2Response(token); Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse); - final String username = getUsername(token, assertion); - if (username == null) { - throw new UsernameNotFoundException("Assertion [" + - assertion.getID() + - "] is missing a user identifier"); - } + String username = getUsername(token, assertion); return new Saml2Authentication( () -> username, token.getSaml2Response(), this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion)) ); - }catch (Saml2Exception | IllegalArgumentException e) { - throw new AuthenticationServiceException(e.getMessage(), e); + } catch (Saml2AuthenticationException e) { + throw e; + } catch (Exception e) { + throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e); } } @@ -167,93 +199,116 @@ private Collection getAssertionAuthorities(Assertion return this.authoritiesExtractor.convert(assertion); } - private String getUsername(Saml2AuthenticationToken token, Assertion assertion) { - final Subject subject = assertion.getSubject(); + private String getUsername(Saml2AuthenticationToken token, Assertion assertion) throws Saml2AuthenticationException { + String username = null; + Subject subject = assertion.getSubject(); if (subject == null) { - return null; + throw authException(SUBJECT_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a subject"); } if (subject.getNameID() != null) { - return subject.getNameID().getValue(); + username = subject.getNameID().getValue(); } - if (subject.getEncryptedID() != null) { + else if (subject.getEncryptedID() != null) { NameID nameId = decrypt(token, subject.getEncryptedID()); - return nameId.getValue(); + username = nameId.getValue(); } - return null; + if (username == null) { + throw authException(USERNAME_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a user identifier"); + } + return username; } private Assertion validateSaml2Response(Saml2AuthenticationToken token, String recipient, - Response samlResponse) throws AuthenticationException { + Response samlResponse) throws Saml2AuthenticationException { + //optional validation if the response contains a destination if (hasText(samlResponse.getDestination()) && !recipient.equals(samlResponse.getDestination())) { - throw new Saml2Exception("Invalid SAML response destination: " + samlResponse.getDestination()); + throw authException(INVALID_DESTINATION, "Invalid SAML response destination: " + samlResponse.getDestination()); } - final String issuer = samlResponse.getIssuer().getValue(); + String issuer = samlResponse.getIssuer().getValue(); if (logger.isDebugEnabled()) { - logger.debug("Processing SAML response from " + issuer); + logger.debug("Validating SAML response from " + issuer); } - if (token == null) { - throw new Saml2Exception(format("SAML 2 Provider for %s was not found.", issuer)); + if (!hasText(issuer) || (!issuer.equals(token.getIdpEntityId()))) { + String message = String.format("Response issuer '%s' doesn't match '%s'", issuer, token.getIdpEntityId()); + throw authException(INVALID_ISSUER, message); } + Saml2AuthenticationException lastValidationError = null; + boolean responseSigned = hasValidSignature(samlResponse, token); for (Assertion a : samlResponse.getAssertions()) { if (logger.isDebugEnabled()) { logger.debug("Checking plain assertion validity " + a); } - if (isValidAssertion(recipient, a, token, !responseSigned)) { - if (logger.isDebugEnabled()) { - logger.debug("Found valid assertion. Skipping potential others."); - } + try { + validateAssertion(recipient, a, token, !responseSigned); return a; + } catch (Saml2AuthenticationException e) { + lastValidationError = e; } } for (EncryptedAssertion ea : samlResponse.getEncryptedAssertions()) { if (logger.isDebugEnabled()) { logger.debug("Checking encrypted assertion validity " + ea); } - - Assertion a = decrypt(token, ea); - if (isValidAssertion(recipient, a, token, false)) { - if (logger.isDebugEnabled()) { - logger.debug("Found valid encrypted assertion. Skipping potential others."); - } + try { + Assertion a = decrypt(token, ea); + validateAssertion(recipient, a, token, false); return a; + } catch (Saml2AuthenticationException e) { + lastValidationError = e; } } - throw new InsufficientAuthenticationException("Unable to find a valid assertion"); + if (lastValidationError != null) { + throw lastValidationError; + } + else { + throw authException(MALFORMED_RESPONSE_DATA, "No assertions found in response."); + } } - private boolean hasValidSignature(SignableSAMLObject samlResponse, Saml2AuthenticationToken token) { - if (!samlResponse.isSigned()) { + private boolean hasValidSignature(SignableSAMLObject samlObject, Saml2AuthenticationToken token) { + if (!samlObject.isSigned()) { + if (logger.isDebugEnabled()) { + logger.debug("SAML object is not signed, no signatures found"); + } return false; } - final List verificationKeys = getVerificationKeys(token); + List verificationKeys = getVerificationCertificates(token); if (verificationKeys.isEmpty()) { return false; } - for (X509Certificate key : verificationKeys) { - final Credential credential = getVerificationCredential(key); + for (X509Certificate certificate : verificationKeys) { + Credential credential = getVerificationCredential(certificate); try { - SignatureValidator.validate(samlResponse.getSignature(), credential); + SignatureValidator.validate(samlObject.getSignature(), credential); + if (logger.isDebugEnabled()) { + logger.debug("Valid signature found in SAML object:"+samlObject.getClass().getName()); + } return true; } catch (SignatureException ignored) { - logger.debug("Signature validation failed", ignored); + if (logger.isTraceEnabled()) { + logger.trace("Signature validation failed with cert:"+certificate.toString(), ignored); + } + else if (logger.isDebugEnabled()) { + logger.debug("Signature validation failed with cert:"+certificate.toString()); + } } } return false; } - private boolean isValidAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) { - final SAML20AssertionValidator validator = getAssertionValidator(token); + private void validateAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) { + SAML20AssertionValidator validator = getAssertionValidator(token); Map validationParams = new HashMap<>(); validationParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false); validationParams.put( SAML2AssertionValidationParameters.CLOCK_SKEW, - this.responseTimeValidationSkew + this.responseTimeValidationSkew.toMillis() ); validationParams.put( SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, @@ -267,55 +322,78 @@ private boolean isValidAssertion(String recipient, Assertion a, Saml2Authenticat if (logger.isDebugEnabled()) { logger.debug(format("Assertion [%s] does not a valid signature.", a.getID())); } - return false; + throw authException(Saml2ErrorCodes.INVALID_SIGNATURE, "Assertion doesn't have a valid signature."); } + //ensure that OpenSAML doesn't attempt signature validation, already performed a.setSignature(null); - // validation for recipient + //remainder of assertion validation ValidationContext vctx = new ValidationContext(validationParams); try { - final ValidationResult result = validator.validate(a, vctx); - final boolean valid = result.equals(ValidationResult.VALID); + ValidationResult result = validator.validate(a, vctx); + boolean valid = result.equals(ValidationResult.VALID); if (!valid) { if (logger.isDebugEnabled()) { - logger.debug(format("Failed to validate assertion from %s with user %s", token.getIdpEntityId(), - getUsername(token, a) - )); + logger.debug(format("Failed to validate assertion from %s", token.getIdpEntityId())); } + throw authException(Saml2ErrorCodes.INVALID_ASSERTION, vctx.getValidationFailureMessage()); } - return valid; } catch (AssertionValidationException e) { if (logger.isDebugEnabled()) { logger.debug("Failed to validate assertion:", e); } - return false; + throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e); } } - private Response getSaml2Response(String xml) throws Saml2Exception, AuthenticationException { - final Object result = this.saml.resolve(xml); - if (result == null) { - throw new AuthenticationCredentialsNotFoundException("SAMLResponse returned null object"); - } - else if (result instanceof Response) { - return (Response) result; + private Response getSaml2Response(Saml2AuthenticationToken token) throws Saml2Exception, Saml2AuthenticationException { + try { + Object result = this.saml.resolve(token.getSaml2Response()); + if (result instanceof Response) { + return (Response) result; + } + else { + throw authException(UNKNOWN_RESPONSE_CLASS, "Invalid response class:" + result.getClass().getName()); + } + } catch (Saml2Exception x) { + throw authException(MALFORMED_RESPONSE_DATA, x.getMessage(), x); } - throw new IllegalArgumentException("Invalid response class:"+result.getClass().getName()); + + } + + private Saml2Error validationError(String code, String description) { + return new Saml2Error( + code, + description + ); + } + + private Saml2AuthenticationException authException(String code, String description) throws Saml2AuthenticationException { + return new Saml2AuthenticationException( + validationError(code, description) + ); + } + + + private Saml2AuthenticationException authException(String code, String description, Exception cause) throws Saml2AuthenticationException { + return new Saml2AuthenticationException( + validationError(code, description), + cause + ); } private SAML20AssertionValidator getAssertionValidator(Saml2AuthenticationToken provider) { List conditions = Collections.singletonList(new AudienceRestrictionConditionValidator()); - final BearerSubjectConfirmationValidator subjectConfirmationValidator = - new BearerSubjectConfirmationValidator(); + BearerSubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator(); List subjects = Collections.singletonList(subjectConfirmationValidator); List statements = Collections.emptyList(); Set credentials = new HashSet<>(); - for (X509Certificate key : getVerificationKeys(provider)) { - final Credential cred = getVerificationCredential(key); + for (X509Certificate key : getVerificationCertificates(provider)) { + Credential cred = getVerificationCredential(key); credentials.add(cred); } CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); @@ -345,37 +423,38 @@ private Decrypter getDecrypter(Saml2X509Credential key) { return decrypter; } - private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion) { - Saml2Exception last = null; + private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion) + throws Saml2AuthenticationException { + Saml2AuthenticationException last = null; List decryptionCredentials = getDecryptionCredentials(token); if (decryptionCredentials.isEmpty()) { - throw new Saml2Exception("No valid decryption credentials found."); + throw authException(DECRYPTION_ERROR, "No valid decryption credentials found."); } for (Saml2X509Credential key : decryptionCredentials) { - final Decrypter decrypter = getDecrypter(key); + Decrypter decrypter = getDecrypter(key); try { return decrypter.decrypt(assertion); } catch (DecryptionException e) { - last = new Saml2Exception(e); + last = authException(DECRYPTION_ERROR, e.getMessage(), e); } } throw last; } - private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) { - Saml2Exception last = null; + private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) throws Saml2AuthenticationException { + Saml2AuthenticationException last = null; List decryptionCredentials = getDecryptionCredentials(token); if (decryptionCredentials.isEmpty()) { - throw new Saml2Exception("No valid decryption credentials found."); + throw authException(DECRYPTION_ERROR, "No valid decryption credentials found."); } for (Saml2X509Credential key : decryptionCredentials) { - final Decrypter decrypter = getDecrypter(key); + Decrypter decrypter = getDecrypter(key); try { return (NameID) decrypter.decrypt(assertion); } catch (DecryptionException e) { - last = new Saml2Exception(e); + last = authException(DECRYPTION_ERROR, e.getMessage(), e); } } throw last; @@ -391,7 +470,7 @@ private List getDecryptionCredentials(Saml2AuthenticationTo return result; } - private List getVerificationKeys(Saml2AuthenticationToken token) { + private List getVerificationCertificates(Saml2AuthenticationToken token) { List result = new LinkedList<>(); for (Saml2X509Credential c : token.getX509Credentials()) { if (c.isSignatureVerficationCredential()) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java index 6b5d68f32bb..1c494ef0a2a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java @@ -19,11 +19,8 @@ import org.springframework.util.Assert; import org.joda.time.DateTime; -import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.Issuer; -import org.opensaml.security.SecurityException; -import org.opensaml.xmlsec.signature.support.SignatureException; import java.time.Clock; import java.time.Instant; @@ -51,16 +48,11 @@ public String createAuthenticationRequest(Saml2AuthenticationRequest request) { issuer.setValue(request.getLocalSpEntityId()); auth.setIssuer(issuer); auth.setDestination(request.getWebSsoUri()); - try { - return this.saml.toXml( - auth, - request.getCredentials(), - request.getLocalSpEntityId() - ); - } - catch (MarshallingException | SignatureException | SecurityException e) { - throw new IllegalStateException(e); - } + return this.saml.toXml( + auth, + request.getCredentials(), + request.getLocalSpEntityId() + ); } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java index 574c7362aba..67d2fb2cc4a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java @@ -15,14 +15,6 @@ */ package org.springframework.security.saml2.provider.service.authentication; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.xml.XMLConstants; -import javax.xml.namespace.QName; - import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.credentials.Saml2X509Credential; @@ -59,6 +51,14 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; + import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Arrays.asList; @@ -165,10 +165,7 @@ T buildSAMLObject(final Class clazz) { QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); return (T) getBuilderFactory().getBuilder(defaultElementName).buildObject(defaultElementName); } - catch (IllegalAccessException e) { - throw new Saml2Exception("Could not create SAML object", e); - } - catch (NoSuchFieldException e) { + catch (NoSuchFieldException | IllegalAccessException e) { throw new Saml2Exception("Could not create SAML object", e); } } @@ -177,6 +174,28 @@ XMLObject resolve(String xml) { return resolve(xml.getBytes(StandardCharsets.UTF_8)); } + String toXml(XMLObject object, List signingCredentials, String localSpEntityId) { + if (object instanceof SignableSAMLObject && null != hasSigningCredential(signingCredentials)) { + signXmlObject( + (SignableSAMLObject) object, + signingCredentials, + localSpEntityId + ); + } + final MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory(); + try { + Element element = marshallerFactory.getMarshaller(object).marshall(object); + return SerializeSupport.nodeToString(element); + } catch (MarshallingException e) { + throw new Saml2Exception(e); + } + } + + /* + * ============================================================== + * PRIVATE METHODS + * ============================================================== + */ private XMLObject resolve(byte[] xml) { XMLObject parsed = parse(xml); if (parsed != null) { @@ -200,18 +219,6 @@ private UnmarshallerFactory getUnmarshallerFactory() { return XMLObjectProviderRegistrySupport.getUnmarshallerFactory(); } - String toXml(XMLObject object, List signingCredentials, String localSpEntityId) - throws MarshallingException, SignatureException, SecurityException { - if (object instanceof SignableSAMLObject && null != hasSigningCredential(signingCredentials)) { - signXmlObject( - (SignableSAMLObject) object, - getSigningCredential(signingCredentials, localSpEntityId) - ); - } - final MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory(); - Element element = marshallerFactory.getMarshaller(object).marshall(object); - return SerializeSupport.nodeToString(element); - } private Saml2X509Credential hasSigningCredential(List credentials) { for (Saml2X509Credential c : credentials) { @@ -222,22 +229,12 @@ private Saml2X509Credential hasSigningCredential(List crede return null; } - private void signXmlObject(SignableSAMLObject object, Credential credential) - throws MarshallingException, SecurityException, SignatureException { - SignatureSigningParameters parameters = new SignatureSigningParameters(); - parameters.setSigningCredential(credential); - parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); - parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); - parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); - SignatureSupport.signObject(object, parameters); - } - private Credential getSigningCredential(List signingCredential, String localSpEntityId ) { Saml2X509Credential credential = hasSigningCredential(signingCredential); if (credential == null) { - throw new IllegalArgumentException("no signing credential configured"); + throw new Saml2Exception("no signing credential configured"); } BasicCredential cred = getBasicCredential(credential); cred.setEntityId(localSpEntityId); @@ -245,6 +242,21 @@ private Credential getSigningCredential(List signingCredent return cred; } + private void signXmlObject(SignableSAMLObject object, List signingCredentials, String entityId) { + SignatureSigningParameters parameters = new SignatureSigningParameters(); + Credential credential = getSigningCredential(signingCredentials, entityId); + parameters.setSigningCredential(credential); + parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); + parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); + parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + try { + SignatureSupport.signObject(object, parameters); + } catch (MarshallingException | SignatureException | SecurityException e) { + throw new Saml2Exception(e); + } + + } + private BasicX509Credential getBasicCredential(Saml2X509Credential credential) { return CredentialSupport.getSimpleCredential( credential.getCertificate(), diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java new file mode 100644 index 00000000000..86709cec1e8 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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 org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.util.Assert; + +/** + * This exception is thrown for all SAML 2.0 related {@link Authentication} errors. + * + *

+ * There are a number of scenarios where an error may occur, for example: + *

    + *
  • The response or assertion request is missing or malformed
  • + *
  • Missing or invalid subject
  • + *
  • Missing or invalid signatures
  • + *
  • The time period validation for the assertion fails
  • + *
  • One of the assertion conditions was not met
  • + *
  • Decryption failed
  • + *
  • Unable to locate a subject identifier, commonly known as username
  • + *
+ * + * @since 5.2 + */ +public class Saml2AuthenticationException extends AuthenticationException { + private Saml2Error error; + + /** + * Constructs a {@code Saml2AuthenticationException} using the provided parameters. + * + * @param error the {@link Saml2Error SAML 2.0 Error} + */ + public Saml2AuthenticationException(Saml2Error error) { + this(error, error.getDescription()); + } + + /** + * Constructs a {@code Saml2AuthenticationException} using the provided parameters. + * + * @param error the {@link Saml2Error SAML 2.0 Error} + * @param cause the root cause + */ + public Saml2AuthenticationException(Saml2Error error, Throwable cause) { + this(error, cause.getMessage(), cause); + } + + /** + * Constructs a {@code Saml2AuthenticationException} using the provided parameters. + * + * @param error the {@link Saml2Error SAML 2.0 Error} + * @param message the detail message + */ + public Saml2AuthenticationException(Saml2Error error, String message) { + super(message); + this.setError(error); + } + + /** + * Constructs a {@code Saml2AuthenticationException} using the provided parameters. + * + * @param error the {@link Saml2Error SAML 2.0 Error} + * @param message the detail message + * @param cause the root cause + */ + public Saml2AuthenticationException(Saml2Error error, String message, Throwable cause) { + super(message, cause); + this.setError(error); + } + + /** + * Returns the {@link Saml2Error SAML 2.0 Error}. + * + * @return the {@link Saml2Error} + */ + public Saml2Error getError() { + return this.error; + } + + private void setError(Saml2Error error) { + Assert.notNull(error, "error cannot be null"); + this.error = error; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Saml2AuthenticationException{"); + sb.append("error=").append(error); + sb.append('}'); + return sb.toString(); + } +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java index a77a4e06a6d..35edb477b1a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java @@ -16,6 +16,8 @@ package org.springframework.security.saml2.provider.service.authentication; +import org.springframework.security.saml2.Saml2Exception; + /** * Component that generates an AuthenticationRequest, samlp:AuthnRequestType as defined by * https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf @@ -33,6 +35,7 @@ public interface Saml2AuthenticationRequestFactory { * accompanying data * @return XML data in the format of a String. This data may be signed, encrypted, both signed and encrypted or * neither signed and encrypted + * @throws Saml2Exception when a SAML library exception occurs */ String createAuthenticationRequest(Saml2AuthenticationRequest request); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java new file mode 100644 index 00000000000..786ae0e0119 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2019 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 org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.util.Assert; + +import java.io.Serializable; + +/** + * A representation of an SAML 2.0 Error. + * + *

+ * At a minimum, an error response will contain an error code. + * The commonly used error code are defined in this class + * or a new codes can be defined in the future as arbitrary strings. + *

+ * @since 5.2 + */ +public class Saml2Error implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private final String errorCode; + private final String description; + + /** + * Constructs a {@code Saml2Error} using the provided parameters. + * + * @param errorCode the error code + * @param description the error description + */ + public Saml2Error(String errorCode, String description) { + Assert.hasText(errorCode, "errorCode cannot be empty"); + this.errorCode = errorCode; + this.description = description; + } + + /** + * Returns the error code. + * + * @return the error code + */ + public final String getErrorCode() { + return this.errorCode; + } + + /** + * Returns the error description. + * + * @return the error description + */ + public final String getDescription() { + return this.description; + } + + @Override + public String toString() { + return "[" + this.getErrorCode() + "] " + + (this.getDescription() != null ? this.getDescription() : ""); + } +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java new file mode 100644 index 00000000000..2b98a5334c9 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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; + +/** + * A list of SAML known 2 error codes used during SAML authentication. + * + * @since 5.2 + */ +public interface Saml2ErrorCodes { + /** + * SAML Data does not represent a SAML 2 Response object. + * A valid XML object was received, but that object was not a + * SAML 2 Response object of type {@code ResponseType} per specification + * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=46 + */ + String UNKNOWN_RESPONSE_CLASS = "unknown_response_class"; + /** + * The response data is malformed or incomplete. + * An invalid XML object was received, and XML unmarshalling failed. + */ + String MALFORMED_RESPONSE_DATA = "malformed_response_data"; + /** + * Response destination does not match the request URL. + * A SAML 2 response object was received at a URL that + * did not match the URL stored in the {code Destination} attribute + * in the Response object. + * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38 + */ + String INVALID_DESTINATION = "invalid_destination"; + /** + * The assertion was not valid. + * The assertion used for authentication failed validation. + * Details around the failure will be present in the error description. + */ + String INVALID_ASSERTION = "invalid_assertion"; + /** + * The signature of response or assertion was invalid. + * Either the response or the assertion was missing a signature + * or the signature could not be verified using the system's + * configured credentials. Most commonly the IDP's + * X509 certificate. + */ + String INVALID_SIGNATURE = "invalid_signature"; + /** + * The assertion did not contain a subject element. + * The subject element, type SubjectType, contains + * a {@code NameID} or an {@code EncryptedID} that is used + * to assign the authenticated principal an identifier, + * typically a username. + * + * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=18 + */ + String SUBJECT_NOT_FOUND = "subject_not_found"; + /** + * The subject did not contain a user identifier + * The assertion contained a subject element, but the subject + * element did not have a {@code NameID} or {@code EncryptedID} + * element + * + * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=18 + */ + String USERNAME_NOT_FOUND = "username_not_found"; + /** + * The system failed to decrypt an assertion or a name identifier. + * This error code will be thrown if the decryption of either a + * {@code EncryptedAssertion} or {@code EncryptedID} fails. + * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=17 + */ + String DECRYPTION_ERROR = "decryption_error"; + /** + * An Issuer element contained a value that didn't + * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=15 + */ + String INVALID_ISSUER = "invalid_issuer"; + /** + * An error happened during validation. + * Used when internal, non classified, errors are caught during the + * authentication process. + */ + String INTERNAL_VALIDATION_ERROR = "internal_validation_error"; +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java new file mode 100644 index 00000000000..292619040dd --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2019 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.credentials; + +import org.springframework.security.converter.RsaKeyConverters; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.ByteArrayInputStream; +import java.security.PrivateKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION; + +public class Saml2X509CredentialTests { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private Saml2X509Credential credential; + private PrivateKey key; + private X509Certificate certificate; + + @Before + public void setup() throws Exception { + String keyData = "-----BEGIN PRIVATE KEY-----\n" + + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" + + "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" + + "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" + + "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" + + "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" + + "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" + + "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" + + "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" + + "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" + + "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" + + "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" + + "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" + + "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" + + "INrtuLp4YHbgk1mi\n" + + "-----END PRIVATE KEY-----"; + key = RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(keyData.getBytes(UTF_8))); + final CertificateFactory factory = CertificateFactory.getInstance("X.509"); + String certificateData = "-----BEGIN CERTIFICATE-----\n" + + "MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" + + "VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" + + "A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" + + "DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" + + "MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" + + "MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" + + "TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" + + "vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" + + "+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" + + "y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" + + "XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" + + "qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" + + "RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" + + "-----END CERTIFICATE-----"; + certificate = (X509Certificate) factory + .generateCertificate(new ByteArrayInputStream(certificateData.getBytes(UTF_8))); + } + + @Test + public void constructorWhenRelyingPartyWithCredentialsThenItSucceeds() { + new Saml2X509Credential(key, certificate, SIGNING); + new Saml2X509Credential(key, certificate, SIGNING, DECRYPTION); + new Saml2X509Credential(key, certificate, DECRYPTION); + } + + @Test + public void constructorWhenAssertingPartyWithCredentialsThenItSucceeds() { + new Saml2X509Credential(certificate, VERIFICATION); + new Saml2X509Credential(certificate, VERIFICATION, ENCRYPTION); + new Saml2X509Credential(certificate, ENCRYPTION); + } + + @Test + public void constructorWhenRelyingPartyWithoutCredentialsThenItFails() { + exception.expect(IllegalArgumentException.class); + new Saml2X509Credential(null, (X509Certificate) null, SIGNING); + } + + @Test + public void constructorWhenRelyingPartyWithoutPrivateKeyThenItFails() { + exception.expect(IllegalArgumentException.class); + new Saml2X509Credential(null, certificate, SIGNING); + } + + @Test + public void constructorWhenRelyingPartyWithoutCertificateThenItFails() { + exception.expect(IllegalArgumentException.class); + new Saml2X509Credential(key, null, SIGNING); + } + + @Test + public void constructorWhenAssertingPartyWithoutCertificateThenItFails() { + exception.expect(IllegalArgumentException.class); + new Saml2X509Credential(null, SIGNING); + } + + @Test + public void constructorWhenRelyingPartyWithEncryptionUsageThenItFails() { + exception.expect(IllegalStateException.class); + new Saml2X509Credential(key, certificate, ENCRYPTION); + } + + @Test + public void constructorWhenRelyingPartyWithVerificationUsageThenItFails() { + exception.expect(IllegalStateException.class); + new Saml2X509Credential(key, certificate, VERIFICATION); + } + + @Test + public void constructorWhenAssertingPartyWithSigningUsageThenItFails() { + exception.expect(IllegalStateException.class); + new Saml2X509Credential(certificate, SIGNING); + } + + @Test + public void constructorWhenAssertingPartyWithDecryptionUsageThenItFails() { + exception.expect(IllegalStateException.class); + new Saml2X509Credential(certificate, DECRYPTION); + } + + +} 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 new file mode 100644 index 00000000000..772ed072f59 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java @@ -0,0 +1,357 @@ +/* + * Copyright 2002-2019 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 org.springframework.security.core.Authentication; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.joda.time.DateTime; +import org.joda.time.Duration; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.core.Assertion; +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 static java.util.Collections.emptyList; +import static org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationObjects.assertion; +import static org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationObjects.response; +import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.encryptAssertion; +import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.encryptNameId; +import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.signXmlObject; +import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.assertingPartyCredentials; +import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials; +import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.springframework.util.StringUtils.hasText; + +public class OpenSamlAuthenticationProviderTests { + + private static String username = "test@saml.user"; + private static String recipientUri = "https://localhost/login/saml2/sso/idp-alias"; + private static String recipientEntityId = "https://localhost/saml2/service-provider-metadata/idp-alias"; + private static String idpEntityId = "https://some.idp.test/saml2/idp"; + + private OpenSamlAuthenticationProvider provider; + private OpenSamlImplementation saml; + private Saml2AuthenticationToken token; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void setup() { + saml = OpenSamlImplementation.getInstance(); + provider = new OpenSamlAuthenticationProvider(); + token = new Saml2AuthenticationToken( + "responseXml", + recipientUri, + idpEntityId, + recipientEntityId, + relyingPartyCredentials() + ); + } + + @Test + public void supportsWhenSaml2AuthenticationTokenThenReturnTrue() { + + assertTrue( + OpenSamlAuthenticationProvider.class + "should support " + token.getClass(), + provider.supports(token.getClass()) + ); + } + + @Test + public void supportsWhenNotSaml2AuthenticationTokenThenReturnFalse() { + assertTrue( + OpenSamlAuthenticationProvider.class + "should not support " + Authentication.class, + !provider.supports(Authentication.class) + ); + } + + @Test + public void authenticateWhenUnknownDataClassThenThrowAuthenticationException() { + Assertion assertion = defaultAssertion(); + token = responseXml(assertion, idpEntityId); + exception.expect(authenticationMatcher(Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS)); + provider.authenticate(token); + } + + @Test + public void authenticateWhenXmlErrorThenThrowAuthenticationException() { + token = new Saml2AuthenticationToken( + "invalid xml string", + recipientUri, + idpEntityId, + recipientEntityId, + relyingPartyCredentials() + ); + exception.expect(authenticationMatcher(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA)); + provider.authenticate(token); + } + + @Test + public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() { + Response response = response(recipientUri + "invalid", idpEntityId); + token = responseXml(response, idpEntityId); + exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_DESTINATION)); + provider.authenticate(token); + } + + @Test + public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() { + Response response = response(recipientUri, idpEntityId); + token = responseXml(response, idpEntityId); + exception.expect( + authenticationMatcher( + Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, + "No assertions found in response." + ) + ); + provider.authenticate(token); + } + + @Test + public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() { + Response response = response(recipientUri, idpEntityId); + Assertion assertion = defaultAssertion(); + response.getAssertions().add(assertion); + token = responseXml(response, idpEntityId); + exception.expect( + authenticationMatcher( + Saml2ErrorCodes.INVALID_SIGNATURE + ) + ); + provider.authenticate(token); + } + + @Test + public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() throws Exception { + Response response = response(recipientUri, idpEntityId); + Assertion assertion = defaultAssertion(); + assertion + .getSubject() + .getSubjectConfirmations() + .get(0) + .getSubjectConfirmationData() + .setNotOnOrAfter(DateTime.now().minus(Duration.standardDays(3))); + signXmlObject( + assertion, + assertingPartyCredentials(), + recipientEntityId + ); + response.getAssertions().add(assertion); + token = responseXml(response, idpEntityId); + + exception.expect( + authenticationMatcher( + Saml2ErrorCodes.INVALID_ASSERTION + ) + ); + provider.authenticate(token); + } + + @Test + public void authenticateWhenMissingSubjectThenThrowAuthenticationException() { + Response response = response(recipientUri, idpEntityId); + Assertion assertion = defaultAssertion(); + assertion.setSubject(null); + signXmlObject( + assertion, + assertingPartyCredentials(), + recipientEntityId + ); + response.getAssertions().add(assertion); + token = responseXml(response, idpEntityId); + + exception.expect( + authenticationMatcher( + Saml2ErrorCodes.SUBJECT_NOT_FOUND + ) + ); + provider.authenticate(token); + } + + @Test + public void authenticateWhenUsernameMissingThenThrowAuthenticationException() throws Exception { + Response response = response(recipientUri, idpEntityId); + Assertion assertion = defaultAssertion(); + assertion + .getSubject() + .getNameID() + .setValue(null); + signXmlObject( + assertion, + assertingPartyCredentials(), + recipientEntityId + ); + response.getAssertions().add(assertion); + token = responseXml(response, idpEntityId); + + exception.expect( + authenticationMatcher( + Saml2ErrorCodes.USERNAME_NOT_FOUND + ) + ); + provider.authenticate(token); + } + + @Test + public void authenticateWhenEncryptedAssertionWithoutSignatureThenItSucceeds() throws Exception { + Response response = response(recipientUri, idpEntityId); + Assertion assertion = defaultAssertion(); + EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); + response.getEncryptedAssertions().add(encryptedAssertion); + token = responseXml(response, idpEntityId); + provider.authenticate(token); + } + + @Test + public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() throws Exception { + Response response = response(recipientUri, idpEntityId); + Assertion assertion = defaultAssertion(); + NameID nameId = assertion.getSubject().getNameID(); + EncryptedID encryptedID = encryptNameId(nameId, assertingPartyCredentials()); + assertion.getSubject().setNameID(null); + assertion.getSubject().setEncryptedID(encryptedID); + signXmlObject( + assertion, + assertingPartyCredentials(), + recipientEntityId + ); + response.getAssertions().add(assertion); + token = responseXml(response, idpEntityId); + provider.authenticate(token); + } + + + @Test + public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() throws Exception { + Response response = response(recipientUri, idpEntityId); + Assertion assertion = defaultAssertion(); + EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); + response.getEncryptedAssertions().add(encryptedAssertion); + token = responseXml(response, idpEntityId); + + token = new Saml2AuthenticationToken( + token.getSaml2Response(), + recipientUri, + idpEntityId, + recipientEntityId, + emptyList() + ); + + exception.expect( + authenticationMatcher( + Saml2ErrorCodes.DECRYPTION_ERROR, + "No valid decryption credentials found." + ) + ); + provider.authenticate(token); + } + + @Test + public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() throws Exception { + Response response = response(recipientUri, idpEntityId); + Assertion assertion = defaultAssertion(); + EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); + response.getEncryptedAssertions().add(encryptedAssertion); + token = responseXml(response, idpEntityId); + + token = new Saml2AuthenticationToken( + token.getSaml2Response(), + recipientUri, + idpEntityId, + recipientEntityId, + assertingPartyCredentials() + ); + + exception.expect( + authenticationMatcher( + Saml2ErrorCodes.DECRYPTION_ERROR, + "Failed to decrypt EncryptedData" + ) + ); + provider.authenticate(token); + } + + private Assertion defaultAssertion() { + return assertion( + username, + idpEntityId, + recipientEntityId, + recipientUri + ); + } + + private Saml2AuthenticationToken responseXml( + XMLObject object, + String issuerEntityId + ) { + String xml = saml.toXml(object, emptyList(), issuerEntityId); + return new Saml2AuthenticationToken( + xml, + recipientUri, + idpEntityId, + recipientEntityId, + relyingPartyCredentials() + ); + + } + + private BaseMatcher authenticationMatcher(String code) { + return authenticationMatcher(code, null); + } + + private BaseMatcher authenticationMatcher(String code, String description) { + return new BaseMatcher() { + private Object value = null; + + @Override + public boolean matches(Object item) { + if (!(item instanceof Saml2AuthenticationException)) { + value = item; + return false; + } + Saml2AuthenticationException ex = (Saml2AuthenticationException) item; + if (!code.equals(ex.getError().getErrorCode())) { + value = item; + return false; + } + if (hasText(description)) { + if (!description.equals(ex.getError().getDescription())) { + value = item; + return false; + } + } + return true; + } + + @Override + public void describeTo(Description desc) { + String excepting = "Saml2AuthenticationException[code="+code+"; description="+description+"]"; + desc.appendText(excepting); + + } + }; + } +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java new file mode 100644 index 00000000000..29cdf8aae22 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2019 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 org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.credentials.Saml2X509Credential; + +import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.encryption.XMLCipherParameters; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.common.SignableSAMLObject; +import org.opensaml.saml.saml2.core.Assertion; +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.encryption.Encrypter; +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.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.SignatureSigningParameters; +import org.opensaml.xmlsec.encryption.support.DataEncryptionParameters; +import org.opensaml.xmlsec.encryption.support.EncryptionException; +import org.opensaml.xmlsec.encryption.support.KeyEncryptionParameters; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.opensaml.xmlsec.signature.support.SignatureException; +import org.opensaml.xmlsec.signature.support.SignatureSupport; + +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.X509Certificate; +import java.util.List; +import javax.crypto.SecretKey; + +import static java.util.Arrays.asList; +import static org.opensaml.security.crypto.KeySupport.generateKey; + +final class Saml2CryptoTestSupport { + static void signXmlObject(SignableSAMLObject object, List signingCredentials, String entityId) { + SignatureSigningParameters parameters = new SignatureSigningParameters(); + Credential credential = getSigningCredential(signingCredentials, entityId); + parameters.setSigningCredential(credential); + parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); + parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); + parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + try { + SignatureSupport.signObject(object, parameters); + } catch (MarshallingException | SignatureException | SecurityException e) { + throw new Saml2Exception(e); + } + + } + + static EncryptedAssertion encryptAssertion(Assertion assertion, List encryptionCredentials) { + X509Certificate certificate = getEncryptionCertificate(encryptionCredentials); + Encrypter encrypter = getEncrypter(certificate); + try { + Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER"); + encrypter.setKeyPlacement(keyPlacement); + return encrypter.encrypt(assertion); + } + catch (EncryptionException e) { + throw new Saml2Exception("Unable to encrypt assertion.", e); + } + } + + static EncryptedID encryptNameId(NameID nameID, List encryptionCredentials) { + X509Certificate certificate = getEncryptionCertificate(encryptionCredentials); + Encrypter encrypter = getEncrypter(certificate); + try { + Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER"); + encrypter.setKeyPlacement(keyPlacement); + return encrypter.encrypt(nameID); + } + catch (EncryptionException e) { + throw new Saml2Exception("Unable to encrypt nameID.", e); + } + } + + private static Encrypter getEncrypter(X509Certificate certificate) { + Credential credential = CredentialSupport.getSimpleCredential(certificate, null); + final String dataAlgorithm = XMLCipherParameters.AES_256; + final String keyAlgorithm = XMLCipherParameters.RSA_1_5; + SecretKey secretKey = generateKeyFromURI(dataAlgorithm); + BasicCredential dataCredential = new BasicCredential(secretKey); + DataEncryptionParameters dataEncryptionParameters = new DataEncryptionParameters(); + dataEncryptionParameters.setEncryptionCredential(dataCredential); + dataEncryptionParameters.setAlgorithm(dataAlgorithm); + + KeyEncryptionParameters keyEncryptionParameters = new KeyEncryptionParameters(); + keyEncryptionParameters.setEncryptionCredential(credential); + keyEncryptionParameters.setAlgorithm(keyAlgorithm); + + Encrypter encrypter = new Encrypter(dataEncryptionParameters, asList(keyEncryptionParameters)); + + return encrypter; + } + + private static SecretKey generateKeyFromURI(String algoURI) { + try { + String jceAlgorithmName = JCEMapper.getJCEKeyAlgorithmFromURI(algoURI); + int keyLength = JCEMapper.getKeyLengthFromURI(algoURI); + return generateKey(jceAlgorithmName, keyLength, null); + } + catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new Saml2Exception(e); + } + } + + private static X509Certificate getEncryptionCertificate(List encryptionCredentials) { + X509Certificate certificate = null; + for (Saml2X509Credential credential : encryptionCredentials) { + if (credential.isEncryptionCredential()) { + certificate = credential.getCertificate(); + break; + } + } + if (certificate == null) { + throw new Saml2Exception("No valid encryption certificate found"); + } + return certificate; + } + + private static Saml2X509Credential hasSigningCredential(List credentials) { + for (Saml2X509Credential c : credentials) { + if (c.isSigningCredential()) { + return c; + } + } + return null; + } + + private static Credential getSigningCredential(List signingCredential, + String localSpEntityId + ) { + Saml2X509Credential credential = hasSigningCredential(signingCredential); + if (credential == null) { + throw new Saml2Exception("no signing credential configured"); + } + BasicCredential cred = getBasicCredential(credential); + cred.setEntityId(localSpEntityId); + cred.setUsageType(UsageType.SIGNING); + return cred; + } + + private static BasicX509Credential getBasicCredential(Saml2X509Credential credential) { + return CredentialSupport.getSimpleCredential( + credential.getCertificate(), + credential.getPrivateKey() + ); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java new file mode 100644 index 00000000000..acc9d9f2abd --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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 org.joda.time.DateTime; +import org.joda.time.Duration; +import org.opensaml.saml.common.SAMLVersion; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Conditions; +import org.opensaml.saml.saml2.core.Issuer; +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.core.SubjectConfirmationData; + +import java.util.UUID; + +final class TestSaml2AuthenticationObjects { + private static OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); + + static Response response(String destination, String issuerEntityId) { + Response response = saml.buildSAMLObject(Response.class); + response.setID("R"+UUID.randomUUID().toString()); + response.setIssueInstant(DateTime.now()); + response.setVersion(SAMLVersion.VERSION_20); + response.setID("_" + UUID.randomUUID().toString()); + response.setDestination(destination); + response.setIssuer(issuer(issuerEntityId)); + return response; + } + static Assertion assertion( + String username, + String issuerEntityId, + String recipientEntityId, + String recipientUri + ) { + Assertion assertion = saml.buildSAMLObject(Assertion.class); + assertion.setID("A"+ UUID.randomUUID().toString()); + assertion.setIssueInstant(DateTime.now()); + assertion.setVersion(SAMLVersion.VERSION_20); + assertion.setIssueInstant(DateTime.now()); + assertion.setIssuer(issuer(issuerEntityId)); + assertion.setSubject(subject(username)); + assertion.setConditions(conditions()); + + SubjectConfirmation subjectConfirmation = subjectConfirmation(); + subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); + SubjectConfirmationData confirmationData = subjectConfirmationData(recipientEntityId); + confirmationData.setRecipient(recipientUri); + subjectConfirmation.setSubjectConfirmationData(confirmationData); + assertion.getSubject().getSubjectConfirmations().add(subjectConfirmation); + return assertion; + } + + + static Issuer issuer(String entityId) { + Issuer issuer = saml.buildSAMLObject(Issuer.class); + issuer.setValue(entityId); + return issuer; + } + + static Subject subject(String principalName) { + Subject subject = saml.buildSAMLObject(Subject.class); + + if (principalName != null) { + subject.setNameID(nameId(principalName)); + } + + return subject; + } + + static NameID nameId(String principalName) { + NameID nameId = saml.buildSAMLObject(NameID.class); + nameId.setValue(principalName); + return nameId; + } + + static SubjectConfirmation subjectConfirmation() { + return saml.buildSAMLObject(SubjectConfirmation.class); + } + + static SubjectConfirmationData subjectConfirmationData(String recipient) { + SubjectConfirmationData subject = saml.buildSAMLObject(SubjectConfirmationData.class); + subject.setRecipient(recipient); + subject.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); + subject.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); + return subject; + } + + static Conditions conditions() { + Conditions conditions = saml.buildSAMLObject(Conditions.class); + conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); + conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); + return conditions; + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java new file mode 100644 index 00000000000..faef9743b59 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2019 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 org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.credentials.Saml2X509Credential; + +import org.opensaml.security.crypto.KeySupport; + +import java.io.ByteArrayInputStream; +import java.security.KeyException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; +import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION; + +final class TestSaml2X509Credentials { + static List assertingPartyCredentials() { + return Arrays.asList( + new Saml2X509Credential( + idpPrivateKey(), + idpCertificate(), + SIGNING, + DECRYPTION + ), + new Saml2X509Credential( + spCertificate(), + ENCRYPTION, + VERIFICATION + ) + ); + } + + static List relyingPartyCredentials() { + return Arrays.asList( + new Saml2X509Credential( + spPrivateKey(), + spCertificate(), + SIGNING, + DECRYPTION + ), + new Saml2X509Credential( + idpCertificate(), + ENCRYPTION, + VERIFICATION + ) + ); + } + + private static X509Certificate certificate(String cert) { + ByteArrayInputStream certBytes = new ByteArrayInputStream(cert.getBytes()); + try { + return (X509Certificate) CertificateFactory + .getInstance("X.509") + .generateCertificate(certBytes); + } + catch (CertificateException e) { + throw new Saml2Exception(e); + } + } + + private static PrivateKey privateKey(String key) { + try { + return KeySupport.decodePrivateKey(key.getBytes(UTF_8), new char[0]); + } + catch (KeyException e) { + throw new Saml2Exception(e); + } + } + + private static X509Certificate idpCertificate() { + return certificate("-----BEGIN CERTIFICATE-----\n" + + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD\n" + + "VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD\n" + + "VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX\n" + + "c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw\n" + + "aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ\n" + + "BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa\n" + + "BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD\n" + + "DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr\n" + + "QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62\n" + + "E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz\n" + + "2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW\n" + + "RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ\n" + + "nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5\n" + + "cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph\n" + + "iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5\n" + + "ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD\n" + + "AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO\n" + + "nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v\n" + + "ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu\n" + + "xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z\n" + + "V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3\n" + + "lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + "-----END CERTIFICATE-----\n"); + } + + + private static PrivateKey idpPrivateKey() { + return privateKey("-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4cn62E1xLqpN3\n" + + "4PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZX\n" + + "W+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHE\n" + + "fDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7h\n" + + "Z6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/T\n" + + "Xy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7\n" + + "I+J5lS8VAgMBAAECggEBAKyxBlIS7mcp3chvq0RF7B3PHFJMMzkwE+t3pLJcs4cZ\n" + + "nezh/KbREfP70QjXzk/llnZCvxeIs5vRu24vbdBm79qLHqBuHp8XfHHtuo2AfoAQ\n" + + "l4h047Xc/+TKMivnPQ0jX9qqndKDLqZDf5wnbslDmlskvF0a/MjsLU0TxtOfo+dB\n" + + "t55FW11cGqxZwhS5Gnr+cbw3OkHz23b9gEOt9qfwPVepeysbmm9FjU+k4yVa7rAN\n" + + "xcbzVb6Y7GCITe2tgvvEHmjB9BLmWrH3mZ3Af17YU/iN6TrpPd6Sj3QoS+2wGtAe\n" + + "HbUs3CKJu7bIHcj4poal6Kh8519S+erJTtqQ8M0ZiEECgYEA43hLYAPaUueFkdfh\n" + + "9K/7ClH6436CUH3VdizwUXi26fdhhV/I/ot6zLfU2mgEHU22LBECWQGtAFm8kv0P\n" + + "zPn+qjaR3e62l5PIlSYbnkIidzoDZ2ztu4jF5LgStlTJQPteFEGgZVl5o9DaSZOq\n" + + "Yd7G3XqXuQ1VGMW58G5FYJPtA1cCgYEAz5TPUtK+R2KXHMjUwlGY9AefQYRYmyX2\n" + + "Tn/OFgKvY8lpAkMrhPKONq7SMYc8E9v9G7A0dIOXvW7QOYSapNhKU+np3lUafR5F\n" + + "4ZN0bxZ9qjHbn3AMYeraKjeutHvlLtbHdIc1j3sxe/EzltRsYmiqLdEBW0p6hwWg\n" + + "tyGhYWVyaXMCgYAfDOKtHpmEy5nOCLwNXKBWDk7DExfSyPqEgSnk1SeS1HP5ctPK\n" + + "+1st6sIhdiVpopwFc+TwJWxqKdW18tlfT5jVv1E2DEnccw3kXilS9xAhWkfwrEvf\n" + + "V5I74GydewFl32o+NZ8hdo9GL1I8zO1rIq/et8dSOWGuWf9BtKu/vTGTTQKBgFxU\n" + + "VjsCnbvmsEwPUAL2hE/WrBFaKocnxXx5AFNt8lEyHtDwy4Sg1nygGcIJ4sD6koQk\n" + + "RdClT3LkvR04TAiSY80bN/i6ZcPNGUwSaDGZEWAIOSWbkwZijZNFnSGOEgxZX/IG\n" + + "yd39766vREEMTwEeiMNEOZQ/dmxkJm4OOVe25cLdAoGACOtPnq1Fxay80UYBf4rQ\n" + + "+bJ9yX1ulB8WIree1hD7OHSB2lRHxrVYWrglrTvkh63Lgx+EcsTV788OsvAVfPPz\n" + + "BZrn8SdDlQqalMxUBYEFwnsYD3cQ8yOUnijFVC4xNcdDv8OIqVgSk4KKxU5AshaA\n" + "xk6Mox+u8Cc2eAK12H13i+8=\n" + + "-----END PRIVATE KEY-----\n"); + } + + private static X509Certificate spCertificate() { + + return certificate("-----BEGIN CERTIFICATE-----\n" + + "MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" + + "VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" + + "A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" + + "DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" + + "MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" + + "MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" + + "TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" + + "vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" + + "+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" + + "y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" + + "XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" + + "qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" + + "RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" + + "-----END CERTIFICATE-----"); + } + + private static PrivateKey spPrivateKey() { + return privateKey("-----BEGIN PRIVATE KEY-----\n" + + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" + + "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" + + "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" + + "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" + + "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" + + "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" + + "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" + + "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" + + "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" + + "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" + + "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" + + "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" + + "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" + + "INrtuLp4YHbgk1mi\n" + + "-----END PRIVATE KEY-----"); + } + +} diff --git a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java index f4051266805..cf0c5dfd930 100644 --- a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java +++ b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java @@ -22,11 +22,15 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.ComponentScan; import org.springframework.http.MediaType; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.util.AssertionErrors; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.hamcrest.Matcher; import org.joda.time.DateTime; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,9 +66,11 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.UUID; +import javax.servlet.http.HttpSession; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.startsWith; import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildConditions; import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildIssuer; @@ -74,6 +80,9 @@ import static org.springframework.security.samples.OpenSamlActionTestingSupport.encryptNameId; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; +import static org.springframework.security.web.WebAttributes.AUTHENTICATION_EXCEPTION; +import static org.springframework.test.util.AssertionErrors.assertEquals; +import static org.springframework.test.util.AssertionErrors.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -86,6 +95,7 @@ public class Saml2LoginIntegrationTests { static final String LOCAL_SP_ENTITY_ID = "http://localhost:8080/saml2/service-provider-metadata/simplesamlphp"; + static final String USERNAME = "testuser@spring.security.saml"; @Autowired MockMvc mockMvc; @@ -97,21 +107,21 @@ public static class SpringBootApplicationTestConfig { } @Test - public void redirectToLoginPageSingleProvider() throws Exception { + public void applicationAccessWhenSingleProviderAndUnauthenticatedThenRedirectsToAuthNRequest() throws Exception { mockMvc.perform(get("http://localhost:8080/some/url")) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("http://localhost:8080/saml2/authenticate/simplesamlphp")); } @Test - public void testAuthNRequest() throws Exception { + public void authenticateRequestWhenUnauthenticatedThenRespondsWithRedirectAuthNRequestXML() throws Exception { mockMvc.perform(get("http://localhost:8080/saml2/authenticate/simplesamlphp")) .andExpect(status().is3xxRedirection()) .andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php?SAMLRequest="))); } @Test - public void testRelayState() throws Exception { + public void authenticateRequestWhenRelayStateThenRespondsWithRedirectAndEncodedRelayState() throws Exception { mockMvc.perform( get("http://localhost:8080/saml2/authenticate/simplesamlphp") .param("RelayState", "relay state value with spaces") @@ -122,96 +132,136 @@ public void testRelayState() throws Exception { } @Test - public void signedResponse() throws Exception { - final String username = "testuser@spring.security.saml"; - Assertion assertion = buildAssertion(username); + public void authenticateWhenResponseIsSignedThenItSucceeds() throws Exception { + Assertion assertion = buildAssertion(USERNAME); Response response = buildResponse(assertion); signXmlObject(response, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); - String xml = toXml(response); - mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) - .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) - .andExpect(authenticated().withUsername(username)); + sendResponse(response, "/") + .andExpect(authenticated().withUsername(USERNAME)); } @Test - public void signedAssertion() throws Exception { - final String username = "testuser@spring.security.saml"; - Assertion assertion = buildAssertion(username); + public void authenticateWhenAssertionIsThenItSignedSucceeds() throws Exception { + Assertion assertion = buildAssertion(USERNAME); Response response = buildResponse(assertion); signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); - String xml = toXml(response); - final ResultActions actions = mockMvc - .perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) - .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) - .andExpect(authenticated().withUsername(username)); + sendResponse(response, "/") + .andExpect(authenticated().withUsername(USERNAME)); } @Test - public void unsigned() throws Exception { - Assertion assertion = buildAssertion("testuser@spring.security.saml"); + public void authenticateWhenXmlObjectIsNotSignedThenItFails() throws Exception { + Assertion assertion = buildAssertion(USERNAME); Response response = buildResponse(assertion); - String xml = toXml(response); - mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/login?error")) + sendResponse(response, "/login?error") .andExpect(unauthenticated()); } @Test - public void signedResponseEncryptedAssertion() throws Exception { - final String username = "testuser@spring.security.saml"; - Assertion assertion = buildAssertion(username); + public void authenticateWhenResponseIsSignedAndAssertionIsEncryptedThenItSucceeds() throws Exception { + Assertion assertion = buildAssertion(USERNAME); EncryptedAssertion encryptedAssertion = OpenSamlActionTestingSupport.encryptAssertion(assertion, decodeCertificate(spCertificate)); Response response = buildResponse(encryptedAssertion); signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); - String xml = toXml(response); - final ResultActions actions = mockMvc - .perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) - .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) - .andExpect(authenticated().withUsername(username)); + sendResponse(response, "/") + .andExpect(authenticated().withUsername(USERNAME)); } @Test - public void unsignedResponseEncryptedAssertion() throws Exception { - final String username = "testuser@spring.security.saml"; - Assertion assertion = buildAssertion(username); + public void authenticateWhenResponseIsNotSignedAndAssertionIsEncryptedThenItSucceeds() throws Exception { + Assertion assertion = buildAssertion(USERNAME); EncryptedAssertion encryptedAssertion = OpenSamlActionTestingSupport.encryptAssertion(assertion, decodeCertificate(spCertificate)); Response response = buildResponse(encryptedAssertion); - String xml = toXml(response); - final ResultActions actions = mockMvc - .perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) - .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) - .andExpect(authenticated().withUsername(username)); + sendResponse(response, "/") + .andExpect(authenticated().withUsername(USERNAME)); } @Test - public void signedResponseEncryptedNameId() throws Exception { - final String username = "testuser@spring.security.saml"; - Assertion assertion = buildAssertion(username); + public void authenticateWhenResponseIsSignedAndNameIDisEncryptedThenItSucceeds() throws Exception { + Assertion assertion = buildAssertion(USERNAME); final EncryptedID nameId = encryptNameId(assertion.getSubject().getNameID(), decodeCertificate(spCertificate)); assertion.getSubject().setEncryptedID(nameId); assertion.getSubject().setNameID(null); Response response = buildResponse(assertion); signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); + sendResponse(response, "/") + .andExpect(authenticated().withUsername(USERNAME)); + } + + @Test + public void authenticateWhenSignatureKeysDontMatchThenItFails() throws Exception { + Assertion assertion = buildAssertion(USERNAME); + Response response = buildResponse(assertion); + signXmlObject(assertion, getSigningCredential(spCertificate, spPrivateKey, UsageType.SIGNING)); + sendResponse(response, "/login?error") + .andExpect( + saml2AuthenticationExceptionMatcher( + "invalid_signature", + equalTo("Assertion doesn't have a valid signature.") + ) + ); + } + + @Test + public void authenticateWhenNotOnOrAfterDontMatchThenItFails() throws Exception { + Assertion assertion = buildAssertion(USERNAME); + assertion.getConditions().setNotOnOrAfter(DateTime.now().minusDays(1)); + Response response = buildResponse(assertion); + signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); + sendResponse(response, "/login?error") + .andExpect( + saml2AuthenticationExceptionMatcher( + "invalid_assertion", + containsString("Assertion 'assertion' with NotOnOrAfter condition of") + ) + ); + } + + @Test + public void authenticateWhenNotOnOrBeforeDontMatchThenItFails() throws Exception { + Assertion assertion = buildAssertion(USERNAME); + assertion.getConditions().setNotBefore(DateTime.now().plusDays(1)); + Response response = buildResponse(assertion); + signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); + sendResponse(response, "/login?error") + .andExpect( + saml2AuthenticationExceptionMatcher( + "invalid_assertion", + containsString("Assertion 'assertion' with NotBefore condition of") + ) + ); + } + + @Test + public void authenticateWhenIssuerIsInvalidThenItFails() throws Exception { + Assertion assertion = buildAssertion(USERNAME); + Response response = buildResponse(assertion); + response.getIssuer().setValue("invalid issuer"); + signXmlObject(response, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); + sendResponse(response, "/login?error") + .andExpect(unauthenticated()) + .andExpect( + saml2AuthenticationExceptionMatcher( + "invalid_issuer", + containsString( + "Response issuer 'invalid issuer' doesn't match "+ + "'https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php'" + ) + ) + ); + } + + private ResultActions sendResponse( + Response response, + String redirectUrl) throws Exception { String xml = toXml(response); - final ResultActions actions = mockMvc - .perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) - .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) - .andExpect(authenticated().withUsername(username)); + return mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl(redirectUrl)); } private Response buildResponse(Assertion assertion) { @@ -359,4 +409,42 @@ private X509Certificate decodeCertificate(String source) { "RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" + "-----END CERTIFICATE-----"; + private String spPrivateKey = "-----BEGIN PRIVATE KEY-----\n" + + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" + + "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" + + "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" + + "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" + + "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" + + "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" + + "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" + + "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" + + "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" + + "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" + + "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" + + "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" + + "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" + + "INrtuLp4YHbgk1mi\n" + + "-----END PRIVATE KEY-----"; + + private static ResultMatcher saml2AuthenticationExceptionMatcher( + String code, + Matcher message + ) { + return result -> { + final HttpSession session = result.getRequest().getSession(false); + AssertionErrors.assertNotNull("HttpSession", session); + Object exception = session.getAttribute(AUTHENTICATION_EXCEPTION); + AssertionErrors.assertNotNull(AUTHENTICATION_EXCEPTION, exception); + if (!(exception instanceof Saml2AuthenticationException)) { + AssertionErrors.fail( + "Invalid exception type", + Saml2AuthenticationException.class, + exception.getClass().getName() + ); + } + Saml2AuthenticationException se = (Saml2AuthenticationException) exception; + assertEquals("SAML 2 Error Code", code, se.getError().getErrorCode()); + assertTrue("SAML 2 Error Description", message.matches(se.getError().getDescription())); + }; + } }