diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index 8ab64e76dd..02cf9aea6f 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -9,6 +9,4 @@ dependencies { compile("org.opensaml:opensaml-saml-impl") provided 'javax.servlet:javax.servlet-api' - - testCompile powerMock2Dependencies } 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 2ca8c9380f..a67d36e98d 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,12 +16,16 @@ 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; @@ -75,59 +79,18 @@ import java.util.Set; 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); @@ -175,16 +138,20 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication; String xml = token.getSaml2Response(); Response samlResponse = getSaml2Response(xml); + Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse); - String username = getUsername(token, assertion); + final String username = getUsername(token, assertion); + if (username == null) { + throw new UsernameNotFoundException("Assertion [" + + assertion.getID() + + "] is missing a user identifier"); + } return new Saml2Authentication( () -> username, token.getSaml2Response(), this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion)) ); - } catch (Saml2AuthenticationException e) { - throw e; - } catch (Exception e) { - throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e); + }catch (Saml2Exception | IllegalArgumentException e) { + throw new AuthenticationServiceException(e.getMessage(), e); } } @@ -200,116 +167,93 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi return this.authoritiesExtractor.convert(assertion); } - private String getUsername(Saml2AuthenticationToken token, Assertion assertion) throws Saml2AuthenticationException { - String username = null; - Subject subject = assertion.getSubject(); + private String getUsername(Saml2AuthenticationToken token, Assertion assertion) { + final Subject subject = assertion.getSubject(); if (subject == null) { - throw authException(SUBJECT_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a subject"); + return null; } if (subject.getNameID() != null) { - username = subject.getNameID().getValue(); + return subject.getNameID().getValue(); } - else if (subject.getEncryptedID() != null) { + if (subject.getEncryptedID() != null) { NameID nameId = decrypt(token, subject.getEncryptedID()); - username = nameId.getValue(); + return nameId.getValue(); } - if (username == null) { - throw authException(USERNAME_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a user identifier"); - } - return username; + return null; } private Assertion validateSaml2Response(Saml2AuthenticationToken token, String recipient, - Response samlResponse) throws Saml2AuthenticationException { - //optional validation if the response contains a destination + Response samlResponse) throws AuthenticationException { if (hasText(samlResponse.getDestination()) && !recipient.equals(samlResponse.getDestination())) { - throw authException(INVALID_DESTINATION, "Invalid SAML response destination: " + samlResponse.getDestination()); + throw new Saml2Exception("Invalid SAML response destination: " + samlResponse.getDestination()); } - String issuer = samlResponse.getIssuer().getValue(); + final String issuer = samlResponse.getIssuer().getValue(); if (logger.isDebugEnabled()) { - logger.debug("Validating SAML response from " + issuer); + logger.debug("Processing SAML response from " + 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); + if (token == null) { + throw new Saml2Exception(format("SAML 2 Provider for %s was not found.", issuer)); } - Saml2AuthenticationException lastValidationError = null; - boolean responseSigned = hasValidSignature(samlResponse, token); for (Assertion a : samlResponse.getAssertions()) { if (logger.isDebugEnabled()) { logger.debug("Checking plain assertion validity " + a); } - try { - validateAssertion(recipient, a, token, !responseSigned); + if (isValidAssertion(recipient, a, token, !responseSigned)) { + if (logger.isDebugEnabled()) { + logger.debug("Found valid assertion. Skipping potential others."); + } return a; - } catch (Saml2AuthenticationException e) { - lastValidationError = e; } } for (EncryptedAssertion ea : samlResponse.getEncryptedAssertions()) { if (logger.isDebugEnabled()) { logger.debug("Checking encrypted assertion validity " + ea); } - try { - Assertion a = decrypt(token, ea); - validateAssertion(recipient, a, token, false); + + Assertion a = decrypt(token, ea); + if (isValidAssertion(recipient, a, token, false)) { + if (logger.isDebugEnabled()) { + logger.debug("Found valid encrypted assertion. Skipping potential others."); + } return a; - } catch (Saml2AuthenticationException e) { - lastValidationError = e; } } - if (lastValidationError != null) { - throw lastValidationError; - } - else { - throw authException(MALFORMED_RESPONSE_DATA, "No assertions found in response."); - } + throw new InsufficientAuthenticationException("Unable to find a valid assertion"); } - private boolean hasValidSignature(SignableSAMLObject samlObject, Saml2AuthenticationToken token) { - if (!samlObject.isSigned()) { - if (logger.isDebugEnabled()) { - logger.debug("SAML object is not signed, no signatures found"); - } + private boolean hasValidSignature(SignableSAMLObject samlResponse, Saml2AuthenticationToken token) { + if (!samlResponse.isSigned()) { return false; } - List verificationKeys = getVerificationCertificates(token); + final List verificationKeys = getVerificationKeys(token); if (verificationKeys.isEmpty()) { return false; } - for (X509Certificate certificate : verificationKeys) { - Credential credential = getVerificationCredential(certificate); + for (X509Certificate key : verificationKeys) { + final Credential credential = getVerificationCredential(key); try { - SignatureValidator.validate(samlObject.getSignature(), credential); - if (logger.isDebugEnabled()) { - logger.debug("Valid signature found in SAML object:"+samlObject.getClass().getName()); - } + SignatureValidator.validate(samlResponse.getSignature(), credential); return true; } catch (SignatureException 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()); - } + logger.debug("Signature validation failed", ignored); } } return false; } - private void validateAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) { - SAML20AssertionValidator validator = getAssertionValidator(token); + private boolean isValidAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) { + final SAML20AssertionValidator validator = getAssertionValidator(token); Map validationParams = new HashMap<>(); validationParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false); validationParams.put( SAML2AssertionValidationParameters.CLOCK_SKEW, - this.responseTimeValidationSkew.toMillis() + this.responseTimeValidationSkew ); validationParams.put( SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, @@ -323,78 +267,55 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi if (logger.isDebugEnabled()) { logger.debug(format("Assertion [%s] does not a valid signature.", a.getID())); } - throw authException(Saml2ErrorCodes.INVALID_SIGNATURE, "Assertion doesn't have a valid signature."); + return false; } - //ensure that OpenSAML doesn't attempt signature validation, already performed a.setSignature(null); - //remainder of assertion validation + // validation for recipient ValidationContext vctx = new ValidationContext(validationParams); try { - ValidationResult result = validator.validate(a, vctx); - boolean valid = result.equals(ValidationResult.VALID); + final ValidationResult result = validator.validate(a, vctx); + final boolean valid = result.equals(ValidationResult.VALID); if (!valid) { if (logger.isDebugEnabled()) { - logger.debug(format("Failed to validate assertion from %s", token.getIdpEntityId())); + logger.debug(format("Failed to validate assertion from %s with user %s", token.getIdpEntityId(), + getUsername(token, a) + )); } - throw authException(Saml2ErrorCodes.INVALID_ASSERTION, vctx.getValidationFailureMessage()); } + return valid; } catch (AssertionValidationException e) { if (logger.isDebugEnabled()) { logger.debug("Failed to validate assertion:", e); } - throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e); + return false; } } - private Response getSaml2Response(String xml) throws Saml2Exception, Saml2AuthenticationException { - try { - Object result = this.saml.resolve(xml); - 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); + 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"); } - - } - - 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 - ); + else if (result instanceof Response) { + return (Response) result; + } + throw new IllegalArgumentException("Invalid response class:"+result.getClass().getName()); } private SAML20AssertionValidator getAssertionValidator(Saml2AuthenticationToken provider) { List conditions = Collections.singletonList(new AudienceRestrictionConditionValidator()); - BearerSubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator(); + final BearerSubjectConfirmationValidator subjectConfirmationValidator = + new BearerSubjectConfirmationValidator(); List subjects = Collections.singletonList(subjectConfirmationValidator); List statements = Collections.emptyList(); Set credentials = new HashSet<>(); - for (X509Certificate key : getVerificationCertificates(provider)) { - Credential cred = getVerificationCredential(key); + for (X509Certificate key : getVerificationKeys(provider)) { + final Credential cred = getVerificationCredential(key); credentials.add(cred); } CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); @@ -424,38 +345,37 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi return decrypter; } - private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion) - throws Saml2AuthenticationException { - Saml2AuthenticationException last = null; + private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion) { + Saml2Exception last = null; List decryptionCredentials = getDecryptionCredentials(token); if (decryptionCredentials.isEmpty()) { - throw authException(DECRYPTION_ERROR, "No valid decryption credentials found."); + throw new Saml2Exception("No valid decryption credentials found."); } for (Saml2X509Credential key : decryptionCredentials) { - Decrypter decrypter = getDecrypter(key); + final Decrypter decrypter = getDecrypter(key); try { return decrypter.decrypt(assertion); } catch (DecryptionException e) { - last = authException(DECRYPTION_ERROR, e.getMessage(), e); + last = new Saml2Exception(e); } } throw last; } - private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) throws Saml2AuthenticationException { - Saml2AuthenticationException last = null; + private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) { + Saml2Exception last = null; List decryptionCredentials = getDecryptionCredentials(token); if (decryptionCredentials.isEmpty()) { - throw authException(DECRYPTION_ERROR, "No valid decryption credentials found."); + throw new Saml2Exception("No valid decryption credentials found."); } for (Saml2X509Credential key : decryptionCredentials) { - Decrypter decrypter = getDecrypter(key); + final Decrypter decrypter = getDecrypter(key); try { return (NameID) decrypter.decrypt(assertion); } catch (DecryptionException e) { - last = authException(DECRYPTION_ERROR, e.getMessage(), e); + last = new Saml2Exception(e); } } throw last; @@ -471,7 +391,7 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi return result; } - private List getVerificationCertificates(Saml2AuthenticationToken token) { + private List getVerificationKeys(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/Saml2AuthenticationException.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java deleted file mode 100644 index 86709cec1e..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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/Saml2Error.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java deleted file mode 100644 index 786ae0e011..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 deleted file mode 100644 index 2b98a5334c..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 deleted file mode 100644 index 292619040d..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 deleted file mode 100644 index ac0cfc5e5e..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java +++ /dev/null @@ -1,456 +0,0 @@ -/* - * 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.saml2.Saml2Exception; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.opensaml.saml.common.assertion.AssertionValidationException; -import org.opensaml.saml.common.assertion.ValidationContext; -import org.opensaml.saml.common.assertion.ValidationResult; -import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator; -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.Issuer; -import org.opensaml.saml.saml2.core.Response; -import org.opensaml.saml.saml2.core.Subject; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -import java.util.Collections; - -import static java.util.Collections.emptyList; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.powermock.api.mockito.PowerMockito.doReturn; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.powermock.api.mockito.PowerMockito.when; -import static org.springframework.test.util.AssertionErrors.assertTrue; -import static org.springframework.util.StringUtils.hasText; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({OpenSamlImplementation.class, OpenSamlAuthenticationProvider.class}) -public class OpenSamlAuthenticationProviderTests { - - private OpenSamlAuthenticationProvider provider; - private OpenSamlImplementation saml; - - @Rule - ExpectedException exception = ExpectedException.none(); - private Saml2AuthenticationToken token; - - @Before - public void setup() { - saml = PowerMockito.mock(OpenSamlImplementation.class); - PowerMockito.mockStatic(OpenSamlImplementation.class); - when(OpenSamlImplementation.getInstance()).thenReturn(saml); - - provider = new OpenSamlAuthenticationProvider(); - token = new Saml2AuthenticationToken( - "responseXml", - "recipientUri", - "idpEntityId", - "localSpEntityId", - emptyList() - ); - } - - @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() { - when(saml.resolve(any(String.class))).thenReturn(mock(Assertion.class)); - exception.expect(authenticationMatcher(Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS)); - provider.authenticate(token); - } - - @Test - public void authenticateWhenXmlErrorThenThrowAuthenticationException() { - when(saml.resolve(any(String.class))).thenThrow(new Saml2Exception("test")); - exception.expect(authenticationMatcher(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA)); - provider.authenticate(token); - } - - @Test - public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() { - final Response response = mock(Response.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn("invalidRecipient"); - exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_DESTINATION)); - provider.authenticate(token); - } - - @Test - public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() { - final Response response = mock(Response.class); - final Issuer issuer = mock(Issuer.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn(token.getRecipientUri()); - when(response.isSigned()).thenReturn(false); - when(response.getAssertions()).thenReturn(emptyList()); - when(response.getEncryptedAssertions()).thenReturn(emptyList()); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, - "No assertions found in response." - ) - ); - provider.authenticate(token); - } - - @Test - public void authenticateWhenInvalidSignatureThenThrowAuthenticationException() { - final Response response = mock(Response.class); - final Issuer issuer = mock(Issuer.class); - final Assertion assertion = mock(Assertion.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn(token.getRecipientUri()); - when(response.isSigned()).thenReturn(false); - when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); - when(response.getEncryptedAssertions()).thenReturn(emptyList()); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.INVALID_SIGNATURE - ) - ); - provider.authenticate(token); - } - - @Test - public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() throws Exception { - final Response response = mock(Response.class); - final Issuer issuer = mock(Issuer.class); - final Assertion assertion = mock(Assertion.class); - final SAML20AssertionValidator validator = mock(SAML20AssertionValidator.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn(token.getRecipientUri()); - when(response.isSigned()).thenReturn(false); - when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); - when(response.getEncryptedAssertions()).thenReturn(emptyList()); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - - - OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); - doReturn(true).when( - spyProvider, - "hasValidSignature", - any(Assertion.class), - any(Saml2AuthenticationToken.class) - ); - doReturn(false).when( - spyProvider, - "hasValidSignature", - any(Response.class), - any(Saml2AuthenticationToken.class) - ); - doReturn(validator).when(spyProvider, "getAssertionValidator", any(Saml2AuthenticationToken.class)); - when(validator.validate( - any(Assertion.class), - any(ValidationContext.class) - )).thenReturn(ValidationResult.INVALID); - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.INVALID_ASSERTION - ) - ); - spyProvider.authenticate(token); - } - - @Test - public void authenticateWhenInternalErrorThenCatchAndThrowAuthenticationException() throws Exception { - final Response response = mock(Response.class); - final Issuer issuer = mock(Issuer.class); - final Assertion assertion = mock(Assertion.class); - final SAML20AssertionValidator validator = mock(SAML20AssertionValidator.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn(token.getRecipientUri()); - when(response.isSigned()).thenReturn(false); - when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); - when(response.getEncryptedAssertions()).thenReturn(emptyList()); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - - - OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); - doReturn(true).when( - spyProvider, - "hasValidSignature", - any(Assertion.class), - any(Saml2AuthenticationToken.class) - ); - doReturn(false).when( - spyProvider, - "hasValidSignature", - any(Response.class), - any(Saml2AuthenticationToken.class) - ); - doReturn(validator).when(spyProvider, "getAssertionValidator", any(Saml2AuthenticationToken.class)); - when(validator.validate( - any(Assertion.class), - any(ValidationContext.class) - )).thenThrow(new AssertionValidationException()); - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR - ) - ); - spyProvider.authenticate(token); - } - - @Test - public void authenticateWhenMissingSubjectThenThrowAuthenticationException() throws Exception { - final Response response = mock(Response.class); - final Issuer issuer = mock(Issuer.class); - final Assertion assertion = mock(Assertion.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn(token.getRecipientUri()); - when(response.isSigned()).thenReturn(false); - when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); - when(response.getEncryptedAssertions()).thenReturn(emptyList()); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - - - OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); - doReturn(true).when( - spyProvider, - "hasValidSignature", - any(Assertion.class), - any(Saml2AuthenticationToken.class) - ); - doReturn(false).when( - spyProvider, - "hasValidSignature", - any(Response.class), - any(Saml2AuthenticationToken.class) - ); - PowerMockito.doNothing() - .when( - spyProvider, - "validateAssertion", - anyString(), - any(Assertion.class), - any(Saml2AuthenticationToken.class), - anyBoolean() - ); - - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.SUBJECT_NOT_FOUND - ) - ); - spyProvider.authenticate(token); - } - - @Test - public void authenticateWhenUsernameMissingThenThrowAuthenticationException() throws Exception { - final Response response = mock(Response.class); - final Issuer issuer = mock(Issuer.class); - final Assertion assertion = mock(Assertion.class); - final Subject subject = mock(Subject.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn(token.getRecipientUri()); - when(response.isSigned()).thenReturn(false); - when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); - when(response.getEncryptedAssertions()).thenReturn(emptyList()); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - when(assertion.getSubject()).thenReturn(subject); - - - OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); - doReturn(true).when( - spyProvider, - "hasValidSignature", - any(Assertion.class), - any(Saml2AuthenticationToken.class) - ); - doReturn(false).when( - spyProvider, - "hasValidSignature", - any(Response.class), - any(Saml2AuthenticationToken.class) - ); - PowerMockito.doNothing() - .when( - spyProvider, - "validateAssertion", - anyString(), - any(Assertion.class), - any(Saml2AuthenticationToken.class), - anyBoolean() - ); - - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.USERNAME_NOT_FOUND - ) - ); - spyProvider.authenticate(token); - } - - @Test - public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() throws Exception { - final Response response = mock(Response.class); - final Issuer issuer = mock(Issuer.class); - final Assertion assertion = mock(Assertion.class); - final Subject subject = mock(Subject.class); - final EncryptedID nameID = mock(EncryptedID.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn(token.getRecipientUri()); - when(response.isSigned()).thenReturn(false); - when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); - when(response.getEncryptedAssertions()).thenReturn(emptyList()); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - when(assertion.getSubject()).thenReturn(subject); - when(subject.getEncryptedID()).thenReturn(nameID); - - - OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); - doReturn(true).when( - spyProvider, - "hasValidSignature", - any(Assertion.class), - any(Saml2AuthenticationToken.class) - ); - doReturn(false).when( - spyProvider, - "hasValidSignature", - any(Response.class), - any(Saml2AuthenticationToken.class) - ); - PowerMockito.doNothing() - .when( - spyProvider, - "validateAssertion", - anyString(), - any(Assertion.class), - any(Saml2AuthenticationToken.class), - anyBoolean() - ); - - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.DECRYPTION_ERROR, - "No valid decryption credentials found." - ) - ); - spyProvider.authenticate(token); - } - - @Test - public void authenticateWhenDecryptionKeyIsMissingThenThrowAuthenticationException() throws Exception { - final Response response = mock(Response.class); - final Issuer issuer = mock(Issuer.class); - final EncryptedAssertion assertion = mock(EncryptedAssertion.class); - when(saml.resolve(any(String.class))).thenReturn(response); - when(response.getDestination()).thenReturn(token.getRecipientUri()); - when(response.isSigned()).thenReturn(false); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(token.getIdpEntityId()); - when(response.getEncryptedAssertions()).thenReturn(Collections.singletonList(assertion)); - - OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); - doReturn(false).when( - spyProvider, - "hasValidSignature", - any(Response.class), - any(Saml2AuthenticationToken.class) - ); - - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.DECRYPTION_ERROR, - "No valid decryption credentials found." - ) - ); - spyProvider.authenticate(token); - } - - - 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 description) { - description.appendText("Expecting a " + Saml2AuthenticationException.class.getName() + - " with code:" + code + " and description:" + description - ) - .appendValue(value); - } - }; - } - -} 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 cf0c5dfd93..f405126680 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,15 +22,11 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock 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; @@ -66,11 +62,9 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.UUID; -import javax.servlet.http.HttpSession; import 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; @@ -80,9 +74,6 @@ import static org.springframework.security.samples.OpenSamlActionTestingSupport. 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; @@ -95,7 +86,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. 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; @@ -107,21 +97,21 @@ public class Saml2LoginIntegrationTests { } @Test - public void applicationAccessWhenSingleProviderAndUnauthenticatedThenRedirectsToAuthNRequest() throws Exception { + public void redirectToLoginPageSingleProvider() throws Exception { mockMvc.perform(get("http://localhost:8080/some/url")) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("http://localhost:8080/saml2/authenticate/simplesamlphp")); } @Test - public void authenticateRequestWhenUnauthenticatedThenRespondsWithRedirectAuthNRequestXML() throws Exception { + public void testAuthNRequest() 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 authenticateRequestWhenRelayStateThenRespondsWithRedirectAndEncodedRelayState() throws Exception { + public void testRelayState() throws Exception { mockMvc.perform( get("http://localhost:8080/saml2/authenticate/simplesamlphp") .param("RelayState", "relay state value with spaces") @@ -132,136 +122,96 @@ public class Saml2LoginIntegrationTests { } @Test - public void authenticateWhenResponseIsSignedThenItSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); + public void signedResponse() throws Exception { + final String username = "testuser@spring.security.saml"; + Assertion assertion = buildAssertion(username); Response response = buildResponse(assertion); signXmlObject(response, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/") - .andExpect(authenticated().withUsername(USERNAME)); + 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)); } @Test - public void authenticateWhenAssertionIsThenItSignedSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); + public void signedAssertion() throws Exception { + final String username = "testuser@spring.security.saml"; + Assertion assertion = buildAssertion(username); Response response = buildResponse(assertion); signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/") - .andExpect(authenticated().withUsername(USERNAME)); + 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)); } @Test - public void authenticateWhenXmlObjectIsNotSignedThenItFails() throws Exception { - Assertion assertion = buildAssertion(USERNAME); + public void unsigned() throws Exception { + Assertion assertion = buildAssertion("testuser@spring.security.saml"); Response response = buildResponse(assertion); - sendResponse(response, "/login?error") + 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")) .andExpect(unauthenticated()); } @Test - public void authenticateWhenResponseIsSignedAndAssertionIsEncryptedThenItSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); + public void signedResponseEncryptedAssertion() throws Exception { + final String username = "testuser@spring.security.saml"; + Assertion assertion = buildAssertion(username); EncryptedAssertion encryptedAssertion = OpenSamlActionTestingSupport.encryptAssertion(assertion, decodeCertificate(spCertificate)); Response response = buildResponse(encryptedAssertion); signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/") - .andExpect(authenticated().withUsername(USERNAME)); + 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)); } @Test - public void authenticateWhenResponseIsNotSignedAndAssertionIsEncryptedThenItSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); + public void unsignedResponseEncryptedAssertion() throws Exception { + final String username = "testuser@spring.security.saml"; + Assertion assertion = buildAssertion(username); EncryptedAssertion encryptedAssertion = OpenSamlActionTestingSupport.encryptAssertion(assertion, decodeCertificate(spCertificate)); Response response = buildResponse(encryptedAssertion); - sendResponse(response, "/") - .andExpect(authenticated().withUsername(USERNAME)); + 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)); } @Test - public void authenticateWhenResponseIsSignedAndNameIDisEncryptedThenItSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); + public void signedResponseEncryptedNameId() throws Exception { + final String username = "testuser@spring.security.saml"; + 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); - 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)); + 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)); } private Response buildResponse(Assertion assertion) { @@ -409,42 +359,4 @@ public class Saml2LoginIntegrationTests { "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())); - }; - } }