Prechádzať zdrojové kódy

Add "How-to: Implement core services with JPA"

Closes gh-545
Steve Riesenberg 3 rokov pred
rodič
commit
718b86c983
29 zmenil súbory, kde vykonal 2460 pridanie a 3 odobranie
  1. 1 1
      docs/local-antora-playbook.yml
  2. 30 0
      docs/modules/guides/examples/docs-modules-guides-examples.gradle
  3. BIN
      docs/modules/guides/examples/gradle/wrapper/gradle-wrapper.jar
  4. 5 0
      docs/modules/guides/examples/gradle/wrapper/gradle-wrapper.properties
  5. 185 0
      docs/modules/guides/examples/gradlew
  6. 89 0
      docs/modules/guides/examples/gradlew.bat
  7. 275 0
      docs/modules/guides/examples/src/main/java/sample/jpa/Authorization.java
  8. 107 0
      docs/modules/guides/examples/src/main/java/sample/jpa/AuthorizationConsent.java
  9. 32 0
      docs/modules/guides/examples/src/main/java/sample/jpa/AuthorizationConsentRepository.java
  10. 42 0
      docs/modules/guides/examples/src/main/java/sample/jpa/AuthorizationRepository.java
  11. 149 0
      docs/modules/guides/examples/src/main/java/sample/jpa/Client.java
  12. 31 0
      docs/modules/guides/examples/src/main/java/sample/jpa/ClientRepository.java
  13. 102 0
      docs/modules/guides/examples/src/main/java/sample/jpa/JpaOAuth2AuthorizationConsentService.java
  14. 263 0
      docs/modules/guides/examples/src/main/java/sample/jpa/JpaOAuth2AuthorizationService.java
  15. 175 0
      docs/modules/guides/examples/src/main/java/sample/jpa/JpaRegisteredClientRepository.java
  16. 6 0
      docs/modules/guides/examples/src/main/resources/application.yml
  17. 6 0
      docs/modules/guides/examples/src/main/resources/oauth2-authorization-consent-schema.sql
  18. 28 0
      docs/modules/guides/examples/src/main/resources/oauth2-authorization-schema.sql
  19. 15 0
      docs/modules/guides/examples/src/main/resources/oauth2-registered-client-schema.sql
  20. 112 0
      docs/modules/guides/examples/src/test/java/sample/jose/TestJwks.java
  21. 154 0
      docs/modules/guides/examples/src/test/java/sample/jose/TestKeys.java
  22. 240 0
      docs/modules/guides/examples/src/test/java/sample/jpa/JpaTests.java
  23. 162 0
      docs/modules/guides/examples/src/test/java/sample/test/SpringTestContext.java
  24. 58 0
      docs/modules/guides/examples/src/test/java/sample/test/SpringTestContextExtension.java
  25. 47 0
      docs/modules/guides/examples/src/test/java/sample/util/RegisteredClients.java
  26. 1 1
      docs/modules/guides/nav.adoc
  27. 144 0
      docs/modules/guides/pages/how-to-jpa.adoc
  28. 0 1
      docs/modules/guides/pages/page-1.adoc
  29. 1 0
      etc/nohttp/allowlist.lines

+ 1 - 1
docs/local-antora-playbook.yml

@@ -8,7 +8,7 @@ asciidoc:
 content:
   sources:
     - url: ../
-      branches: [guides]
+      branches: [main]
       start_path: docs
 ui:
   bundle:

+ 30 - 0
docs/modules/guides/examples/docs-modules-guides-examples.gradle

@@ -0,0 +1,30 @@
+plugins {
+    id 'java'
+}
+
+group = 'org.springframework.security'
+version = '0.0.1-SNAPSHOT'
+sourceCompatibility = '1.8'
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    implementation platform('org.springframework.boot:spring-boot-dependencies:2.6.2')
+    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
+    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-oauth2-authorization-server:0.2.1'
+    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
+    runtimeOnly 'com.h2database:h2'
+    testImplementation 'org.springframework.boot:spring-boot-starter-test'
+    testImplementation 'org.springframework.security:spring-security-test'
+}
+
+test {
+    useJUnitPlatform()
+}

BIN
docs/modules/guides/examples/gradle/wrapper/gradle-wrapper.jar


+ 5 - 0
docs/modules/guides/examples/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
docs/modules/guides/examples/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
+    ;;
+  MSYS* | 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" "$@"

+ 89 - 0
docs/modules/guides/examples/gradlew.bat

@@ -0,0 +1,89 @@
+@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 execute
+
+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 execute
+
+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
+
+: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 %*
+
+: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

+ 275 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/Authorization.java

@@ -0,0 +1,275 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.time.Instant;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Entity
+public class Authorization {
+	@Id
+	private String id;
+	private String registeredClientId;
+	private String principalName;
+	private String authorizationGrantType;
+	@Column(length = 4000)
+	private String attributes;
+	@Column(length = 500)
+	private String state;
+
+	@Column(length = 4000)
+	private String authorizationCodeValue;
+	private Instant authorizationCodeIssuedAt;
+	private Instant authorizationCodeExpiresAt;
+	private String authorizationCodeMetadata;
+
+	@Column(length = 4000)
+	private String accessTokenValue;
+	private Instant accessTokenIssuedAt;
+	private Instant accessTokenExpiresAt;
+	@Column(length = 2000)
+	private String accessTokenMetadata;
+	private String accessTokenType;
+	@Column(length = 1000)
+	private String accessTokenScopes;
+
+	@Column(length = 4000)
+	private String refreshTokenValue;
+	private Instant refreshTokenIssuedAt;
+	private Instant refreshTokenExpiresAt;
+	@Column(length = 2000)
+	private String refreshTokenMetadata;
+
+	@Column(length = 4000)
+	private String oidcIdTokenValue;
+	private Instant oidcIdTokenIssuedAt;
+	private Instant oidcIdTokenExpiresAt;
+	@Column(length = 2000)
+	private String oidcIdTokenMetadata;
+	@Column(length = 2000)
+	private String oidcIdTokenClaims;
+
+	// getters and setters
+// end::class[]
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getRegisteredClientId() {
+		return registeredClientId;
+	}
+
+	public void setRegisteredClientId(String registeredClientId) {
+		this.registeredClientId = registeredClientId;
+	}
+
+	public String getPrincipalName() {
+		return principalName;
+	}
+
+	public void setPrincipalName(String principalName) {
+		this.principalName = principalName;
+	}
+
+	public String getAuthorizationGrantType() {
+		return authorizationGrantType;
+	}
+
+	public void setAuthorizationGrantType(String authorizationGrantType) {
+		this.authorizationGrantType = authorizationGrantType;
+	}
+
+	public String getAttributes() {
+		return attributes;
+	}
+
+	public void setAttributes(String attributes) {
+		this.attributes = attributes;
+	}
+
+	public String getState() {
+		return state;
+	}
+
+	public void setState(String state) {
+		this.state = state;
+	}
+
+	public String getAuthorizationCodeValue() {
+		return authorizationCodeValue;
+	}
+
+	public void setAuthorizationCodeValue(String authorizationCode) {
+		this.authorizationCodeValue = authorizationCode;
+	}
+
+	public Instant getAuthorizationCodeIssuedAt() {
+		return authorizationCodeIssuedAt;
+	}
+
+	public void setAuthorizationCodeIssuedAt(Instant authorizationCodeIssuedAt) {
+		this.authorizationCodeIssuedAt = authorizationCodeIssuedAt;
+	}
+
+	public Instant getAuthorizationCodeExpiresAt() {
+		return authorizationCodeExpiresAt;
+	}
+
+	public void setAuthorizationCodeExpiresAt(Instant authorizationCodeExpiresAt) {
+		this.authorizationCodeExpiresAt = authorizationCodeExpiresAt;
+	}
+
+	public String getAuthorizationCodeMetadata() {
+		return authorizationCodeMetadata;
+	}
+
+	public void setAuthorizationCodeMetadata(String authorizationCodeMetadata) {
+		this.authorizationCodeMetadata = authorizationCodeMetadata;
+	}
+
+	public String getAccessTokenValue() {
+		return accessTokenValue;
+	}
+
+	public void setAccessTokenValue(String accessToken) {
+		this.accessTokenValue = accessToken;
+	}
+
+	public Instant getAccessTokenIssuedAt() {
+		return accessTokenIssuedAt;
+	}
+
+	public void setAccessTokenIssuedAt(Instant accessTokenIssuedAt) {
+		this.accessTokenIssuedAt = accessTokenIssuedAt;
+	}
+
+	public Instant getAccessTokenExpiresAt() {
+		return accessTokenExpiresAt;
+	}
+
+	public void setAccessTokenExpiresAt(Instant accessTokenExpiresAt) {
+		this.accessTokenExpiresAt = accessTokenExpiresAt;
+	}
+
+	public String getAccessTokenMetadata() {
+		return accessTokenMetadata;
+	}
+
+	public void setAccessTokenMetadata(String accessTokenMetadata) {
+		this.accessTokenMetadata = accessTokenMetadata;
+	}
+
+	public String getAccessTokenType() {
+		return accessTokenType;
+	}
+
+	public void setAccessTokenType(String accessTokenType) {
+		this.accessTokenType = accessTokenType;
+	}
+
+	public String getAccessTokenScopes() {
+		return accessTokenScopes;
+	}
+
+	public void setAccessTokenScopes(String accessTokenScopes) {
+		this.accessTokenScopes = accessTokenScopes;
+	}
+
+	public String getRefreshTokenValue() {
+		return refreshTokenValue;
+	}
+
+	public void setRefreshTokenValue(String refreshToken) {
+		this.refreshTokenValue = refreshToken;
+	}
+
+	public Instant getRefreshTokenIssuedAt() {
+		return refreshTokenIssuedAt;
+	}
+
+	public void setRefreshTokenIssuedAt(Instant refreshTokenIssuedAt) {
+		this.refreshTokenIssuedAt = refreshTokenIssuedAt;
+	}
+
+	public Instant getRefreshTokenExpiresAt() {
+		return refreshTokenExpiresAt;
+	}
+
+	public void setRefreshTokenExpiresAt(Instant refreshTokenExpiresAt) {
+		this.refreshTokenExpiresAt = refreshTokenExpiresAt;
+	}
+
+	public String getRefreshTokenMetadata() {
+		return refreshTokenMetadata;
+	}
+
+	public void setRefreshTokenMetadata(String refreshTokenMetadata) {
+		this.refreshTokenMetadata = refreshTokenMetadata;
+	}
+
+	public String getOidcIdTokenValue() {
+		return oidcIdTokenValue;
+	}
+
+	public void setOidcIdTokenValue(String idToken) {
+		this.oidcIdTokenValue = idToken;
+	}
+
+	public Instant getOidcIdTokenIssuedAt() {
+		return oidcIdTokenIssuedAt;
+	}
+
+	public void setOidcIdTokenIssuedAt(Instant idTokenIssuedAt) {
+		this.oidcIdTokenIssuedAt = idTokenIssuedAt;
+	}
+
+	public Instant getOidcIdTokenExpiresAt() {
+		return oidcIdTokenExpiresAt;
+	}
+
+	public void setOidcIdTokenExpiresAt(Instant idTokenExpiresAt) {
+		this.oidcIdTokenExpiresAt = idTokenExpiresAt;
+	}
+
+	public String getOidcIdTokenMetadata() {
+		return oidcIdTokenMetadata;
+	}
+
+	public void setOidcIdTokenMetadata(String idTokenMetadata) {
+		this.oidcIdTokenMetadata = idTokenMetadata;
+	}
+
+	public String getOidcIdTokenClaims() {
+		return oidcIdTokenClaims;
+	}
+
+	public void setOidcIdTokenClaims(String idTokenClaims) {
+		this.oidcIdTokenClaims = idTokenClaims;
+	}
+// tag::class[]
+}
+// end::class[]

