diff --git a/.gitignore b/.gitignore index 9f11b75..632a353 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .idea/ +*.iml +target/ \ No newline at end of file diff --git a/ServiceA/pom.xml b/ServiceA/pom.xml index a79e56d..0988911 100644 --- a/ServiceA/pom.xml +++ b/ServiceA/pom.xml @@ -8,7 +8,7 @@ 2.6.3 - com.example + de.synyx.cl.oauth.example ServiceA 0.0.1-SNAPSHOT ServiceA diff --git a/ServiceB/pom.xml b/ServiceB/pom.xml index 096406c..05bd028 100644 --- a/ServiceB/pom.xml +++ b/ServiceB/pom.xml @@ -8,7 +8,7 @@ 2.6.3 - com.example + de.synyx.cl.oauth.example ServiceB 0.0.1-SNAPSHOT demo diff --git a/ServiceB/src/main/java/de/synyx/cl/oauth/examples/service/b/ServiceBController.java b/ServiceB/src/main/java/de/synyx/cl/oauth/examples/service/b/ServiceBController.java index a0f61d8..5612651 100644 --- a/ServiceB/src/main/java/de/synyx/cl/oauth/examples/service/b/ServiceBController.java +++ b/ServiceB/src/main/java/de/synyx/cl/oauth/examples/service/b/ServiceBController.java @@ -1,5 +1,7 @@ package de.synyx.cl.oauth.examples.service.b; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -12,8 +14,11 @@ @RestController public class ServiceBController { + private final Logger log = LoggerFactory.getLogger(ServiceBController.class); + @GetMapping("/api") - public String api() { + public String api(Principal principal) { + log.info(principal.toString()); return "{\"answer\": 42}"; } diff --git a/ServiceC/.gitignore b/ServiceC/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/ServiceC/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/ServiceC/.mvn/wrapper/MavenWrapperDownloader.java b/ServiceC/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..e76d1f3 --- /dev/null +++ b/ServiceC/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/ServiceC/.mvn/wrapper/maven-wrapper.jar b/ServiceC/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..2cc7d4a Binary files /dev/null and b/ServiceC/.mvn/wrapper/maven-wrapper.jar differ diff --git a/ServiceC/.mvn/wrapper/maven-wrapper.properties b/ServiceC/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..a9f1ef8 --- /dev/null +++ b/ServiceC/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/ServiceC/mvnw b/ServiceC/mvnw new file mode 100755 index 0000000..a16b543 --- /dev/null +++ b/ServiceC/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + 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 + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/ServiceC/mvnw.cmd b/ServiceC/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/ServiceC/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/ServiceC/pom.xml b/ServiceC/pom.xml new file mode 100644 index 0000000..45e5792 --- /dev/null +++ b/ServiceC/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.3 + + + de.synyx.cl.oauth.example + ServiceC + 0.0.1-SNAPSHOT + demo + Demo project for Spring Boot + + 11 + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + io.rest-assured + rest-assured + test + + + + org.springframework.security.oauth + spring-security-oauth2 + 2.5.1.RELEASE + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/ServiceCApplication.java b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/ServiceCApplication.java new file mode 100644 index 0000000..7e5cdb5 --- /dev/null +++ b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/ServiceCApplication.java @@ -0,0 +1,13 @@ +package de.synyx.cl.oauth.examples.service.c; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ServiceCApplication { + + public static void main(String[] args) { + SpringApplication.run(ServiceCApplication.class, args); + } + +} diff --git a/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/ServiceCController.java b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/ServiceCController.java new file mode 100644 index 0000000..0dfbf4c --- /dev/null +++ b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/ServiceCController.java @@ -0,0 +1,47 @@ +package de.synyx.cl.oauth.examples.service.c; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class ServiceCController { + + private final Logger log = LoggerFactory.getLogger(ServiceCController.class); + + // http://192.168.178.37:8083/api?userEmail=lange%2B2@synyx.de + @GetMapping("/api") + public Map api(String userEmail, Principal principal) { + log.info("principal {}", principal.toString()); + log.info("name {}", principal.getName()); + log.info("email {}", userEmail); + + + Map map = new HashMap<>(); + map.put("rightA", true); + map.put("rightB", false); + map.put("rightC", true); + return map; + } + + @GetMapping("/info") + public Map getUserInfo(Principal principal) { + + Map map = new HashMap<>(); + if (principal instanceof JwtAuthenticationToken) { + JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) principal; + map.put("clientId", jwtToken.getTokenAttributes().get("azp").toString()); + } + + map.put("name", principal.getName()); + + return Collections.unmodifiableMap(map); + } +} diff --git a/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/AudienceValidator.java b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/AudienceValidator.java new file mode 100644 index 0000000..60278a5 --- /dev/null +++ b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/AudienceValidator.java @@ -0,0 +1,29 @@ +package de.synyx.cl.oauth.examples.service.b.config; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; + +import java.util.List; + +public class AudienceValidator implements OAuth2TokenValidator { + + private final String audience; + + AudienceValidator(String audience) { + Assert.hasText(audience, "audience is null or empty"); + this.audience = audience; + } + + public OAuth2TokenValidatorResult validate(Jwt jwt) { + List audiences = jwt.getAudience(); + if (audiences.contains(this.audience)) { + return OAuth2TokenValidatorResult.success(); + } + OAuth2Error err = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN); + return OAuth2TokenValidatorResult.failure(err); + } +} diff --git a/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/KeycloakRealmRoleConverter.java b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/KeycloakRealmRoleConverter.java new file mode 100644 index 0000000..408e1b8 --- /dev/null +++ b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/KeycloakRealmRoleConverter.java @@ -0,0 +1,32 @@ +package de.synyx.cl.oauth.examples.service.b.config; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class KeycloakRealmRoleConverter implements Converter> { + + @Override + public Collection convert(Jwt jwt) { + final Map realmAccess = (Map) jwt.getClaims().get("realm_access"); + + System.out.println(realmAccess); + + for (String role : (List) realmAccess.get("roles")) { + System.out.println("role: " + role); + } + + + return ((List)realmAccess.get("roles")).stream() + .map(roleName -> "ROLE_" + roleName) // prefix to map to a Spring Security "role" + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + +} diff --git a/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/SecurityConfig.java b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/SecurityConfig.java new file mode 100644 index 0000000..a6a1f5b --- /dev/null +++ b/ServiceC/src/main/java/de/synyx/cl/oauth/examples/service/c/config/SecurityConfig.java @@ -0,0 +1,25 @@ +package de.synyx.cl.oauth.examples.service.b.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private String audience = "test"; +// private String issuer = "http://localhost:8080/auth/realms/example/protocol/openid-connect/certs"; + private String issuer = "http://localhost:8080/auth/realms/example"; + + String jwkSetUri = "https://localhost:8080/auth/realms/example/protocol/openid-connect/certs"; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated() + .and().oauth2ResourceServer().jwt(); + } +} diff --git a/ServiceC/src/main/resources/application.yml b/ServiceC/src/main/resources/application.yml new file mode 100644 index 0000000..b734a09 --- /dev/null +++ b/ServiceC/src/main/resources/application.yml @@ -0,0 +1,18 @@ + +server: + port: 8083 + +# https://www.baeldung.com/spring-security-oauth-jwt +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://localhost:8080/auth/realms/example + jwk-set-uri: http://localhost:8080/auth/realms/example/protocol/openid-connect/certs + +logging: + level: + org.springframework.security: DEBUG + + diff --git a/ServiceC/src/test/java/de/synyx/cl/oauth/examples/service/b/DemoApplicationTests.java b/ServiceC/src/test/java/de/synyx/cl/oauth/examples/service/b/DemoApplicationTests.java new file mode 100644 index 0000000..c6515b7 --- /dev/null +++ b/ServiceC/src/test/java/de/synyx/cl/oauth/examples/service/b/DemoApplicationTests.java @@ -0,0 +1,13 @@ +package de.synyx.cl.oauth.examples.service.b; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/ServiceC/src/test/java/de/synyx/cl/oauth/examples/service/b/ResourceServerLiveTest.java b/ServiceC/src/test/java/de/synyx/cl/oauth/examples/service/b/ResourceServerLiveTest.java new file mode 100644 index 0000000..313656a --- /dev/null +++ b/ServiceC/src/test/java/de/synyx/cl/oauth/examples/service/b/ResourceServerLiveTest.java @@ -0,0 +1,123 @@ +package de.synyx.cl.oauth.examples.service.b; + +import io.restassured.RestAssured; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ResourceServerLiveTest { + + public static final String baseUrl = "http://localhost:8082"; + private final String redirectUrl = "http://localhost:8084/"; + private final String authorizeUrlPattern = "http://localhost:8080/auth/realms/example/protocol/openid-connect/auth?response_type=code&client_id=service_a&scope=%s&redirect_uri=" + redirectUrl; + private final String tokenUrl = "http://localhost:8080/auth/realms/example/protocol/openid-connect/token"; + + + public OAuth2ProtectedResourceDetails details() { + AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails(); + details.setId("client_id"); + details.setClientId("service_a"); + details.setClientSecret("7f7367d1-f394-4a98-af5b-6c11886ff26f"); + details.setAccessTokenUri(tokenUrl); + details.setUserAuthorizationUri(tokenUrl); + return details; + } + + @Test + void testOAuthRestTemplate() { + ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails(); + resource.setAccessTokenUri(tokenUrl); + resource.setId("service_a"); + resource.setClientId("service_a"); + resource.setClientSecret("7f7367d1-f394-4a98-af5b-6c11886ff26f"); + + resource.setGrantType("client_credentials"); + + resource.setScope(Arrays.asList("openid")); + + OAuth2RestTemplate template = new OAuth2RestTemplate(resource); + System.out.println(" CALLING: " + baseUrl+"/api"); + + String result = template.getForObject(baseUrl+"/api", String.class); + + System.err.println(result); + assertEquals("Hello, Trusted User marissa", result); + } + + @Test + void testRestAssured() { + + Response response = RestAssured.given() + .redirects() + .follow(false) + .formParams("client_id", "service_a", "client_secret", "7f7367d1-f394-4a98-af5b-6c11886ff26f", "grant_type", "client_credentials") + .post("http://localhost:8080/auth/realms/example/protocol/openid-connect/token"); + + assertThat(HttpStatus.OK.value()).isEqualTo(response.getStatusCode()); + + String[] parts = response.getBody().asString().split(":"); + String accessToken = parts[1].split("\"")[1]; + System.out.println("parts:" + accessToken); + + Response api = RestAssured.given() + .redirects().follow(false) + .header(HttpHeaders.ACCEPT, "application/json") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .get(baseUrl + "/api"); + + api.then() + .statusCode(200) + .body("answer", equalTo(42)); + } + + + + String obtainAccesToken(String scopes) { + Response response = RestAssured.given() + .redirects() + .follow(false) + .get(String.format(authorizeUrlPattern, scopes)); + String authSessionId = response.getCookie("AUTH_SESSION_ID"); + String kcPostAuthenticationUrl = response.asString() + .split("action=\"")[1].split("\"")[0].replace("&", "&"); + + // obtain authentication code and state + response = RestAssured.given() + .redirects() + .follow(false) + .cookie("AUTH_SESSION_ID", authSessionId) + .formParams("client_id", "service_a", "client_secret", "7f7367d1-f394-4a98-af5b-6c11886ff26f", "grant_type", "client_credentials") + .post(kcPostAuthenticationUrl); + assertThat(HttpStatus.FOUND.value()).isEqualTo(response.getStatusCode()); + + // extract authorization code + String location = response.getHeader(HttpHeaders.LOCATION); + String code = location.split("code=")[1].split("&")[0]; + + // get access token + Map params = new HashMap(); + params.put("grant_type", "authorization_code"); + params.put("code", code); + params.put("client_id", "jwtClient"); + params.put("redirect_uri", redirectUrl); + params.put("client_secret", "jwtClientSecret"); + response = RestAssured.given() + .formParams(params) + .post(tokenUrl); + return response.jsonPath() + .getString("access_token"); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 9eb35e1..8a0464c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,49 @@ services: keycloak: - image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} + image: jboss/keycloak:16.1.0 + container_name: keycloak environment: - KEYCLOAK_PASSWORD: admin123 + KEYCLOAK_PASSWORD: admin KEYCLOAK_USER: admin + DB_VENDOR: h2 KEYCLOAK_LOGLEVEL: INFO ROOT_LOGLEVEL: INFO + DEBUG: 'true' + DEBUG_PORT: '*:8787' ports: - "8080:8080" - "8787:8787" command: - "-c" - "standalone.xml" - - "-Dkeycloak.profile.feature.upload_scripts=enabled" \ No newline at end of file + - "-Dkeycloak.profile.feature.upload_scripts=enabled" + healthcheck: + test: curl -f http://localhost:9990 || exit 1 + interval: 5s + timeout: 5s + retries: 3 + start_period: 30s + + keycloak-cfg: + image: adorsys/keycloak-config-cli:v4.6.1-16.1.0 + container_name: keycloak-cfg + volumes: + - ./keycloak_config:/config + environment: + KEYCLOAK_URL: http://keycloak:8080/auth + KEYCLOAK_SSL-VERIFY: "true" + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: admin + IMPORT_PATH: /config/example-realm.yml + IMPORT_FORCE: "false" + depends_on: + keycloak: + condition: service_healthy + + mailhog: + image: mailhog/mailhog + container_name: mailhog + ports: + - "1025:1025" + - "8025:8025" \ No newline at end of file diff --git a/keycloak-service-c-protocol-mapper/pom.xml b/keycloak-service-c-protocol-mapper/pom.xml new file mode 100755 index 0000000..5b58592 --- /dev/null +++ b/keycloak-service-c-protocol-mapper/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + Keycloak service-c protocol mapper + + de.synyx.cl.oauth.example + keycloak-service-c-protocol-mapper + 0.0.1-SNAPSHOT + jar + + + Provides an SPI Implementation to provide additional + claims for the session. + + + + 11 + 16.1.0 + 5.8.1 + 3.6.28 + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + + org.keycloak + keycloak-core + ${keycloak-version} + provided + + + org.keycloak + keycloak-server-spi + ${keycloak-version} + provided + + + org.keycloak + keycloak-server-spi-private + ${keycloak-version} + provided + + + org.keycloak + keycloak-services + ${keycloak-version} + provided + + + org.junit.jupiter + junit-jupiter-api + ${junit-platform.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-platform.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + org.hamcrest + hamcrest-library + 2.2 + test + + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + + diff --git a/keycloak-service-c-protocol-mapper/readme.md b/keycloak-service-c-protocol-mapper/readme.md new file mode 100644 index 0000000..2fb2f08 --- /dev/null +++ b/keycloak-service-c-protocol-mapper/readme.md @@ -0,0 +1,9 @@ + + +```shell +mvn clean verify + +docker cp target/keycloak-service-c-protocol-mapper.jar keycloak:/opt/jboss/keycloak/standalone/deployments/ + +curl -s http://localhost:8080/auth/realms/example/protocol/openid-connect/token -d grant_type=client_credentials -d client_id=ProtocolMapperTestClient -d client_secret=hWOVZkbeBx8Vl1oFp80UIBcdaEXsRGbA +``` \ No newline at end of file diff --git a/keycloak-service-c-protocol-mapper/src/main/java/de/synyx/cl/oauth/example/keycloak/ExternalAttributeMapper.java b/keycloak-service-c-protocol-mapper/src/main/java/de/synyx/cl/oauth/example/keycloak/ExternalAttributeMapper.java new file mode 100644 index 0000000..e24d17d --- /dev/null +++ b/keycloak-service-c-protocol-mapper/src/main/java/de/synyx/cl/oauth/example/keycloak/ExternalAttributeMapper.java @@ -0,0 +1,179 @@ +package de.synyx.cl.oauth.example.keycloak; + +import org.keycloak.crypto.AsymmetricSignatureSignerContext; +import org.keycloak.crypto.KeyUse; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.models.*; +import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; +import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; +import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; +import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper; +import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; +import org.keycloak.services.Urls; + +import javax.json.*; +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * inspired by https://github.com/mschwartau/keycloak-custom-protocol-mapper-example + */ +public class ExternalAttributeMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { + + /* + * A config which keycloak uses to display a generic dialog to configure the token. + */ + private static final List configProperties = new ArrayList<>(); + + private static final HttpClient httpClient = HttpClient.newHttpClient(); + + /* + * The ID of the token mapper. Is public, because we need this id in our data-setup project to + * configure the protocol mapper in keycloak. + */ + public static final String PROVIDER_ID = "oidc-service-c-protocol-mapper"; + + private static final String URL_NAME = PROVIDER_ID + ".url"; + + static { + // The builtin protocol mapper let the user define under which claim name (key) + // the protocol mapper writes its value. To display this option in the generic dialog + // in keycloak, execute the following method. + OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties); + + ProviderConfigProperty propertyUrl = new ProviderConfigProperty(); + propertyUrl.setName(URL_NAME); + propertyUrl.setLabel("URL"); + propertyUrl.setType(ProviderConfigProperty.STRING_TYPE); + propertyUrl.setHelpText("URL of the service to retrive information from"); + configProperties.add(propertyUrl); + + // The builtin protocol mapper let the user define for which tokens the protocol mapper + // is executed (access token, id token, user info). To add the config options for the different types + // to the dialog execute the following method. Note that the following method uses the interfaces + // this token mapper implements to decide which options to add to the config. So if this token + // mapper should never be available for some sort of options, e.g. like the id token, just don't + // implement the corresponding interface. + OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, ExternalAttributeMapper.class); + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "Service C Attribute Mapper"; + } + + @Override + public String getHelpText() { + return "Adds information from service c to claim"; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { + // adds our data to the token. Uses the parameters like the claim name which were set by the user + // when this protocol mapper was configured in keycloak. Note that the parameters which can + // be configured in keycloak for this protocol mapper were set in the static initializer of this class. + // + // Sets a static "Hello world" string, but we could write a dynamic value like a group attribute here too. + String accessToken = getAccessToken(clientSessionCtx.getClientSession().getClient().getId(), keycloakSession); + + + System.out.println("***************************************"); + System.out.println("1:"); + System.out.println(accessToken); + System.out.println("***************************************"); + + KeyWrapper key = keycloakSession.keys().getActiveKey(keycloakSession.getContext().getRealm(), KeyUse.SIG, "RS256"); + String bearerToken = new JWSBuilder().kid(key.getKid()).type("JWT").jsonContent(token).sign(new AsymmetricSignatureSignerContext(key)); + System.out.println("2:"); + System.out.println("loginUserName: " + userSession.getLoginUsername()); + System.out.println("authMethod: " + userSession.getAuthMethod()); + System.out.println("userId: " + userSession.getUser().getId()); + System.out.println("userEmail: " + userSession.getUser().getEmail()); + System.out.println(bearerToken); + System.out.println("***************************************"); + + String url = mappingModel.getConfig().get(URL_NAME); + + JsonObject attributes = getExternalAttribute(url, bearerToken); + final Map list = convert(attributes); + + OIDCAttributeMapperHelper.mapClaim(token, mappingModel, list); + } + + private JsonObject getExternalAttribute(String url, String token) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .setHeader("User-Agent", ExternalAttributeMapper.class.getSimpleName()) + .setHeader("Authorization", "Bearer " + token) + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new RuntimeException("request not successful: " + response.statusCode()); + } + String body = response.body(); + + System.out.println("response body: " + body); + JsonReader reader = Json.createReader(new StringReader(body)); + return reader.readObject(); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + return Json.createReader(new StringReader("{\"error\": \"unknown error\"}")).readObject(); + } + } + + // https://stackoverflow.com/questions/63742225/how-to-obtain-an-access-token-within-a-keycloak-spi + public String getAccessToken(String userId, KeycloakSession keycloakSession) { + KeycloakContext keycloakContext = keycloakSession.getContext(); + + AccessToken token = new AccessToken(); + token.subject(userId); + token.issuer(Urls.realmIssuer(keycloakContext.getUri().getBaseUri(), keycloakContext.getRealm().getName())); + token.issuedNow(); + token.type("Bearer"); + token.expiration((int) (token.getIat() + 60L)); //Lifetime of 60 seconds + + KeyWrapper key = keycloakSession.keys().getActiveKey(keycloakContext.getRealm(), KeyUse.SIG, "RS256"); + + return new JWSBuilder().kid(key.getKid()).type("JWT").jsonContent(token).sign(new AsymmetricSignatureSignerContext(key)); + } + + public static Map convert(JsonObject jsonObject) { + Map result = new HashMap<>(); + for(String key: jsonObject.keySet()) { + final JsonValue jsonValue = jsonObject.get(key); + result.put(key, jsonValue.toString().equals("true") ? true : false); + }; + + return result; + } +} diff --git a/keycloak-service-c-protocol-mapper/src/main/resources/META-INF/jboss-deployment-structure.xml b/keycloak-service-c-protocol-mapper/src/main/resources/META-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000..7fdc6c4 --- /dev/null +++ b/keycloak-service-c-protocol-mapper/src/main/resources/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/keycloak-service-c-protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/keycloak-service-c-protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper new file mode 100644 index 0000000..1fd6192 --- /dev/null +++ b/keycloak-service-c-protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -0,0 +1,4 @@ +# the name of this file should not be changed, this is how the Service provider API works. +# +# List here all protocol mappers which should be loaded by keycloak. +de.synyx.cl.oauth.example.keycloak.ExternalAttributeMapper diff --git a/keycloak-service-c-protocol-mapper/src/test/java/de/synyx/cl/oauth/example/keycloak/ExternalAttributeMapperTest.java b/keycloak-service-c-protocol-mapper/src/test/java/de/synyx/cl/oauth/example/keycloak/ExternalAttributeMapperTest.java new file mode 100644 index 0000000..8e1dbf7 --- /dev/null +++ b/keycloak-service-c-protocol-mapper/src/test/java/de/synyx/cl/oauth/example/keycloak/ExternalAttributeMapperTest.java @@ -0,0 +1,33 @@ +package de.synyx.cl.oauth.example.keycloak; + +import org.junit.jupiter.api.Test; + +import javax.json.*; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; + +import static de.synyx.cl.oauth.example.keycloak.ExternalAttributeMapper.convert; +import static org.assertj.core.api.BDDAssertions.then; + +class ExternalAttributeMapperTest { + + @Test + void convertJsonStructure() { + String body = "{ " + + "\"rightA\" : true," + + "\"rightB\" : false " + + "}"; + JsonReader reader = Json.createReader(new StringReader(body)); + + final JsonObject jsonObject = reader.readObject(); + + Map result = convert(jsonObject); + + Map rights = new HashMap<>(); + rights.put("rightA", true); + rights.put("rightB", false); + + then(result).isEqualTo(rights); + } +} \ No newline at end of file diff --git a/keycloak_config/example-realm.yml b/keycloak_config/example-realm.yml index bc15e1a..1117cef 100644 --- a/keycloak_config/example-realm.yml +++ b/keycloak_config/example-realm.yml @@ -24,6 +24,43 @@ clients: webOrigins: - "*" serviceAccountsEnabled: true + protocolMappers: + - id: 0a623cae-8396-11ec-b97f-38f3ab2c9eaf + name: test protocol mapper + protocol: openid-connect + protocolMapper: oidc-service-c-protocol-mapper + config: + multivalued: 'false' + claim.name: test_claim + access.token.claim: 'true' + id.token.claim: 'true' + userinfo.token.claim: 'false' + jsonType.label: JSON + oidc-service-c-protocol-mapper.url: http://192.168.178.37:8083/api + + - clientId: ProtocolMapperTestClient + name: ProtocolMapperTestClient + clientAuthenticatorType: client-secret + secret: hWOVZkbeBx8Vl1oFp80UIBcdaEXsRGbA + redirectUris: + - "*" + webOrigins: + - "*" + serviceAccountsEnabled: true + protocolMappers: + - id: 1184dfc8-8396-11ec-a55f-38f3ab2c9eaf + name: test protocol mapper + protocol: openid-connect + protocolMapper: oidc-service-c-protocol-mapper + config: + multivalued: 'false' + claim.name: test_claim + access.token.claim: 'true' + id.token.claim: 'true' + userinfo.token.claim: 'false' + jsonType.label: JSON + oidc-service-c-protocol-mapper.url: http://192.168.178.37:8083/api + clientScopes: - name: roles protocolMappers: @@ -51,6 +88,7 @@ clientScopes: jsonType.label: String users: - username: test + id: f12d05a8-f8ba-420b-9c3e-320a2facda64 email: test@test.de enabled: true firstName: first test diff --git a/overview.png b/overview.png index 6f22640..a45f041 100644 Binary files a/overview.png and b/overview.png differ diff --git a/readme.adoc b/readme.adoc index 31bf65d..fa48453 100644 --- a/readme.adoc +++ b/readme.adoc @@ -8,6 +8,9 @@ Ports: * keycloak - 8080 * ServiceA - 8081 * ServiceB - 8082 +* ServiceC - 8083 + +// https://app.conceptboard.com/board/fasi-2ncp-d6e1-fuhk-zpge image::overview.png[overview]