From d236e24cc009e05a544356bce48217b09216726d Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 13 Nov 2020 12:31:45 -0700 Subject: [PATCH] Simplify saml2Login Sample --- .../example/OpenSamlActionTestingSupport.java | 466 ----------------- .../example/Saml2LoginApplicationITests.java | 93 ++++ .../example/Saml2LoginIntegrationTests.java | 473 ------------------ .../integTest/java/example/Saml2Utils.java | 75 --- .../main/java/example/IndexController.java | 8 +- .../src/main/resources/application.yml | 10 +- .../src/main/resources/templates/index.html | 35 +- 7 files changed, 125 insertions(+), 1035 deletions(-) delete mode 100644 servlet/spring-boot/java/saml2-login/src/integTest/java/example/OpenSamlActionTestingSupport.java create mode 100644 servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginApplicationITests.java delete mode 100644 servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginIntegrationTests.java delete mode 100644 servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2Utils.java diff --git a/servlet/spring-boot/java/saml2-login/src/integTest/java/example/OpenSamlActionTestingSupport.java b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/OpenSamlActionTestingSupport.java deleted file mode 100644 index ae10564..0000000 --- a/servlet/spring-boot/java/saml2-login/src/integTest/java/example/OpenSamlActionTestingSupport.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright 2002-2020 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 example; - -import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty; -import org.apache.xml.security.algorithms.JCEMapper; -import org.apache.xml.security.encryption.XMLCipherParameters; -import org.joda.time.DateTime; -import org.joda.time.Duration; -import org.junit.Assert; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.profile.action.EventIds; -import org.opensaml.profile.context.EventContext; -import org.opensaml.profile.context.ProfileRequestContext; -import org.opensaml.saml.common.SAMLObjectBuilder; -import org.opensaml.saml.common.SAMLVersion; -import org.opensaml.saml.saml2.core.Artifact; -import org.opensaml.saml.saml2.core.ArtifactResolve; -import org.opensaml.saml.saml2.core.ArtifactResponse; -import org.opensaml.saml.saml2.core.Assertion; -import org.opensaml.saml.saml2.core.AttributeQuery; -import org.opensaml.saml.saml2.core.AttributeStatement; -import org.opensaml.saml.saml2.core.AuthnRequest; -import org.opensaml.saml.saml2.core.AuthnStatement; -import org.opensaml.saml.saml2.core.Conditions; -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.LogoutRequest; -import org.opensaml.saml.saml2.core.LogoutResponse; -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 org.opensaml.saml.saml2.encryption.Encrypter; -import org.opensaml.security.credential.BasicCredential; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialSupport; -import org.opensaml.xmlsec.encryption.support.DataEncryptionParameters; -import org.opensaml.xmlsec.encryption.support.EncryptionException; -import org.opensaml.xmlsec.encryption.support.KeyEncryptionParameters; -import org.springframework.security.saml2.Saml2Exception; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.crypto.SecretKey; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.X509Certificate; - -import static java.util.Arrays.asList; -import static org.opensaml.security.crypto.KeySupport.generateKey; - -/** - * Copied from OpenSAML Source Code Helper methods for creating/testing SAML 2 - * objects within profile action tests. When methods herein refer to mock objects they are - * always objects that have been created via Mockito unless otherwise noted. - */ -public class OpenSamlActionTestingSupport { - - /** ID used for all generated {@link Response} objects. */ - final static String REQUEST_ID = "request"; - - /** ID used for all generated {@link Response} objects. */ - final static String RESPONSE_ID = "response"; - - /** ID used for all generated {@link Assertion} objects. */ - final static String ASSERTION_ID = "assertion"; - - static EncryptedAssertion encryptAssertion(Assertion assertion, X509Certificate certificate) { - 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, X509Certificate certificate) { - 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); - } - } - - 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; - } - - 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); - } - } - - /** - * Builds an empty response. The ID of the message is {@link #OUTBOUND_MSG_ID}, the - * issue instant is 1970-01-01T00:00:00Z and the SAML version is - * {@link SAMLVersion#VERSION_11}. - * @return the constructed response - */ - @Nonnull - static Response buildResponse() { - final SAMLObjectBuilder responseBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Response.DEFAULT_ELEMENT_NAME); - - final Response response = responseBuilder.buildObject(); - response.setID(OUTBOUND_MSG_ID); - response.setIssueInstant(DateTime.now()); - response.setVersion(SAMLVersion.VERSION_20); - - return response; - } - - /** - * Builds an empty artifact response. The ID of the message is - * {@link #OUTBOUND_MSG_ID}, the issue instant is 1970-01-01T00:00:00Z and the SAML - * version is {@link SAMLVersion#VERSION_11}. - * @return the constructed response - */ - @Nonnull - static ArtifactResponse buildArtifactResponse() { - final SAMLObjectBuilder responseBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(ArtifactResponse.DEFAULT_ELEMENT_NAME); - - final ArtifactResponse response = responseBuilder.buildObject(); - response.setID(OUTBOUND_MSG_ID); - response.setIssueInstant(DateTime.now()); - response.setVersion(SAMLVersion.VERSION_20); - - return response; - } - - /** - * Builds an {@link LogoutRequest}. If a {@link NameID} is given, it will be added to - * the constructed {@link LogoutRequest}. - * @param name the NameID to add to the request - * @return the built request - */ - @Nonnull - static LogoutRequest buildLogoutRequest(final @Nullable NameID name) { - final SAMLObjectBuilder issuerBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Issuer.DEFAULT_ELEMENT_NAME); - - final SAMLObjectBuilder reqBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(LogoutRequest.DEFAULT_ELEMENT_NAME); - - final Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(INBOUND_MSG_ISSUER); - - final LogoutRequest req = reqBuilder.buildObject(); - req.setID(REQUEST_ID); - req.setIssueInstant(DateTime.now()); - req.setIssuer(issuer); - req.setVersion(SAMLVersion.VERSION_20); - - if (name != null) { - req.setNameID(name); - } - - return req; - } - - /** - * Builds an empty logout response. The ID of the message is {@link #OUTBOUND_MSG_ID}, - * the issue instant is 1970-01-01T00:00:00Z and the SAML version is - * {@link SAMLVersion#VERSION_11}. - * @return the constructed response - */ - @Nonnull - static LogoutResponse buildLogoutResponse() { - final SAMLObjectBuilder responseBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(LogoutResponse.DEFAULT_ELEMENT_NAME); - - final LogoutResponse response = responseBuilder.buildObject(); - response.setID(OUTBOUND_MSG_ID); - response.setIssueInstant(DateTime.now()); - response.setVersion(SAMLVersion.VERSION_20); - - return response; - } - - /** - * Builds an empty assertion. The ID of the message is {@link #ASSERTION_ID}, the - * issue instant is 1970-01-01T00:00:00Z and the SAML version is - * {@link SAMLVersion#VERSION_11}. - * @return the constructed assertion - */ - @Nonnull - static Assertion buildAssertion() { - final SAMLObjectBuilder assertionBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Assertion.DEFAULT_ELEMENT_NAME); - - final Assertion assertion = assertionBuilder.buildObject(); - assertion.setID(ASSERTION_ID); - assertion.setIssueInstant(DateTime.now()); - assertion.setVersion(SAMLVersion.VERSION_20); - - return assertion; - } - - @Nonnull - static SubjectConfirmation buildSubjectConfirmation() { - final SAMLObjectBuilder subjectConfirmation = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(SubjectConfirmation.DEFAULT_ELEMENT_NAME); - - return subjectConfirmation.buildObject(); - } - - /** - * Builds an authentication statement. The authn instant is set to - * 1970-01-01T00:00:00Z. - * @return the constructed statement - */ - @Nonnull - static AuthnStatement buildAuthnStatement() { - final SAMLObjectBuilder statementBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(AuthnStatement.DEFAULT_ELEMENT_NAME); - - final AuthnStatement statement = statementBuilder.buildObject(); - statement.setAuthnInstant(DateTime.now()); - - return statement; - } - - /** - * Builds an empty attribute statement. - * @return the constructed statement - */ - @Nonnull - static AttributeStatement buildAttributeStatement() { - final SAMLObjectBuilder statementBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(AttributeStatement.DEFAULT_ELEMENT_NAME); - - final AttributeStatement statement = statementBuilder.buildObject(); - - return statement; - } - - /** - * Builds a {@link Subject}. If a principal name is given a {@link NameID}, whose - * value is the given principal name, will be created and added to the - * {@link Subject}. - * @param principalName the principal name to add to the subject - * @return the built subject - */ - @Nonnull - static Subject buildSubject(final @Nullable String principalName) { - final SAMLObjectBuilder subjectBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Subject.DEFAULT_ELEMENT_NAME); - final Subject subject = subjectBuilder.buildObject(); - - if (principalName != null) { - subject.setNameID(buildNameID(principalName)); - } - - return subject; - } - - @Nonnull - static SubjectConfirmationData buildSubjectConfirmationData(String localSpEntityId) { - final SAMLObjectBuilder subjectBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory() - .getBuilderOrThrow(SubjectConfirmationData.DEFAULT_ELEMENT_NAME); - final SubjectConfirmationData subject = subjectBuilder.buildObject(); - subject.setRecipient(localSpEntityId); - subject.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); - subject.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); - return subject; - } - - @Nonnull - static Conditions buildConditions() { - final SAMLObjectBuilder subjectBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Conditions.DEFAULT_ELEMENT_NAME); - final Conditions conditions = subjectBuilder.buildObject(); - conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); - conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); - return conditions; - } - - /** - * Builds a {@link NameID}. - * @param principalName the principal name to use in the NameID - * @return the built NameID - */ - @Nonnull - static NameID buildNameID(final @Nonnull @NotEmpty String principalName) { - final SAMLObjectBuilder nameIdBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(NameID.DEFAULT_ELEMENT_NAME); - final NameID nameId = nameIdBuilder.buildObject(); - nameId.setValue(principalName); - return nameId; - } - - /** - * Builds a {@link Issuer}. - * @param entityID the entity ID to use in the Issuer - * @return the built Issuer - */ - @Nonnull - static Issuer buildIssuer(final @Nonnull @NotEmpty String entityID) { - final SAMLObjectBuilder issuerBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Issuer.DEFAULT_ELEMENT_NAME); - final Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(entityID); - return issuer; - } - - /** - * Builds an {@link AttributeQuery}. If a {@link Subject} is given, it will be added - * to the constructed {@link AttributeQuery}. - * @param subject the subject to add to the query - * @return the built query - */ - @Nonnull - static AttributeQuery buildAttributeQueryRequest(final @Nullable Subject subject) { - final SAMLObjectBuilder issuerBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Issuer.DEFAULT_ELEMENT_NAME); - - final SAMLObjectBuilder queryBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(AttributeQuery.DEFAULT_ELEMENT_NAME); - - final Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(INBOUND_MSG_ISSUER); - - final AttributeQuery query = queryBuilder.buildObject(); - query.setID(REQUEST_ID); - query.setIssueInstant(DateTime.now()); - query.setIssuer(issuer); - query.setVersion(SAMLVersion.VERSION_20); - - if (subject != null) { - query.setSubject(subject); - } - - return query; - } - - /** - * Builds an {@link AuthnRequest}. - * @return the built request - */ - @Nonnull - static AuthnRequest buildAuthnRequest() { - final SAMLObjectBuilder issuerBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Issuer.DEFAULT_ELEMENT_NAME); - - final SAMLObjectBuilder requestBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(AuthnRequest.DEFAULT_ELEMENT_NAME); - - final Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(INBOUND_MSG_ISSUER); - - final AuthnRequest request = requestBuilder.buildObject(); - request.setID(REQUEST_ID); - request.setIssueInstant(DateTime.now()); - request.setIssuer(issuer); - request.setVersion(SAMLVersion.VERSION_20); - - return request; - } - - /** - * Builds a {@link ArtifactResolve}. - * @param artifact the artifact to add to the request - * @return the built request - */ - @Nonnull - static ArtifactResolve buildArtifactResolve(final @Nullable String artifact) { - final SAMLObjectBuilder requestBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(ArtifactResolve.DEFAULT_ELEMENT_NAME); - final ArtifactResolve request = requestBuilder.buildObject(); - request.setID(REQUEST_ID); - request.setIssueInstant(DateTime.now()); - request.setVersion(SAMLVersion.VERSION_11); - - if (artifact != null) { - final SAMLObjectBuilder artifactBuilder = (SAMLObjectBuilder) XMLObjectProviderRegistrySupport - .getBuilderFactory().getBuilderOrThrow(Artifact.DEFAULT_ELEMENT_NAME); - final Artifact art = artifactBuilder.buildObject(); - art.setArtifact(artifact); - request.setArtifact(art); - } - - return request; - } - - /** ID of the inbound message. */ - public final static String INBOUND_MSG_ID = "inbound"; - - /** Issuer of the inbound message. */ - public final static String INBOUND_MSG_ISSUER = "http://sp.example.org"; - - /** ID of the outbound message. */ - public final static String OUTBOUND_MSG_ID = "outbound"; - - /** Issuer of the outbound message. */ - public final static String OUTBOUND_MSG_ISSUER = "http://idp.example.org"; - - /** - * Checks that the request context contains an EventContext, and that the event - * content is as given. - * @param profileRequestContext the context to check - * @param event event to check - */ - static void assertEvent(@Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final Object event) { - EventContext ctx = profileRequestContext.getSubcontext(EventContext.class); - Assert.assertNotNull(ctx); - Assert.assertEquals(ctx.getEvent(), event); - } - - /** - * Checks that the given request context does not contain an EventContext (thus - * signaling a "proceed" event). - * @param profileRequestContext the context to check - */ - static void assertProceedEvent(@Nonnull final ProfileRequestContext profileRequestContext) { - EventContext ctx = profileRequestContext.getSubcontext(EventContext.class); - Assert.assertTrue(ctx == null || ctx.getEvent().equals(EventIds.PROCEED_EVENT_ID)); - } - -} diff --git a/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginApplicationITests.java b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginApplicationITests.java new file mode 100644 index 0000000..8d1269e --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginApplicationITests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2020 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 example; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpSession; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +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.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class Saml2LoginApplicationITests { + + static final String SIGNED_RESPONSE = "PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfOGU1NjU1ZWQ3Y2E1NWUyODZiNjE1MDA4OGVjNzU1ZjY2MGY1NzQxY2M3IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAyMC0wOC0yOFQyMToxMDoyNVoiIERlc3RpbmF0aW9uPSJodHRwOi8vbG9jYWxob3N0OjgwODAvbG9naW4vc2FtbDIvc3NvL29uZSIgSW5SZXNwb25zZVRvPSJBUlFiOGYzMzQzLWQzMzMtNDFhMy1hMTk0LTE2MTdhYTY0YjNiMyI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vc2ltcGxlc2FtbC1mb3Itc3ByaW5nLXNhbWwuY2ZhcHBzLmlvL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjXzhlNTY1NWVkN2NhNTVlMjg2YjYxNTAwODhlYzc1NWY2NjBmNTc0MWNjNyI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPjNOQkxaaHBjMEpxOVAwSDhVQzFPRmFUN2hPY0QxNGZneWNlRFZ3dEw5VDA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmN1YVhvYVl2Y2ZBOWg5bENRbFNsVVZjMzV6K294SGRWZkFVMk5sQ2U4cFhLa25RVkF6ZkNQSVJRZDdyWTdyQlc4OVpTb0Jsd2RsS1Iwemx4cHBySnVNenZyZE1lOWhybU1pU2hWcWlXQzVKRmhDSndKWGxpdzBEWGk5VEZ6NkcxSDBRYm1nVHRkZDJOYUIzR25WRFhsRi93ZWx0aUFDUTNDNUp2aE1HN1B5UmdQWGJFa0lkWWxzZ2FmTS9RYW9yUDdNQjJXNU1sWnhCYmFla043Qm9EdTVNZkhhMU9RK3ZDQ0h5R0hQbG1JTjdmbW80N29xWFNhMTVyUTBvVFBld1lMakVFMldrL2x6WiszdThOSHArbVY5Ukc2dXlWRHEyU1JsVm5XUElQL0pXaVdFVjhVRU13K0Q2R1NyNnQxN1E2QWV2VHNCRmRxZ1A3UTIySWRyZzBrUT09PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJRUV6Q0NBdnVnQXdJQkFnSUpBSWMxcXpMcnYrNW5NQTBHQ1NxR1NJYjNEUUVCQ3dVQU1JR2ZNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0F3Q1EwOHhGREFTQmdOVkJBY01DME5oYzNSc1pTQlNiMk5yTVJ3d0dnWURWUVFLREJOVFlXMXNJRlJsYzNScGJtY2dVMlZ5ZG1WeU1Rc3dDUVlEVlFRTERBSkpWREVnTUI0R0ExVUVBd3dYYzJsdGNHeGxjMkZ0YkhCb2NDNWpabUZ3Y0hNdWFXOHhJREFlQmdrcWhraUc5dzBCQ1FFV0VXWm9ZVzVwYTBCd2FYWnZkR0ZzTG1sdk1CNFhEVEUxTURJeU16SXlORFV3TTFvWERUSTFNREl5TWpJeU5EVXdNMW93Z1o4eEN6QUpCZ05WQkFZVEFsVlRNUXN3Q1FZRFZRUUlEQUpEVHpFVU1CSUdBMVVFQnd3TFEyRnpkR3hsSUZKdlkyc3hIREFhQmdOVkJBb01FMU5oYld3Z1ZHVnpkR2x1WnlCVFpYSjJaWEl4Q3pBSkJnTlZCQXNNQWtsVU1TQXdIZ1lEVlFRRERCZHphVzF3YkdWellXMXNjR2h3TG1ObVlYQndjeTVwYnpFZ01CNEdDU3FHU0liM0RRRUpBUllSWm1oaGJtbHJRSEJwZG05MFlXd3VhVzh3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQzRjbjYyRTF4THFwTjM0UG1icktCYmtPWEZqeldnSjliK3BYdWFSZnQ2QTMzOXV1SVFlb2VINXFlU0tSVlRsMzJMMGdkejJaaXZMd1pYVytjcXZmdFZXMXR2RUh2ekpGeXhlVFczZkNVZUNRc2ViTG5BMnFSYTA3Umt4VG82TmYyNDRtV1dSRG9kY29IRWZEVVNieGZUWjZJRXhTb2pTSVUyUm5ENldsbFlXRmREMUdGcEJKT21RQjhyQWM4d0pJQmRIRmRRblg4VHRsN2haNnJ0Z3FFWU16WVZNdUoyRjJyMUhTVTF6U0F2d3BkWVA2clJHRlJKRWZkQTltbTNXS2ZOTFNjNWNsanowWC9UWHkwdlZsQVY5NWw5cWNmRnpQbXJrTklzdDlGWlN3cHZCNDlMeUFWa2UwNEZRUFB3TGdWSDRncGhpSkgzanZaN0krSjVsUzhWQWdNQkFBR2pVREJPTUIwR0ExVWREZ1FXQkJUVHlQNkNjNUhsQko1K3VjVkN3R2M1b2dLTkd6QWZCZ05WSFNNRUdEQVdnQlRUeVA2Q2M1SGxCSjUrdWNWQ3dHYzVvZ0tOR3pBTUJnTlZIUk1FQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUF2TVM0RVFlUC9pcFY0ak9HNWxPNi90WUNiL2lKZUFkdU9uUmhrSmswRGJYMzI5bERMWmhUVEwveC93LzltdUNWY3ZMcnpFcDZQTitWV2Z3NUU1Rld0Wk4weWhHdFA5Uit2Wm5yVitvYzJ6R0Qrbm8xL3lTRk9lM0VpSkNPNWRlaHhLallFbUJSdjVzVS9MWkZLWnBvektOL0JNRWE2Q3FMdXhiemI3eWt4VnI3RVZGWHdsdFB4ekU5VG1MOU9BQ05OeUY1ZUpIV01STWxsYXJVdmtjWGxoNHB1eDRrczllNnpWOURRQnkyemRzOWYxSTNxeGcwZVg2Sm5HclhpL1ppQ1QrbEpnVmUzWkZYaWVqaUxBaUtCMDRzWFczdGkwTFczbHgxM1kxWWxRNC90bHBnVGdmSUp4S1Y2bnlQaUxvSzBueXdiTWQrdnBBaXJEdDJPYytoazwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJfYmE3MjllMmNhYTRkZmExNmY3ZDQwM2MxNzUwNmE5MTM1Zjk3YzAwN2Y4IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAyMC0wOC0yOFQyMToxMDoyNVoiPjxzYW1sOklzc3Vlcj5odHRwczovL3NpbXBsZXNhbWwtZm9yLXNwcmluZy1zYW1sLmNmYXBwcy5pby9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI19iYTcyOWUyY2FhNGRmYTE2ZjdkNDAzYzE3NTA2YTkxMzVmOTdjMDA3ZjgiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPjxkczpEaWdlc3RWYWx1ZT5EdHpJNnJ5SjE0VjdFelpGaU51UHl2RUZiWEYxbmZBTk5ZWTE3QWJaMTEwPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5LQ2ZmRytCSWJHeC92ZlV1VnEyU25qOHdYM0htKytPZFVnRmFkR0hhazhBL1hPQzJwVERoNkJZcWFjTzhDcUhBR29vekZrcFZhTU9pN053MmpFUnVMaGg3aG1GVzB2aE9tK2c4UUVxM3VCMGVkY3dsaUtya1RtZ0VjZE5lRW1pem15QUJpeHdEbHFHd1MzSnA4MHdrQ1NxSyt5TEFsMlJoWkg0Q0kvMTVrZVcxSURjSDg3Ly9pMXNlOVk5U1lVVTZjaGpsVjFPa0MxWXR5dGpwNXlUd1pxb1JqUXRKMFlVSnp6TFVOTHJaR0xvZG50azR0VzNJZ2FVM293OGk2dDZSZWpReG1nNHNJTGcxNzgvblU3dG8vRmdtRnc2UmM4TmhqYW5EY2FyZjdyb1crVVlmQnpBYmNLYU1NaEFVeUh4RjB0VU4xS2NHeDVxTXFNRVFOdS9iRFE9PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUVFekNDQXZ1Z0F3SUJBZ0lKQUljMXF6THJ2KzVuTUEwR0NTcUdTSWIzRFFFQkN3VUFNSUdmTVFzd0NRWURWUVFHRXdKVlV6RUxNQWtHQTFVRUNBd0NRMDh4RkRBU0JnTlZCQWNNQzBOaGMzUnNaU0JTYjJOck1Sd3dHZ1lEVlFRS0RCTlRZVzFzSUZSbGMzUnBibWNnVTJWeWRtVnlNUXN3Q1FZRFZRUUxEQUpKVkRFZ01CNEdBMVVFQXd3WGMybHRjR3hsYzJGdGJIQm9jQzVqWm1Gd2NITXVhVzh4SURBZUJna3Foa2lHOXcwQkNRRVdFV1pvWVc1cGEwQndhWFp2ZEdGc0xtbHZNQjRYRFRFMU1ESXlNekl5TkRVd00xb1hEVEkxTURJeU1qSXlORFV3TTFvd2daOHhDekFKQmdOVkJBWVRBbFZUTVFzd0NRWURWUVFJREFKRFR6RVVNQklHQTFVRUJ3d0xRMkZ6ZEd4bElGSnZZMnN4SERBYUJnTlZCQW9NRTFOaGJXd2dWR1Z6ZEdsdVp5QlRaWEoyWlhJeEN6QUpCZ05WQkFzTUFrbFVNU0F3SGdZRFZRUUREQmR6YVcxd2JHVnpZVzFzY0dod0xtTm1ZWEJ3Y3k1cGJ6RWdNQjRHQ1NxR1NJYjNEUUVKQVJZUlptaGhibWxyUUhCcGRtOTBZV3d1YVc4d2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUM0Y242MkUxeExxcE4zNFBtYnJLQmJrT1hGanpXZ0o5YitwWHVhUmZ0NkEzMzl1dUlRZW9lSDVxZVNLUlZUbDMyTDBnZHoyWml2THdaWFcrY3F2ZnRWVzF0dkVIdnpKRnl4ZVRXM2ZDVWVDUXNlYkxuQTJxUmEwN1JreFRvNk5mMjQ0bVdXUkRvZGNvSEVmRFVTYnhmVFo2SUV4U29qU0lVMlJuRDZXbGxZV0ZkRDFHRnBCSk9tUUI4ckFjOHdKSUJkSEZkUW5YOFR0bDdoWjZydGdxRVlNellWTXVKMkYycjFIU1UxelNBdndwZFlQNnJSR0ZSSkVmZEE5bW0zV0tmTkxTYzVjbGp6MFgvVFh5MHZWbEFWOTVsOXFjZkZ6UG1ya05Jc3Q5RlpTd3B2QjQ5THlBVmtlMDRGUVBQd0xnVkg0Z3BoaUpIM2p2WjdJK0o1bFM4VkFnTUJBQUdqVURCT01CMEdBMVVkRGdRV0JCVFR5UDZDYzVIbEJKNSt1Y1ZDd0djNW9nS05HekFmQmdOVkhTTUVHREFXZ0JUVHlQNkNjNUhsQko1K3VjVkN3R2M1b2dLTkd6QU1CZ05WSFJNRUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBdk1TNEVRZVAvaXBWNGpPRzVsTzYvdFlDYi9pSmVBZHVPblJoa0prMERiWDMyOWxETFpoVFRML3gvdy85bXVDVmN2THJ6RXA2UE4rVldmdzVFNUZXdFpOMHloR3RQOVIrdlpuclYrb2MyekdEK25vMS95U0ZPZTNFaUpDTzVkZWh4S2pZRW1CUnY1c1UvTFpGS1pwb3pLTi9CTUVhNkNxTHV4YnpiN3lreFZyN0VWRlh3bHRQeHpFOVRtTDlPQUNOTnlGNWVKSFdNUk1sbGFyVXZrY1hsaDRwdXg0a3M5ZTZ6VjlEUUJ5MnpkczlmMUkzcXhnMGVYNkpuR3JYaS9aaUNUK2xKZ1ZlM1pGWGllamlMQWlLQjA0c1hXM3RpMExXM2x4MTNZMVlsUTQvdGxwZ1RnZklKeEtWNm55UGlMb0swbnl3Yk1kK3ZwQWlyRHQyT2MraGs8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYW1sMi9zZXJ2aWNlLXByb3ZpZGVyLW1ldGFkYXRhL29uZSIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnRlc3R1c2VyQHNwcmluZy5zZWN1cml0eS5zYW1sPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDUyLTA1LTA2VDIyOjU3OjA1WiIgUmVjaXBpZW50PSJodHRwOi8vbG9jYWxob3N0OjgwODAvbG9naW4vc2FtbDIvc3NvL29uZSIgSW5SZXNwb25zZVRvPSJBUlFiOGYzMzQzLWQzMzMtNDFhMy1hMTk0LTE2MTdhYTY0YjNiMyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDIwLTA4LTI4VDIxOjA5OjU1WiIgTm90T25PckFmdGVyPSIyMDUyLTA1LTA2VDIyOjU3OjA1WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwOi8vbG9jYWxob3N0OjgwODAvc2FtbDIvc2VydmljZS1wcm92aWRlci1tZXRhZGF0YS9vbmU8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDIwLTA4LTI4VDIxOjEwOjI1WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMC0wOC0yOVQwNToxMDoyNVoiIFNlc3Npb25JbmRleD0iXzFhN2E4NjM4ZTg2Y2VhYTkyY2M1ODY0NmM0MjMxODc1YTZjOWQxY2FiYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0dXNlckBzcHJpbmcuc2VjdXJpdHkuc2FtbDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+bWVtYmVyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZW1haWxBZGRyZXNzIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0dXNlckBzcHJpbmcuc2VjdXJpdHkuc2FtbDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg=="; + + static final Map> USER_ATTRIBUTES = new LinkedHashMap<>(); + + static { + USER_ATTRIBUTES.put("uid", Arrays.asList("testuser@spring.security.saml")); + USER_ATTRIBUTES.put("eduPersonAffiliation", Arrays.asList("member", "user")); + USER_ATTRIBUTES.put("emailAddress", Arrays.asList("testuser@spring.security.saml")); + } + + @Autowired + MockMvc mvc; + + @Autowired + WebClient webClient; + + @Test + void indexWhenSamlResponseThenShowsUserInformation() throws Exception { + HttpSession session = this.mvc.perform(get("http://localhost:8080/")).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost:8080/saml2/authenticate/one")).andReturn().getRequest() + .getSession(); + + this.mvc.perform(post("http://localhost:8080/login/saml2/sso/one").param("SAMLResponse", SIGNED_RESPONSE) + .session((MockHttpSession) session)).andExpect(redirectedUrl("http://localhost:8080/")); + + this.mvc.perform(get("http://localhost:8080/").session((MockHttpSession) session)) + .andExpect(model().attribute("emailAddress", "testuser@spring.security.saml")) + .andExpect(model().attribute("userAttributes", USER_ATTRIBUTES)); + } + + @Test + void authenticationAttemptWhenValidThenShowsUserEmailAddress() throws Exception { + HtmlPage assertingParty = this.webClient.getPage("/"); + HtmlForm form = assertingParty.getFormByName("f"); + HtmlInput username = form.getInputByName("username"); + HtmlInput password = form.getInputByName("password"); + HtmlSubmitInput submit = assertingParty.getHtmlElementById("submit_button"); + username.setValueAttribute("user"); + password.setValueAttribute("password"); + HtmlPage relyingParty = submit.click(); + assertThat(relyingParty.asText()).contains("You're email address is testuser@spring.security.saml"); + } + +} diff --git a/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginIntegrationTests.java b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginIntegrationTests.java deleted file mode 100644 index a6ad96f..0000000 --- a/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginIntegrationTests.java +++ /dev/null @@ -1,473 +0,0 @@ -/* - * Copyright 2002-2020 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 example; - -import java.io.ByteArrayInputStream; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.security.KeyException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.UUID; - -import javax.servlet.http.HttpSession; - -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; -import net.shibboleth.utilities.java.support.xml.BasicParserPool; -import net.shibboleth.utilities.java.support.xml.SerializeSupport; -import net.shibboleth.utilities.java.support.xml.XMLParserException; -import org.hamcrest.Matcher; -import org.joda.time.DateTime; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.core.xml.io.MarshallerFactory; -import org.opensaml.core.xml.io.MarshallingException; -import org.opensaml.core.xml.io.UnmarshallingException; -import org.opensaml.saml.common.SignableSAMLObject; -import org.opensaml.saml.saml2.core.Assertion; -import org.opensaml.saml.saml2.core.AuthnRequest; -import org.opensaml.saml.saml2.core.EncryptedAssertion; -import org.opensaml.saml.saml2.core.EncryptedID; -import org.opensaml.saml.saml2.core.Response; -import org.opensaml.saml.saml2.core.SubjectConfirmation; -import org.opensaml.saml.saml2.core.SubjectConfirmationData; -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.crypto.KeySupport; -import org.opensaml.xmlsec.SignatureSigningParameters; -import org.opensaml.xmlsec.signature.support.SignatureConstants; -import org.opensaml.xmlsec.signature.support.SignatureException; -import org.opensaml.xmlsec.signature.support.SignatureSupport; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; -import org.springframework.security.web.WebAttributes; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.ResultMatcher; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.util.MultiValueMap; -import org.springframework.web.util.UriComponentsBuilder; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.matchesRegex; -import static org.hamcrest.Matchers.startsWith; -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.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; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest -@AutoConfigureMockMvc -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"; - - // @formatter:off - private String idpCertificate = "-----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"; - // @formatter:on - - // @formatter:off - private String idpPrivateKey = "-----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"; - // @formatter:on - - // @formatter:off - private String spCertificate = "-----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-----"; - // @formatter:on - - // @formatter:off - 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-----"; - // @formatter:on - - @Autowired - MockMvc mockMvc; - - @Test - void applicationAccessWhenSingleProviderAndUnauthenticatedThenRedirectsToAuthNRequest() throws Exception { - // @formatter:off - this.mockMvc.perform(get("http://localhost:8080/some/url")) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost:8080/saml2/authenticate/simplesamlphp")); - // @formatter:on - } - - @Test - void authenticateRequestWhenUnauthenticatedThenRespondsWithRedirectAuthNRequestXML() throws Exception { - // @formatter:off - this.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="))); - // @formatter:on - } - - @Test - void authenticateRequestWhenRelayStateThenRespondsWithRedirectAndEncodedRelayState() throws Exception { - // @formatter:off - MockHttpServletRequestBuilder request = get("http://localhost:8080/saml2/authenticate/simplesamlphp") - .param("RelayState", "relay state value with spaces") - .param("OtherParam", "OtherParamValue") - .param("OtherParam2", "OtherParamValue2"); - this.mockMvc.perform(request) - .andExpect(status().is3xxRedirection()) - .andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php?SAMLRequest="))) - .andExpect(header().string("Location", containsString("RelayState=relay%20state%20value%20with%20spaces"))) - //check order of parameters - .andExpect(header().string("Location", matchesRegex(".*\\?SAMLRequest\\=.*\\&RelayState\\=.*\\&SigAlg\\=.*\\&Signature\\=.*"))); - // @formatter:on - - } - - @Test - void authenticateRequestWhenWorkingThenDestinationAttributeIsSet() throws Exception { - // @formatter:off - final String redirectedUrl = this.mockMvc.perform(get("http://localhost:8080/saml2/authenticate/simplesamlphp")) - .andExpect(status().is3xxRedirection()) - .andReturn() - .getResponse() - .getRedirectedUrl(); - MultiValueMap parameters = UriComponentsBuilder.fromUriString(redirectedUrl) - .build(true) - .getQueryParams(); - // @formatter:on - String request = parameters.getFirst("SAMLRequest"); - assertThat(request).isNotNull().describedAs("SAMLRequest parameter is missing"); - request = URLDecoder.decode(request); - request = Saml2Utils.samlInflate(Saml2Utils.samlDecode(request)); - AuthnRequest authnRequest = (AuthnRequest) fromXml(request); - String destination = authnRequest.getDestination(); - assertThat(destination).isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php") - .describedAs("Destination must match"); - String acsURL = authnRequest.getAssertionConsumerServiceURL(); - assertThat(acsURL).isEqualTo("http://localhost:8080/login/saml2/sso/simplesamlphp") - .describedAs("AssertionConsumerServiceURL must match"); - } - - @Test - void authenticateWhenResponseIsSignedThenItSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - Response response = buildResponse(assertion); - signXmlObject(response, getSigningCredential(this.idpCertificate, this.idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/").andExpect(authenticated().withUsername(USERNAME)); - } - - @Test - void authenticateWhenAssertionIsThenItSignedSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - Response response = buildResponse(assertion); - signXmlObject(assertion, getSigningCredential(this.idpCertificate, this.idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/").andExpect(authenticated().withUsername(USERNAME)); - } - - @Test - void authenticateWhenXmlObjectIsNotSignedThenItFails() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - Response response = buildResponse(assertion); - sendResponse(response, "/login?error").andExpect(unauthenticated()); - } - - @Test - void authenticateWhenResponseIsSignedAndAssertionIsEncryptedThenItSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - EncryptedAssertion encryptedAssertion = OpenSamlActionTestingSupport.encryptAssertion(assertion, - decodeCertificate(this.spCertificate)); - Response response = buildResponse(encryptedAssertion); - signXmlObject(response, getSigningCredential(this.idpCertificate, this.idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/").andExpect(authenticated().withUsername(USERNAME)); - } - - @Test - @Disabled("updating spring security broke this test") - void authenticateWhenResponseIsNotSignedAndAssertionIsEncryptedAndSignedThenItSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - signXmlObject(assertion, getSigningCredential(this.idpCertificate, this.idpPrivateKey, UsageType.SIGNING)); - EncryptedAssertion encryptedAssertion = OpenSamlActionTestingSupport.encryptAssertion(assertion, - decodeCertificate(this.spCertificate)); - Response response = buildResponse(encryptedAssertion); - sendResponse(response, "/").andExpect(authenticated().withUsername(USERNAME)); - } - - @Test - void authenticateWhenResponseIsSignedAndNameIDisEncryptedThenItSucceeds() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - final EncryptedID nameId = OpenSamlActionTestingSupport.encryptNameId(assertion.getSubject().getNameID(), - decodeCertificate(this.spCertificate)); - assertion.getSubject().setEncryptedID(nameId); - assertion.getSubject().setNameID(null); - Response response = buildResponse(assertion); - signXmlObject(assertion, getSigningCredential(this.idpCertificate, this.idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/").andExpect(authenticated().withUsername(USERNAME)); - } - - @Test - void authenticateWhenSignatureKeysDontMatchThenItFails() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - Response response = buildResponse(assertion); - signXmlObject(assertion, getSigningCredential(this.spCertificate, this.spPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/login?error").andExpect(saml2AuthenticationExceptionMatcher("invalid_signature", - containsString("Invalid assertion [assertion] for SAML response"))); - } - - @Test - void authenticateWhenNotOnOrAfterDontMatchThenItFails() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - assertion.getConditions().setNotOnOrAfter(DateTime.now().minusDays(1)); - Response response = buildResponse(assertion); - signXmlObject(assertion, getSigningCredential(this.idpCertificate, this.idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/login?error").andExpect(saml2AuthenticationExceptionMatcher("invalid_assertion", - containsString("Invalid assertion [assertion] for SAML response"))); - } - - @Test - void authenticateWhenNotOnOrBeforeDontMatchThenItFails() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - assertion.getConditions().setNotBefore(DateTime.now().plusDays(1)); - Response response = buildResponse(assertion); - signXmlObject(assertion, getSigningCredential(this.idpCertificate, this.idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/login?error").andExpect(saml2AuthenticationExceptionMatcher("invalid_assertion", - containsString("Invalid assertion [assertion] for SAML response"))); - } - - @Test - void authenticateWhenIssuerIsInvalidThenItFails() throws Exception { - Assertion assertion = buildAssertion(USERNAME); - Response response = buildResponse(assertion); - response.getIssuer().setValue("invalid issuer"); - signXmlObject(response, getSigningCredential(this.idpCertificate, this.idpPrivateKey, UsageType.SIGNING)); - sendResponse(response, "/login?error").andExpect(unauthenticated()).andExpect( - saml2AuthenticationExceptionMatcher("invalid_signature", containsString("Invalid signature"))); - } - - private ResultActions sendResponse(Response response, String redirectUrl) throws Exception { - String xml = toXml(response); - // @formatter:off - return this.mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .param("SAMLResponse", Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8)))) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl(redirectUrl)); - // @formatter:on - } - - private Response buildResponse(Assertion assertion) { - Response response = buildResponse(); - response.getAssertions().add(assertion); - return response; - } - - private Response buildResponse(EncryptedAssertion assertion) { - Response response = buildResponse(); - response.getEncryptedAssertions().add(assertion); - return response; - } - - private Response buildResponse() { - Response response = OpenSamlActionTestingSupport.buildResponse(); - response.setID("_" + UUID.randomUUID().toString()); - response.setDestination("http://localhost:8080/login/saml2/sso/simplesamlphp"); - response.setIssuer(OpenSamlActionTestingSupport - .buildIssuer("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php")); - return response; - } - - private Assertion buildAssertion(String username) { - Assertion assertion = OpenSamlActionTestingSupport.buildAssertion(); - assertion.setIssueInstant(DateTime.now()); - assertion.setIssuer(OpenSamlActionTestingSupport - .buildIssuer("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php")); - assertion.setSubject(OpenSamlActionTestingSupport.buildSubject(username)); - assertion.setConditions(OpenSamlActionTestingSupport.buildConditions()); - - SubjectConfirmation subjectConfirmation = OpenSamlActionTestingSupport.buildSubjectConfirmation(); - - // Default to bearer with basic valid confirmation data, but the test can change - // as appropriate - subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); - final SubjectConfirmationData confirmationData = OpenSamlActionTestingSupport - .buildSubjectConfirmationData(LOCAL_SP_ENTITY_ID); - confirmationData.setRecipient("http://localhost:8080/login/saml2/sso/simplesamlphp"); - subjectConfirmation.setSubjectConfirmationData(confirmationData); - assertion.getSubject().getSubjectConfirmations().add(subjectConfirmation); - return assertion; - } - - protected Credential getSigningCredential(String certificate, String key, UsageType usageType) - throws CertificateException, KeyException { - PublicKey publicKey = decodeCertificate(certificate).getPublicKey(); - final PrivateKey privateKey = KeySupport.decodePrivateKey(key.getBytes(StandardCharsets.UTF_8), new char[0]); - BasicCredential cred = CredentialSupport.getSimpleCredential(publicKey, privateKey); - cred.setUsageType(usageType); - cred.setEntityId("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"); - return cred; - } - - 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 String toXml(XMLObject object) throws MarshallingException { - final MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory(); - Element element = marshallerFactory.getMarshaller(object).marshall(object); - return SerializeSupport.nodeToString(element); - } - - private XMLObject fromXml(String xml) - throws XMLParserException, UnmarshallingException, ComponentInitializationException { - BasicParserPool parserPool = new BasicParserPool(); - parserPool.initialize(); - Document document = parserPool.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); - Element element = document.getDocumentElement(); - return XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element).unmarshall(element); - - } - - private X509Certificate decodeCertificate(String source) { - try { - final CertificateFactory factory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) factory - .generateCertificate(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))); - } - catch (Exception ex) { - throw new IllegalArgumentException(ex); - } - } - - private static ResultMatcher saml2AuthenticationExceptionMatcher(String code, Matcher message) { - return (result) -> { - final HttpSession session = result.getRequest().getSession(false); - assertThat(session).isNotNull().describedAs("HttpSession"); - Object exception = session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); - assertThat(exception).isInstanceOf(Saml2AuthenticationException.class); - Saml2AuthenticationException se = (Saml2AuthenticationException) exception; - assertThat(se.getSaml2Error().getErrorCode()).isEqualTo(code); - assertThat(message.matches(se.getSaml2Error().getDescription())).isTrue(); - }; - } - - @SpringBootConfiguration - @EnableAutoConfiguration - public static class SpringBootApplicationTestConfig { - - } - -} diff --git a/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2Utils.java b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2Utils.java deleted file mode 100644 index 90f4884..0000000 --- a/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2Utils.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2002-2020 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 example; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterOutputStream; - -import org.apache.commons.codec.binary.Base64; - -import org.springframework.security.saml2.Saml2Exception; - -/** - * @since 5.3 - */ -final class Saml2Utils { - - private static Base64 BASE64 = new Base64(0, new byte[] { '\n' }); - - private Saml2Utils() { - } - - static String samlEncode(byte[] b) { - return BASE64.encodeAsString(b); - } - - static byte[] samlDecode(String s) { - return BASE64.decode(s); - } - - static byte[] samlDeflate(String s) { - try { - ByteArrayOutputStream b = new ByteArrayOutputStream(); - DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true)); - deflater.write(s.getBytes(StandardCharsets.UTF_8)); - deflater.finish(); - return b.toByteArray(); - } - catch (IOException ex) { - throw new Saml2Exception("Unable to deflate string", ex); - } - } - - static String samlInflate(byte[] b) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true)); - iout.write(b); - iout.finish(); - return new String(out.toByteArray(), StandardCharsets.UTF_8); - } - catch (IOException ex) { - throw new Saml2Exception("Unable to inflate string", ex); - } - } - -} diff --git a/servlet/spring-boot/java/saml2-login/src/main/java/example/IndexController.java b/servlet/spring-boot/java/saml2-login/src/main/java/example/IndexController.java index 9453f91..81c4c9f 100644 --- a/servlet/spring-boot/java/saml2-login/src/main/java/example/IndexController.java +++ b/servlet/spring-boot/java/saml2-login/src/main/java/example/IndexController.java @@ -16,14 +16,20 @@ package example; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class IndexController { @GetMapping("/") - public String index() { + public String index(Model model, @AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) { + String emailAddress = principal.getFirstAttribute("emailAddress"); + model.addAttribute("emailAddress", emailAddress); + model.addAttribute("userAttributes", principal.getAttributes()); return "index"; } diff --git a/servlet/spring-boot/java/saml2-login/src/main/resources/application.yml b/servlet/spring-boot/java/saml2-login/src/main/resources/application.yml index afee02e..865c79a 100644 --- a/servlet/spring-boot/java/saml2-login/src/main/resources/application.yml +++ b/servlet/spring-boot/java/saml2-login/src/main/resources/application.yml @@ -3,12 +3,6 @@ spring: saml2: relyingparty: registration: - simplesamlphp: - signing.credentials: - - private-key-location: "classpath:credentials/rp-private.key" - certificate-location: "classpath:credentials/rp-certificate.crt" + one: identityprovider: - entity-id: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php - verification.credentials: - - certificate-location: "classpath:credentials/idp-certificate.crt" - sso-url: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php + metadata-uri: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php diff --git a/servlet/spring-boot/java/saml2-login/src/main/resources/templates/index.html b/servlet/spring-boot/java/saml2-login/src/main/resources/templates/index.html index e278cbe..6073dfb 100644 --- a/servlet/spring-boot/java/saml2-login/src/main/resources/templates/index.html +++ b/servlet/spring-boot/java/saml2-login/src/main/resources/templates/index.html @@ -17,19 +17,30 @@ - Spring Security - SAML 2.0 Login - + Spring Security - SAML 2.0 Login + + - -

SAML 2.0 Login with Spring Security

-
You are successfully logged in as
+ +

SAML 2.0 Login with Spring Security

+

You are successfully logged in as

+

You're email address is

+

All Your Attributes

+
+
+
+