+ 107 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/AuthorizationConsent.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Entity
+@IdClass(AuthorizationConsent.AuthorizationConsentId.class)
+public class AuthorizationConsent {
+	@Id
+	private String registeredClientId;
+	@Id
+	private String principalName;
+	@Column(length = 1000)
+	private String authorities;
+
+	// getters and setters
+// end::class[]
+	public String getRegisteredClientId() {
+		return registeredClientId;
+	}
+
+	public void setRegisteredClientId(String registeredClientId) {
+		this.registeredClientId = registeredClientId;
+	}
+
+	public String getPrincipalName() {
+		return principalName;
+	}
+
+	public void setPrincipalName(String principalName) {
+		this.principalName = principalName;
+	}
+
+	public String getAuthorities() {
+		return authorities;
+	}
+
+	public void setAuthorities(String authorities) {
+		this.authorities = authorities;
+	}
+// tag::class[]
+
+	public static class AuthorizationConsentId implements Serializable {
+		private String registeredClientId;
+		private String principalName;
+
+		// getters and setters
+// end::class[]
+		public String getRegisteredClientId() {
+			return registeredClientId;
+		}
+
+		public void setRegisteredClientId(String registeredClientId) {
+			this.registeredClientId = registeredClientId;
+		}
+
+		public String getPrincipalName() {
+			return principalName;
+		}
+
+		public void setPrincipalName(String principalName) {
+			this.principalName = principalName;
+		}
+// tag::class[]
+
+		// equals and hashCode
+// end::class[]
+		@Override
+		public boolean equals(Object o) {
+			if (this == o) return true;
+			if (o == null || getClass() != o.getClass()) return false;
+			AuthorizationConsentId that = (AuthorizationConsentId) o;
+			return registeredClientId.equals(that.registeredClientId) && principalName.equals(that.principalName);
+		}
+
+		@Override
+		public int hashCode() {
+			return Objects.hash(registeredClientId, principalName);
+		}
+// tag::class[]
+	}
+}
+// end::class[]

+ 32 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/AuthorizationConsentRepository.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Repository
+public interface AuthorizationConsentRepository extends JpaRepository<AuthorizationConsent, AuthorizationConsent.AuthorizationConsentId> {
+	Optional<AuthorizationConsent> findByRegisteredClientIdAndPrincipalName(String registeredClientId, String principalName);
+	void deleteByRegisteredClientIdAndPrincipalName(String registeredClientId, String principalName);
+}
+// end::class[]

+ 42 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/AuthorizationRepository.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Repository
+public interface AuthorizationRepository extends JpaRepository<Authorization, String> {
+	Optional<Authorization> findByState(String state);
+	Optional<Authorization> findByAuthorizationCodeValue(String authorizationCode);
+	Optional<Authorization> findByAccessTokenValue(String accessToken);
+	Optional<Authorization> findByRefreshTokenValue(String refreshToken);
+	@Query("select a from Authorization a where a.state = :token" +
+			" or a.authorizationCodeValue = :token" +
+			" or a.accessTokenValue = :token" +
+			" or a.refreshTokenValue = :token"
+	)
+	Optional<Authorization> findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValue(@Param("token") String token);
+}
+// end::class[]

+ 149 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/Client.java

@@ -0,0 +1,149 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.time.Instant;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Entity
+public class Client {
+	@Id
+	private String id;
+	private String clientId;
+	private Instant clientIdIssuedAt;
+	private String clientSecret;
+	private Instant clientSecretExpiresAt;
+	private String clientName;
+	@Column(length = 1000)
+	private String clientAuthenticationMethods;
+	@Column(length = 1000)
+	private String authorizationGrantTypes;
+	@Column(length = 1000)
+	private String redirectUris;
+	@Column(length = 1000)
+	private String scopes;
+	@Column(length = 2000)
+	private String clientSettings;
+	@Column(length = 2000)
+	private String tokenSettings;
+
+	// getters and setters
+// end::class[]
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getClientId() {
+		return clientId;
+	}
+
+	public void setClientId(String clientId) {
+		this.clientId = clientId;
+	}
+
+	public Instant getClientIdIssuedAt() {
+		return clientIdIssuedAt;
+	}
+
+	public void setClientIdIssuedAt(Instant clientIdIssuedAt) {
+		this.clientIdIssuedAt = clientIdIssuedAt;
+	}
+
+	public String getClientSecret() {
+		return clientSecret;
+	}
+
+	public void setClientSecret(String clientSecret) {
+		this.clientSecret = clientSecret;
+	}
+
+	public Instant getClientSecretExpiresAt() {
+		return clientSecretExpiresAt;
+	}
+
+	public void setClientSecretExpiresAt(Instant clientSecretExpiresAt) {
+		this.clientSecretExpiresAt = clientSecretExpiresAt;
+	}
+
+	public String getClientName() {
+		return clientName;
+	}
+
+	public void setClientName(String clientName) {
+		this.clientName = clientName;
+	}
+
+	public String getClientAuthenticationMethods() {
+		return clientAuthenticationMethods;
+	}
+
+	public void setClientAuthenticationMethods(String clientAuthenticationMethods) {
+		this.clientAuthenticationMethods = clientAuthenticationMethods;
+	}
+
+	public String getAuthorizationGrantTypes() {
+		return authorizationGrantTypes;
+	}
+
+	public void setAuthorizationGrantTypes(String authorizationGrantTypes) {
+		this.authorizationGrantTypes = authorizationGrantTypes;
+	}
+
+	public String getRedirectUris() {
+		return redirectUris;
+	}
+
+	public void setRedirectUris(String redirectUris) {
+		this.redirectUris = redirectUris;
+	}
+
+	public String getScopes() {
+		return scopes;
+	}
+
+	public void setScopes(String scopes) {
+		this.scopes = scopes;
+	}
+
+	public String getClientSettings() {
+		return clientSettings;
+	}
+
+	public void setClientSettings(String clientSettings) {
+		this.clientSettings = clientSettings;
+	}
+
+	public String getTokenSettings() {
+		return tokenSettings;
+	}
+
+	public void setTokenSettings(String tokenSettings) {
+		this.tokenSettings = tokenSettings;
+	}
+// tag::class[]
+}
+// end::class[]

+ 31 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/ClientRepository.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Repository
+public interface ClientRepository extends JpaRepository<Client, String> {
+	Optional<Client> findByClientId(String clientId);
+}
+// end::class[]

+ 102 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/JpaOAuth2AuthorizationConsentService.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.dao.DataRetrievalFailureException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Component
+public class JpaOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
+	private final AuthorizationConsentRepository authorizationConsentRepository;
+	private final RegisteredClientRepository registeredClientRepository;
+
+	public JpaOAuth2AuthorizationConsentService(AuthorizationConsentRepository authorizationConsentRepository, RegisteredClientRepository registeredClientRepository) {
+		Assert.notNull(authorizationConsentRepository, "authorizationConsentRepository cannot be null");
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		this.authorizationConsentRepository = authorizationConsentRepository;
+		this.registeredClientRepository = registeredClientRepository;
+	}
+
+	@Override
+	public void save(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		this.authorizationConsentRepository.save(toEntity(authorizationConsent));
+	}
+
+	@Override
+	public void remove(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		this.authorizationConsentRepository.deleteByRegisteredClientIdAndPrincipalName(
+				authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
+	}
+
+	@Override
+	public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
+		Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
+		Assert.hasText(principalName, "principalName cannot be empty");
+		return this.authorizationConsentRepository.findByRegisteredClientIdAndPrincipalName(
+				registeredClientId, principalName).map(this::toObject).orElse(null);
+	}
+
+	private OAuth2AuthorizationConsent toObject(AuthorizationConsent authorizationConsent) {
+		String registeredClientId = authorizationConsent.getRegisteredClientId();
+		RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
+		if (registeredClient == null) {
+			throw new DataRetrievalFailureException(
+					"The RegisteredClient with id '" + registeredClientId + "' was not found in the RegisteredClientRepository.");
+		}
+
+		OAuth2AuthorizationConsent.Builder builder = OAuth2AuthorizationConsent.withId(
+				registeredClientId, authorizationConsent.getPrincipalName());
+		if (authorizationConsent.getAuthorities() != null) {
+			for (String authority : StringUtils.commaDelimitedListToSet(authorizationConsent.getAuthorities())) {
+				builder.authority(new SimpleGrantedAuthority(authority));
+			}
+		}
+
+		return builder.build();
+	}
+
+	private AuthorizationConsent toEntity(OAuth2AuthorizationConsent authorizationConsent) {
+		AuthorizationConsent entity = new AuthorizationConsent();
+		entity.setRegisteredClientId(authorizationConsent.getRegisteredClientId());
+		entity.setPrincipalName(authorizationConsent.getPrincipalName());
+
+		Set<String> authorities = new HashSet<>();
+		for (GrantedAuthority authority : authorizationConsent.getAuthorities()) {
+			authorities.add(authority.getAuthority());
+		}
+		entity.setAuthorities(StringUtils.collectionToCommaDelimitedString(authorities));
+
+		return entity;
+	}
+}
+// end::class[]

+ 263 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/JpaOAuth2AuthorizationService.java

@@ -0,0 +1,263 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.dao.DataRetrievalFailureException;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Component
+public class JpaOAuth2AuthorizationService implements OAuth2AuthorizationService {
+	private final AuthorizationRepository authorizationRepository;
+	private final RegisteredClientRepository registeredClientRepository;
+	private final ObjectMapper objectMapper = new ObjectMapper();
+
+	public JpaOAuth2AuthorizationService(AuthorizationRepository authorizationRepository, RegisteredClientRepository registeredClientRepository) {
+		Assert.notNull(authorizationRepository, "authorizationRepository cannot be null");
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		this.authorizationRepository = authorizationRepository;
+		this.registeredClientRepository = registeredClientRepository;
+
+		ClassLoader classLoader = JpaOAuth2AuthorizationService.class.getClassLoader();
+		List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+		this.objectMapper.registerModules(securityModules);
+		this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+	}
+
+	@Override
+	public void save(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		this.authorizationRepository.save(toEntity(authorization));
+	}
+
+	@Override
+	public void remove(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		this.authorizationRepository.deleteById(authorization.getId());
+	}
+
+	@Override
+	public OAuth2Authorization findById(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		return this.authorizationRepository.findById(id).map(this::toObject).orElse(null);
+	}
+
+	@Override
+	public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
+		Assert.hasText(token, "token cannot be empty");
+
+		Optional<Authorization> result;
+		if (tokenType == null) {
+			result = this.authorizationRepository.findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValue(token);
+		} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
+			result = this.authorizationRepository.findByState(token);
+		} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
+			result = this.authorizationRepository.findByAuthorizationCodeValue(token);
+		} else if (OAuth2ParameterNames.ACCESS_TOKEN.equals(tokenType.getValue())) {
+			result = this.authorizationRepository.findByAccessTokenValue(token);
+		} else if (OAuth2ParameterNames.REFRESH_TOKEN.equals(tokenType.getValue())) {
+			result = this.authorizationRepository.findByRefreshTokenValue(token);
+		} else {
+			result = Optional.empty();
+		}
+
+		return result.map(this::toObject).orElse(null);
+	}
+
+	private OAuth2Authorization toObject(Authorization entity) {
+		RegisteredClient registeredClient = this.registeredClientRepository.findById(entity.getRegisteredClientId());
+		if (registeredClient == null) {
+			throw new DataRetrievalFailureException(
+					"The RegisteredClient with id '" + entity.getRegisteredClientId() + "' was not found in the RegisteredClientRepository.");
+		}
+
+		OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient)
+				.id(entity.getId())
+				.principalName(entity.getPrincipalName())
+				.authorizationGrantType(resolveAuthorizationGrantType(entity.getAuthorizationGrantType()))
+				.attributes(attributes -> attributes.putAll(parseMap(entity.getAttributes())));
+		if (entity.getState() != null) {
+			builder.attribute(OAuth2ParameterNames.STATE, entity.getState());
+		}
+
+		if (entity.getAuthorizationCodeValue() != null) {
+			OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
+					entity.getAuthorizationCodeValue(),
+					entity.getAuthorizationCodeIssuedAt(),
+					entity.getAuthorizationCodeExpiresAt());
+			builder.token(authorizationCode, metadata -> metadata.putAll(parseMap(entity.getAuthorizationCodeMetadata())));
+		}
+
+		if (entity.getAccessTokenValue() != null) {
+			OAuth2AccessToken accessToken = new OAuth2AccessToken(
+					OAuth2AccessToken.TokenType.BEARER,
+					entity.getAccessTokenValue(),
+					entity.getAccessTokenIssuedAt(),
+					entity.getAccessTokenExpiresAt());
+			builder.token(accessToken, metadata -> metadata.putAll(parseMap(entity.getAccessTokenMetadata())));
+		}
+
+		if (entity.getRefreshTokenValue() != null) {
+			OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
+					entity.getRefreshTokenValue(),
+					entity.getRefreshTokenIssuedAt(),
+					entity.getRefreshTokenExpiresAt());
+			builder.token(refreshToken, metadata -> metadata.putAll(parseMap(entity.getRefreshTokenMetadata())));
+		}
+
+		if (entity.getOidcIdTokenValue() != null) {
+			OidcIdToken idToken = new OidcIdToken(
+					entity.getOidcIdTokenValue(),
+					entity.getOidcIdTokenIssuedAt(),
+					entity.getOidcIdTokenExpiresAt(),
+					parseMap(entity.getOidcIdTokenClaims()));
+			builder.token(idToken, metadata -> metadata.putAll(parseMap(entity.getOidcIdTokenMetadata())));
+		}
+
+		return builder.build();
+	}
+
+	private Authorization toEntity(OAuth2Authorization authorization) {
+		Authorization entity = new Authorization();
+		entity.setId(authorization.getId());
+		entity.setRegisteredClientId(authorization.getRegisteredClientId());
+		entity.setPrincipalName(authorization.getPrincipalName());
+		entity.setAuthorizationGrantType(authorization.getAuthorizationGrantType().getValue());
+		entity.setAttributes(writeMap(authorization.getAttributes()));
+		entity.setState(authorization.getAttribute(OAuth2ParameterNames.STATE));
+
+		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
+				authorization.getToken(OAuth2AuthorizationCode.class);
+		setTokenValues(
+				authorizationCode,
+				entity::setAuthorizationCodeValue,
+				entity::setAuthorizationCodeIssuedAt,
+				entity::setAuthorizationCodeExpiresAt,
+				entity::setAuthorizationCodeMetadata
+		);
+
+		OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
+				authorization.getToken(OAuth2AccessToken.class);
+		setTokenValues(
+				accessToken,
+				entity::setAccessTokenValue,
+				entity::setAccessTokenIssuedAt,
+				entity::setAccessTokenExpiresAt,
+				entity::setAccessTokenMetadata
+		);
+		if (accessToken != null && accessToken.getToken().getScopes() != null) {
+			entity.setAccessTokenScopes(StringUtils.collectionToDelimitedString(accessToken.getToken().getScopes(), ","));
+		}
+
+		OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
+				authorization.getToken(OAuth2RefreshToken.class);
+		setTokenValues(
+				refreshToken,
+				entity::setRefreshTokenValue,
+				entity::setRefreshTokenIssuedAt,
+				entity::setRefreshTokenExpiresAt,
+				entity::setRefreshTokenMetadata
+		);
+
+		OAuth2Authorization.Token<OidcIdToken> oidcIdToken =
+				authorization.getToken(OidcIdToken.class);
+		setTokenValues(
+				oidcIdToken,
+				entity::setOidcIdTokenValue,
+				entity::setOidcIdTokenIssuedAt,
+				entity::setOidcIdTokenExpiresAt,
+				entity::setOidcIdTokenMetadata
+		);
+		if (oidcIdToken != null) {
+			entity.setOidcIdTokenClaims(writeMap(oidcIdToken.getClaims()));
+		}
+
+		return entity;
+	}
+
+	private void setTokenValues(
+			OAuth2Authorization.Token<?> token,
+			Consumer<String> tokenValueConsumer,
+			Consumer<Instant> issuedAtConsumer,
+			Consumer<Instant> expiresAtConsumer,
+			Consumer<String> metadataConsumer) {
+		if (token != null) {
+			OAuth2Token oAuth2Token = token.getToken();
+			tokenValueConsumer.accept(oAuth2Token.getTokenValue());
+			issuedAtConsumer.accept(oAuth2Token.getIssuedAt());
+			expiresAtConsumer.accept(oAuth2Token.getExpiresAt());
+			metadataConsumer.accept(writeMap(token.getMetadata()));
+		}
+	}
+
+	private Map<String, Object> parseMap(String data) {
+		try {
+			return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {
+			});
+		} catch (Exception ex) {
+			throw new IllegalArgumentException(ex.getMessage(), ex);
+		}
+	}
+
+	private String writeMap(Map<String, Object> metadata) {
+		try {
+			return this.objectMapper.writeValueAsString(metadata);
+		} catch (Exception ex) {
+			throw new IllegalArgumentException(ex.getMessage(), ex);
+		}
+	}
+
+	private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
+		if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
+			return AuthorizationGrantType.AUTHORIZATION_CODE;
+		} else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
+			return AuthorizationGrantType.CLIENT_CREDENTIALS;
+		} else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
+			return AuthorizationGrantType.REFRESH_TOKEN;
+		}
+		return new AuthorizationGrantType(authorizationGrantType);              // Custom authorization grant type
+	}
+}
+// end::class[]

+ 175 - 0
docs/modules/guides/examples/src/main/java/sample/jpa/JpaRegisteredClientRepository.java

@@ -0,0 +1,175 @@
+/*
+ * Copyright 2022 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 sample.jpa;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Component
+public class JpaRegisteredClientRepository implements RegisteredClientRepository {
+	private final ClientRepository clientRepository;
+	private final ObjectMapper objectMapper = new ObjectMapper();
+
+	public JpaRegisteredClientRepository(ClientRepository clientRepository) {
+		Assert.notNull(clientRepository, "clientRepository cannot be null");
+		this.clientRepository = clientRepository;
+
+		ClassLoader classLoader = JpaRegisteredClientRepository.class.getClassLoader();
+		List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
+		this.objectMapper.registerModules(securityModules);
+		this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+	}
+
+	@Override
+	public void save(RegisteredClient registeredClient) {
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		this.clientRepository.save(toEntity(registeredClient));
+	}
+
+	@Override
+	public RegisteredClient findById(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		return this.clientRepository.findById(id).map(this::toObject).orElse(null);
+	}
+
+	@Override
+	public RegisteredClient findByClientId(String clientId) {
+		Assert.hasText(clientId, "clientId cannot be empty");
+		return this.clientRepository.findByClientId(clientId).map(this::toObject).orElse(null);
+	}
+
+	private RegisteredClient toObject(Client client) {
+		Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(
+				client.getClientAuthenticationMethods());
+		Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(
+				client.getAuthorizationGrantTypes());
+		Set<String> redirectUris = StringUtils.commaDelimitedListToSet(
+				client.getRedirectUris());
+		Set<String> clientScopes = StringUtils.commaDelimitedListToSet(
+				client.getScopes());
+
+		RegisteredClient.Builder builder = RegisteredClient.withId(client.getId())
+				.clientId(client.getClientId())
+				.clientIdIssuedAt(client.getClientIdIssuedAt())
+				.clientSecret(client.getClientSecret())
+				.clientSecretExpiresAt(client.getClientSecretExpiresAt())
+				.clientName(client.getClientName())
+				.clientAuthenticationMethods(authenticationMethods ->
+						clientAuthenticationMethods.forEach(authenticationMethod ->
+								authenticationMethods.add(resolveClientAuthenticationMethod(authenticationMethod))))
+				.authorizationGrantTypes((grantTypes) ->
+						authorizationGrantTypes.forEach(grantType ->
+								grantTypes.add(resolveAuthorizationGrantType(grantType))))
+				.redirectUris((uris) -> uris.addAll(redirectUris))
+				.scopes((scopes) -> scopes.addAll(clientScopes));
+
+		Map<String, Object> clientSettingsMap = parseMap(client.getClientSettings());
+		builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());
+
+		Map<String, Object> tokenSettingsMap = parseMap(client.getTokenSettings());
+		builder.tokenSettings(TokenSettings.withSettings(tokenSettingsMap).build());
+
+		return builder.build();
+	}
+
+	private Client toEntity(RegisteredClient registeredClient) {
+		List<String> clientAuthenticationMethods = new ArrayList<>(registeredClient.getClientAuthenticationMethods().size());
+		registeredClient.getClientAuthenticationMethods().forEach(clientAuthenticationMethod ->
+				clientAuthenticationMethods.add(clientAuthenticationMethod.getValue()));
+
+		List<String> authorizationGrantTypes = new ArrayList<>(registeredClient.getAuthorizationGrantTypes().size());
+		registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
+				authorizationGrantTypes.add(authorizationGrantType.getValue()));
+
+		Client entity = new Client();
+		entity.setId(registeredClient.getId());
+		entity.setClientId(registeredClient.getClientId());
+		entity.setClientIdIssuedAt(registeredClient.getClientIdIssuedAt());
+		entity.setClientSecret(registeredClient.getClientSecret());
+		entity.setClientSecretExpiresAt(registeredClient.getClientSecretExpiresAt());
+		entity.setClientName(registeredClient.getClientName());
+		entity.setClientAuthenticationMethods(StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods));
+		entity.setAuthorizationGrantTypes(StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes));
+		entity.setRedirectUris(StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris()));
+		entity.setScopes(StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes()));
+		entity.setClientSettings(writeMap(registeredClient.getClientSettings().getSettings()));
+		entity.setTokenSettings(writeMap(registeredClient.getTokenSettings().getSettings()));
+
+		return entity;
+	}
+
+	private Map<String, Object> parseMap(String data) {
+		try {
+			return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {
+			});
+		} catch (Exception ex) {
+			throw new IllegalArgumentException(ex.getMessage(), ex);
+		}
+	}
+
+	private String writeMap(Map<String, Object> data) {
+		try {
+			return this.objectMapper.writeValueAsString(data);
+		} catch (Exception ex) {
+			throw new IllegalArgumentException(ex.getMessage(), ex);
+		}
+	}
+
+	private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
+		if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
+			return AuthorizationGrantType.AUTHORIZATION_CODE;
+		} else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
+			return AuthorizationGrantType.CLIENT_CREDENTIALS;
+		} else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
+			return AuthorizationGrantType.REFRESH_TOKEN;
+		}
+		return new AuthorizationGrantType(authorizationGrantType);              // Custom authorization grant type
+	}
+
+	private static ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
+		if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
+			return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
+		} else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
+			return ClientAuthenticationMethod.CLIENT_SECRET_POST;
+		} else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) {
+			return ClientAuthenticationMethod.NONE;
+		}
+		return new ClientAuthenticationMethod(clientAuthenticationMethod);      // Custom client authentication method
+	}
+}
+// end::class[]

+ 6 - 0
docs/modules/guides/examples/src/main/resources/application.yml

@@ -0,0 +1,6 @@
+server:
+  port: 9000
+
+logging:
+  level:
+    org.springframework.security: trace

+ 6 - 0
docs/modules/guides/examples/src/main/resources/oauth2-authorization-consent-schema.sql

@@ -0,0 +1,6 @@
+CREATE TABLE authorizationConsent (
+    registeredClientId varchar(255) NOT NULL,
+    principalName varchar(255) NOT NULL,
+    authorities varchar(1000) NOT NULL,
+    PRIMARY KEY (registeredClientId, principalName)
+);

+ 28 - 0
docs/modules/guides/examples/src/main/resources/oauth2-authorization-schema.sql

@@ -0,0 +1,28 @@
+CREATE TABLE authorization (
+    id varchar(255) NOT NULL,
+    registeredClientId varchar(255) NOT NULL,
+    principalName varchar(255) NOT NULL,
+    authorizationGrantType varchar(255) NOT NULL,
+    attributes varchar(4000) DEFAULT NULL,
+    state varchar(500) DEFAULT NULL,
+    authorizationCodeValue varchar(4000) DEFAULT NULL,
+    authorizationCodeIssuedAt timestamp DEFAULT NULL,
+    authorizationCodeExpiresAt timestamp DEFAULT NULL,
+    authorizationCodeMetadata varchar(2000) DEFAULT NULL,
+    accessTokenValue varchar(4000) DEFAULT NULL,
+    accessTokenIssuedAt timestamp DEFAULT NULL,
+    accessTokenExpiresAt timestamp DEFAULT NULL,
+    accessTokenMetadata varchar(2000) DEFAULT NULL,
+    accessTokenType varchar(255) DEFAULT NULL,
+    accessTokenScopes varchar(1000) DEFAULT NULL,
+    refreshTokenValue varchar(4000) DEFAULT NULL,
+    refreshTokenIssuedAt timestamp DEFAULT NULL,
+    refreshTokenExpiresAt timestamp DEFAULT NULL,
+    refreshTokenMetadata varchar(2000) DEFAULT NULL,
+    oidcIdTokenValue varchar(4000) DEFAULT NULL,
+    oidcIdTokenIssuedAt timestamp DEFAULT NULL,
+    oidcIdTokenExpiresAt timestamp DEFAULT NULL,
+    oidcIdTokenMetadata varchar(2000) DEFAULT NULL,
+    oidcIdTokenClaims varchar(2000) DEFAULT NULL,
+    PRIMARY KEY (id)
+);

+ 15 - 0
docs/modules/guides/examples/src/main/resources/oauth2-registered-client-schema.sql

@@ -0,0 +1,15 @@
+CREATE TABLE client (
+    id varchar(255) NOT NULL,
+    clientId varchar(255) NOT NULL,
+    clientIdIssuedAt timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    clientSecret varchar(255) DEFAULT NULL,
+    clientSecretExpiresAt timestamp DEFAULT NULL,
+    clientName varchar(255) NOT NULL,
+    clientAuthenticationMethods varchar(1000) NOT NULL,
+    authorizationGrantTypes varchar(1000) NOT NULL,
+    redirectUris varchar(1000) DEFAULT NULL,
+    scopes varchar(1000) NOT NULL,
+    clientSettings varchar(2000) NOT NULL,
+    tokenSettings varchar(2000) NOT NULL,
+    PRIMARY KEY (id)
+);

+ 112 - 0
docs/modules/guides/examples/src/test/java/sample/jose/TestJwks.java

@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020-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 sample.jose;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.UUID;
+
+import javax.crypto.SecretKey;
+
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.OctetSequenceKey;
+import com.nimbusds.jose.jwk.RSAKey;
+
+/**
+ * @author Joe Grandja
+ */
+public final class TestJwks {
+
+	private static final KeyPairGenerator rsaKeyPairGenerator;
+	static {
+		try {
+			rsaKeyPairGenerator = KeyPairGenerator.getInstance("RSA");
+			rsaKeyPairGenerator.initialize(2048);
+		} catch (Exception ex) {
+			throw new IllegalStateException(ex);
+		}
+	}
+
+	// @formatter:off
+	public static final RSAKey DEFAULT_RSA_JWK =
+			jwk(
+					TestKeys.DEFAULT_PUBLIC_KEY,
+					TestKeys.DEFAULT_PRIVATE_KEY
+			).build();
+	// @formatter:on
+
+	// @formatter:off
+	public static final ECKey DEFAULT_EC_JWK =
+			jwk(
+					(ECPublicKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPublic(),
+					(ECPrivateKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPrivate()
+			).build();
+	// @formatter:on
+
+	// @formatter:off
+	public static final OctetSequenceKey DEFAULT_SECRET_JWK =
+			jwk(
+					TestKeys.DEFAULT_SECRET_KEY
+			).build();
+	// @formatter:on
+
+	private TestJwks() {
+	}
+
+	public static RSAKey.Builder generateRsaJwk() {
+		KeyPair keyPair = rsaKeyPairGenerator.generateKeyPair();
+		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+		RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
+		// @formatter:off
+		return jwk(publicKey, privateKey)
+				.keyID(UUID.randomUUID().toString());
+		// @formatter:on
+	}
+
+	public static RSAKey.Builder jwk(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
+		// @formatter:off
+		return new RSAKey.Builder(publicKey)
+				.privateKey(privateKey)
+				.keyUse(KeyUse.SIGNATURE)
+				.keyID("rsa-jwk-kid");
+		// @formatter:on
+	}
+
+	public static ECKey.Builder jwk(ECPublicKey publicKey, ECPrivateKey privateKey) {
+		// @formatter:off
+		Curve curve = Curve.forECParameterSpec(publicKey.getParams());
+		return new ECKey.Builder(curve, publicKey)
+				.privateKey(privateKey)
+				.keyUse(KeyUse.SIGNATURE)
+				.keyID("ec-jwk-kid");
+		// @formatter:on
+	}
+
+	public static OctetSequenceKey.Builder jwk(SecretKey secretKey) {
+		// @formatter:off
+		return new OctetSequenceKey.Builder(secretKey)
+				.keyUse(KeyUse.SIGNATURE)
+				.keyID("secret-jwk-kid");
+		// @formatter:on
+	}
+
+}

+ 154 - 0
docs/modules/guides/examples/src/test/java/sample/jose/TestKeys.java

@@ -0,0 +1,154 @@
+/*
+ * 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 sample.jose;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * TODO
+ * This class is a straight copy from Spring Security.
+ * It should be removed when merging this codebase into Spring Security.
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ */
+public final class TestKeys {
+
+	public static final KeyFactory kf;
+	static {
+		try {
+			kf = KeyFactory.getInstance("RSA");
+		}
+		catch (NoSuchAlgorithmException ex) {
+			throw new IllegalStateException(ex);
+		}
+	}
+	public static final String DEFAULT_ENCODED_SECRET_KEY = "bCzY/M48bbkwBEWjmNSIEPfwApcvXOnkCxORBEbPr+4=";
+
+	public static final SecretKey DEFAULT_SECRET_KEY = new SecretKeySpec(
+			Base64.getDecoder().decode(DEFAULT_ENCODED_SECRET_KEY), "AES");
+
+	// @formatter:off
+	public static final String DEFAULT_RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd"
+			+ "7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv"
+			+ "c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6"
+			+ "iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2"
+			+ "kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o"
+			+ "RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj"
+			+ "KwIDAQAB";
+	// @formatter:on
+
+	public static final RSAPublicKey DEFAULT_PUBLIC_KEY;
+	static {
+		X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PUBLIC_KEY));
+		try {
+			DEFAULT_PUBLIC_KEY = (RSAPublicKey) kf.generatePublic(spec);
+		}
+		catch (InvalidKeySpecException ex) {
+			throw new IllegalArgumentException(ex);
+		}
+	}
+
+	// @formatter:off
+	public static final String DEFAULT_RSA_PRIVATE_KEY = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA"
+			+ "iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM"
+			+ "g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK"
+			+ "LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF"
+			+ "oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc"
+			+ "3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn"
+			+ "+jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE"
+			+ "E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek"
+			+ "lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG"
+			+ "mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7"
+			+ "62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0"
+			+ "bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA"
+			+ "+Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH"
+			+ "Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA"
+			+ "8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd"
+			+ "I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY"
+			+ "QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d"
+			+ "rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk"
+			+ "HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA"
+			+ "Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN"
+			+ "HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a"
+			+ "FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF"
+			+ "snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H"
+			+ "c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM"
+			+ "TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR"
+			+ "47jndeyIaMTNETEmOnms+as17g==";
+	// @formatter:on
+
+	public static final RSAPrivateKey DEFAULT_PRIVATE_KEY;
+	static {
+		PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PRIVATE_KEY));
+		try {
+			DEFAULT_PRIVATE_KEY = (RSAPrivateKey) kf.generatePrivate(spec);
+		}
+		catch (InvalidKeySpecException ex) {
+			throw new IllegalArgumentException(ex);
+		}
+	}
+
+	public static final KeyPair DEFAULT_RSA_KEY_PAIR = new KeyPair(DEFAULT_PUBLIC_KEY, DEFAULT_PRIVATE_KEY);
+
+	public static final KeyPair DEFAULT_EC_KEY_PAIR = generateEcKeyPair();
+
+	static KeyPair generateEcKeyPair() {
+		EllipticCurve ellipticCurve = new EllipticCurve(
+				new ECFieldFp(new BigInteger(
+						"115792089210356248762697446949407573530086143415290314195533631308867097853951")),
+				new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
+				new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
+		ECPoint ecPoint = new ECPoint(
+				new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
+				new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
+		ECParameterSpec ecParameterSpec = new ECParameterSpec(ellipticCurve, ecPoint,
+				new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"), 1);
+
+		KeyPair keyPair;
+		try {
+			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
+			keyPairGenerator.initialize(ecParameterSpec);
+			keyPair = keyPairGenerator.generateKeyPair();
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException(ex);
+		}
+		return keyPair;
+	}
+
+	private TestKeys() {
+	}
+
+}

+ 240 - 0
docs/modules/guides/examples/src/test/java/sample/jpa/JpaTests.java

@@ -0,0 +1,240 @@
+/*
+ * Copyright 2020-2022 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 sample.jpa;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
+import org.assertj.core.api.ObjectAssert;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import sample.jose.TestJwks;
+import sample.test.SpringTestContext;
+import sample.test.SpringTestContextExtension;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+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.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static sample.util.RegisteredClients.messagingClient;
+
+/**
+ * Tests for the guide How-to: Implement core services with JPA.
+ *
+ * @author Steve Riesenberg
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class JpaTests {
+
+	private static final Pattern HIDDEN_STATE_INPUT_PATTERN = Pattern.compile(".+<input type=\"hidden\" name=\"state\" value=\"([^\"]+)\">.+");
+	private static final TypeReference<Map<String, Object>> TOKEN_RESPONSE_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
+	};
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	private MockMvc mockMvc;
+
+	@Autowired
+	private RegisteredClientRepository registeredClientRepository;
+
+	@Autowired
+	private OAuth2AuthorizationService authorizationService;
+
+	@Autowired
+	private OAuth2AuthorizationConsentService authorizationConsentService;
+
+	@Test
+	public void oidcLoginWhenJpaCoreServicesAutowiredThenUsed() throws Exception {
+		this.spring.register(AuthorizationServerConfig.class).autowire();
+		assertThat(this.registeredClientRepository).isInstanceOf(JpaRegisteredClientRepository.class);
+		assertThat(this.authorizationService).isInstanceOf(JpaOAuth2AuthorizationService.class);
+		assertThat(this.authorizationConsentService).isInstanceOf(JpaOAuth2AuthorizationConsentService.class);
+
+		RegisteredClient registeredClient = messagingClient();
+		this.registeredClientRepository.save(registeredClient);
+
+		String state = performAuthorizationCodeRequest(registeredClient);
+		assertThatAuthorization(state, OAuth2ParameterNames.STATE).isNotNull();
+		assertThatAuthorization(state, null).isNotNull();
+
+		String authorizationCode = performAuthorizationConsentRequest(registeredClient, state);
+		assertThatAuthorization(authorizationCode, OAuth2ParameterNames.CODE).isNotNull();
+		assertThatAuthorization(authorizationCode, null).isNotNull();
+
+		Map<String, Object> tokenResponse = performTokenRequest(registeredClient, authorizationCode);
+		String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
+		assertThatAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN).isNotNull();
+		assertThatAuthorization(accessToken, null).isNotNull();
+
+		String refreshToken = (String) tokenResponse.get(OAuth2ParameterNames.REFRESH_TOKEN);
+		assertThatAuthorization(refreshToken, OAuth2ParameterNames.REFRESH_TOKEN).isNotNull();
+		assertThatAuthorization(refreshToken, null).isNotNull();
+
+		String idToken = (String) tokenResponse.get(OidcParameterNames.ID_TOKEN);
+		assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNull(); // id_token is not searchable
+
+		OAuth2Authorization authorization = findAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN);
+		assertThat(authorization.getToken(idToken)).isNotNull();
+
+		String scopes = (String) tokenResponse.get(OAuth2ParameterNames.SCOPE);
+		OAuth2AuthorizationConsent authorizationConsent = this.authorizationConsentService.findById(
+				registeredClient.getId(), "user");
+		assertThat(authorizationConsent).isNotNull();
+		assertThat(authorizationConsent.getScopes()).containsExactlyInAnyOrder(
+				StringUtils.delimitedListToStringArray(scopes, " "));
+	}
+
+	private ObjectAssert<OAuth2Authorization> assertThatAuthorization(String token, String tokenType) {
+		return assertThat(findAuthorization(token, tokenType));
+	}
+
+	private OAuth2Authorization findAuthorization(String token, String tokenType) {
+		return this.authorizationService.findByToken(token, tokenType == null ? null : new OAuth2TokenType(tokenType));
+	}
+
+	private String performAuthorizationCodeRequest(RegisteredClient registeredClient) throws Exception {
+		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
+		parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
+		parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
+		parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
+		parameters.set(OAuth2ParameterNames.SCOPE,
+				StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
+		parameters.set(OAuth2ParameterNames.STATE, "state");
+
+		MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorize")
+				.params(parameters)
+				.with(user("user").roles("USER")))
+				.andExpect(status().isOk())
+				.andExpect(header().string("content-type", containsString(MediaType.TEXT_HTML_VALUE)))
+				.andReturn();
+		String responseHtml = mvcResult.getResponse().getContentAsString();
+		Matcher matcher = HIDDEN_STATE_INPUT_PATTERN.matcher(responseHtml);
+
+		return matcher.matches() ? matcher.group(1) : null;
+	}
+
+	private String performAuthorizationConsentRequest(RegisteredClient registeredClient, String state) throws Exception {
+		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
+		parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
+		parameters.set(OAuth2ParameterNames.STATE, state);
+		parameters.add(OAuth2ParameterNames.SCOPE, "message.read");
+		parameters.add(OAuth2ParameterNames.SCOPE, "message.write");
+
+		MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/authorize")
+				.params(parameters)
+				.with(user("user").roles("USER")))
+				.andExpect(status().is3xxRedirection())
+				.andReturn();
+		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
+		assertThat(redirectedUrl).isNotNull();
+		assertThat(redirectedUrl).matches("http://127.0.0.1:8080/authorized\\?code=.{15,}&state=state");
+
+		String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name());
+		UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
+
+		return uriComponents.getQueryParams().getFirst("code");
+	}
+
+	private Map<String, Object> performTokenRequest(RegisteredClient registeredClient, String authorizationCode) throws Exception {
+		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
+		parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
+		parameters.set(OAuth2ParameterNames.CODE, authorizationCode);
+		parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
+
+		HttpHeaders basicAuth = new HttpHeaders();
+		basicAuth.setBasicAuth(registeredClient.getClientId(), "secret");
+
+		MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/token")
+				.params(parameters)
+				.headers(basicAuth))
+				.andExpect(status().isOk())
+				.andExpect(header().string(HttpHeaders.CONTENT_TYPE, containsString(MediaType.APPLICATION_JSON_VALUE)))
+				.andExpect(jsonPath("$.access_token").isNotEmpty())
+				.andExpect(jsonPath("$.token_type").isNotEmpty())
+				.andExpect(jsonPath("$.expires_in").isNotEmpty())
+				.andExpect(jsonPath("$.refresh_token").isNotEmpty())
+				.andExpect(jsonPath("$.scope").isNotEmpty())
+				.andExpect(jsonPath("$.id_token").isNotEmpty())
+				.andReturn();
+
+		ObjectMapper objectMapper = new ObjectMapper();
+		String responseJson = mvcResult.getResponse().getContentAsString();
+		return objectMapper.readValue(responseJson, TOKEN_RESPONSE_TYPE_REFERENCE);
+	}
+
+	@EnableWebSecurity
+	@EnableAutoConfiguration
+	@ComponentScan
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfig {
+
+		@Bean
+		public JWKSource<SecurityContext> jwkSource() {
+			JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
+			return new ImmutableJWKSet<>(jwkSet);
+		}
+
+		@Bean
+		public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
+			return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
+		}
+
+	}
+
+}

+ 162 - 0
docs/modules/guides/examples/src/test/java/sample/test/SpringTestContext.java

@@ -0,0 +1,162 @@
+/*
+ * Copyright 2002-2017 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 sample.test;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
+import org.springframework.mock.web.MockServletConfig;
+import org.springframework.mock.web.MockServletContext;
+import org.springframework.security.config.BeanIds;
+import org.springframework.test.context.web.GenericXmlWebContextLoader;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
+import org.springframework.web.context.ConfigurableWebApplicationContext;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
+import org.springframework.web.context.support.XmlWebApplicationContext;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+
+/**
+ * TODO
+ * This class is a straight copy from Spring Security.
+ *
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class SpringTestContext implements Closeable {
+
+	private Object test;
+
+	private ConfigurableWebApplicationContext context;
+
+	private List<Filter> filters = new ArrayList<>();
+
+	public SpringTestContext(Object test) {
+		setTest(test);
+	}
+
+	public void setTest(Object test) {
+		this.test = test;
+	}
+
+	@Override
+	public void close() {
+		try {
+			this.context.close();
+		}
+		catch (Exception ex) {
+		}
+	}
+
+	public SpringTestContext context(ConfigurableWebApplicationContext context) {
+		this.context = context;
+		return this;
+	}
+
+	public SpringTestContext register(Class<?>... classes) {
+		AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
+		applicationContext.register(classes);
+		this.context = applicationContext;
+		return this;
+	}
+
+	public SpringTestContext testConfigLocations(String... configLocations) {
+		GenericXmlWebContextLoader loader = new GenericXmlWebContextLoader();
+		String[] locations = loader.processLocations(this.test.getClass(), configLocations);
+		return configLocations(locations);
+	}
+
+	public SpringTestContext configLocations(String... configLocations) {
+		XmlWebApplicationContext context = new XmlWebApplicationContext();
+		context.setConfigLocations(configLocations);
+		this.context = context;
+		return this;
+	}
+
+//	public SpringTestContext context(String configuration) {
+//		InMemoryXmlWebApplicationContext context = new InMemoryXmlWebApplicationContext(configuration);
+//		this.context = context;
+//		return this;
+//	}
+
+	public SpringTestContext mockMvcAfterSpringSecurityOk() {
+		return addFilter(new OncePerRequestFilter() {
+			@Override
+			protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+					FilterChain filterChain) {
+				response.setStatus(HttpServletResponse.SC_OK);
+			}
+		});
+	}
+
+	private SpringTestContext addFilter(Filter filter) {
+		this.filters.add(filter);
+		return this;
+	}
+
+	public ConfigurableWebApplicationContext getContext() {
+		if (!this.context.isRunning()) {
+			this.context.setServletContext(new MockServletContext());
+			this.context.setServletConfig(new MockServletConfig());
+			this.context.refresh();
+		}
+		return this.context;
+	}
+
+	public void autowire() {
+		this.context.setServletContext(new MockServletContext());
+		this.context.setServletConfig(new MockServletConfig());
+		this.context.refresh();
+		if (this.context.containsBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN)) {
+			// @formatter:off
+			MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).
+					apply(springSecurity())
+					.apply(new AddFilter())
+					.build();
+			// @formatter:on
+			this.context.getBeanFactory().registerResolvableDependency(MockMvc.class, mockMvc);
+		}
+		AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
+		bpp.setBeanFactory(this.context.getBeanFactory());
+		bpp.processInjection(this.test);
+	}
+
+	private class AddFilter implements MockMvcConfigurer {
+
+		@Override
+		public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder<?> builder,
+				WebApplicationContext context) {
+			builder.addFilters(SpringTestContext.this.filters.toArray(new Filter[0]));
+			return null;
+		}
+
+	}
+
+}

+ 58 - 0
docs/modules/guides/examples/src/test/java/sample/test/SpringTestContextExtension.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2017 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 sample.test;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import org.springframework.security.test.context.TestSecurityContextHolder;
+
+/**
+ * TODO
+ * This class is a straight copy from Spring Security.
+ */
+public class SpringTestContextExtension implements BeforeEachCallback, AfterEachCallback {
+
+	@Override
+	public void afterEach(ExtensionContext context) throws Exception {
+		TestSecurityContextHolder.clearContext();
+		getContexts(context.getRequiredTestInstance()).forEach((springTestContext) -> springTestContext.close());
+	}
+
+	@Override
+	public void beforeEach(ExtensionContext context) throws Exception {
+		Object testInstance = context.getRequiredTestInstance();
+		getContexts(testInstance).forEach((springTestContext) -> springTestContext.setTest(testInstance));
+	}
+
+	private static List<SpringTestContext> getContexts(Object test) throws IllegalAccessException {
+		Field[] declaredFields = test.getClass().getDeclaredFields();
+		List<SpringTestContext> result = new ArrayList<>();
+		for (Field field : declaredFields) {
+			if (SpringTestContext.class.isAssignableFrom(field.getType())) {
+				result.add((SpringTestContext) field.get(test));
+			}
+		}
+		return result;
+	}
+
+}

+ 47 - 0
docs/modules/guides/examples/src/test/java/sample/util/RegisteredClients.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020-2022 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 sample.util;
+
+import java.util.UUID;
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
+
+/**
+ * @author Steve Riesenberg
+ */
+public class RegisteredClients {
+	// @formatter:off
+	public static RegisteredClient messagingClient() {
+		return RegisteredClient.withId(UUID.randomUUID().toString())
+				.clientId("messaging-client")
+				.clientSecret("{noop}secret")
+				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+				.redirectUri("http://127.0.0.1:8080/authorized")
+				.scope(OidcScopes.OPENID)
+				.scope("message.read")
+				.scope("message.write")
+				.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
+				.build();
+	}
+	// @formatter:on
+}

+ 1 - 1
docs/modules/guides/nav.adoc

@@ -1,2 +1,2 @@
 .xref:index.adoc["How-to" Guides]
-* xref:page-1.adoc[Example 1]
+* xref:how-to-jpa.adoc[Implement core services with JPA]

+ 144 - 0
docs/modules/guides/pages/how-to-jpa.adoc

@@ -0,0 +1,144 @@
+= How-to: Implement core services with JPA
+
+[[getting-started]]
+== Getting Started
+
+In this guide, we will demonstrate how to implement the xref:ROOT:core-components.adoc[core services] of xref:ROOT:index.adoc[Spring Authorization Server] with JPA. The purpose of this guide is to provide a starting point for implementing these services yourself, with the intention that you will make modifications as necessary to suit your needs.
+
+[[define-data-model]]
+== Define the data model
+
+This guide seeks to provide a starting point for the data model and uses the simplest possible structure and data types. To come up with the initial schema, we begin by reviewing the xref:ROOT:core-components.adoc[domain objects] used by the core services. Please note:
+
+NOTE: Except for token, state, metadata, settings and claims values, we will use the JPA default column length of 255 for all columns. In reality, the length and even type of columns you use may need to be customized. Your mileage may vary and you are encouraged to experiment and test before deploying to production.
+
+The xref:ROOT:core-components.adoc#registered-client-repository[`RegisteredClient`] domain object contains a few multi-valued fields and some settings fields that require storing arbitrary key/value data. The following is an example that we will use to create a JPA entity.
+
+[[client-schema]]
+.Client Schema
+[source,sql]
+----
+include::example$src/main/resources/oauth2-registered-client-schema.sql[]
+----
+
+The xref:ROOT:core-components.adoc#oauth2-authorization-service[`OAuth2Authorization`] domain object is more complex, and contains several multi-valued fields as well as numerous arbitrarily long token values, metadata, settings and claims values. The built-in JDBC implementation utilizes a flattened structure that prefers performance over normalization, which we adopt here as well.
+
+CAUTION: It has been difficult to find a flattened database schema that works well in all cases and with all database vendors. You may need to normalize or heavily alter the following schema for your needs.
+
+The following is an example that we will use to create a JPA entity.
+
+[[authorization-schema]]
+.Authorization Schema
+[source,sql]
+----
+include::example$src/main/resources/oauth2-authorization-schema.sql[]
+----
+
+The xref:ROOT:core-components.adoc#oauth2-authorization-consent-service[`OAuth2AuthorizationConsent`] domain object is the simplest to model, and only contains a single multi-valued field in addition to a composite key.  The following is an example that we will use to create a JPA entity.
+
+[[authorization-consent-schema]]
+.Authorization Consent Schema
+[source,sql]
+----
+include::example$src/main/resources/oauth2-authorization-consent-schema.sql[]
+----
+
+[[create-jpa-entities]]
+== Create JPA entities
+
+The preceding schema examples provide a reference for the structure of the entities we need to create.
+
+NOTE: The following entities are minimally annotated and are just examples. They allow the schema to be created dynamically and therefore do not require the above sql scripts to be executed manually.
+
+The following is an example of the `Client` entity which is used to persist information mapped from the xref:ROOT:core-components.adoc#registered-client-repository[`RegisteredClient`] domain object.
+
+[[client-entity]]
+.Client Entity
+[source,java]
+----
+include::example$src/main/java/sample/jpa/Client.java[tag=class]
+----
+
+The following is an example of the `Authorization` entity which is used to persist information mapped from the xref:ROOT:core-components.adoc#oauth2-authorization-service[`OAuth2Authorization`] domain object.
+
+[[authorization-entity]]
+.Authorization Entity
+[source,java]
+----
+include::example$src/main/java/sample/jpa/Authorization.java[tag=class]
+----
+
+The following is an example of the `AuthorizationConsent` entity which is used to persist information mapped from the xref:ROOT:core-components.adoc#oauth2-authorization-consent-service[`OAuth2AuthorizationConsent`] domain object.
+
+[[authorization-consent-entity]]
+.Authorization Consent Entity
+[source,java]
+----
+include::example$src/main/java/sample/jpa/AuthorizationConsent.java[tag=class]
+----
+
+[[create-spring-data-repositories]]
+== Create Spring Data repositories
+
+By closely examining the interfaces of each core service and reviewing the `Jdbc` implementations, we can derive a minimal set of queries needed for supporting a JPA version of each interface.
+
+The following is an example of the `ClientRepository` capable of finding a <<client-entity,`Client`>> by the `id` and `clientId` fields.
+
+[[client-repository]]
+.Client Repository
+[source,java]
+----
+include::example$src/main/java/sample/jpa/ClientRepository.java[tag=class]
+----
+
+The following is an example of the `AuthorizationRepository` capable of finding an <<authorization-entity,`Authorization`>> by the `id` field as well as the `state`, `authorizationCodeValue`, `accessTokenValue` and `refreshTokenValue` token fields. It also allows querying a combination of token fields.
+
+[[authorization-repository]]
+.Authorization Repository
+[source,java]
+----
+include::example$src/main/java/sample/jpa/AuthorizationRepository.java[tag=class]
+----
+
+The following is an example of the `AuthorizationConsentRepository` capable of finding and deleting an <<authorization-consent-entity,`AuthorizationConsent`>> by the `registeredClientId` and `principalName` fields, which form a composite primary key.
+
+[[authorization-consent-repository]]
+.Authorization Consent Repository
+[source,java]
+----
+include::example$src/main/java/sample/jpa/AuthorizationConsentRepository.java[tag=class]
+----
+
+[[implement-core-services]]
+== Implement core services
+
+With the above <<create-jpa-entities,entities>> and <<create-spring-data-repositories,repositories>>, we can begin implementing the core services. By reviewing the `Jdbc` implementations, we can derive a minimal set of internal utilities for converting to/from string values for enumerations and reading/writing JSON data for attributes, settings, metadata and claims fields.
+
+CAUTION: Keep in mind that writing JSON data to text columns with a fixed length has proven problematic with the `Jdbc` implementations. While these examples continue to do so, you may need to split these fields out into a separate table or data store that supports arbitrarily long data values.
+
+The following is an example of the `JpaRegisteredClientRepository` which uses a <<client-repository,`ClientRepository`>> for persisting a <<client-entity,`Client`>>, and maps to/from the xref:ROOT:core-components.adoc#registered-client-repository[`RegisteredClient`] domain object.
+
+[[jpa-registered-client-repository]]
+.`RegisteredClientRepository` Implementation
+[source,java]
+----
+include::example$src/main/java/sample/jpa/JpaRegisteredClientRepository.java[tag=class]
+----
+
+The following is an example of the `JpaOAuth2AuthorizationService` which uses an <<authorization-repository,`AuthorizationRepository`>> for persisting an <<authorization-entity,`Authorization`>>, and maps to/from the xref:ROOT:core-components.adoc#oauth2-authorization-service[`OAuth2Authorization`] domain object.
+
+[[jpa-oauth2-authorization-service]]
+.`OAuth2AuthorizationService` Implementation
+[source,java]
+----
+include::example$src/main/java/sample/jpa/JpaOAuth2AuthorizationService.java[tag=class]
+----
+
+The following is an example of the `JpaOAuth2AuthorizationConsentService` which uses an <<authorization-consent-repository,`AuthorizationConsentRepository`>> for persisting an <<authorization-consent-entity,`AuthorizationConsent`>>, and maps to/from the xref:ROOT:core-components.adoc#oauth2-authorization-consent-service[`OAuth2AuthorizationConsent`] domain object.
+
+[[jpa-oauth2-authorization-consent-service]]
+.`OAuth2AuthorizationConsentService` Implementation
+[source,java]
+----
+include::example$src/main/java/sample/jpa/JpaOAuth2AuthorizationConsentService.java[tag=class]
+----

+ 0 - 1
docs/modules/guides/pages/page-1.adoc

@@ -1 +0,0 @@
-= Example 1

+ 1 - 0
etc/nohttp/allowlist.lines

@@ -5,3 +5,4 @@
 ^http://lists.webappsec.org/.*
 ^http://webblaze.cs.berkeley.edu/.*
 ^http://www.w3.org/2000/09/xmldsig.*
+^http://www.gnu.org/.*