diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java
index a67d36e98d6..185da0398bf 100644
--- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java
@@ -16,16 +16,12 @@
package org.springframework.security.saml2.provider.service.authentication;
import org.springframework.core.convert.converter.Converter;
-import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
-import org.springframework.security.authentication.AuthenticationServiceException;
-import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.credentials.Saml2X509Credential;
import org.springframework.util.Assert;
@@ -79,18 +75,59 @@
import static java.lang.String.format;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.DECRYPTION_ERROR;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_DESTINATION;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_ISSUER;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.MALFORMED_RESPONSE_DATA;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.SUBJECT_NOT_FOUND;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.USERNAME_NOT_FOUND;
import static org.springframework.util.Assert.notNull;
import static org.springframework.util.StringUtils.hasText;
/**
+ * Implementation of {@link AuthenticationProvider} for SAML authentications when receiving a
+ * {@code Response} object containing an {@code Assertion}. This implementation uses
+ * the {@code OpenSAML 3} library.
+ *
+ *
+ * The {@link OpenSamlAuthenticationProvider} supports {@link Saml2AuthenticationToken} objects
+ * that contain a SAML response in its decoded XML format {@link Saml2AuthenticationToken#getSaml2Response()}
+ * along with the information about the asserting party, the identity provider (IDP), as well as
+ * the relying party, the service provider (SP, this application).
+ *
+ *
+ * The {@link Saml2AuthenticationToken} will be processed into a SAML Response object.
+ * The SAML response object can be signed. If the Response is signed, a signature will not be required on the assertion.
+ *
+ *
+ * While a response object can contain a list of assertion, this provider will only leverage
+ * the first valid assertion for the purpose of authentication. Assertions that do not pass validation
+ * will be ignored. If no valid assertions are found a {@link Saml2AuthenticationException} is thrown.
+ *
+ *
+ * This provider supports two types of encrypted SAML elements
+ *
+ * If the assertion is encrypted, then signature validation on the assertion is no longer required.
+ *
+ *
+ * This provider does not perform an X509 certificate validation on the configured asserting party, IDP, verification
+ * certificates.
+ *
* @since 5.2
+ * @see SAML 2 StatusResponse
+ * @see OpenSAML 3
*/
public final class OpenSamlAuthenticationProvider implements AuthenticationProvider {
private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class);
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
- private Converter> authoritiesExtractor = (a -> singletonList(new SimpleGrantedAuthority("ROLE_USER")));
+ private Converter> authoritiesExtractor =
+ (a -> singletonList(new SimpleGrantedAuthority("ROLE_USER")));
private GrantedAuthoritiesMapper authoritiesMapper = (a -> a);
private Duration responseTimeValidationSkew = Duration.ofMinutes(5);
@@ -136,22 +173,17 @@ public void setResponseTimeValidationSkew(Duration responseTimeValidationSkew) {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication;
- String xml = token.getSaml2Response();
- Response samlResponse = getSaml2Response(xml);
-
+ Response samlResponse = getSaml2Response(token);
Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse);
- final String username = getUsername(token, assertion);
- if (username == null) {
- throw new UsernameNotFoundException("Assertion [" +
- assertion.getID() +
- "] is missing a user identifier");
- }
+ String username = getUsername(token, assertion);
return new Saml2Authentication(
() -> username, token.getSaml2Response(),
this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))
);
- }catch (Saml2Exception | IllegalArgumentException e) {
- throw new AuthenticationServiceException(e.getMessage(), e);
+ } catch (Saml2AuthenticationException e) {
+ throw e;
+ } catch (Exception e) {
+ throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e);
}
}
@@ -167,93 +199,116 @@ private Collection extends GrantedAuthority> getAssertionAuthorities(Assertion
return this.authoritiesExtractor.convert(assertion);
}
- private String getUsername(Saml2AuthenticationToken token, Assertion assertion) {
- final Subject subject = assertion.getSubject();
+ private String getUsername(Saml2AuthenticationToken token, Assertion assertion) throws Saml2AuthenticationException {
+ String username = null;
+ Subject subject = assertion.getSubject();
if (subject == null) {
- return null;
+ throw authException(SUBJECT_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a subject");
}
if (subject.getNameID() != null) {
- return subject.getNameID().getValue();
+ username = subject.getNameID().getValue();
}
- if (subject.getEncryptedID() != null) {
+ else if (subject.getEncryptedID() != null) {
NameID nameId = decrypt(token, subject.getEncryptedID());
- return nameId.getValue();
+ username = nameId.getValue();
}
- return null;
+ if (username == null) {
+ throw authException(USERNAME_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a user identifier");
+ }
+ return username;
}
private Assertion validateSaml2Response(Saml2AuthenticationToken token,
String recipient,
- Response samlResponse) throws AuthenticationException {
+ Response samlResponse) throws Saml2AuthenticationException {
+ //optional validation if the response contains a destination
if (hasText(samlResponse.getDestination()) && !recipient.equals(samlResponse.getDestination())) {
- throw new Saml2Exception("Invalid SAML response destination: " + samlResponse.getDestination());
+ throw authException(INVALID_DESTINATION, "Invalid SAML response destination: " + samlResponse.getDestination());
}
- final String issuer = samlResponse.getIssuer().getValue();
+ String issuer = samlResponse.getIssuer().getValue();
if (logger.isDebugEnabled()) {
- logger.debug("Processing SAML response from " + issuer);
+ logger.debug("Validating SAML response from " + issuer);
}
- if (token == null) {
- throw new Saml2Exception(format("SAML 2 Provider for %s was not found.", issuer));
+ if (!hasText(issuer) || (!issuer.equals(token.getIdpEntityId()))) {
+ String message = String.format("Response issuer '%s' doesn't match '%s'", issuer, token.getIdpEntityId());
+ throw authException(INVALID_ISSUER, message);
}
+ Saml2AuthenticationException lastValidationError = null;
+
boolean responseSigned = hasValidSignature(samlResponse, token);
for (Assertion a : samlResponse.getAssertions()) {
if (logger.isDebugEnabled()) {
logger.debug("Checking plain assertion validity " + a);
}
- if (isValidAssertion(recipient, a, token, !responseSigned)) {
- if (logger.isDebugEnabled()) {
- logger.debug("Found valid assertion. Skipping potential others.");
- }
+ try {
+ validateAssertion(recipient, a, token, !responseSigned);
return a;
+ } catch (Saml2AuthenticationException e) {
+ lastValidationError = e;
}
}
for (EncryptedAssertion ea : samlResponse.getEncryptedAssertions()) {
if (logger.isDebugEnabled()) {
logger.debug("Checking encrypted assertion validity " + ea);
}
-
- Assertion a = decrypt(token, ea);
- if (isValidAssertion(recipient, a, token, false)) {
- if (logger.isDebugEnabled()) {
- logger.debug("Found valid encrypted assertion. Skipping potential others.");
- }
+ try {
+ Assertion a = decrypt(token, ea);
+ validateAssertion(recipient, a, token, false);
return a;
+ } catch (Saml2AuthenticationException e) {
+ lastValidationError = e;
}
}
- throw new InsufficientAuthenticationException("Unable to find a valid assertion");
+ if (lastValidationError != null) {
+ throw lastValidationError;
+ }
+ else {
+ throw authException(MALFORMED_RESPONSE_DATA, "No assertions found in response.");
+ }
}
- private boolean hasValidSignature(SignableSAMLObject samlResponse, Saml2AuthenticationToken token) {
- if (!samlResponse.isSigned()) {
+ private boolean hasValidSignature(SignableSAMLObject samlObject, Saml2AuthenticationToken token) {
+ if (!samlObject.isSigned()) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("SAML object is not signed, no signatures found");
+ }
return false;
}
- final List verificationKeys = getVerificationKeys(token);
+ List verificationKeys = getVerificationCertificates(token);
if (verificationKeys.isEmpty()) {
return false;
}
- for (X509Certificate key : verificationKeys) {
- final Credential credential = getVerificationCredential(key);
+ for (X509Certificate certificate : verificationKeys) {
+ Credential credential = getVerificationCredential(certificate);
try {
- SignatureValidator.validate(samlResponse.getSignature(), credential);
+ SignatureValidator.validate(samlObject.getSignature(), credential);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Valid signature found in SAML object:"+samlObject.getClass().getName());
+ }
return true;
}
catch (SignatureException ignored) {
- logger.debug("Signature validation failed", ignored);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Signature validation failed with cert:"+certificate.toString(), ignored);
+ }
+ else if (logger.isDebugEnabled()) {
+ logger.debug("Signature validation failed with cert:"+certificate.toString());
+ }
}
}
return false;
}
- private boolean isValidAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) {
- final SAML20AssertionValidator validator = getAssertionValidator(token);
+ private void validateAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) {
+ SAML20AssertionValidator validator = getAssertionValidator(token);
Map validationParams = new HashMap<>();
validationParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false);
validationParams.put(
SAML2AssertionValidationParameters.CLOCK_SKEW,
- this.responseTimeValidationSkew
+ this.responseTimeValidationSkew.toMillis()
);
validationParams.put(
SAML2AssertionValidationParameters.COND_VALID_AUDIENCES,
@@ -267,55 +322,78 @@ private boolean isValidAssertion(String recipient, Assertion a, Saml2Authenticat
if (logger.isDebugEnabled()) {
logger.debug(format("Assertion [%s] does not a valid signature.", a.getID()));
}
- return false;
+ throw authException(Saml2ErrorCodes.INVALID_SIGNATURE, "Assertion doesn't have a valid signature.");
}
+ //ensure that OpenSAML doesn't attempt signature validation, already performed
a.setSignature(null);
- // validation for recipient
+ //remainder of assertion validation
ValidationContext vctx = new ValidationContext(validationParams);
try {
- final ValidationResult result = validator.validate(a, vctx);
- final boolean valid = result.equals(ValidationResult.VALID);
+ ValidationResult result = validator.validate(a, vctx);
+ boolean valid = result.equals(ValidationResult.VALID);
if (!valid) {
if (logger.isDebugEnabled()) {
- logger.debug(format("Failed to validate assertion from %s with user %s", token.getIdpEntityId(),
- getUsername(token, a)
- ));
+ logger.debug(format("Failed to validate assertion from %s", token.getIdpEntityId()));
}
+ throw authException(Saml2ErrorCodes.INVALID_ASSERTION, vctx.getValidationFailureMessage());
}
- return valid;
}
catch (AssertionValidationException e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to validate assertion:", e);
}
- return false;
+ throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e);
}
}
- private Response getSaml2Response(String xml) throws Saml2Exception, AuthenticationException {
- final Object result = this.saml.resolve(xml);
- if (result == null) {
- throw new AuthenticationCredentialsNotFoundException("SAMLResponse returned null object");
- }
- else if (result instanceof Response) {
- return (Response) result;
+ private Response getSaml2Response(Saml2AuthenticationToken token) throws Saml2Exception, Saml2AuthenticationException {
+ try {
+ Object result = this.saml.resolve(token.getSaml2Response());
+ if (result instanceof Response) {
+ return (Response) result;
+ }
+ else {
+ throw authException(UNKNOWN_RESPONSE_CLASS, "Invalid response class:" + result.getClass().getName());
+ }
+ } catch (Saml2Exception x) {
+ throw authException(MALFORMED_RESPONSE_DATA, x.getMessage(), x);
}
- throw new IllegalArgumentException("Invalid response class:"+result.getClass().getName());
+
+ }
+
+ private Saml2Error validationError(String code, String description) {
+ return new Saml2Error(
+ code,
+ description
+ );
+ }
+
+ private Saml2AuthenticationException authException(String code, String description) throws Saml2AuthenticationException {
+ return new Saml2AuthenticationException(
+ validationError(code, description)
+ );
+ }
+
+
+ private Saml2AuthenticationException authException(String code, String description, Exception cause) throws Saml2AuthenticationException {
+ return new Saml2AuthenticationException(
+ validationError(code, description),
+ cause
+ );
}
private SAML20AssertionValidator getAssertionValidator(Saml2AuthenticationToken provider) {
List conditions = Collections.singletonList(new AudienceRestrictionConditionValidator());
- final BearerSubjectConfirmationValidator subjectConfirmationValidator =
- new BearerSubjectConfirmationValidator();
+ BearerSubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator();
List subjects = Collections.singletonList(subjectConfirmationValidator);
List statements = Collections.emptyList();
Set credentials = new HashSet<>();
- for (X509Certificate key : getVerificationKeys(provider)) {
- final Credential cred = getVerificationCredential(key);
+ for (X509Certificate key : getVerificationCertificates(provider)) {
+ Credential cred = getVerificationCredential(key);
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
@@ -345,37 +423,38 @@ private Decrypter getDecrypter(Saml2X509Credential key) {
return decrypter;
}
- private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion) {
- Saml2Exception last = null;
+ private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion)
+ throws Saml2AuthenticationException {
+ Saml2AuthenticationException last = null;
List decryptionCredentials = getDecryptionCredentials(token);
if (decryptionCredentials.isEmpty()) {
- throw new Saml2Exception("No valid decryption credentials found.");
+ throw authException(DECRYPTION_ERROR, "No valid decryption credentials found.");
}
for (Saml2X509Credential key : decryptionCredentials) {
- final Decrypter decrypter = getDecrypter(key);
+ Decrypter decrypter = getDecrypter(key);
try {
return decrypter.decrypt(assertion);
}
catch (DecryptionException e) {
- last = new Saml2Exception(e);
+ last = authException(DECRYPTION_ERROR, e.getMessage(), e);
}
}
throw last;
}
- private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) {
- Saml2Exception last = null;
+ private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) throws Saml2AuthenticationException {
+ Saml2AuthenticationException last = null;
List decryptionCredentials = getDecryptionCredentials(token);
if (decryptionCredentials.isEmpty()) {
- throw new Saml2Exception("No valid decryption credentials found.");
+ throw authException(DECRYPTION_ERROR, "No valid decryption credentials found.");
}
for (Saml2X509Credential key : decryptionCredentials) {
- final Decrypter decrypter = getDecrypter(key);
+ Decrypter decrypter = getDecrypter(key);
try {
return (NameID) decrypter.decrypt(assertion);
}
catch (DecryptionException e) {
- last = new Saml2Exception(e);
+ last = authException(DECRYPTION_ERROR, e.getMessage(), e);
}
}
throw last;
@@ -391,7 +470,7 @@ private List getDecryptionCredentials(Saml2AuthenticationTo
return result;
}
- private List getVerificationKeys(Saml2AuthenticationToken token) {
+ private List getVerificationCertificates(Saml2AuthenticationToken token) {
List result = new LinkedList<>();
for (Saml2X509Credential c : token.getX509Credentials()) {
if (c.isSignatureVerficationCredential()) {
diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java
index 6b5d68f32bb..1c494ef0a2a 100644
--- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java
@@ -19,11 +19,8 @@
import org.springframework.util.Assert;
import org.joda.time.DateTime;
-import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Issuer;
-import org.opensaml.security.SecurityException;
-import org.opensaml.xmlsec.signature.support.SignatureException;
import java.time.Clock;
import java.time.Instant;
@@ -51,16 +48,11 @@ public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
issuer.setValue(request.getLocalSpEntityId());
auth.setIssuer(issuer);
auth.setDestination(request.getWebSsoUri());
- try {
- return this.saml.toXml(
- auth,
- request.getCredentials(),
- request.getLocalSpEntityId()
- );
- }
- catch (MarshallingException | SignatureException | SecurityException e) {
- throw new IllegalStateException(e);
- }
+ return this.saml.toXml(
+ auth,
+ request.getCredentials(),
+ request.getLocalSpEntityId()
+ );
}
/**
diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java
index 574c7362aba..67d2fb2cc4a 100644
--- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java
@@ -15,14 +15,6 @@
*/
package org.springframework.security.saml2.provider.service.authentication;
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import javax.xml.XMLConstants;
-import javax.xml.namespace.QName;
-
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.credentials.Saml2X509Credential;
@@ -59,6 +51,14 @@
import org.w3c.dom.Document;
import org.w3c.dom.Element;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
+
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.util.Arrays.asList;
@@ -165,10 +165,7 @@ T buildSAMLObject(final Class clazz) {
QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null);
return (T) getBuilderFactory().getBuilder(defaultElementName).buildObject(defaultElementName);
}
- catch (IllegalAccessException e) {
- throw new Saml2Exception("Could not create SAML object", e);
- }
- catch (NoSuchFieldException e) {
+ catch (NoSuchFieldException | IllegalAccessException e) {
throw new Saml2Exception("Could not create SAML object", e);
}
}
@@ -177,6 +174,28 @@ XMLObject resolve(String xml) {
return resolve(xml.getBytes(StandardCharsets.UTF_8));
}
+ String toXml(XMLObject object, List signingCredentials, String localSpEntityId) {
+ if (object instanceof SignableSAMLObject && null != hasSigningCredential(signingCredentials)) {
+ signXmlObject(
+ (SignableSAMLObject) object,
+ signingCredentials,
+ localSpEntityId
+ );
+ }
+ final MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory();
+ try {
+ Element element = marshallerFactory.getMarshaller(object).marshall(object);
+ return SerializeSupport.nodeToString(element);
+ } catch (MarshallingException e) {
+ throw new Saml2Exception(e);
+ }
+ }
+
+ /*
+ * ==============================================================
+ * PRIVATE METHODS
+ * ==============================================================
+ */
private XMLObject resolve(byte[] xml) {
XMLObject parsed = parse(xml);
if (parsed != null) {
@@ -200,18 +219,6 @@ private UnmarshallerFactory getUnmarshallerFactory() {
return XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
}
- String toXml(XMLObject object, List signingCredentials, String localSpEntityId)
- throws MarshallingException, SignatureException, SecurityException {
- if (object instanceof SignableSAMLObject && null != hasSigningCredential(signingCredentials)) {
- signXmlObject(
- (SignableSAMLObject) object,
- getSigningCredential(signingCredentials, localSpEntityId)
- );
- }
- final MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory();
- Element element = marshallerFactory.getMarshaller(object).marshall(object);
- return SerializeSupport.nodeToString(element);
- }
private Saml2X509Credential hasSigningCredential(List credentials) {
for (Saml2X509Credential c : credentials) {
@@ -222,22 +229,12 @@ private Saml2X509Credential hasSigningCredential(List crede
return null;
}
- private void signXmlObject(SignableSAMLObject object, Credential credential)
- throws MarshallingException, SecurityException, SignatureException {
- SignatureSigningParameters parameters = new SignatureSigningParameters();
- parameters.setSigningCredential(credential);
- parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
- parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256);
- parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
- SignatureSupport.signObject(object, parameters);
- }
-
private Credential getSigningCredential(List signingCredential,
String localSpEntityId
) {
Saml2X509Credential credential = hasSigningCredential(signingCredential);
if (credential == null) {
- throw new IllegalArgumentException("no signing credential configured");
+ throw new Saml2Exception("no signing credential configured");
}
BasicCredential cred = getBasicCredential(credential);
cred.setEntityId(localSpEntityId);
@@ -245,6 +242,21 @@ private Credential getSigningCredential(List signingCredent
return cred;
}
+ private void signXmlObject(SignableSAMLObject object, List signingCredentials, String entityId) {
+ SignatureSigningParameters parameters = new SignatureSigningParameters();
+ Credential credential = getSigningCredential(signingCredentials, entityId);
+ parameters.setSigningCredential(credential);
+ parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
+ parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256);
+ parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
+ try {
+ SignatureSupport.signObject(object, parameters);
+ } catch (MarshallingException | SignatureException | SecurityException e) {
+ throw new Saml2Exception(e);
+ }
+
+ }
+
private BasicX509Credential getBasicCredential(Saml2X509Credential credential) {
return CredentialSupport.getSimpleCredential(
credential.getCertificate(),
diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java
new file mode 100644
index 00000000000..86709cec1e8
--- /dev/null
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.util.Assert;
+
+/**
+ * This exception is thrown for all SAML 2.0 related {@link Authentication} errors.
+ *
+ *
+ * There are a number of scenarios where an error may occur, for example:
+ *
+ * - The response or assertion request is missing or malformed
+ * - Missing or invalid subject
+ * - Missing or invalid signatures
+ * - The time period validation for the assertion fails
+ * - One of the assertion conditions was not met
+ * - Decryption failed
+ * - Unable to locate a subject identifier, commonly known as username
+ *
+ *
+ * @since 5.2
+ */
+public class Saml2AuthenticationException extends AuthenticationException {
+ private Saml2Error error;
+
+ /**
+ * Constructs a {@code Saml2AuthenticationException} using the provided parameters.
+ *
+ * @param error the {@link Saml2Error SAML 2.0 Error}
+ */
+ public Saml2AuthenticationException(Saml2Error error) {
+ this(error, error.getDescription());
+ }
+
+ /**
+ * Constructs a {@code Saml2AuthenticationException} using the provided parameters.
+ *
+ * @param error the {@link Saml2Error SAML 2.0 Error}
+ * @param cause the root cause
+ */
+ public Saml2AuthenticationException(Saml2Error error, Throwable cause) {
+ this(error, cause.getMessage(), cause);
+ }
+
+ /**
+ * Constructs a {@code Saml2AuthenticationException} using the provided parameters.
+ *
+ * @param error the {@link Saml2Error SAML 2.0 Error}
+ * @param message the detail message
+ */
+ public Saml2AuthenticationException(Saml2Error error, String message) {
+ super(message);
+ this.setError(error);
+ }
+
+ /**
+ * Constructs a {@code Saml2AuthenticationException} using the provided parameters.
+ *
+ * @param error the {@link Saml2Error SAML 2.0 Error}
+ * @param message the detail message
+ * @param cause the root cause
+ */
+ public Saml2AuthenticationException(Saml2Error error, String message, Throwable cause) {
+ super(message, cause);
+ this.setError(error);
+ }
+
+ /**
+ * Returns the {@link Saml2Error SAML 2.0 Error}.
+ *
+ * @return the {@link Saml2Error}
+ */
+ public Saml2Error getError() {
+ return this.error;
+ }
+
+ private void setError(Saml2Error error) {
+ Assert.notNull(error, "error cannot be null");
+ this.error = error;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuffer sb = new StringBuffer("Saml2AuthenticationException{");
+ sb.append("error=").append(error);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java
index a77a4e06a6d..35edb477b1a 100644
--- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java
@@ -16,6 +16,8 @@
package org.springframework.security.saml2.provider.service.authentication;
+import org.springframework.security.saml2.Saml2Exception;
+
/**
* Component that generates an AuthenticationRequest, samlp:AuthnRequestType
as defined by
* https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf
@@ -33,6 +35,7 @@ public interface Saml2AuthenticationRequestFactory {
* accompanying data
* @return XML data in the format of a String. This data may be signed, encrypted, both signed and encrypted or
* neither signed and encrypted
+ * @throws Saml2Exception when a SAML library exception occurs
*/
String createAuthenticationRequest(Saml2AuthenticationRequest request);
}
diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java
new file mode 100644
index 00000000000..786ae0e0119
--- /dev/null
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.util.Assert;
+
+import java.io.Serializable;
+
+/**
+ * A representation of an SAML 2.0 Error.
+ *
+ *
+ * At a minimum, an error response will contain an error code.
+ * The commonly used error code are defined in this class
+ * or a new codes can be defined in the future as arbitrary strings.
+ *
+ * @since 5.2
+ */
+public class Saml2Error implements Serializable {
+ private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+
+ private final String errorCode;
+ private final String description;
+
+ /**
+ * Constructs a {@code Saml2Error} using the provided parameters.
+ *
+ * @param errorCode the error code
+ * @param description the error description
+ */
+ public Saml2Error(String errorCode, String description) {
+ Assert.hasText(errorCode, "errorCode cannot be empty");
+ this.errorCode = errorCode;
+ this.description = description;
+ }
+
+ /**
+ * Returns the error code.
+ *
+ * @return the error code
+ */
+ public final String getErrorCode() {
+ return this.errorCode;
+ }
+
+ /**
+ * Returns the error description.
+ *
+ * @return the error description
+ */
+ public final String getDescription() {
+ return this.description;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + this.getErrorCode() + "] " +
+ (this.getDescription() != null ? this.getDescription() : "");
+ }
+}
diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java
new file mode 100644
index 00000000000..2b98a5334c9
--- /dev/null
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.authentication;
+
+/**
+ * A list of SAML known 2 error codes used during SAML authentication.
+ *
+ * @since 5.2
+ */
+public interface Saml2ErrorCodes {
+ /**
+ * SAML Data does not represent a SAML 2 Response object.
+ * A valid XML object was received, but that object was not a
+ * SAML 2 Response object of type {@code ResponseType} per specification
+ * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=46
+ */
+ String UNKNOWN_RESPONSE_CLASS = "unknown_response_class";
+ /**
+ * The response data is malformed or incomplete.
+ * An invalid XML object was received, and XML unmarshalling failed.
+ */
+ String MALFORMED_RESPONSE_DATA = "malformed_response_data";
+ /**
+ * Response destination does not match the request URL.
+ * A SAML 2 response object was received at a URL that
+ * did not match the URL stored in the {code Destination} attribute
+ * in the Response object.
+ * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38
+ */
+ String INVALID_DESTINATION = "invalid_destination";
+ /**
+ * The assertion was not valid.
+ * The assertion used for authentication failed validation.
+ * Details around the failure will be present in the error description.
+ */
+ String INVALID_ASSERTION = "invalid_assertion";
+ /**
+ * The signature of response or assertion was invalid.
+ * Either the response or the assertion was missing a signature
+ * or the signature could not be verified using the system's
+ * configured credentials. Most commonly the IDP's
+ * X509 certificate.
+ */
+ String INVALID_SIGNATURE = "invalid_signature";
+ /**
+ * The assertion did not contain a subject element.
+ * The subject element, type SubjectType, contains
+ * a {@code NameID} or an {@code EncryptedID} that is used
+ * to assign the authenticated principal an identifier,
+ * typically a username.
+ *
+ * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=18
+ */
+ String SUBJECT_NOT_FOUND = "subject_not_found";
+ /**
+ * The subject did not contain a user identifier
+ * The assertion contained a subject element, but the subject
+ * element did not have a {@code NameID} or {@code EncryptedID}
+ * element
+ *
+ * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=18
+ */
+ String USERNAME_NOT_FOUND = "username_not_found";
+ /**
+ * The system failed to decrypt an assertion or a name identifier.
+ * This error code will be thrown if the decryption of either a
+ * {@code EncryptedAssertion} or {@code EncryptedID} fails.
+ * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=17
+ */
+ String DECRYPTION_ERROR = "decryption_error";
+ /**
+ * An Issuer element contained a value that didn't
+ * https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=15
+ */
+ String INVALID_ISSUER = "invalid_issuer";
+ /**
+ * An error happened during validation.
+ * Used when internal, non classified, errors are caught during the
+ * authentication process.
+ */
+ String INTERNAL_VALIDATION_ERROR = "internal_validation_error";
+}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java
new file mode 100644
index 00000000000..292619040dd
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.credentials;
+
+import org.springframework.security.converter.RsaKeyConverters;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.io.ByteArrayInputStream;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION;
+import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION;
+import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING;
+import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION;
+
+public class Saml2X509CredentialTests {
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ private Saml2X509Credential credential;
+ private PrivateKey key;
+ private X509Certificate certificate;
+
+ @Before
+ public void setup() throws Exception {
+ String keyData = "-----BEGIN PRIVATE KEY-----\n" +
+ "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" +
+ "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" +
+ "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" +
+ "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" +
+ "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" +
+ "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" +
+ "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" +
+ "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" +
+ "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" +
+ "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" +
+ "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" +
+ "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" +
+ "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" +
+ "INrtuLp4YHbgk1mi\n" +
+ "-----END PRIVATE KEY-----";
+ key = RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(keyData.getBytes(UTF_8)));
+ final CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ String certificateData = "-----BEGIN CERTIFICATE-----\n" +
+ "MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" +
+ "VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" +
+ "A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" +
+ "DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" +
+ "MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" +
+ "MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" +
+ "TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" +
+ "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" +
+ "vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" +
+ "+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" +
+ "y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" +
+ "XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" +
+ "qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" +
+ "RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" +
+ "-----END CERTIFICATE-----";
+ certificate = (X509Certificate) factory
+ .generateCertificate(new ByteArrayInputStream(certificateData.getBytes(UTF_8)));
+ }
+
+ @Test
+ public void constructorWhenRelyingPartyWithCredentialsThenItSucceeds() {
+ new Saml2X509Credential(key, certificate, SIGNING);
+ new Saml2X509Credential(key, certificate, SIGNING, DECRYPTION);
+ new Saml2X509Credential(key, certificate, DECRYPTION);
+ }
+
+ @Test
+ public void constructorWhenAssertingPartyWithCredentialsThenItSucceeds() {
+ new Saml2X509Credential(certificate, VERIFICATION);
+ new Saml2X509Credential(certificate, VERIFICATION, ENCRYPTION);
+ new Saml2X509Credential(certificate, ENCRYPTION);
+ }
+
+ @Test
+ public void constructorWhenRelyingPartyWithoutCredentialsThenItFails() {
+ exception.expect(IllegalArgumentException.class);
+ new Saml2X509Credential(null, (X509Certificate) null, SIGNING);
+ }
+
+ @Test
+ public void constructorWhenRelyingPartyWithoutPrivateKeyThenItFails() {
+ exception.expect(IllegalArgumentException.class);
+ new Saml2X509Credential(null, certificate, SIGNING);
+ }
+
+ @Test
+ public void constructorWhenRelyingPartyWithoutCertificateThenItFails() {
+ exception.expect(IllegalArgumentException.class);
+ new Saml2X509Credential(key, null, SIGNING);
+ }
+
+ @Test
+ public void constructorWhenAssertingPartyWithoutCertificateThenItFails() {
+ exception.expect(IllegalArgumentException.class);
+ new Saml2X509Credential(null, SIGNING);
+ }
+
+ @Test
+ public void constructorWhenRelyingPartyWithEncryptionUsageThenItFails() {
+ exception.expect(IllegalStateException.class);
+ new Saml2X509Credential(key, certificate, ENCRYPTION);
+ }
+
+ @Test
+ public void constructorWhenRelyingPartyWithVerificationUsageThenItFails() {
+ exception.expect(IllegalStateException.class);
+ new Saml2X509Credential(key, certificate, VERIFICATION);
+ }
+
+ @Test
+ public void constructorWhenAssertingPartyWithSigningUsageThenItFails() {
+ exception.expect(IllegalStateException.class);
+ new Saml2X509Credential(certificate, SIGNING);
+ }
+
+ @Test
+ public void constructorWhenAssertingPartyWithDecryptionUsageThenItFails() {
+ exception.expect(IllegalStateException.class);
+ new Saml2X509Credential(certificate, DECRYPTION);
+ }
+
+
+}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java
new file mode 100644
index 00000000000..772ed072f59
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.core.Authentication;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.joda.time.DateTime;
+import org.joda.time.Duration;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.EncryptedAssertion;
+import org.opensaml.saml.saml2.core.EncryptedID;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Response;
+
+import static java.util.Collections.emptyList;
+import static org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationObjects.assertion;
+import static org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationObjects.response;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.encryptAssertion;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.encryptNameId;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.signXmlObject;
+import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.assertingPartyCredentials;
+import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials;
+import static org.springframework.test.util.AssertionErrors.assertTrue;
+import static org.springframework.util.StringUtils.hasText;
+
+public class OpenSamlAuthenticationProviderTests {
+
+ private static String username = "test@saml.user";
+ private static String recipientUri = "https://localhost/login/saml2/sso/idp-alias";
+ private static String recipientEntityId = "https://localhost/saml2/service-provider-metadata/idp-alias";
+ private static String idpEntityId = "https://some.idp.test/saml2/idp";
+
+ private OpenSamlAuthenticationProvider provider;
+ private OpenSamlImplementation saml;
+ private Saml2AuthenticationToken token;
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Before
+ public void setup() {
+ saml = OpenSamlImplementation.getInstance();
+ provider = new OpenSamlAuthenticationProvider();
+ token = new Saml2AuthenticationToken(
+ "responseXml",
+ recipientUri,
+ idpEntityId,
+ recipientEntityId,
+ relyingPartyCredentials()
+ );
+ }
+
+ @Test
+ public void supportsWhenSaml2AuthenticationTokenThenReturnTrue() {
+
+ assertTrue(
+ OpenSamlAuthenticationProvider.class + "should support " + token.getClass(),
+ provider.supports(token.getClass())
+ );
+ }
+
+ @Test
+ public void supportsWhenNotSaml2AuthenticationTokenThenReturnFalse() {
+ assertTrue(
+ OpenSamlAuthenticationProvider.class + "should not support " + Authentication.class,
+ !provider.supports(Authentication.class)
+ );
+ }
+
+ @Test
+ public void authenticateWhenUnknownDataClassThenThrowAuthenticationException() {
+ Assertion assertion = defaultAssertion();
+ token = responseXml(assertion, idpEntityId);
+ exception.expect(authenticationMatcher(Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS));
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenXmlErrorThenThrowAuthenticationException() {
+ token = new Saml2AuthenticationToken(
+ "invalid xml string",
+ recipientUri,
+ idpEntityId,
+ recipientEntityId,
+ relyingPartyCredentials()
+ );
+ exception.expect(authenticationMatcher(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA));
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
+ Response response = response(recipientUri + "invalid", idpEntityId);
+ token = responseXml(response, idpEntityId);
+ exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_DESTINATION));
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() {
+ Response response = response(recipientUri, idpEntityId);
+ token = responseXml(response, idpEntityId);
+ exception.expect(
+ authenticationMatcher(
+ Saml2ErrorCodes.MALFORMED_RESPONSE_DATA,
+ "No assertions found in response."
+ )
+ );
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() {
+ Response response = response(recipientUri, idpEntityId);
+ Assertion assertion = defaultAssertion();
+ response.getAssertions().add(assertion);
+ token = responseXml(response, idpEntityId);
+ exception.expect(
+ authenticationMatcher(
+ Saml2ErrorCodes.INVALID_SIGNATURE
+ )
+ );
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() throws Exception {
+ Response response = response(recipientUri, idpEntityId);
+ Assertion assertion = defaultAssertion();
+ assertion
+ .getSubject()
+ .getSubjectConfirmations()
+ .get(0)
+ .getSubjectConfirmationData()
+ .setNotOnOrAfter(DateTime.now().minus(Duration.standardDays(3)));
+ signXmlObject(
+ assertion,
+ assertingPartyCredentials(),
+ recipientEntityId
+ );
+ response.getAssertions().add(assertion);
+ token = responseXml(response, idpEntityId);
+
+ exception.expect(
+ authenticationMatcher(
+ Saml2ErrorCodes.INVALID_ASSERTION
+ )
+ );
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenMissingSubjectThenThrowAuthenticationException() {
+ Response response = response(recipientUri, idpEntityId);
+ Assertion assertion = defaultAssertion();
+ assertion.setSubject(null);
+ signXmlObject(
+ assertion,
+ assertingPartyCredentials(),
+ recipientEntityId
+ );
+ response.getAssertions().add(assertion);
+ token = responseXml(response, idpEntityId);
+
+ exception.expect(
+ authenticationMatcher(
+ Saml2ErrorCodes.SUBJECT_NOT_FOUND
+ )
+ );
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenUsernameMissingThenThrowAuthenticationException() throws Exception {
+ Response response = response(recipientUri, idpEntityId);
+ Assertion assertion = defaultAssertion();
+ assertion
+ .getSubject()
+ .getNameID()
+ .setValue(null);
+ signXmlObject(
+ assertion,
+ assertingPartyCredentials(),
+ recipientEntityId
+ );
+ response.getAssertions().add(assertion);
+ token = responseXml(response, idpEntityId);
+
+ exception.expect(
+ authenticationMatcher(
+ Saml2ErrorCodes.USERNAME_NOT_FOUND
+ )
+ );
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenEncryptedAssertionWithoutSignatureThenItSucceeds() throws Exception {
+ Response response = response(recipientUri, idpEntityId);
+ Assertion assertion = defaultAssertion();
+ EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials());
+ response.getEncryptedAssertions().add(encryptedAssertion);
+ token = responseXml(response, idpEntityId);
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() throws Exception {
+ Response response = response(recipientUri, idpEntityId);
+ Assertion assertion = defaultAssertion();
+ NameID nameId = assertion.getSubject().getNameID();
+ EncryptedID encryptedID = encryptNameId(nameId, assertingPartyCredentials());
+ assertion.getSubject().setNameID(null);
+ assertion.getSubject().setEncryptedID(encryptedID);
+ signXmlObject(
+ assertion,
+ assertingPartyCredentials(),
+ recipientEntityId
+ );
+ response.getAssertions().add(assertion);
+ token = responseXml(response, idpEntityId);
+ provider.authenticate(token);
+ }
+
+
+ @Test
+ public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() throws Exception {
+ Response response = response(recipientUri, idpEntityId);
+ Assertion assertion = defaultAssertion();
+ EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials());
+ response.getEncryptedAssertions().add(encryptedAssertion);
+ token = responseXml(response, idpEntityId);
+
+ token = new Saml2AuthenticationToken(
+ token.getSaml2Response(),
+ recipientUri,
+ idpEntityId,
+ recipientEntityId,
+ emptyList()
+ );
+
+ exception.expect(
+ authenticationMatcher(
+ Saml2ErrorCodes.DECRYPTION_ERROR,
+ "No valid decryption credentials found."
+ )
+ );
+ provider.authenticate(token);
+ }
+
+ @Test
+ public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() throws Exception {
+ Response response = response(recipientUri, idpEntityId);
+ Assertion assertion = defaultAssertion();
+ EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials());
+ response.getEncryptedAssertions().add(encryptedAssertion);
+ token = responseXml(response, idpEntityId);
+
+ token = new Saml2AuthenticationToken(
+ token.getSaml2Response(),
+ recipientUri,
+ idpEntityId,
+ recipientEntityId,
+ assertingPartyCredentials()
+ );
+
+ exception.expect(
+ authenticationMatcher(
+ Saml2ErrorCodes.DECRYPTION_ERROR,
+ "Failed to decrypt EncryptedData"
+ )
+ );
+ provider.authenticate(token);
+ }
+
+ private Assertion defaultAssertion() {
+ return assertion(
+ username,
+ idpEntityId,
+ recipientEntityId,
+ recipientUri
+ );
+ }
+
+ private Saml2AuthenticationToken responseXml(
+ XMLObject object,
+ String issuerEntityId
+ ) {
+ String xml = saml.toXml(object, emptyList(), issuerEntityId);
+ return new Saml2AuthenticationToken(
+ xml,
+ recipientUri,
+ idpEntityId,
+ recipientEntityId,
+ relyingPartyCredentials()
+ );
+
+ }
+
+ private BaseMatcher authenticationMatcher(String code) {
+ return authenticationMatcher(code, null);
+ }
+
+ private BaseMatcher authenticationMatcher(String code, String description) {
+ return new BaseMatcher() {
+ private Object value = null;
+
+ @Override
+ public boolean matches(Object item) {
+ if (!(item instanceof Saml2AuthenticationException)) {
+ value = item;
+ return false;
+ }
+ Saml2AuthenticationException ex = (Saml2AuthenticationException) item;
+ if (!code.equals(ex.getError().getErrorCode())) {
+ value = item;
+ return false;
+ }
+ if (hasText(description)) {
+ if (!description.equals(ex.getError().getDescription())) {
+ value = item;
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void describeTo(Description desc) {
+ String excepting = "Saml2AuthenticationException[code="+code+"; description="+description+"]";
+ desc.appendText(excepting);
+
+ }
+ };
+ }
+}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java
new file mode 100644
index 00000000000..29cdf8aae22
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.saml2.Saml2Exception;
+import org.springframework.security.saml2.credentials.Saml2X509Credential;
+
+import org.apache.xml.security.algorithms.JCEMapper;
+import org.apache.xml.security.encryption.XMLCipherParameters;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.saml.common.SignableSAMLObject;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.EncryptedAssertion;
+import org.opensaml.saml.saml2.core.EncryptedID;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.encryption.Encrypter;
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.BasicCredential;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialSupport;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.x509.BasicX509Credential;
+import org.opensaml.xmlsec.SignatureSigningParameters;
+import org.opensaml.xmlsec.encryption.support.DataEncryptionParameters;
+import org.opensaml.xmlsec.encryption.support.EncryptionException;
+import org.opensaml.xmlsec.encryption.support.KeyEncryptionParameters;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+import org.opensaml.xmlsec.signature.support.SignatureException;
+import org.opensaml.xmlsec.signature.support.SignatureSupport;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import javax.crypto.SecretKey;
+
+import static java.util.Arrays.asList;
+import static org.opensaml.security.crypto.KeySupport.generateKey;
+
+final class Saml2CryptoTestSupport {
+ static void signXmlObject(SignableSAMLObject object, List signingCredentials, String entityId) {
+ SignatureSigningParameters parameters = new SignatureSigningParameters();
+ Credential credential = getSigningCredential(signingCredentials, entityId);
+ parameters.setSigningCredential(credential);
+ parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
+ parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256);
+ parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
+ try {
+ SignatureSupport.signObject(object, parameters);
+ } catch (MarshallingException | SignatureException | SecurityException e) {
+ throw new Saml2Exception(e);
+ }
+
+ }
+
+ static EncryptedAssertion encryptAssertion(Assertion assertion, List encryptionCredentials) {
+ X509Certificate certificate = getEncryptionCertificate(encryptionCredentials);
+ Encrypter encrypter = getEncrypter(certificate);
+ try {
+ Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER");
+ encrypter.setKeyPlacement(keyPlacement);
+ return encrypter.encrypt(assertion);
+ }
+ catch (EncryptionException e) {
+ throw new Saml2Exception("Unable to encrypt assertion.", e);
+ }
+ }
+
+ static EncryptedID encryptNameId(NameID nameID, List encryptionCredentials) {
+ X509Certificate certificate = getEncryptionCertificate(encryptionCredentials);
+ Encrypter encrypter = getEncrypter(certificate);
+ try {
+ Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER");
+ encrypter.setKeyPlacement(keyPlacement);
+ return encrypter.encrypt(nameID);
+ }
+ catch (EncryptionException e) {
+ throw new Saml2Exception("Unable to encrypt nameID.", e);
+ }
+ }
+
+ private static Encrypter getEncrypter(X509Certificate certificate) {
+ Credential credential = CredentialSupport.getSimpleCredential(certificate, null);
+ final String dataAlgorithm = XMLCipherParameters.AES_256;
+ final String keyAlgorithm = XMLCipherParameters.RSA_1_5;
+ SecretKey secretKey = generateKeyFromURI(dataAlgorithm);
+ BasicCredential dataCredential = new BasicCredential(secretKey);
+ DataEncryptionParameters dataEncryptionParameters = new DataEncryptionParameters();
+ dataEncryptionParameters.setEncryptionCredential(dataCredential);
+ dataEncryptionParameters.setAlgorithm(dataAlgorithm);
+
+ KeyEncryptionParameters keyEncryptionParameters = new KeyEncryptionParameters();
+ keyEncryptionParameters.setEncryptionCredential(credential);
+ keyEncryptionParameters.setAlgorithm(keyAlgorithm);
+
+ Encrypter encrypter = new Encrypter(dataEncryptionParameters, asList(keyEncryptionParameters));
+
+ return encrypter;
+ }
+
+ private static SecretKey generateKeyFromURI(String algoURI) {
+ try {
+ String jceAlgorithmName = JCEMapper.getJCEKeyAlgorithmFromURI(algoURI);
+ int keyLength = JCEMapper.getKeyLengthFromURI(algoURI);
+ return generateKey(jceAlgorithmName, keyLength, null);
+ }
+ catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ throw new Saml2Exception(e);
+ }
+ }
+
+ private static X509Certificate getEncryptionCertificate(List encryptionCredentials) {
+ X509Certificate certificate = null;
+ for (Saml2X509Credential credential : encryptionCredentials) {
+ if (credential.isEncryptionCredential()) {
+ certificate = credential.getCertificate();
+ break;
+ }
+ }
+ if (certificate == null) {
+ throw new Saml2Exception("No valid encryption certificate found");
+ }
+ return certificate;
+ }
+
+ private static Saml2X509Credential hasSigningCredential(List credentials) {
+ for (Saml2X509Credential c : credentials) {
+ if (c.isSigningCredential()) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ private static Credential getSigningCredential(List signingCredential,
+ String localSpEntityId
+ ) {
+ Saml2X509Credential credential = hasSigningCredential(signingCredential);
+ if (credential == null) {
+ throw new Saml2Exception("no signing credential configured");
+ }
+ BasicCredential cred = getBasicCredential(credential);
+ cred.setEntityId(localSpEntityId);
+ cred.setUsageType(UsageType.SIGNING);
+ return cred;
+ }
+
+ private static BasicX509Credential getBasicCredential(Saml2X509Credential credential) {
+ return CredentialSupport.getSimpleCredential(
+ credential.getCertificate(),
+ credential.getPrivateKey()
+ );
+ }
+
+}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java
new file mode 100644
index 00000000000..acc9d9f2abd
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.authentication;
+
+import org.joda.time.DateTime;
+import org.joda.time.Duration;
+import org.opensaml.saml.common.SAMLVersion;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Conditions;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.core.SubjectConfirmation;
+import org.opensaml.saml.saml2.core.SubjectConfirmationData;
+
+import java.util.UUID;
+
+final class TestSaml2AuthenticationObjects {
+ private static OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
+
+ static Response response(String destination, String issuerEntityId) {
+ Response response = saml.buildSAMLObject(Response.class);
+ response.setID("R"+UUID.randomUUID().toString());
+ response.setIssueInstant(DateTime.now());
+ response.setVersion(SAMLVersion.VERSION_20);
+ response.setID("_" + UUID.randomUUID().toString());
+ response.setDestination(destination);
+ response.setIssuer(issuer(issuerEntityId));
+ return response;
+ }
+ static Assertion assertion(
+ String username,
+ String issuerEntityId,
+ String recipientEntityId,
+ String recipientUri
+ ) {
+ Assertion assertion = saml.buildSAMLObject(Assertion.class);
+ assertion.setID("A"+ UUID.randomUUID().toString());
+ assertion.setIssueInstant(DateTime.now());
+ assertion.setVersion(SAMLVersion.VERSION_20);
+ assertion.setIssueInstant(DateTime.now());
+ assertion.setIssuer(issuer(issuerEntityId));
+ assertion.setSubject(subject(username));
+ assertion.setConditions(conditions());
+
+ SubjectConfirmation subjectConfirmation = subjectConfirmation();
+ subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER);
+ SubjectConfirmationData confirmationData = subjectConfirmationData(recipientEntityId);
+ confirmationData.setRecipient(recipientUri);
+ subjectConfirmation.setSubjectConfirmationData(confirmationData);
+ assertion.getSubject().getSubjectConfirmations().add(subjectConfirmation);
+ return assertion;
+ }
+
+
+ static Issuer issuer(String entityId) {
+ Issuer issuer = saml.buildSAMLObject(Issuer.class);
+ issuer.setValue(entityId);
+ return issuer;
+ }
+
+ static Subject subject(String principalName) {
+ Subject subject = saml.buildSAMLObject(Subject.class);
+
+ if (principalName != null) {
+ subject.setNameID(nameId(principalName));
+ }
+
+ return subject;
+ }
+
+ static NameID nameId(String principalName) {
+ NameID nameId = saml.buildSAMLObject(NameID.class);
+ nameId.setValue(principalName);
+ return nameId;
+ }
+
+ static SubjectConfirmation subjectConfirmation() {
+ return saml.buildSAMLObject(SubjectConfirmation.class);
+ }
+
+ static SubjectConfirmationData subjectConfirmationData(String recipient) {
+ SubjectConfirmationData subject = saml.buildSAMLObject(SubjectConfirmationData.class);
+ subject.setRecipient(recipient);
+ subject.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000)));
+ subject.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000)));
+ return subject;
+ }
+
+ static Conditions conditions() {
+ Conditions conditions = saml.buildSAMLObject(Conditions.class);
+ conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000)));
+ conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000)));
+ return conditions;
+ }
+
+}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java
new file mode 100644
index 00000000000..faef9743b59
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.saml2.Saml2Exception;
+import org.springframework.security.saml2.credentials.Saml2X509Credential;
+
+import org.opensaml.security.crypto.KeySupport;
+
+import java.io.ByteArrayInputStream;
+import java.security.KeyException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION;
+import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION;
+import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING;
+import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION;
+
+final class TestSaml2X509Credentials {
+ static List assertingPartyCredentials() {
+ return Arrays.asList(
+ new Saml2X509Credential(
+ idpPrivateKey(),
+ idpCertificate(),
+ SIGNING,
+ DECRYPTION
+ ),
+ new Saml2X509Credential(
+ spCertificate(),
+ ENCRYPTION,
+ VERIFICATION
+ )
+ );
+ }
+
+ static List relyingPartyCredentials() {
+ return Arrays.asList(
+ new Saml2X509Credential(
+ spPrivateKey(),
+ spCertificate(),
+ SIGNING,
+ DECRYPTION
+ ),
+ new Saml2X509Credential(
+ idpCertificate(),
+ ENCRYPTION,
+ VERIFICATION
+ )
+ );
+ }
+
+ private static X509Certificate certificate(String cert) {
+ ByteArrayInputStream certBytes = new ByteArrayInputStream(cert.getBytes());
+ try {
+ return (X509Certificate) CertificateFactory
+ .getInstance("X.509")
+ .generateCertificate(certBytes);
+ }
+ catch (CertificateException e) {
+ throw new Saml2Exception(e);
+ }
+ }
+
+ private static PrivateKey privateKey(String key) {
+ try {
+ return KeySupport.decodePrivateKey(key.getBytes(UTF_8), new char[0]);
+ }
+ catch (KeyException e) {
+ throw new Saml2Exception(e);
+ }
+ }
+
+ private static X509Certificate idpCertificate() {
+ return certificate("-----BEGIN CERTIFICATE-----\n"
+ + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD\n"
+ + "VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD\n"
+ + "VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX\n"
+ + "c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw\n"
+ + "aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ\n"
+ + "BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa\n"
+ + "BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD\n"
+ + "DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr\n"
+ + "QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62\n"
+ + "E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz\n"
+ + "2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW\n"
+ + "RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ\n"
+ + "nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5\n"
+ + "cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph\n"
+ + "iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5\n"
+ + "ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD\n"
+ + "AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO\n"
+ + "nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v\n"
+ + "ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu\n"
+ + "xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z\n"
+ + "V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3\n"
+ + "lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n"
+ + "-----END CERTIFICATE-----\n");
+ }
+
+
+ private static PrivateKey idpPrivateKey() {
+ return privateKey("-----BEGIN PRIVATE KEY-----\n"
+ + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4cn62E1xLqpN3\n"
+ + "4PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZX\n"
+ + "W+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHE\n"
+ + "fDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7h\n"
+ + "Z6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/T\n"
+ + "Xy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7\n"
+ + "I+J5lS8VAgMBAAECggEBAKyxBlIS7mcp3chvq0RF7B3PHFJMMzkwE+t3pLJcs4cZ\n"
+ + "nezh/KbREfP70QjXzk/llnZCvxeIs5vRu24vbdBm79qLHqBuHp8XfHHtuo2AfoAQ\n"
+ + "l4h047Xc/+TKMivnPQ0jX9qqndKDLqZDf5wnbslDmlskvF0a/MjsLU0TxtOfo+dB\n"
+ + "t55FW11cGqxZwhS5Gnr+cbw3OkHz23b9gEOt9qfwPVepeysbmm9FjU+k4yVa7rAN\n"
+ + "xcbzVb6Y7GCITe2tgvvEHmjB9BLmWrH3mZ3Af17YU/iN6TrpPd6Sj3QoS+2wGtAe\n"
+ + "HbUs3CKJu7bIHcj4poal6Kh8519S+erJTtqQ8M0ZiEECgYEA43hLYAPaUueFkdfh\n"
+ + "9K/7ClH6436CUH3VdizwUXi26fdhhV/I/ot6zLfU2mgEHU22LBECWQGtAFm8kv0P\n"
+ + "zPn+qjaR3e62l5PIlSYbnkIidzoDZ2ztu4jF5LgStlTJQPteFEGgZVl5o9DaSZOq\n"
+ + "Yd7G3XqXuQ1VGMW58G5FYJPtA1cCgYEAz5TPUtK+R2KXHMjUwlGY9AefQYRYmyX2\n"
+ + "Tn/OFgKvY8lpAkMrhPKONq7SMYc8E9v9G7A0dIOXvW7QOYSapNhKU+np3lUafR5F\n"
+ + "4ZN0bxZ9qjHbn3AMYeraKjeutHvlLtbHdIc1j3sxe/EzltRsYmiqLdEBW0p6hwWg\n"
+ + "tyGhYWVyaXMCgYAfDOKtHpmEy5nOCLwNXKBWDk7DExfSyPqEgSnk1SeS1HP5ctPK\n"
+ + "+1st6sIhdiVpopwFc+TwJWxqKdW18tlfT5jVv1E2DEnccw3kXilS9xAhWkfwrEvf\n"
+ + "V5I74GydewFl32o+NZ8hdo9GL1I8zO1rIq/et8dSOWGuWf9BtKu/vTGTTQKBgFxU\n"
+ + "VjsCnbvmsEwPUAL2hE/WrBFaKocnxXx5AFNt8lEyHtDwy4Sg1nygGcIJ4sD6koQk\n"
+ + "RdClT3LkvR04TAiSY80bN/i6ZcPNGUwSaDGZEWAIOSWbkwZijZNFnSGOEgxZX/IG\n"
+ + "yd39766vREEMTwEeiMNEOZQ/dmxkJm4OOVe25cLdAoGACOtPnq1Fxay80UYBf4rQ\n"
+ + "+bJ9yX1ulB8WIree1hD7OHSB2lRHxrVYWrglrTvkh63Lgx+EcsTV788OsvAVfPPz\n"
+ + "BZrn8SdDlQqalMxUBYEFwnsYD3cQ8yOUnijFVC4xNcdDv8OIqVgSk4KKxU5AshaA\n" + "xk6Mox+u8Cc2eAK12H13i+8=\n"
+ + "-----END PRIVATE KEY-----\n");
+ }
+
+ private static X509Certificate spCertificate() {
+
+ return certificate("-----BEGIN CERTIFICATE-----\n" +
+ "MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" +
+ "VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" +
+ "A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" +
+ "DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" +
+ "MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" +
+ "MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" +
+ "TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" +
+ "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" +
+ "vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" +
+ "+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" +
+ "y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" +
+ "XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" +
+ "qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" +
+ "RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" +
+ "-----END CERTIFICATE-----");
+ }
+
+ private static PrivateKey spPrivateKey() {
+ return privateKey("-----BEGIN PRIVATE KEY-----\n" +
+ "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" +
+ "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" +
+ "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" +
+ "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" +
+ "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" +
+ "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" +
+ "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" +
+ "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" +
+ "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" +
+ "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" +
+ "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" +
+ "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" +
+ "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" +
+ "INrtuLp4YHbgk1mi\n" +
+ "-----END PRIVATE KEY-----");
+ }
+
+}
diff --git a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java
index f4051266805..cf0c5dfd930 100644
--- a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java
+++ b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java
@@ -22,11 +22,15 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.MediaType;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.util.AssertionErrors;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
+import org.hamcrest.Matcher;
import org.joda.time.DateTime;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -62,9 +66,11 @@
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.UUID;
+import javax.servlet.http.HttpSession;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildConditions;
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildIssuer;
@@ -74,6 +80,9 @@
import static org.springframework.security.samples.OpenSamlActionTestingSupport.encryptNameId;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
+import static org.springframework.security.web.WebAttributes.AUTHENTICATION_EXCEPTION;
+import static org.springframework.test.util.AssertionErrors.assertEquals;
+import static org.springframework.test.util.AssertionErrors.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
@@ -86,6 +95,7 @@
public class Saml2LoginIntegrationTests {
static final String LOCAL_SP_ENTITY_ID = "http://localhost:8080/saml2/service-provider-metadata/simplesamlphp";
+ static final String USERNAME = "testuser@spring.security.saml";
@Autowired
MockMvc mockMvc;
@@ -97,21 +107,21 @@ public static class SpringBootApplicationTestConfig {
}
@Test
- public void redirectToLoginPageSingleProvider() throws Exception {
+ public void applicationAccessWhenSingleProviderAndUnauthenticatedThenRedirectsToAuthNRequest() throws Exception {
mockMvc.perform(get("http://localhost:8080/some/url"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost:8080/saml2/authenticate/simplesamlphp"));
}
@Test
- public void testAuthNRequest() throws Exception {
+ public void authenticateRequestWhenUnauthenticatedThenRespondsWithRedirectAuthNRequestXML() throws Exception {
mockMvc.perform(get("http://localhost:8080/saml2/authenticate/simplesamlphp"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php?SAMLRequest=")));
}
@Test
- public void testRelayState() throws Exception {
+ public void authenticateRequestWhenRelayStateThenRespondsWithRedirectAndEncodedRelayState() throws Exception {
mockMvc.perform(
get("http://localhost:8080/saml2/authenticate/simplesamlphp")
.param("RelayState", "relay state value with spaces")
@@ -122,96 +132,136 @@ public void testRelayState() throws Exception {
}
@Test
- public void signedResponse() throws Exception {
- final String username = "testuser@spring.security.saml";
- Assertion assertion = buildAssertion(username);
+ public void authenticateWhenResponseIsSignedThenItSucceeds() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
Response response = buildResponse(assertion);
signXmlObject(response, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING));
- String xml = toXml(response);
- mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp")
- .contentType(MediaType.APPLICATION_FORM_URLENCODED)
- .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8))))
- .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/"))
- .andExpect(authenticated().withUsername(username));
+ sendResponse(response, "/")
+ .andExpect(authenticated().withUsername(USERNAME));
}
@Test
- public void signedAssertion() throws Exception {
- final String username = "testuser@spring.security.saml";
- Assertion assertion = buildAssertion(username);
+ public void authenticateWhenAssertionIsThenItSignedSucceeds() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
Response response = buildResponse(assertion);
signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING));
- String xml = toXml(response);
- final ResultActions actions = mockMvc
- .perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp")
- .contentType(MediaType.APPLICATION_FORM_URLENCODED)
- .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8))))
- .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/"))
- .andExpect(authenticated().withUsername(username));
+ sendResponse(response, "/")
+ .andExpect(authenticated().withUsername(USERNAME));
}
@Test
- public void unsigned() throws Exception {
- Assertion assertion = buildAssertion("testuser@spring.security.saml");
+ public void authenticateWhenXmlObjectIsNotSignedThenItFails() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
Response response = buildResponse(assertion);
- String xml = toXml(response);
- mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp")
- .contentType(MediaType.APPLICATION_FORM_URLENCODED)
- .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8))))
- .andExpect(status().is3xxRedirection())
- .andExpect(redirectedUrl("/login?error"))
+ sendResponse(response, "/login?error")
.andExpect(unauthenticated());
}
@Test
- public void signedResponseEncryptedAssertion() throws Exception {
- final String username = "testuser@spring.security.saml";
- Assertion assertion = buildAssertion(username);
+ public void authenticateWhenResponseIsSignedAndAssertionIsEncryptedThenItSucceeds() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
EncryptedAssertion encryptedAssertion =
OpenSamlActionTestingSupport.encryptAssertion(assertion, decodeCertificate(spCertificate));
Response response = buildResponse(encryptedAssertion);
signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING));
- String xml = toXml(response);
- final ResultActions actions = mockMvc
- .perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp")
- .contentType(MediaType.APPLICATION_FORM_URLENCODED)
- .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8))))
- .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/"))
- .andExpect(authenticated().withUsername(username));
+ sendResponse(response, "/")
+ .andExpect(authenticated().withUsername(USERNAME));
}
@Test
- public void unsignedResponseEncryptedAssertion() throws Exception {
- final String username = "testuser@spring.security.saml";
- Assertion assertion = buildAssertion(username);
+ public void authenticateWhenResponseIsNotSignedAndAssertionIsEncryptedThenItSucceeds() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
EncryptedAssertion encryptedAssertion =
OpenSamlActionTestingSupport.encryptAssertion(assertion, decodeCertificate(spCertificate));
Response response = buildResponse(encryptedAssertion);
- String xml = toXml(response);
- final ResultActions actions = mockMvc
- .perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp")
- .contentType(MediaType.APPLICATION_FORM_URLENCODED)
- .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8))))
- .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/"))
- .andExpect(authenticated().withUsername(username));
+ sendResponse(response, "/")
+ .andExpect(authenticated().withUsername(USERNAME));
}
@Test
- public void signedResponseEncryptedNameId() throws Exception {
- final String username = "testuser@spring.security.saml";
- Assertion assertion = buildAssertion(username);
+ public void authenticateWhenResponseIsSignedAndNameIDisEncryptedThenItSucceeds() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
final EncryptedID nameId = encryptNameId(assertion.getSubject().getNameID(), decodeCertificate(spCertificate));
assertion.getSubject().setEncryptedID(nameId);
assertion.getSubject().setNameID(null);
Response response = buildResponse(assertion);
signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING));
+ sendResponse(response, "/")
+ .andExpect(authenticated().withUsername(USERNAME));
+ }
+
+ @Test
+ public void authenticateWhenSignatureKeysDontMatchThenItFails() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
+ Response response = buildResponse(assertion);
+ signXmlObject(assertion, getSigningCredential(spCertificate, spPrivateKey, UsageType.SIGNING));
+ sendResponse(response, "/login?error")
+ .andExpect(
+ saml2AuthenticationExceptionMatcher(
+ "invalid_signature",
+ equalTo("Assertion doesn't have a valid signature.")
+ )
+ );
+ }
+
+ @Test
+ public void authenticateWhenNotOnOrAfterDontMatchThenItFails() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
+ assertion.getConditions().setNotOnOrAfter(DateTime.now().minusDays(1));
+ Response response = buildResponse(assertion);
+ signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING));
+ sendResponse(response, "/login?error")
+ .andExpect(
+ saml2AuthenticationExceptionMatcher(
+ "invalid_assertion",
+ containsString("Assertion 'assertion' with NotOnOrAfter condition of")
+ )
+ );
+ }
+
+ @Test
+ public void authenticateWhenNotOnOrBeforeDontMatchThenItFails() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
+ assertion.getConditions().setNotBefore(DateTime.now().plusDays(1));
+ Response response = buildResponse(assertion);
+ signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING));
+ sendResponse(response, "/login?error")
+ .andExpect(
+ saml2AuthenticationExceptionMatcher(
+ "invalid_assertion",
+ containsString("Assertion 'assertion' with NotBefore condition of")
+ )
+ );
+ }
+
+ @Test
+ public void authenticateWhenIssuerIsInvalidThenItFails() throws Exception {
+ Assertion assertion = buildAssertion(USERNAME);
+ Response response = buildResponse(assertion);
+ response.getIssuer().setValue("invalid issuer");
+ signXmlObject(response, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING));
+ sendResponse(response, "/login?error")
+ .andExpect(unauthenticated())
+ .andExpect(
+ saml2AuthenticationExceptionMatcher(
+ "invalid_issuer",
+ containsString(
+ "Response issuer 'invalid issuer' doesn't match "+
+ "'https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php'"
+ )
+ )
+ );
+ }
+
+ private ResultActions sendResponse(
+ Response response,
+ String redirectUrl) throws Exception {
String xml = toXml(response);
- final ResultActions actions = mockMvc
- .perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp")
- .contentType(MediaType.APPLICATION_FORM_URLENCODED)
- .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8))))
- .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/"))
- .andExpect(authenticated().withUsername(username));
+ return mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8))))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl(redirectUrl));
}
private Response buildResponse(Assertion assertion) {
@@ -359,4 +409,42 @@ private X509Certificate decodeCertificate(String source) {
"RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" +
"-----END CERTIFICATE-----";
+ private String spPrivateKey = "-----BEGIN PRIVATE KEY-----\n" +
+ "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" +
+ "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" +
+ "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" +
+ "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" +
+ "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" +
+ "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" +
+ "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" +
+ "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" +
+ "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" +
+ "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" +
+ "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" +
+ "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" +
+ "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" +
+ "INrtuLp4YHbgk1mi\n" +
+ "-----END PRIVATE KEY-----";
+
+ private static ResultMatcher saml2AuthenticationExceptionMatcher(
+ String code,
+ Matcher message
+ ) {
+ return result -> {
+ final HttpSession session = result.getRequest().getSession(false);
+ AssertionErrors.assertNotNull("HttpSession", session);
+ Object exception = session.getAttribute(AUTHENTICATION_EXCEPTION);
+ AssertionErrors.assertNotNull(AUTHENTICATION_EXCEPTION, exception);
+ if (!(exception instanceof Saml2AuthenticationException)) {
+ AssertionErrors.fail(
+ "Invalid exception type",
+ Saml2AuthenticationException.class,
+ exception.getClass().getName()
+ );
+ }
+ Saml2AuthenticationException se = (Saml2AuthenticationException) exception;
+ assertEquals("SAML 2 Error Code", code, se.getError().getErrorCode());
+ assertTrue("SAML 2 Error Description", message.matches(se.getError().getDescription()));
+ };
+ }
}