CAS Login & Single Logout with Spring Security
+You are successfully logged in as
+You're email address is
+All Your Attributes
+-
+
+
+
diff --git a/.gitignore b/.gitignore
index 36c79c363..27881bc89 100644
--- a/.gitignore
+++ b/.gitignore
@@ -211,3 +211,4 @@ gradle-app.setting
# End of https://www.toptal.com/developers/gitignore/api/gradle,java,intellij,eclipse
+/servlet/java-configuration/cas/install-cas/cas-server
diff --git a/gradle.properties b/gradle.properties
index 2fa63d564..798e8e10e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=6.0.0-SNAPSHOT
+version=6.1.0-SNAPSHOT
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
diff --git a/servlet/java-configuration/cas/install-cas/install-cas.sh b/servlet/java-configuration/cas/install-cas/install-cas.sh
new file mode 100644
index 000000000..fe4b21c71
--- /dev/null
+++ b/servlet/java-configuration/cas/install-cas/install-cas.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+#
+# Copyright 2023 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.
+#
+jq -h > /dev/null 2>&1
+if [[ $? -ne 0 ]]; then
+ echo jq not installed
+ exit 1
+fi
+curl -h > /dev/null 2>&1
+if [[ $? -ne 0 ]]; then
+ echo curl not installed
+ exit 1
+fi
+
+INSTALL_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+command -v cygpath > /dev/null && test ! -z "$MSYSTEM"
+if [[ $? -eq 0 ]]; then
+ INSTALL_DIR=$(cygpath -w "$INSTALL_DIR")
+fi
+# The supported version of CAS in the cas initializr changes over time so query the current value (for 6.6.x)
+# Get valid combinations with this: curl -s https://casinit.herokuapp.com/actuator/info/ | jq '."supported-versions"[] | select(.branch == "6.6")'
+CAS_VERSION=$(curl -s https://casinit.herokuapp.com/actuator/info/ | jq -r '."supported-versions"'[2].version)
+BOOT_VERSION=$(curl -s https://casinit.herokuapp.com/actuator/info/ | jq -r '."supported-versions"'[2].bootVersion)
+set -e
+SERVER_DIR=${INSTALL_DIR}/cas-server
+mkdir -p $SERVER_DIR
+cd $SERVER_DIR
+curl https://casinit.herokuapp.com/starter.tgz -d "dependencies=support-json-service-registry&casVersion=${CAS_VERSION}&bootVersion=${BOOT_VERSION}" | tar -xzvf -
+echo Building cas server
+./gradlew build
+mkdir -p ./etc/cas/config
+
+echo Service Directory is ${INSTALL_DIR}/services
+DEBUG=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5555
+
+java $DEBUG -jar build/libs/cas.war \
+ --cas.standalone.configuration-directory=./etc/cas/config \
+ --server.ssl.enabled=false \
+ --server.port=8090 \
+ --cas.service-registry.core.init-from-json=false \
+ --cas.service-registry.json.location=file:${INSTALL_DIR}/services
diff --git a/servlet/java-configuration/cas/install-cas/run-cas-docker.sh b/servlet/java-configuration/cas/install-cas/run-cas-docker.sh
new file mode 100644
index 000000000..2061ef7fa
--- /dev/null
+++ b/servlet/java-configuration/cas/install-cas/run-cas-docker.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+#
+# Copyright 2023 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.
+#
+docker --version > /dev/null 2>&1
+if [[ $? -ne 0 ]]; then
+ echo docker not installed
+ exit 1
+fi
+INSTALL_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+command -v cygpath > /dev/null && test ! -z "$MSYSTEM"
+if [[ $? -eq 0 ]]; then
+ INSTALL_DIR=$(cygpath -w "$INSTALL_DIR")
+fi
+
+CAS_VERSION=6.6.6
+# Note that the default apereo/cas image doesn't contain the modules you will need
+# You need to start with an cas overlay template and build your own image with the modules you want to use.
+# See https://github.com/apereo/cas-overlay-template or https://github.com/apereo/cas-initializr
+docker run -d -p 8090:8080 --name cas -v $INSTALL_DIR/services:/etc/cas/services --rm apereo/cas:$CAS_VERSION \
+ --cas.standalone.configuration-directory=/etc/cas/config \
+ --server.ssl.enabled=false \
+ --server.port=8080 \
+ --cas.service-registry.core.init-from-json=false \
+ --cas.service-registry.json.location=file:/etc/cas/services
+
+docker logs cas -f
diff --git a/servlet/java-configuration/cas/install-cas/services/https-1.json b/servlet/java-configuration/cas/install-cas/services/https-1.json
new file mode 100644
index 000000000..8fe31e1d5
--- /dev/null
+++ b/servlet/java-configuration/cas/install-cas/services/https-1.json
@@ -0,0 +1,8 @@
+{
+ "@class": "org.apereo.cas.services.CasRegisteredService",
+ "serviceId": "^(https?)://.*",
+ "name": "HTTP/HTTPS",
+ "id": 1,
+ "description": "This service definition authorizes all application urls that support HTTP and HTTPS protocols.",
+ "evaluationOrder": 10000
+}
diff --git a/servlet/java-configuration/cas/install-cas/stop-cas-docker.sh b/servlet/java-configuration/cas/install-cas/stop-cas-docker.sh
new file mode 100644
index 000000000..e02975b9a
--- /dev/null
+++ b/servlet/java-configuration/cas/install-cas/stop-cas-docker.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+#
+# Copyright 2023 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.
+#
+
+docker stop cas
diff --git a/servlet/java-configuration/cas/login/README.adoc b/servlet/java-configuration/cas/login/README.adoc
new file mode 100644
index 000000000..61bbd7509
--- /dev/null
+++ b/servlet/java-configuration/cas/login/README.adoc
@@ -0,0 +1,68 @@
+= CAS Login & Logout Sample
+
+This guide provides instructions on setting up an application that uses the CAS protocol for login.
+It uses https://casserver.herokuapp.com/cas/login as the server supporting the CAS protocol.
+
+The sample application uses Spring Boot and the `spring-security-cas`.
+
+== Goals
+
+=== CAS Login
+
+`cas/login` provides a very simple implementation of a CAS Service (application) that use a public test CAS server for
+ authentication.
+
+The following features are implemented in the MVP:
+
+1. Login
+2. Logout
+
+== Run the Sample
+
+=== Start up the Sample Application in Tomcat with Gretty
+[source,bash]
+----
+ ./gradlew :servlet:java-configuration:cas:login:appRun
+----
+
+=== Open a Browser
+
+https://localhost:8443/
+
+You will be redirect to a sample CAS server: https://casserver.herokuapp.com/cas
+
+=== Type in the credentials
+
+[source,bash]
+----
+User: casuser
+Password: Mellon
+----
+
+=== Run a local CAS server
+Run the following script to install and start a CAS server that responds at http://localhost:8090/cas/login
+
+[source,bash]
+----
+servlet/java-configuration/cas/install-cas/install-cas.sh
+----
+or with Docker:
+[source,bash]
+----
+servlet/java-configuration/cas/install-cas/run-cas-docker.sh
+----
+
+Adjust the `servlet/java-configuration/cas/login/src/main/resources/security.properties` file to point at local CAS server:
+
+[source,bash]
+----
+cas.base.url=http://localhost:8090/cas
+cas.login.url=http://localhost:8090/cas/login
+----
+
+Then run the CAS login app in gretty and browse to https://localhost:8443.
+
+[source,bash]
+----
+ ./gradlew :servlet:java-configuration:cas:login:appRun
+----
diff --git a/servlet/java-configuration/cas/login/build.gradle b/servlet/java-configuration/cas/login/build.gradle
new file mode 100644
index 000000000..2ec06753a
--- /dev/null
+++ b/servlet/java-configuration/cas/login/build.gradle
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2021 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.
+ */
+
+plugins {
+ id "java"
+ id "nebula.integtest" version "8.2.0"
+ id "org.gretty" version "4.0.3"
+ id "war"
+}
+
+apply from: "gradle/gretty.gradle"
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+ maven { url "https://repo.spring.io/milestone" }
+ maven { url "https://repo.spring.io/snapshot" }
+ maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
+}
+
+dependencies {
+ implementation platform("org.springframework:spring-framework-bom:6.0.5")
+ implementation platform("org.springframework.security:spring-security-bom:6.1.0-SNAPSHOT")
+ implementation platform("org.junit:junit-bom:5.7.0")
+
+ implementation "org.springframework.security:spring-security-config"
+ implementation "org.springframework.security:spring-security-web"
+ implementation "org.springframework:spring-webmvc"
+ implementation "org.springframework.security:spring-security-cas"
+ implementation "org.thymeleaf:thymeleaf-spring6:3.1.0.M1"
+ implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.0.M1"
+
+ implementation 'ch.qos.logback:logback-classic:1.4.5'
+
+ providedCompile "jakarta.servlet:jakarta.servlet-api:6.0.0"
+ providedCompile "org.glassfish.web:jakarta.servlet.jsp.jstl:2.0.0"
+
+ testImplementation "org.assertj:assertj-core:3.18.0"
+ testImplementation "org.springframework:spring-test"
+ testImplementation "org.springframework.security:spring-security-test"
+ testImplementation("org.junit.jupiter:junit-jupiter-api")
+ testImplementation "org.seleniumhq.selenium:htmlunit-driver:3.64.0"
+ testImplementation 'org.hamcrest:hamcrest:2.2'
+ testImplementation 'org.awaitility:awaitility:4.2.0'
+
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
+}
+
+tasks.withType(Test).configureEach {
+ useJUnitPlatform()
+ outputs.upToDateWhen { false }
+}
diff --git a/servlet/java-configuration/cas/login/gradle/gretty.gradle b/servlet/java-configuration/cas/login/gradle/gretty.gradle
new file mode 100644
index 000000000..a264050a8
--- /dev/null
+++ b/servlet/java-configuration/cas/login/gradle/gretty.gradle
@@ -0,0 +1,42 @@
+gretty {
+ servletContainer = "tomcat10"
+ contextPath = "/"
+ fileLogEnabled = false
+ integrationTestTask = 'integrationTest'
+ httpsEnabled = true
+}
+
+Task prepareAppServerForIntegrationTests = project.tasks.create('prepareAppServerForIntegrationTests') {
+ group = 'Verification'
+ description = 'Prepares the app server for integration tests'
+ doFirst {
+ project.gretty {
+ httpPort = -1
+ }
+ }
+}
+
+project.tasks.matching { it.name == "appBeforeIntegrationTest" }.all { task ->
+ task.dependsOn prepareAppServerForIntegrationTests
+}
+
+project.tasks.matching { it.name == "integrationTest" }.all {
+ task -> task.doFirst {
+ def gretty = project.gretty
+ String host = project.gretty.host ?: 'localhost'
+ boolean isHttps = gretty.httpsEnabled
+ Integer httpPort = integrationTest.systemProperties['gretty.httpPort']
+ Integer httpsPort = integrationTest.systemProperties['gretty.httpsPort']
+ int port = isHttps ? httpsPort : httpPort
+ String contextPath = project.gretty.contextPath
+ String httpBaseUrl = "http://${host}:${httpPort}${contextPath}"
+ String httpsBaseUrl = "https://${host}:${httpsPort}${contextPath}"
+ String baseUrl = isHttps ? httpsBaseUrl : httpBaseUrl
+ integrationTest.systemProperty 'app.port', port
+ integrationTest.systemProperty 'app.httpPort', httpPort
+ integrationTest.systemProperty 'app.httpsPort', httpsPort
+ integrationTest.systemProperty 'app.baseURI', baseUrl
+ integrationTest.systemProperty 'app.httpBaseURI', httpBaseUrl
+ integrationTest.systemProperty 'app.httpsBaseURI', httpsBaseUrl
+ }
+}
diff --git a/servlet/java-configuration/cas/login/gradle/wrapper/gradle-wrapper.jar b/servlet/java-configuration/cas/login/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..62d4c0535
Binary files /dev/null and b/servlet/java-configuration/cas/login/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/servlet/java-configuration/cas/login/gradle/wrapper/gradle-wrapper.properties b/servlet/java-configuration/cas/login/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..9256e2f38
--- /dev/null
+++ b/servlet/java-configuration/cas/login/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/servlet/java-configuration/cas/login/gradlew b/servlet/java-configuration/cas/login/gradlew
new file mode 100644
index 000000000..fbd7c5158
--- /dev/null
+++ b/servlet/java-configuration/cas/login/gradlew
@@ -0,0 +1,185 @@
+#!/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/java-configuration/cas/login/gradlew.bat b/servlet/java-configuration/cas/login/gradlew.bat
new file mode 100644
index 000000000..5093609d5
--- /dev/null
+++ b/servlet/java-configuration/cas/login/gradlew.bat
@@ -0,0 +1,104 @@
+@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/java-configuration/cas/login/settings.gradle b/servlet/java-configuration/cas/login/settings.gradle
new file mode 100644
index 000000000..e69de29bb
diff --git a/servlet/java-configuration/cas/login/src/integTest/java/example/CasJavaConfigurationITests.java b/servlet/java-configuration/cas/login/src/integTest/java/example/CasJavaConfigurationITests.java
new file mode 100644
index 000000000..6f0c83f4e
--- /dev/null
+++ b/servlet/java-configuration/cas/login/src/integTest/java/example/CasJavaConfigurationITests.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021 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.concurrent.TimeUnit;
+
+import com.gargoylesoftware.htmlunit.ElementNotFoundException;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.html.HtmlButton;
+import com.gargoylesoftware.htmlunit.html.HtmlElement;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlInput;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = ApplicationConfiguration.class)
+@WebAppConfiguration
+public class CasJavaConfigurationITests {
+
+ private MockMvc mvc;
+
+ private WebClient webClient;
+
+ @Autowired
+ WebApplicationContext webApplicationContext;
+
+ @Autowired
+ Environment environment;
+
+ @BeforeEach
+ void setup() {
+ this.mvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
+ .apply(SecurityMockMvcConfigurers.springSecurity()).build();
+ this.webClient = MockMvcWebClientBuilder.mockMvcSetup(this.mvc)
+ .withDelegate(new LocalHostWebClient(this.environment)).build();
+ this.webClient.getCookieManager().clearCookies();
+ }
+
+ @Test
+ void login() throws Exception {
+ performLogin();
+ HtmlPage home = (HtmlPage) this.webClient.getCurrentWindow().getEnclosedPage();
+ assertThat(home.asNormalizedText()).contains("You're email address is");
+ }
+
+ @Test
+ void loginAndLogout() throws Exception {
+ performLogin();
+ HtmlPage home = (HtmlPage) this.webClient.getCurrentWindow().getEnclosedPage();
+ HtmlElement rpLogoutButton = home.getHtmlElementById("rp_logout_button");
+ HtmlPage loginPage = rpLogoutButton.click();
+ this.webClient.waitForBackgroundJavaScript(10000);
+ assertThat(loginPage.asNormalizedText()).contains("You are successfully logged out of the app, but not CAS");
+ }
+
+ private void performLogin() throws Exception {
+ HtmlPage login = this.webClient.getPage("/");
+ this.webClient.waitForBackgroundJavaScript(10000);
+ HtmlForm form = findForm(login);
+ HtmlInput username = form.getInputByName("username");
+ HtmlPasswordInput password = form.getInputByName("password");
+ HtmlButton submit = login.getElementByName("submitBtn");
+ username.type("casuser");
+ password.type("Mellon");
+ submit.click();
+ this.webClient.waitForBackgroundJavaScript(10000);
+ }
+
+ private HtmlForm findForm(HtmlPage login) {
+ await().atMost(10, TimeUnit.SECONDS)
+ .until(() -> login.getForms().stream().map(HtmlForm::getId).anyMatch("fm1"::equals));
+ for (HtmlForm form : login.getForms()) {
+ try {
+ if (form.getId().equals("fm1")) {
+ return form;
+ }
+ }
+ catch (ElementNotFoundException ex) {
+ // Continue
+ }
+ }
+ throw new IllegalStateException("Could not resolve login form");
+ }
+
+}
diff --git a/servlet/java-configuration/cas/login/src/integTest/java/example/LocalHostWebClient.java b/servlet/java-configuration/cas/login/src/integTest/java/example/LocalHostWebClient.java
new file mode 100644
index 000000000..162ec392b
--- /dev/null
+++ b/servlet/java-configuration/cas/login/src/integTest/java/example/LocalHostWebClient.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 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.IOException;
+
+import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
+import com.gargoylesoftware.htmlunit.Page;
+import com.gargoylesoftware.htmlunit.WebClient;
+
+import org.springframework.core.env.Environment;
+import org.springframework.util.Assert;
+
+/**
+ * {@link WebClient} will automatically prefix relative URLs with
+ * localhost:${local.server.port}
.
+ *
+ * @author Phillip Webb
+ * @since 1.4.0
+ */
+public class LocalHostWebClient extends WebClient {
+
+ private final Environment environment;
+
+ public LocalHostWebClient(Environment environment) {
+ Assert.notNull(environment, "Environment must not be null");
+ this.environment = environment;
+ }
+
+ @Override
+ public
P getPage(String url) throws IOException, FailingHttpStatusCodeException {
+ if (url.startsWith("/")) {
+ String port = this.environment.getProperty("local.server.port", "8443");
+ url = "https://localhost:" + port + url;
+ }
+ return super.getPage(url);
+ }
+
+}
diff --git a/servlet/java-configuration/cas/login/src/main/java/example/ApplicationConfiguration.java b/servlet/java-configuration/cas/login/src/main/java/example/ApplicationConfiguration.java
new file mode 100644
index 000000000..9b0122799
--- /dev/null
+++ b/servlet/java-configuration/cas/login/src/main/java/example/ApplicationConfiguration.java
@@ -0,0 +1,26 @@
+/*
+ * 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.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan
+public class ApplicationConfiguration {
+
+}
diff --git a/servlet/java-configuration/cas/login/src/main/java/example/IndexController.java b/servlet/java-configuration/cas/login/src/main/java/example/IndexController.java
new file mode 100644
index 000000000..27fab7c89
--- /dev/null
+++ b/servlet/java-configuration/cas/login/src/main/java/example/IndexController.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2002-2021 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.apereo.cas.client.authentication.AttributePrincipal;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+
+/**
+ * Controller for "/".
+ *
+ * @author Rob WInch
+ */
+@Controller
+@PropertySource(value = "classpath:security.properties")
+public class IndexController {
+
+ @Value("${cas.base.url}")
+ private String casBaseUrl;
+
+ @GetMapping("/")
+ public String index(Model model, @AuthenticationPrincipal AttributePrincipal principal) {
+ if (principal != null) {
+ String emailAddress = (String) principal.getAttributes().get("email");
+ model.addAttribute("emailAddress", emailAddress);
+ model.addAttribute("userAttributes", principal.getAttributes());
+ }
+ return "index";
+ }
+
+ @GetMapping("/loggedout")
+ public String loggedout(Model model) {
+ model.addAttribute("casLogout", this.casBaseUrl + "/logout");
+ return "loggedout";
+ }
+
+}
diff --git a/servlet/java-configuration/cas/login/src/main/java/example/MvcWebApplicationInitializer.java b/servlet/java-configuration/cas/login/src/main/java/example/MvcWebApplicationInitializer.java
new file mode 100644
index 000000000..567940501
--- /dev/null
+++ b/servlet/java-configuration/cas/login/src/main/java/example/MvcWebApplicationInitializer.java
@@ -0,0 +1,46 @@
+/*
+ * 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 jakarta.servlet.Filter;
+
+import org.springframework.web.filter.HiddenHttpMethodFilter;
+import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
+
+public class MvcWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
+
+ @Override
+ protected Class>[] getRootConfigClasses() {
+ return null;
+ }
+
+ @Override
+ protected Class>[] getServletConfigClasses() {
+ return new Class[] { ApplicationConfiguration.class };
+ }
+
+ @Override
+ protected String[] getServletMappings() {
+ return new String[] { "/" };
+ }
+
+ @Override
+ protected Filter[] getServletFilters() {
+ return new Filter[] { new HiddenHttpMethodFilter() };
+ }
+
+}
diff --git a/servlet/java-configuration/cas/login/src/main/java/example/SecurityConfiguration.java b/servlet/java-configuration/cas/login/src/main/java/example/SecurityConfiguration.java
new file mode 100644
index 000000000..640cb81a1
--- /dev/null
+++ b/servlet/java-configuration/cas/login/src/main/java/example/SecurityConfiguration.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2002-2016 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.apereo.cas.client.session.SingleSignOutFilter;
+import org.apereo.cas.client.validation.Cas30ServiceTicketValidator;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.cas.ServiceProperties;
+import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
+import org.springframework.security.cas.authentication.CasAuthenticationProvider;
+import org.springframework.security.cas.userdetails.GrantedAuthorityFromAssertionAttributesUserDetailsService;
+import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
+import org.springframework.security.cas.web.CasAuthenticationFilter;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
+import org.springframework.security.web.context.DelegatingSecurityContextRepository;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
+import org.springframework.web.filter.CorsFilter;
+
+@Configuration
+@EnableWebSecurity
+@PropertySource(value = "classpath:/security.properties")
+public class SecurityConfiguration {
+
+
+ @Value("${cas.base.url}")
+ private String casBaseUrl;
+
+ @Value("${cas.login.url}")
+ private String casLoginUrl;
+
+ @Value("${service.base.url}")
+ private String serviceBaseUrl;
+
+ @Value("${service.target.uri:/login/cas}")
+ private String serviceTargetUri;
+
+ ServiceProperties serviceProperties() {
+ ServiceProperties serviceProperties = new ServiceProperties();
+ serviceProperties.setService(this.serviceBaseUrl + this.serviceTargetUri);
+ return serviceProperties;
+ }
+
+ Cas30ServiceTicketValidator casServiceTicketValidator() {
+ return new Cas30ServiceTicketValidator(this.casBaseUrl);
+ }
+
+ @Bean
+ AuthenticationUserDetailsService You are successfully logged in as You're email address is You are successfully logged out of the app, but not CAS Click here to re-login automatically via CAS SSO Click here to log out of all CAS server (and any applications configured for single-sign-out).CAS Login & Single Logout with Spring Security
+ All Your Attributes
+
+
+
+
+
+ Visit the Apereo CAS documentation for more details.
+ CAS Login & Single Logout with Spring Security
+ Visit the Apereo CAS documentation for more details.
+