Revert "Merge pull request #7432 from fhanik/feature/propagate_saml_authentication_exception"
This reverts commite9619fb0e7
, reversing changes made to45a1490d5d
.
This commit is contained in:
parent
e9619fb0e7
commit
adde18b873
|
@ -9,6 +9,4 @@ dependencies {
|
|||
compile("org.opensaml:opensaml-saml-impl")
|
||||
|
||||
provided 'javax.servlet:javax.servlet-api'
|
||||
|
||||
testCompile powerMock2Dependencies
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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).
|
||||
* </p>
|
||||
* <p>
|
||||
* The {@link Saml2AuthenticationToken} will be processed into a SAML Response object.
|
||||
* The SAML response object can be signed. If the Response is signed, a signature will not be required on the assertion.
|
||||
* </p>
|
||||
* <p>
|
||||
* While a response object can contain a list of assertion, this provider will only leverage
|
||||
* the first valid assertion for the purpose of authentication. Assertions that do not pass validation
|
||||
* will be ignored. If no valid assertions are found a {@link Saml2AuthenticationException} is thrown.
|
||||
* </p>
|
||||
* <p>
|
||||
* This provider supports two types of encrypted SAML elements
|
||||
* <ul>
|
||||
* <li><a href="https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=17">EncryptedAssertion</a></li>
|
||||
* <li><a href="https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=14">EncryptedID</a></li>
|
||||
* </ul>
|
||||
* If the assertion is encrypted, then signature validation on the assertion is no longer required.
|
||||
* </p>
|
||||
* <p>
|
||||
* This provider does not perform an X509 certificate validation on the configured asserting party, IDP, verification
|
||||
* certificates.
|
||||
* </p>
|
||||
* @since 5.2
|
||||
* @see <a href="https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38">SAML 2 StatusResponse</a>
|
||||
* @see <a href="https://wiki.shibboleth.net/confluence/display/OS30/Home">OpenSAML 3</a>
|
||||
*/
|
||||
public final class OpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class);
|
||||
|
||||
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
|
||||
private Converter<Assertion, Collection<? extends GrantedAuthority>> authoritiesExtractor =
|
||||
(a -> singletonList(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
private Converter<Assertion, Collection<? extends GrantedAuthority>> 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<X509Certificate> verificationKeys = getVerificationCertificates(token);
|
||||
final List<X509Certificate> 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<String, Object> 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<ConditionValidator> conditions = Collections.singletonList(new AudienceRestrictionConditionValidator());
|
||||
BearerSubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator();
|
||||
final BearerSubjectConfirmationValidator subjectConfirmationValidator =
|
||||
new BearerSubjectConfirmationValidator();
|
||||
|
||||
List<SubjectConfirmationValidator> subjects = Collections.singletonList(subjectConfirmationValidator);
|
||||
List<StatementValidator> statements = Collections.emptyList();
|
||||
|
||||
Set<Credential> 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<Saml2X509Credential> 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<Saml2X509Credential> 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<X509Certificate> getVerificationCertificates(Saml2AuthenticationToken token) {
|
||||
private List<X509Certificate> getVerificationKeys(Saml2AuthenticationToken token) {
|
||||
List<X509Certificate> result = new LinkedList<>();
|
||||
for (Saml2X509Credential c : token.getX509Credentials()) {
|
||||
if (c.isSignatureVerficationCredential()) {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* There are a number of scenarios where an error may occur, for example:
|
||||
* <ul>
|
||||
* <li>The response or assertion request is missing or malformed</li>
|
||||
* <li>Missing or invalid subject</li>
|
||||
* <li>Missing or invalid signatures</li>
|
||||
* <li>The time period validation for the assertion fails</li>
|
||||
* <li>One of the assertion conditions was not met</li>
|
||||
* <li>Decryption failed</li>
|
||||
* <li>Unable to locate a subject identifier, commonly known as username</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* @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() : "");
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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<Saml2AuthenticationException> authenticationMatcher(String code) {
|
||||
return authenticationMatcher(code, null);
|
||||
}
|
||||
|
||||
private BaseMatcher<Saml2AuthenticationException> authenticationMatcher(String code, String description) {
|
||||
return new BaseMatcher<Saml2AuthenticationException>() {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> 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()));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue