8 changed files with 1191 additions and 139 deletions
@ -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. |
||||||
|
* |
||||||
|
* <p> |
||||||
|
* There are a number of scenarios where an error may occur, for example: |
||||||
|
* <ul> |
||||||
|
* <li>The response or assertion request is missing or malformed</li> |
||||||
|
* <li>Missing or invalid subject</li> |
||||||
|
* <li>Missing or invalid signatures</li> |
||||||
|
* <li>The time period validation for the assertion fails</li> |
||||||
|
* <li>One of the assertion conditions was not met</li> |
||||||
|
* <li>Decryption failed</li> |
||||||
|
* <li>Unable to locate a subject identifier, commonly known as username</li> |
||||||
|
* </ul> |
||||||
|
* |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public class Saml2AuthenticationException extends AuthenticationException { |
||||||
|
private Saml2Error error; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a {@code Saml2AuthenticationException} using the provided parameters. |
||||||
|
* |
||||||
|
* @param error the {@link Saml2Error SAML 2.0 Error} |
||||||
|
*/ |
||||||
|
public Saml2AuthenticationException(Saml2Error error) { |
||||||
|
this(error, error.getDescription()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a {@code Saml2AuthenticationException} using the provided parameters. |
||||||
|
* |
||||||
|
* @param error the {@link Saml2Error SAML 2.0 Error} |
||||||
|
* @param cause the root cause |
||||||
|
*/ |
||||||
|
public Saml2AuthenticationException(Saml2Error error, Throwable cause) { |
||||||
|
this(error, cause.getMessage(), cause); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a {@code Saml2AuthenticationException} using the provided parameters. |
||||||
|
* |
||||||
|
* @param error the {@link Saml2Error SAML 2.0 Error} |
||||||
|
* @param message the detail message |
||||||
|
*/ |
||||||
|
public Saml2AuthenticationException(Saml2Error error, String message) { |
||||||
|
super(message); |
||||||
|
this.setError(error); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a {@code Saml2AuthenticationException} using the provided parameters. |
||||||
|
* |
||||||
|
* @param error the {@link Saml2Error SAML 2.0 Error} |
||||||
|
* @param message the detail message |
||||||
|
* @param cause the root cause |
||||||
|
*/ |
||||||
|
public Saml2AuthenticationException(Saml2Error error, String message, Throwable cause) { |
||||||
|
super(message, cause); |
||||||
|
this.setError(error); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the {@link Saml2Error SAML 2.0 Error}. |
||||||
|
* |
||||||
|
* @return the {@link Saml2Error} |
||||||
|
*/ |
||||||
|
public Saml2Error getError() { |
||||||
|
return this.error; |
||||||
|
} |
||||||
|
|
||||||
|
private void setError(Saml2Error error) { |
||||||
|
Assert.notNull(error, "error cannot be null"); |
||||||
|
this.error = error; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
final StringBuffer sb = new StringBuffer("Saml2AuthenticationException{"); |
||||||
|
sb.append("error=").append(error); |
||||||
|
sb.append('}'); |
||||||
|
return sb.toString(); |
||||||
|
} |
||||||
|
} |
||||||
@ -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. |
||||||
|
* |
||||||
|
* <p> |
||||||
|
* At a minimum, an error response will contain an error code. |
||||||
|
* The commonly used error code are defined in this class
|
||||||
|
* or a new codes can be defined in the future as arbitrary strings. |
||||||
|
* </p> |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public class Saml2Error implements Serializable { |
||||||
|
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; |
||||||
|
|
||||||
|
private final String errorCode; |
||||||
|
private final String description; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a {@code Saml2Error} using the provided parameters. |
||||||
|
* |
||||||
|
* @param errorCode the error code |
||||||
|
* @param description the error description |
||||||
|
*/ |
||||||
|
public Saml2Error(String errorCode, String description) { |
||||||
|
Assert.hasText(errorCode, "errorCode cannot be empty"); |
||||||
|
this.errorCode = errorCode; |
||||||
|
this.description = description; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the error code. |
||||||
|
* |
||||||
|
* @return the error code |
||||||
|
*/ |
||||||
|
public final String getErrorCode() { |
||||||
|
return this.errorCode; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the error description. |
||||||
|
* |
||||||
|
* @return the error description |
||||||
|
*/ |
||||||
|
public final String getDescription() { |
||||||
|
return this.description; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return "[" + this.getErrorCode() + "] " + |
||||||
|
(this.getDescription() != null ? this.getDescription() : ""); |
||||||
|
} |
||||||
|
} |
||||||
@ -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"; |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,456 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.security.saml2.provider.service.authentication; |
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.saml2.Saml2Exception; |
||||||
|
|
||||||
|
import org.hamcrest.BaseMatcher; |
||||||
|
import org.hamcrest.Description; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Rule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.rules.ExpectedException; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.opensaml.saml.common.assertion.AssertionValidationException; |
||||||
|
import org.opensaml.saml.common.assertion.ValidationContext; |
||||||
|
import org.opensaml.saml.common.assertion.ValidationResult; |
||||||
|
import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator; |
||||||
|
import org.opensaml.saml.saml2.core.Assertion; |
||||||
|
import org.opensaml.saml.saml2.core.EncryptedAssertion; |
||||||
|
import org.opensaml.saml.saml2.core.EncryptedID; |
||||||
|
import org.opensaml.saml.saml2.core.Issuer; |
||||||
|
import org.opensaml.saml.saml2.core.Response; |
||||||
|
import org.opensaml.saml.saml2.core.Subject; |
||||||
|
import org.powermock.api.mockito.PowerMockito; |
||||||
|
import org.powermock.core.classloader.annotations.PrepareForTest; |
||||||
|
import org.powermock.modules.junit4.PowerMockRunner; |
||||||
|
|
||||||
|
import java.util.Collections; |
||||||
|
|
||||||
|
import static java.util.Collections.emptyList; |
||||||
|
import static org.mockito.ArgumentMatchers.any; |
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean; |
||||||
|
import static org.mockito.ArgumentMatchers.anyString; |
||||||
|
import static org.powermock.api.mockito.PowerMockito.doReturn; |
||||||
|
import static org.powermock.api.mockito.PowerMockito.mock; |
||||||
|
import static org.powermock.api.mockito.PowerMockito.when; |
||||||
|
import static org.springframework.test.util.AssertionErrors.assertTrue; |
||||||
|
import static org.springframework.util.StringUtils.hasText; |
||||||
|
|
||||||
|
@RunWith(PowerMockRunner.class) |
||||||
|
@PrepareForTest({OpenSamlImplementation.class, OpenSamlAuthenticationProvider.class}) |
||||||
|
public class OpenSamlAuthenticationProviderTests { |
||||||
|
|
||||||
|
private OpenSamlAuthenticationProvider provider; |
||||||
|
private OpenSamlImplementation saml; |
||||||
|
|
||||||
|
@Rule |
||||||
|
ExpectedException exception = ExpectedException.none(); |
||||||
|
private Saml2AuthenticationToken token; |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setup() { |
||||||
|
saml = PowerMockito.mock(OpenSamlImplementation.class); |
||||||
|
PowerMockito.mockStatic(OpenSamlImplementation.class); |
||||||
|
when(OpenSamlImplementation.getInstance()).thenReturn(saml); |
||||||
|
|
||||||
|
provider = new OpenSamlAuthenticationProvider(); |
||||||
|
token = new Saml2AuthenticationToken( |
||||||
|
"responseXml", |
||||||
|
"recipientUri", |
||||||
|
"idpEntityId", |
||||||
|
"localSpEntityId", |
||||||
|
emptyList() |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void supportsWhenSaml2AuthenticationTokenThenReturnTrue() { |
||||||
|
|
||||||
|
assertTrue( |
||||||
|
OpenSamlAuthenticationProvider.class + "should support " + token.getClass(), |
||||||
|
provider.supports(token.getClass()) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void supportsWhenNotSaml2AuthenticationTokenThenReturnFalse() { |
||||||
|
assertTrue( |
||||||
|
OpenSamlAuthenticationProvider.class + "should not support " + Authentication.class, |
||||||
|
!provider.supports(Authentication.class) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenUnknownDataClassThenThrowAuthenticationException() { |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(mock(Assertion.class)); |
||||||
|
exception.expect(authenticationMatcher(Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS)); |
||||||
|
provider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenXmlErrorThenThrowAuthenticationException() { |
||||||
|
when(saml.resolve(any(String.class))).thenThrow(new Saml2Exception("test")); |
||||||
|
exception.expect(authenticationMatcher(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA)); |
||||||
|
provider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn("invalidRecipient"); |
||||||
|
exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_DESTINATION)); |
||||||
|
provider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
final Issuer issuer = mock(Issuer.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn(token.getRecipientUri()); |
||||||
|
when(response.isSigned()).thenReturn(false); |
||||||
|
when(response.getAssertions()).thenReturn(emptyList()); |
||||||
|
when(response.getEncryptedAssertions()).thenReturn(emptyList()); |
||||||
|
when(response.getIssuer()).thenReturn(issuer); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
exception.expect( |
||||||
|
authenticationMatcher( |
||||||
|
Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, |
||||||
|
"No assertions found in response." |
||||||
|
) |
||||||
|
); |
||||||
|
provider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenInvalidSignatureThenThrowAuthenticationException() { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
final Issuer issuer = mock(Issuer.class); |
||||||
|
final Assertion assertion = mock(Assertion.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn(token.getRecipientUri()); |
||||||
|
when(response.isSigned()).thenReturn(false); |
||||||
|
when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); |
||||||
|
when(response.getEncryptedAssertions()).thenReturn(emptyList()); |
||||||
|
when(response.getIssuer()).thenReturn(issuer); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
|
||||||
|
exception.expect( |
||||||
|
authenticationMatcher( |
||||||
|
Saml2ErrorCodes.INVALID_SIGNATURE |
||||||
|
) |
||||||
|
); |
||||||
|
provider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() throws Exception { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
final Issuer issuer = mock(Issuer.class); |
||||||
|
final Assertion assertion = mock(Assertion.class); |
||||||
|
final SAML20AssertionValidator validator = mock(SAML20AssertionValidator.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn(token.getRecipientUri()); |
||||||
|
when(response.isSigned()).thenReturn(false); |
||||||
|
when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); |
||||||
|
when(response.getEncryptedAssertions()).thenReturn(emptyList()); |
||||||
|
when(response.getIssuer()).thenReturn(issuer); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
|
||||||
|
|
||||||
|
OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); |
||||||
|
doReturn(true).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Assertion.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
doReturn(false).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Response.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
doReturn(validator).when(spyProvider, "getAssertionValidator", any(Saml2AuthenticationToken.class)); |
||||||
|
when(validator.validate( |
||||||
|
any(Assertion.class), |
||||||
|
any(ValidationContext.class) |
||||||
|
)).thenReturn(ValidationResult.INVALID); |
||||||
|
exception.expect( |
||||||
|
authenticationMatcher( |
||||||
|
Saml2ErrorCodes.INVALID_ASSERTION |
||||||
|
) |
||||||
|
); |
||||||
|
spyProvider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenInternalErrorThenCatchAndThrowAuthenticationException() throws Exception { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
final Issuer issuer = mock(Issuer.class); |
||||||
|
final Assertion assertion = mock(Assertion.class); |
||||||
|
final SAML20AssertionValidator validator = mock(SAML20AssertionValidator.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn(token.getRecipientUri()); |
||||||
|
when(response.isSigned()).thenReturn(false); |
||||||
|
when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); |
||||||
|
when(response.getEncryptedAssertions()).thenReturn(emptyList()); |
||||||
|
when(response.getIssuer()).thenReturn(issuer); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
|
||||||
|
|
||||||
|
OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); |
||||||
|
doReturn(true).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Assertion.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
doReturn(false).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Response.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
doReturn(validator).when(spyProvider, "getAssertionValidator", any(Saml2AuthenticationToken.class)); |
||||||
|
when(validator.validate( |
||||||
|
any(Assertion.class), |
||||||
|
any(ValidationContext.class) |
||||||
|
)).thenThrow(new AssertionValidationException()); |
||||||
|
exception.expect( |
||||||
|
authenticationMatcher( |
||||||
|
Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR |
||||||
|
) |
||||||
|
); |
||||||
|
spyProvider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenMissingSubjectThenThrowAuthenticationException() throws Exception { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
final Issuer issuer = mock(Issuer.class); |
||||||
|
final Assertion assertion = mock(Assertion.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn(token.getRecipientUri()); |
||||||
|
when(response.isSigned()).thenReturn(false); |
||||||
|
when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); |
||||||
|
when(response.getEncryptedAssertions()).thenReturn(emptyList()); |
||||||
|
when(response.getIssuer()).thenReturn(issuer); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
|
||||||
|
|
||||||
|
OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); |
||||||
|
doReturn(true).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Assertion.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
doReturn(false).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Response.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
PowerMockito.doNothing() |
||||||
|
.when( |
||||||
|
spyProvider, |
||||||
|
"validateAssertion", |
||||||
|
anyString(), |
||||||
|
any(Assertion.class), |
||||||
|
any(Saml2AuthenticationToken.class), |
||||||
|
anyBoolean() |
||||||
|
); |
||||||
|
|
||||||
|
exception.expect( |
||||||
|
authenticationMatcher( |
||||||
|
Saml2ErrorCodes.SUBJECT_NOT_FOUND |
||||||
|
) |
||||||
|
); |
||||||
|
spyProvider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenUsernameMissingThenThrowAuthenticationException() throws Exception { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
final Issuer issuer = mock(Issuer.class); |
||||||
|
final Assertion assertion = mock(Assertion.class); |
||||||
|
final Subject subject = mock(Subject.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn(token.getRecipientUri()); |
||||||
|
when(response.isSigned()).thenReturn(false); |
||||||
|
when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); |
||||||
|
when(response.getEncryptedAssertions()).thenReturn(emptyList()); |
||||||
|
when(response.getIssuer()).thenReturn(issuer); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
when(assertion.getSubject()).thenReturn(subject); |
||||||
|
|
||||||
|
|
||||||
|
OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); |
||||||
|
doReturn(true).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Assertion.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
doReturn(false).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Response.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
PowerMockito.doNothing() |
||||||
|
.when( |
||||||
|
spyProvider, |
||||||
|
"validateAssertion", |
||||||
|
anyString(), |
||||||
|
any(Assertion.class), |
||||||
|
any(Saml2AuthenticationToken.class), |
||||||
|
anyBoolean() |
||||||
|
); |
||||||
|
|
||||||
|
exception.expect( |
||||||
|
authenticationMatcher( |
||||||
|
Saml2ErrorCodes.USERNAME_NOT_FOUND |
||||||
|
) |
||||||
|
); |
||||||
|
spyProvider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() throws Exception { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
final Issuer issuer = mock(Issuer.class); |
||||||
|
final Assertion assertion = mock(Assertion.class); |
||||||
|
final Subject subject = mock(Subject.class); |
||||||
|
final EncryptedID nameID = mock(EncryptedID.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn(token.getRecipientUri()); |
||||||
|
when(response.isSigned()).thenReturn(false); |
||||||
|
when(response.getAssertions()).thenReturn(Collections.singletonList(assertion)); |
||||||
|
when(response.getEncryptedAssertions()).thenReturn(emptyList()); |
||||||
|
when(response.getIssuer()).thenReturn(issuer); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
when(assertion.getSubject()).thenReturn(subject); |
||||||
|
when(subject.getEncryptedID()).thenReturn(nameID); |
||||||
|
|
||||||
|
|
||||||
|
OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); |
||||||
|
doReturn(true).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Assertion.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
doReturn(false).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Response.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
PowerMockito.doNothing() |
||||||
|
.when( |
||||||
|
spyProvider, |
||||||
|
"validateAssertion", |
||||||
|
anyString(), |
||||||
|
any(Assertion.class), |
||||||
|
any(Saml2AuthenticationToken.class), |
||||||
|
anyBoolean() |
||||||
|
); |
||||||
|
|
||||||
|
exception.expect( |
||||||
|
authenticationMatcher( |
||||||
|
Saml2ErrorCodes.DECRYPTION_ERROR, |
||||||
|
"No valid decryption credentials found." |
||||||
|
) |
||||||
|
); |
||||||
|
spyProvider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authenticateWhenDecryptionKeyIsMissingThenThrowAuthenticationException() throws Exception { |
||||||
|
final Response response = mock(Response.class); |
||||||
|
final Issuer issuer = mock(Issuer.class); |
||||||
|
final EncryptedAssertion assertion = mock(EncryptedAssertion.class); |
||||||
|
when(saml.resolve(any(String.class))).thenReturn(response); |
||||||
|
when(response.getDestination()).thenReturn(token.getRecipientUri()); |
||||||
|
when(response.isSigned()).thenReturn(false); |
||||||
|
when(response.getIssuer()).thenReturn(issuer); |
||||||
|
when(issuer.getValue()).thenReturn(token.getIdpEntityId()); |
||||||
|
when(response.getEncryptedAssertions()).thenReturn(Collections.singletonList(assertion)); |
||||||
|
|
||||||
|
OpenSamlAuthenticationProvider spyProvider = PowerMockito.spy(this.provider); |
||||||
|
doReturn(false).when( |
||||||
|
spyProvider, |
||||||
|
"hasValidSignature", |
||||||
|
any(Response.class), |
||||||
|
any(Saml2AuthenticationToken.class) |
||||||
|
); |
||||||
|
|
||||||
|
exception.expect( |
||||||
|
authenticationMatcher( |
||||||
|
Saml2ErrorCodes.DECRYPTION_ERROR, |
||||||
|
"No valid decryption credentials found." |
||||||
|
) |
||||||
|
); |
||||||
|
spyProvider.authenticate(token); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private BaseMatcher<Saml2AuthenticationException> authenticationMatcher(String code) { |
||||||
|
return authenticationMatcher(code, null); |
||||||
|
} |
||||||
|
|
||||||
|
private BaseMatcher<Saml2AuthenticationException> authenticationMatcher(String code, String description) { |
||||||
|
return new BaseMatcher<Saml2AuthenticationException>() { |
||||||
|
private Object value = null; |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean matches(Object item) { |
||||||
|
if (!(item instanceof Saml2AuthenticationException)) { |
||||||
|
value = item; |
||||||
|
return false; |
||||||
|
} |
||||||
|
Saml2AuthenticationException ex = (Saml2AuthenticationException) item; |
||||||
|
if (!code.equals(ex.getError().getErrorCode())) { |
||||||
|
value = item; |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (hasText(description)) { |
||||||
|
if (!description.equals(ex.getError().getDescription())) { |
||||||
|
value = item; |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void describeTo(Description description) { |
||||||
|
description.appendText("Expecting a " + Saml2AuthenticationException.class.getName() + |
||||||
|
" with code:" + code + " and description:" + description |
||||||
|
) |
||||||
|
.appendValue(value); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue