diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 8635da6..d124e15 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -3,10 +3,14 @@ "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd"> + + + + org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*, + org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*" /> diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000..0f75407 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/gradle.properties b/gradle.properties index 119a24c..ccd2ee8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ version=5.4.0-SNAPSHOT +spring-security.version=5.4.0-SNAPSHOT diff --git a/servlet/spring-boot/java/saml2-login/README.adoc b/servlet/spring-boot/java/saml2-login/README.adoc new file mode 100644 index 0000000..c0023a3 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/README.adoc @@ -0,0 +1,38 @@ += OAuth 2.0 Login Sample + +This guide provides instructions on setting up this SAML 2.0 Login sample application. + +The sample application uses Spring Boot and the `spring-security-saml2-service-provider` +module which is new in Spring Security 5.2. + +== Goals + +`saml2Login()` provides a very simple implementation of a Service Provider that can receive a SAML 2.0 Response via the HTTP-POST and HTTP-REDIRECT bindings against the SimpleSAMLphp SAML 2.0 reference implementation. + +The following features are implemented in the MVP: + +1. Receive and validate a SAML 2.0 Response containing an assertion, and create a corresponding authentication in Spring Security +2. Send a SAML 2.0 AuthNRequest to an Identity Provider +3. Provide a framework for components used in SAML 2.0 authentication that can be swapped by configuration +4. Work against the SimpleSAMLphp reference implementation + +== Run the Sample + +=== Start up the Sample Boot Application +``` + ./gradlew :spring-security-samples-boot-saml2login:bootRun +``` + +=== Open a Browser + +http://localhost:8080/ + +You will be redirect to the SimpleSAMLphp IDP + +=== Type in your credentials + +``` +User: user +Password: password +``` + diff --git a/servlet/spring-boot/java/saml2-login/build.gradle b/servlet/spring-boot/java/saml2-login/build.gradle new file mode 100644 index 0000000..246c85d --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'org.springframework.boot' version '2.3.1.RELEASE' + id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id "nebula.integtest" version "7.0.9" + id 'java' +} + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.security:spring-security-saml2-service-provider' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' + + testImplementation 'net.sourceforge.htmlunit:htmlunit' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2-login/gradle.properties b/servlet/spring-boot/java/saml2-login/gradle.properties new file mode 100644 index 0000000..a0dae04 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/gradle.properties @@ -0,0 +1 @@ +spring-security.version=5.4.0-SNAPSHOT diff --git a/servlet/spring-boot/java/saml2-login/gradle/wrapper/gradle-wrapper.jar b/servlet/spring-boot/java/saml2-login/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f3d88b1 Binary files /dev/null and b/servlet/spring-boot/java/saml2-login/gradle/wrapper/gradle-wrapper.jar differ diff --git a/servlet/spring-boot/java/saml2-login/gradle/wrapper/gradle-wrapper.properties b/servlet/spring-boot/java/saml2-login/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a2bf131 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/servlet/spring-boot/java/saml2-login/gradlew b/servlet/spring-boot/java/saml2-login/gradlew new file mode 100755 index 0000000..2fe81a7 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/servlet/spring-boot/java/saml2-login/gradlew.bat b/servlet/spring-boot/java/saml2-login/gradlew.bat new file mode 100644 index 0000000..9109989 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/servlet/spring-boot/java/saml2-login/settings.gradle b/servlet/spring-boot/java/saml2-login/settings.gradle new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..ae10564 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/OpenSamlActionTestingSupport.java @@ -0,0 +1,466 @@ +/* + * 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/Saml2LoginIntegrationTests.java b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginIntegrationTests.java new file mode 100644 index 0000000..9746f7e --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2LoginIntegrationTests.java @@ -0,0 +1,471 @@ +/* + * 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.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 + 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 new file mode 100644 index 0000000..90f4884 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/integTest/java/example/Saml2Utils.java @@ -0,0 +1,75 @@ +/* + * 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 new file mode 100644 index 0000000..9453f91 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/main/java/example/IndexController.java @@ -0,0 +1,30 @@ +/* + * Copyright 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 org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class IndexController { + + @GetMapping("/") + public String index() { + return "index"; + } + +} diff --git a/servlet/spring-boot/java/saml2-login/src/main/java/example/Saml2LoginApplication.java b/servlet/spring-boot/java/saml2-login/src/main/java/example/Saml2LoginApplication.java new file mode 100644 index 0000000..c8cb6a5 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/main/java/example/Saml2LoginApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 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 org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saml2LoginApplication { + + public static void main(String[] args) { + SpringApplication.run(Saml2LoginApplication.class, args); + } + +} 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 new file mode 100644 index 0000000..afee02e --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/main/resources/application.yml @@ -0,0 +1,14 @@ +spring: + security: + saml2: + relyingparty: + registration: + simplesamlphp: + signing.credentials: + - private-key-location: "classpath:credentials/rp-private.key" + certificate-location: "classpath:credentials/rp-certificate.crt" + 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 diff --git a/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/idp-certificate.crt b/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/idp-certificate.crt new file mode 100644 index 0000000..9c4ee07 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/idp-certificate.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD +VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD +VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX +c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw +aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa +BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD +DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr +QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62 +E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz +2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW +RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ +nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5 +cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph +iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5 +ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO +nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v +ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu +xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z +V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3 +lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk +-----END CERTIFICATE----- diff --git a/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/rp-certificate.crt b/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/rp-certificate.crt new file mode 100644 index 0000000..b907e2f --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/rp-certificate.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC +VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG +A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD +DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1 +MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES +MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN +TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos +vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM ++U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG +y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi +XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+ +qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD +RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B +-----END CERTIFICATE----- diff --git a/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/rp-private.key b/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/rp-private.key new file mode 100644 index 0000000..73196e0 --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/main/resources/credentials/rp-private.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE +VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK +cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6 +Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn +x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5 +wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd +vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY +8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX +oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx +EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0 +KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt +YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr +9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM +INrtuLp4YHbgk1mi +-----END PRIVATE KEY----- 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 new file mode 100644 index 0000000..e278cbe --- /dev/null +++ b/servlet/spring-boot/java/saml2-login/src/main/resources/templates/index.html @@ -0,0 +1,35 @@ + + + + + + Spring Security - SAML 2.0 Login + + + + +

SAML 2.0 Login with Spring Security

+
You are successfully logged in as
+ + diff --git a/settings.gradle b/settings.gradle index 6f8651a..03d7a46 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,5 +36,5 @@ include ":servlet:spring-boot:java:oauth2:resource-server:multi-tenancy" include ":servlet:spring-boot:java:oauth2:resource-server:opaque" include ":servlet:spring-boot:java:oauth2:resource-server:static" include ":servlet:spring-boot:java:oauth2:webclient" +include ":servlet:spring-boot:java:saml2-login" include ":servlet:spring-boot:kotlin:hello-security" -