Преглед изворни кода

Add How-to: Implement core services with Redis

Closes gh-1019
Joe Grandja пре 11 месеци
родитељ
комит
fffe227d75
26 измењених фајлова са 2237 додато и 0 уклоњено
  1. 1 0
      docs/modules/ROOT/nav.adoc
  2. 225 0
      docs/modules/ROOT/pages/guides/how-to-redis.adoc
  3. 8 0
      docs/spring-authorization-server-docs.gradle
  4. 85 0
      docs/src/main/java/sample/redis/config/RedisConfig.java
  5. 48 0
      docs/src/main/java/sample/redis/convert/BytesToClaimsHolderConverter.java
  6. 45 0
      docs/src/main/java/sample/redis/convert/BytesToOAuth2AuthorizationRequestConverter.java
  7. 44 0
      docs/src/main/java/sample/redis/convert/BytesToUsernamePasswordAuthenticationTokenConverter.java
  8. 36 0
      docs/src/main/java/sample/redis/convert/ClaimsHolderMixin.java
  9. 48 0
      docs/src/main/java/sample/redis/convert/ClaimsHolderToBytesConverter.java
  10. 45 0
      docs/src/main/java/sample/redis/convert/OAuth2AuthorizationRequestToBytesConverter.java
  11. 44 0
      docs/src/main/java/sample/redis/convert/UsernamePasswordAuthenticationTokenToBytesConverter.java
  12. 73 0
      docs/src/main/java/sample/redis/entity/OAuth2AuthorizationCodeGrantAuthorization.java
  13. 176 0
      docs/src/main/java/sample/redis/entity/OAuth2AuthorizationGrantAuthorization.java
  14. 29 0
      docs/src/main/java/sample/redis/entity/OAuth2ClientCredentialsGrantAuthorization.java
  15. 87 0
      docs/src/main/java/sample/redis/entity/OAuth2DeviceCodeGrantAuthorization.java
  16. 246 0
      docs/src/main/java/sample/redis/entity/OAuth2RegisteredClient.java
  17. 29 0
      docs/src/main/java/sample/redis/entity/OAuth2TokenExchangeGrantAuthorization.java
  18. 65 0
      docs/src/main/java/sample/redis/entity/OAuth2UserConsent.java
  19. 59 0
      docs/src/main/java/sample/redis/entity/OidcAuthorizationCodeGrantAuthorization.java
  20. 50 0
      docs/src/main/java/sample/redis/repository/OAuth2AuthorizationGrantAuthorizationRepository.java
  21. 28 0
      docs/src/main/java/sample/redis/repository/OAuth2RegisteredClientRepository.java
  22. 30 0
      docs/src/main/java/sample/redis/repository/OAuth2UserConsentRepository.java
  23. 498 0
      docs/src/main/java/sample/redis/service/ModelMapper.java
  24. 59 0
      docs/src/main/java/sample/redis/service/RedisOAuth2AuthorizationConsentService.java
  25. 122 0
      docs/src/main/java/sample/redis/service/RedisOAuth2AuthorizationService.java
  26. 57 0
      docs/src/main/java/sample/redis/service/RedisRegisteredClientRepository.java

+ 1 - 0
docs/modules/ROOT/nav.adoc

@@ -11,5 +11,6 @@
 ** xref:guides/how-to-multitenancy.adoc[]
 ** xref:guides/how-to-multitenancy.adoc[]
 ** xref:guides/how-to-userinfo.adoc[]
 ** xref:guides/how-to-userinfo.adoc[]
 ** xref:guides/how-to-jpa.adoc[]
 ** xref:guides/how-to-jpa.adoc[]
+** xref:guides/how-to-redis.adoc[]
 ** xref:guides/how-to-custom-claims-authorities.adoc[]
 ** xref:guides/how-to-custom-claims-authorities.adoc[]
 ** xref:guides/how-to-dynamic-client-registration.adoc[]
 ** xref:guides/how-to-dynamic-client-registration.adoc[]

+ 225 - 0
docs/modules/ROOT/pages/guides/how-to-redis.adoc

@@ -0,0 +1,225 @@
+
+[[how-to-redis]]
+= How-to: Implement core services with Redis
+:index-link: ../how-to.html
+:docs-dir: ..
+
+This guide shows how to implement the xref:core-model-components.adoc[core services] of xref:index.adoc[Spring Authorization Server] with https://redis.io/[Redis].
+The purpose of this guide is to provide a starting point for implementing these services yourself, with the intention that you can make modifications to suit your needs.
+
+* xref:guides/how-to-redis.adoc#define-entity-model[Define the entity model]
+* xref:guides/how-to-redis.adoc#create-spring-data-repositories[Create Spring Data repositories]
+* xref:guides/how-to-redis.adoc#implement-core-services[Implement core services]
+* xref:guides/how-to-redis.adoc#configure-core-services[Configure core services]
+
+TIP: The code samples provided in this guide are located in the https://github.com/spring-projects/spring-authorization-server/tree/main/docs/src/main/java/sample[documentation samples] directory under the *_redis_* subdirectory.
+
+[[define-entity-model]]
+== Define the entity model
+
+The following defines the entity model representation for the `RegisteredClient`, `OAuth2Authorization` and `OAuth2AuthorizationConsent` domain classes.
+
+* xref:guides/how-to-redis.adoc#registered-client-entity[Registered Client Entity]
+* xref:guides/how-to-redis.adoc#authorization-grant-entity[Authorization Grant _Base_ Entity]
+* xref:guides/how-to-redis.adoc#oauth2-authorization-code-grant-entity[Authorization Code Grant Entity (OAuth 2.0)]
+* xref:guides/how-to-redis.adoc#oidc-authorization-code-grant-entity[Authorization Code Grant Entity (OpenID Connect 1.0)]
+* xref:guides/how-to-redis.adoc#client-credentials-grant-entity[Client Credentials Grant Entity]
+* xref:guides/how-to-redis.adoc#device-code-grant-entity[Device Code Grant Entity]
+* xref:guides/how-to-redis.adoc#token-exchange-grant-entity[Token Exchange Grant Entity]
+* xref:guides/how-to-redis.adoc#authorization-consent-entity[Authorization Consent Entity]
+
+[[registered-client-entity]]
+=== Registered Client Entity
+
+The following listing shows the `OAuth2RegisteredClient` entity, which is used to persist information mapped from the xref:core-model-components.adoc#registered-client[`RegisteredClient`] domain class.
+
+.OAuth2RegisteredClient Entity
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/entity/OAuth2RegisteredClient.java[]
+----
+
+TIP: Click on the "Expand folded text" icon in the code sample above to display the full example.
+
+[[authorization-grant-entity]]
+=== Authorization Grant _Base_ Entity
+
+The entity model for the xref:core-model-components.adoc#oauth2-authorization[`OAuth2Authorization`] domain class is designed with a class hierarchy based on authorization grant type.
+
+The following listing shows the `OAuth2AuthorizationGrantAuthorization` _base_ entity, which defines common attributes for each authorization grant type.
+
+.OAuth2AuthorizationGrantAuthorization _Base_ Entity
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/entity/OAuth2AuthorizationGrantAuthorization.java[]
+----
+
+[[oauth2-authorization-code-grant-entity]]
+=== Authorization Code Grant Entity (OAuth 2.0)
+
+The following listing shows the `OAuth2AuthorizationCodeGrantAuthorization` entity, which extends `OAuth2AuthorizationGrantAuthorization`, and defines additional attributes for the OAuth 2.0 `authorization_code` grant type.
+
+.OAuth2AuthorizationCodeGrantAuthorization Entity
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/entity/OAuth2AuthorizationCodeGrantAuthorization.java[]
+----
+
+[[oidc-authorization-code-grant-entity]]
+=== Authorization Code Grant Entity (OpenID Connect 1.0)
+
+The following listing shows the `OidcAuthorizationCodeGrantAuthorization` entity, which extends `OAuth2AuthorizationCodeGrantAuthorization`, and defines additional attributes for the OpenID Connect 1.0 `authorization_code` grant type.
+
+.OidcAuthorizationCodeGrantAuthorization Entity
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/entity/OidcAuthorizationCodeGrantAuthorization.java[]
+----
+
+[[client-credentials-grant-entity]]
+=== Client Credentials Grant Entity
+
+The following listing shows the `OAuth2ClientCredentialsGrantAuthorization` entity, which extends `OAuth2AuthorizationGrantAuthorization`, for the `client_credentials` grant type.
+
+.OAuth2ClientCredentialsGrantAuthorization Entity
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/entity/OAuth2ClientCredentialsGrantAuthorization.java[]
+----
+
+[[device-code-grant-entity]]
+=== Device Code Grant Entity
+
+The following listing shows the `OAuth2DeviceCodeGrantAuthorization` entity, which extends `OAuth2AuthorizationGrantAuthorization`, and defines additional attributes for the `urn:ietf:params:oauth:grant-type:device_code` grant type.
+
+.OAuth2DeviceCodeGrantAuthorization Entity
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/entity/OAuth2DeviceCodeGrantAuthorization.java[]
+----
+
+[[token-exchange-grant-entity]]
+=== Token Exchange Grant Entity
+
+The following listing shows the `OAuth2TokenExchangeGrantAuthorization` entity, which extends `OAuth2AuthorizationGrantAuthorization`, for the `urn:ietf:params:oauth:grant-type:token-exchange` grant type.
+
+.OAuth2TokenExchangeGrantAuthorization Entity
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/entity/OAuth2TokenExchangeGrantAuthorization.java[]
+----
+
+[[authorization-consent-entity]]
+=== Authorization Consent Entity
+
+The following listing shows the `OAuth2UserConsent` entity, which is used to persist information mapped from the xref:core-model-components.adoc#oauth2-authorization-consent[`OAuth2AuthorizationConsent`] domain class.
+
+.OAuth2UserConsent Entity
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/entity/OAuth2UserConsent.java[]
+----
+
+[[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 Redis version of each interface.
+
+* xref:guides/how-to-redis.adoc#registered-client-repository[Registered Client Repository]
+* xref:guides/how-to-redis.adoc#authorization-grant-repository[Authorization Grant Repository]
+* xref:guides/how-to-redis.adoc#authorization-consent-repository[Authorization Consent Repository]
+
+[[registered-client-repository]]
+=== Registered Client Repository
+
+The following listing shows the `OAuth2RegisteredClientRepository`, which is able to find a xref:guides/how-to-redis.adoc#registered-client-entity[`OAuth2RegisteredClient`] by the `id` and `clientId` fields.
+
+.OAuth2RegisteredClientRepository
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/repository/OAuth2RegisteredClientRepository.java[]
+----
+
+[[authorization-grant-repository]]
+=== Authorization Grant Repository
+
+The following listing shows the `OAuth2AuthorizationGrantAuthorizationRepository`, which is able to find an xref:guides/how-to-redis.adoc#authorization-grant-entity[`OAuth2AuthorizationGrantAuthorization`] by the `id` field as well as by `state`, `authorizationCode`, `accessToken`, `refreshToken`, `idToken`, `deviceState`, `userCode` and `deviceCode` values.
+
+.OAuth2AuthorizationGrantAuthorizationRepository
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/repository/OAuth2AuthorizationGrantAuthorizationRepository.java[]
+----
+
+[[authorization-consent-repository]]
+=== Authorization Consent Repository
+
+The following listing shows the `OAuth2UserConsentRepository`, which is able to find and delete an xref:guides/how-to-redis.adoc#authorization-consent-entity[`OAuth2UserConsent`] by the `registeredClientId` and `principalName` fields that form the composite primary key.
+
+.OAuth2UserConsentRepository
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/repository/OAuth2UserConsentRepository.java[]
+----
+
+[[implement-core-services]]
+== Implement core services
+
+With the above xref:guides/how-to-redis.adoc#define-entity-model[entities] and xref:guides/how-to-redis.adoc#create-spring-data-repositories[repositories], we can begin implementing the core services.
+
+TIP: The core services make use of the `ModelMapper` utility class for converting to and from the domain object (e.g. `RegisteredClient`) to the entity model representation (e.g. `OAuth2RegisteredClient`).
+
+* xref:guides/how-to-redis.adoc#redis-registered-client-repository[Registered Client Repository]
+* xref:guides/how-to-redis.adoc#redis-authorization-service[Authorization Service]
+* xref:guides/how-to-redis.adoc#redis-authorization-consent-service[Authorization Consent Service]
+
+[[redis-registered-client-repository]]
+=== Registered Client Repository
+
+The following listing shows the `RedisRegisteredClientRepository`, which uses an xref:guides/how-to-redis.adoc#registered-client-repository[`OAuth2RegisteredClientRepository`] for persisting an xref:guides/how-to-redis.adoc#registered-client-entity[`OAuth2RegisteredClient`] and maps to and from the xref:core-model-components.adoc#registered-client[`RegisteredClient`] domain object, using the `ModelMapper` utility class.
+
+.RedisRegisteredClientRepository
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/service/RedisRegisteredClientRepository.java[]
+----
+
+[[redis-authorization-service]]
+=== Authorization Service
+
+The following listing shows the `RedisOAuth2AuthorizationService`, which uses an xref:guides/how-to-redis.adoc#authorization-grant-repository[`OAuth2AuthorizationGrantAuthorizationRepository`] for persisting an xref:guides/how-to-redis.adoc#authorization-grant-entity[`OAuth2AuthorizationGrantAuthorization`] and maps to and from the xref:core-model-components.adoc#oauth2-authorization[`OAuth2Authorization`] domain object, using the `ModelMapper` utility class.
+
+.RedisOAuth2AuthorizationService
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/service/RedisOAuth2AuthorizationService.java[]
+----
+
+[[redis-authorization-consent-service]]
+=== Authorization Consent Service
+
+The following listing shows the `RedisOAuth2AuthorizationConsentService`, which uses an xref:guides/how-to-redis.adoc#authorization-consent-repository[`OAuth2UserConsentRepository`] for persisting an xref:guides/how-to-redis.adoc#authorization-consent-entity[`OAuth2UserConsent`] and maps to and from the xref:core-model-components.adoc#oauth2-authorization-consent[`OAuth2AuthorizationConsent`] domain object, using the `ModelMapper` utility class.
+
+.RedisOAuth2AuthorizationConsentService
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/service/RedisOAuth2AuthorizationConsentService.java[]
+----
+
+[[configure-core-services]]
+== Configure core services
+
+The following example shows how to configure the core services:
+
+.RedisConfig
+[source,java]
+----
+include::{examples-dir}/main/java/sample/redis/config/RedisConfig.java[]
+----
+
+<1> Activate the Spring Data Redis repositories under the `sample.redis.repository` base package.
+<2> Use the https://docs.spring.io/spring-data/redis/reference/redis/drivers.html#redis:connectors:jedis[Jedis] Connector.
+<3> Register the custom ``Converter``'s that perform the Object-to-Hash conversion before persisting to Redis.
+<4> Register the `RedisRegisteredClientRepository` with the activated `OAuth2RegisteredClientRepository`.
+<5> Register the `RedisOAuth2AuthorizationService` with the activated `OAuth2AuthorizationGrantAuthorizationRepository`.
+<6> Register the `RedisOAuth2AuthorizationConsentService` with the activated `OAuth2UserConsentRepository`.

+ 8 - 0
docs/spring-authorization-server-docs.gradle

@@ -12,6 +12,10 @@ java {
 	sourceCompatibility = JavaVersion.VERSION_17
 	sourceCompatibility = JavaVersion.VERSION_17
 }
 }
 
 
+compileJava {
+	options.compilerArgs << '-parameters'
+}
+
 antora {
 antora {
 	options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
 	options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
 	environment = [
 	environment = [
@@ -57,6 +61,10 @@ dependencies {
 	implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
 	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-oauth2-resource-server"
 	implementation "org.springframework.boot:spring-boot-starter-data-jpa"
 	implementation "org.springframework.boot:spring-boot-starter-data-jpa"
+	implementation ("org.springframework.boot:spring-boot-starter-data-redis") {
+		exclude group: "io.lettuce", module: "lettuce-core"
+	}
+	implementation "redis.clients:jedis"
 	implementation "org.springframework:spring-webflux"
 	implementation "org.springframework:spring-webflux"
 	implementation project(":spring-security-oauth2-authorization-server")
 	implementation project(":spring-security-oauth2-authorization-server")
 	runtimeOnly "com.h2database:h2"
 	runtimeOnly "com.h2database:h2"

+ 85 - 0
docs/src/main/java/sample/redis/config/RedisConfig.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020-2024 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.redis.config;
+
+import java.util.Arrays;
+
+import sample.redis.convert.BytesToClaimsHolderConverter;
+import sample.redis.convert.BytesToOAuth2AuthorizationRequestConverter;
+import sample.redis.convert.BytesToUsernamePasswordAuthenticationTokenConverter;
+import sample.redis.convert.ClaimsHolderToBytesConverter;
+import sample.redis.convert.OAuth2AuthorizationRequestToBytesConverter;
+import sample.redis.convert.UsernamePasswordAuthenticationTokenToBytesConverter;
+import sample.redis.repository.OAuth2AuthorizationGrantAuthorizationRepository;
+import sample.redis.repository.OAuth2RegisteredClientRepository;
+import sample.redis.repository.OAuth2UserConsentRepository;
+import sample.redis.service.RedisOAuth2AuthorizationConsentService;
+import sample.redis.service.RedisOAuth2AuthorizationService;
+import sample.redis.service.RedisRegisteredClientRepository;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.convert.RedisCustomConversions;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+
+@EnableRedisRepositories("sample.redis.repository")	// <1>
+@Configuration(proxyBeanMethods = false)
+public class RedisConfig {
+
+	@Bean
+	public RedisConnectionFactory redisConnectionFactory() {
+		return new JedisConnectionFactory();	// <2>
+	}
+
+	@Bean
+	public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+		RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
+		redisTemplate.setConnectionFactory(redisConnectionFactory);
+		return redisTemplate;
+	}
+
+	@Bean
+	public RedisCustomConversions redisCustomConversions() {	// <3>
+		return new RedisCustomConversions(Arrays.asList(new UsernamePasswordAuthenticationTokenToBytesConverter(),
+				new BytesToUsernamePasswordAuthenticationTokenConverter(),
+				new OAuth2AuthorizationRequestToBytesConverter(), new BytesToOAuth2AuthorizationRequestConverter(),
+				new ClaimsHolderToBytesConverter(), new BytesToClaimsHolderConverter()));
+	}
+
+	@Bean
+	public RedisRegisteredClientRepository registeredClientRepository(
+			OAuth2RegisteredClientRepository registeredClientRepository) {
+		return new RedisRegisteredClientRepository(registeredClientRepository);	// <4>
+	}
+
+	@Bean
+	public RedisOAuth2AuthorizationService authorizationService(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationGrantAuthorizationRepository authorizationGrantAuthorizationRepository) {
+		return new RedisOAuth2AuthorizationService(registeredClientRepository,
+				authorizationGrantAuthorizationRepository);	// <5>
+	}
+
+	@Bean
+	public RedisOAuth2AuthorizationConsentService authorizationConsentService(
+			OAuth2UserConsentRepository userConsentRepository) {
+		return new RedisOAuth2AuthorizationConsentService(userConsentRepository);	// <6>
+	}
+
+}

+ 48 - 0
docs/src/main/java/sample/redis/convert/BytesToClaimsHolderConverter.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020-2024 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.redis.convert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.ReadingConverter;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+
+@ReadingConverter
+public class BytesToClaimsHolderConverter
+		implements Converter<byte[], OAuth2AuthorizationGrantAuthorization.ClaimsHolder> {
+
+	private final Jackson2JsonRedisSerializer<OAuth2AuthorizationGrantAuthorization.ClaimsHolder> serializer;
+
+	public BytesToClaimsHolderConverter() {
+		ObjectMapper objectMapper = new ObjectMapper();
+		objectMapper
+			.registerModules(SecurityJackson2Modules.getModules(BytesToClaimsHolderConverter.class.getClassLoader()));
+		objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+		objectMapper.addMixIn(OAuth2AuthorizationGrantAuthorization.ClaimsHolder.class, ClaimsHolderMixin.class);
+		this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper,
+				OAuth2AuthorizationGrantAuthorization.ClaimsHolder.class);
+	}
+
+	@Override
+	public OAuth2AuthorizationGrantAuthorization.ClaimsHolder convert(byte[] value) {
+		return this.serializer.deserialize(value);
+	}
+
+}

+ 45 - 0
docs/src/main/java/sample/redis/convert/BytesToOAuth2AuthorizationRequestConverter.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020-2024 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.redis.convert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.ReadingConverter;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+
+@ReadingConverter
+public class BytesToOAuth2AuthorizationRequestConverter implements Converter<byte[], OAuth2AuthorizationRequest> {
+
+	private final Jackson2JsonRedisSerializer<OAuth2AuthorizationRequest> serializer;
+
+	public BytesToOAuth2AuthorizationRequestConverter() {
+		ObjectMapper objectMapper = new ObjectMapper();
+		objectMapper.registerModules(
+				SecurityJackson2Modules.getModules(BytesToOAuth2AuthorizationRequestConverter.class.getClassLoader()));
+		objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
+		this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper, OAuth2AuthorizationRequest.class);
+	}
+
+	@Override
+	public OAuth2AuthorizationRequest convert(byte[] value) {
+		return this.serializer.deserialize(value);
+	}
+
+}

+ 44 - 0
docs/src/main/java/sample/redis/convert/BytesToUsernamePasswordAuthenticationTokenConverter.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020-2024 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.redis.convert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.ReadingConverter;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+
+@ReadingConverter
+public class BytesToUsernamePasswordAuthenticationTokenConverter
+		implements Converter<byte[], UsernamePasswordAuthenticationToken> {
+
+	private final Jackson2JsonRedisSerializer<UsernamePasswordAuthenticationToken> serializer;
+
+	public BytesToUsernamePasswordAuthenticationTokenConverter() {
+		ObjectMapper objectMapper = new ObjectMapper();
+		objectMapper.registerModules(SecurityJackson2Modules
+			.getModules(BytesToUsernamePasswordAuthenticationTokenConverter.class.getClassLoader()));
+		this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper, UsernamePasswordAuthenticationToken.class);
+	}
+
+	@Override
+	public UsernamePasswordAuthenticationToken convert(byte[] value) {
+		return this.serializer.deserialize(value);
+	}
+
+}

+ 36 - 0
docs/src/main/java/sample/redis/convert/ClaimsHolderMixin.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020-2024 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.redis.convert;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+abstract class ClaimsHolderMixin {
+
+	@JsonCreator
+	ClaimsHolderMixin(@JsonProperty("claims") Map<String, Object> claims) {
+	}
+
+}

+ 48 - 0
docs/src/main/java/sample/redis/convert/ClaimsHolderToBytesConverter.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020-2024 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.redis.convert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.WritingConverter;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+
+@WritingConverter
+public class ClaimsHolderToBytesConverter
+		implements Converter<OAuth2AuthorizationGrantAuthorization.ClaimsHolder, byte[]> {
+
+	private final Jackson2JsonRedisSerializer<OAuth2AuthorizationGrantAuthorization.ClaimsHolder> serializer;
+
+	public ClaimsHolderToBytesConverter() {
+		ObjectMapper objectMapper = new ObjectMapper();
+		objectMapper
+			.registerModules(SecurityJackson2Modules.getModules(ClaimsHolderToBytesConverter.class.getClassLoader()));
+		objectMapper.registerModules(new OAuth2AuthorizationServerJackson2Module());
+		objectMapper.addMixIn(OAuth2AuthorizationGrantAuthorization.ClaimsHolder.class, ClaimsHolderMixin.class);
+		this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper,
+				OAuth2AuthorizationGrantAuthorization.ClaimsHolder.class);
+	}
+
+	@Override
+	public byte[] convert(OAuth2AuthorizationGrantAuthorization.ClaimsHolder value) {
+		return this.serializer.serialize(value);
+	}
+
+}

+ 45 - 0
docs/src/main/java/sample/redis/convert/OAuth2AuthorizationRequestToBytesConverter.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020-2024 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.redis.convert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.WritingConverter;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
+
+@WritingConverter
+public class OAuth2AuthorizationRequestToBytesConverter implements Converter<OAuth2AuthorizationRequest, byte[]> {
+
+	private final Jackson2JsonRedisSerializer<OAuth2AuthorizationRequest> serializer;
+
+	public OAuth2AuthorizationRequestToBytesConverter() {
+		ObjectMapper objectMapper = new ObjectMapper();
+		objectMapper.registerModules(
+				SecurityJackson2Modules.getModules(OAuth2AuthorizationRequestToBytesConverter.class.getClassLoader()));
+		objectMapper.registerModules(new OAuth2AuthorizationServerJackson2Module());
+		this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper, OAuth2AuthorizationRequest.class);
+	}
+
+	@Override
+	public byte[] convert(OAuth2AuthorizationRequest value) {
+		return this.serializer.serialize(value);
+	}
+
+}

+ 44 - 0
docs/src/main/java/sample/redis/convert/UsernamePasswordAuthenticationTokenToBytesConverter.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020-2024 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.redis.convert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.WritingConverter;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+
+@WritingConverter
+public class UsernamePasswordAuthenticationTokenToBytesConverter
+		implements Converter<UsernamePasswordAuthenticationToken, byte[]> {
+
+	private final Jackson2JsonRedisSerializer<UsernamePasswordAuthenticationToken> serializer;
+
+	public UsernamePasswordAuthenticationTokenToBytesConverter() {
+		ObjectMapper objectMapper = new ObjectMapper();
+		objectMapper.registerModules(SecurityJackson2Modules
+			.getModules(BytesToUsernamePasswordAuthenticationTokenConverter.class.getClassLoader()));
+		this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper, UsernamePasswordAuthenticationToken.class);
+	}
+
+	@Override
+	public byte[] convert(UsernamePasswordAuthenticationToken value) {
+		return this.serializer.serialize(value);
+	}
+
+}

+ 73 - 0
docs/src/main/java/sample/redis/entity/OAuth2AuthorizationCodeGrantAuthorization.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020-2024 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.redis.entity;
+
+import java.security.Principal;
+import java.time.Instant;
+import java.util.Set;
+
+import org.springframework.data.redis.core.index.Indexed;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+
+public class OAuth2AuthorizationCodeGrantAuthorization extends OAuth2AuthorizationGrantAuthorization {
+
+	private final Principal principal;
+
+	private final OAuth2AuthorizationRequest authorizationRequest;
+
+	private final AuthorizationCode authorizationCode;
+
+	@Indexed
+	private final String state; // Used to correlate the request during the authorization
+								// consent flow
+
+	// @fold:on
+	public OAuth2AuthorizationCodeGrantAuthorization(String id, String registeredClientId, String principalName,
+			Set<String> authorizedScopes, AccessToken accessToken, RefreshToken refreshToken, Principal principal,
+			OAuth2AuthorizationRequest authorizationRequest, AuthorizationCode authorizationCode, String state) {
+		super(id, registeredClientId, principalName, authorizedScopes, accessToken, refreshToken);
+		this.principal = principal;
+		this.authorizationRequest = authorizationRequest;
+		this.authorizationCode = authorizationCode;
+		this.state = state;
+	}
+
+	public Principal getPrincipal() {
+		return this.principal;
+	}
+
+	public OAuth2AuthorizationRequest getAuthorizationRequest() {
+		return this.authorizationRequest;
+	}
+
+	public AuthorizationCode getAuthorizationCode() {
+		return this.authorizationCode;
+	}
+
+	public String getState() {
+		return this.state;
+	}
+
+	public static class AuthorizationCode extends AbstractToken {
+
+		public AuthorizationCode(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
+			super(tokenValue, issuedAt, expiresAt, invalidated);
+		}
+
+	}
+	// @fold:off
+
+}

+ 176 - 0
docs/src/main/java/sample/redis/entity/OAuth2AuthorizationGrantAuthorization.java

@@ -0,0 +1,176 @@
+/*
+ * Copyright 2020-2024 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.redis.entity;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+
+@RedisHash("oauth2_authorization")
+public abstract class OAuth2AuthorizationGrantAuthorization {
+
+	@Id
+	private final String id;
+
+	private final String registeredClientId;
+
+	private final String principalName;
+
+	private final Set<String> authorizedScopes;
+
+	private final AccessToken accessToken;
+
+	private final RefreshToken refreshToken;
+
+	// @fold:on
+	protected OAuth2AuthorizationGrantAuthorization(String id, String registeredClientId, String principalName,
+			Set<String> authorizedScopes, AccessToken accessToken, RefreshToken refreshToken) {
+		this.id = id;
+		this.registeredClientId = registeredClientId;
+		this.principalName = principalName;
+		this.authorizedScopes = authorizedScopes;
+		this.accessToken = accessToken;
+		this.refreshToken = refreshToken;
+	}
+
+	public String getId() {
+		return this.id;
+	}
+
+	public String getRegisteredClientId() {
+		return this.registeredClientId;
+	}
+
+	public String getPrincipalName() {
+		return this.principalName;
+	}
+
+	public Set<String> getAuthorizedScopes() {
+		return this.authorizedScopes;
+	}
+
+	public AccessToken getAccessToken() {
+		return this.accessToken;
+	}
+
+	public RefreshToken getRefreshToken() {
+		return this.refreshToken;
+	}
+
+	protected abstract static class AbstractToken {
+
+		@Indexed
+		private final String tokenValue;
+
+		private final Instant issuedAt;
+
+		private final Instant expiresAt;
+
+		private final boolean invalidated;
+
+		protected AbstractToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
+			this.tokenValue = tokenValue;
+			this.issuedAt = issuedAt;
+			this.expiresAt = expiresAt;
+			this.invalidated = invalidated;
+		}
+
+		public String getTokenValue() {
+			return this.tokenValue;
+		}
+
+		public Instant getIssuedAt() {
+			return this.issuedAt;
+		}
+
+		public Instant getExpiresAt() {
+			return this.expiresAt;
+		}
+
+		public boolean isInvalidated() {
+			return this.invalidated;
+		}
+
+	}
+
+	public static class ClaimsHolder {
+
+		private final Map<String, Object> claims;
+
+		public ClaimsHolder(Map<String, Object> claims) {
+			this.claims = claims;
+		}
+
+		public Map<String, Object> getClaims() {
+			return this.claims;
+		}
+
+	}
+
+	public static class AccessToken extends AbstractToken {
+
+		private final OAuth2AccessToken.TokenType tokenType;
+
+		private final Set<String> scopes;
+
+		private final OAuth2TokenFormat tokenFormat;
+
+		private final ClaimsHolder claims;
+
+		public AccessToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated,
+				OAuth2AccessToken.TokenType tokenType, Set<String> scopes, OAuth2TokenFormat tokenFormat,
+				ClaimsHolder claims) {
+			super(tokenValue, issuedAt, expiresAt, invalidated);
+			this.tokenType = tokenType;
+			this.scopes = scopes;
+			this.tokenFormat = tokenFormat;
+			this.claims = claims;
+		}
+
+		public OAuth2AccessToken.TokenType getTokenType() {
+			return this.tokenType;
+		}
+
+		public Set<String> getScopes() {
+			return this.scopes;
+		}
+
+		public OAuth2TokenFormat getTokenFormat() {
+			return this.tokenFormat;
+		}
+
+		public ClaimsHolder getClaims() {
+			return this.claims;
+		}
+
+	}
+
+	public static class RefreshToken extends AbstractToken {
+
+		public RefreshToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
+			super(tokenValue, issuedAt, expiresAt, invalidated);
+		}
+
+	}
+	// @fold:off
+
+}

+ 29 - 0
docs/src/main/java/sample/redis/entity/OAuth2ClientCredentialsGrantAuthorization.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020-2024 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.redis.entity;
+
+import java.util.Set;
+
+public class OAuth2ClientCredentialsGrantAuthorization extends OAuth2AuthorizationGrantAuthorization {
+
+	// @fold:on
+	public OAuth2ClientCredentialsGrantAuthorization(String id, String registeredClientId, String principalName,
+			Set<String> authorizedScopes, AccessToken accessToken) {
+		super(id, registeredClientId, principalName, authorizedScopes, accessToken, null);
+	}
+	// @fold:off
+
+}

+ 87 - 0
docs/src/main/java/sample/redis/entity/OAuth2DeviceCodeGrantAuthorization.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020-2024 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.redis.entity;
+
+import java.security.Principal;
+import java.time.Instant;
+import java.util.Set;
+
+import org.springframework.data.redis.core.index.Indexed;
+
+public class OAuth2DeviceCodeGrantAuthorization extends OAuth2AuthorizationGrantAuthorization {
+
+	private final Principal principal;
+
+	private final DeviceCode deviceCode;
+
+	private final UserCode userCode;
+
+	private final Set<String> requestedScopes;
+
+	@Indexed
+	private final String deviceState; // Used to correlate the request during the
+										// authorization consent flow
+
+	// @fold:on
+	public OAuth2DeviceCodeGrantAuthorization(String id, String registeredClientId, String principalName,
+			Set<String> authorizedScopes, AccessToken accessToken, RefreshToken refreshToken, Principal principal,
+			DeviceCode deviceCode, UserCode userCode, Set<String> requestedScopes, String deviceState) {
+		super(id, registeredClientId, principalName, authorizedScopes, accessToken, refreshToken);
+		this.principal = principal;
+		this.deviceCode = deviceCode;
+		this.userCode = userCode;
+		this.requestedScopes = requestedScopes;
+		this.deviceState = deviceState;
+	}
+
+	public Principal getPrincipal() {
+		return this.principal;
+	}
+
+	public DeviceCode getDeviceCode() {
+		return this.deviceCode;
+	}
+
+	public UserCode getUserCode() {
+		return this.userCode;
+	}
+
+	public Set<String> getRequestedScopes() {
+		return this.requestedScopes;
+	}
+
+	public String getDeviceState() {
+		return this.deviceState;
+	}
+
+	public static class DeviceCode extends AbstractToken {
+
+		public DeviceCode(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
+			super(tokenValue, issuedAt, expiresAt, invalidated);
+		}
+
+	}
+
+	public static class UserCode extends AbstractToken {
+
+		public UserCode(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
+			super(tokenValue, issuedAt, expiresAt, invalidated);
+		}
+
+	}
+	// @fold:off
+
+}

+ 246 - 0
docs/src/main/java/sample/redis/entity/OAuth2RegisteredClient.java

@@ -0,0 +1,246 @@
+/*
+ * Copyright 2020-2024 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.redis.entity;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Set;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+
+@RedisHash("oauth2_registered_client")
+public class OAuth2RegisteredClient {
+
+	@Id
+	private final String id;
+
+	@Indexed
+	private final String clientId;
+
+	private final Instant clientIdIssuedAt;
+
+	private final String clientSecret;
+
+	private final Instant clientSecretExpiresAt;
+
+	private final String clientName;
+
+	private final Set<ClientAuthenticationMethod> clientAuthenticationMethods;
+
+	private final Set<AuthorizationGrantType> authorizationGrantTypes;
+
+	private final Set<String> redirectUris;
+
+	private final Set<String> postLogoutRedirectUris;
+
+	private final Set<String> scopes;
+
+	private final ClientSettings clientSettings;
+
+	private final TokenSettings tokenSettings;
+
+	// @fold:on
+	public OAuth2RegisteredClient(String id, String clientId, Instant clientIdIssuedAt, String clientSecret,
+			Instant clientSecretExpiresAt, String clientName,
+			Set<ClientAuthenticationMethod> clientAuthenticationMethods,
+			Set<AuthorizationGrantType> authorizationGrantTypes, Set<String> redirectUris,
+			Set<String> postLogoutRedirectUris, Set<String> scopes, ClientSettings clientSettings,
+			TokenSettings tokenSettings) {
+		this.id = id;
+		this.clientId = clientId;
+		this.clientIdIssuedAt = clientIdIssuedAt;
+		this.clientSecret = clientSecret;
+		this.clientSecretExpiresAt = clientSecretExpiresAt;
+		this.clientName = clientName;
+		this.clientAuthenticationMethods = clientAuthenticationMethods;
+		this.authorizationGrantTypes = authorizationGrantTypes;
+		this.redirectUris = redirectUris;
+		this.postLogoutRedirectUris = postLogoutRedirectUris;
+		this.scopes = scopes;
+		this.clientSettings = clientSettings;
+		this.tokenSettings = tokenSettings;
+	}
+
+	public String getId() {
+		return this.id;
+	}
+
+	public String getClientId() {
+		return this.clientId;
+	}
+
+	public Instant getClientIdIssuedAt() {
+		return this.clientIdIssuedAt;
+	}
+
+	public String getClientSecret() {
+		return this.clientSecret;
+	}
+
+	public Instant getClientSecretExpiresAt() {
+		return this.clientSecretExpiresAt;
+	}
+
+	public String getClientName() {
+		return this.clientName;
+	}
+
+	public Set<ClientAuthenticationMethod> getClientAuthenticationMethods() {
+		return this.clientAuthenticationMethods;
+	}
+
+	public Set<AuthorizationGrantType> getAuthorizationGrantTypes() {
+		return this.authorizationGrantTypes;
+	}
+
+	public Set<String> getRedirectUris() {
+		return this.redirectUris;
+	}
+
+	public Set<String> getPostLogoutRedirectUris() {
+		return this.postLogoutRedirectUris;
+	}
+
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+	public ClientSettings getClientSettings() {
+		return this.clientSettings;
+	}
+
+	public TokenSettings getTokenSettings() {
+		return this.tokenSettings;
+	}
+
+	public static class ClientSettings {
+
+		private final boolean requireProofKey;
+
+		private final boolean requireAuthorizationConsent;
+
+		private final String jwkSetUrl;
+
+		private final JwsAlgorithm tokenEndpointAuthenticationSigningAlgorithm;
+
+		private final String x509CertificateSubjectDN;
+
+		public ClientSettings(boolean requireProofKey, boolean requireAuthorizationConsent, String jwkSetUrl,
+				JwsAlgorithm tokenEndpointAuthenticationSigningAlgorithm, String x509CertificateSubjectDN) {
+			this.requireProofKey = requireProofKey;
+			this.requireAuthorizationConsent = requireAuthorizationConsent;
+			this.jwkSetUrl = jwkSetUrl;
+			this.tokenEndpointAuthenticationSigningAlgorithm = tokenEndpointAuthenticationSigningAlgorithm;
+			this.x509CertificateSubjectDN = x509CertificateSubjectDN;
+		}
+
+		public boolean isRequireProofKey() {
+			return this.requireProofKey;
+		}
+
+		public boolean isRequireAuthorizationConsent() {
+			return this.requireAuthorizationConsent;
+		}
+
+		public String getJwkSetUrl() {
+			return this.jwkSetUrl;
+		}
+
+		public JwsAlgorithm getTokenEndpointAuthenticationSigningAlgorithm() {
+			return this.tokenEndpointAuthenticationSigningAlgorithm;
+		}
+
+		public String getX509CertificateSubjectDN() {
+			return this.x509CertificateSubjectDN;
+		}
+
+	}
+
+	public static class TokenSettings {
+
+		private final Duration authorizationCodeTimeToLive;
+
+		private final Duration accessTokenTimeToLive;
+
+		private final OAuth2TokenFormat accessTokenFormat;
+
+		private final Duration deviceCodeTimeToLive;
+
+		private final boolean reuseRefreshTokens;
+
+		private final Duration refreshTokenTimeToLive;
+
+		private final SignatureAlgorithm idTokenSignatureAlgorithm;
+
+		private final boolean x509CertificateBoundAccessTokens;
+
+		public TokenSettings(Duration authorizationCodeTimeToLive, Duration accessTokenTimeToLive,
+				OAuth2TokenFormat accessTokenFormat, Duration deviceCodeTimeToLive, boolean reuseRefreshTokens,
+				Duration refreshTokenTimeToLive, SignatureAlgorithm idTokenSignatureAlgorithm,
+				boolean x509CertificateBoundAccessTokens) {
+			this.authorizationCodeTimeToLive = authorizationCodeTimeToLive;
+			this.accessTokenTimeToLive = accessTokenTimeToLive;
+			this.accessTokenFormat = accessTokenFormat;
+			this.deviceCodeTimeToLive = deviceCodeTimeToLive;
+			this.reuseRefreshTokens = reuseRefreshTokens;
+			this.refreshTokenTimeToLive = refreshTokenTimeToLive;
+			this.idTokenSignatureAlgorithm = idTokenSignatureAlgorithm;
+			this.x509CertificateBoundAccessTokens = x509CertificateBoundAccessTokens;
+		}
+
+		public Duration getAuthorizationCodeTimeToLive() {
+			return this.authorizationCodeTimeToLive;
+		}
+
+		public Duration getAccessTokenTimeToLive() {
+			return this.accessTokenTimeToLive;
+		}
+
+		public OAuth2TokenFormat getAccessTokenFormat() {
+			return this.accessTokenFormat;
+		}
+
+		public Duration getDeviceCodeTimeToLive() {
+			return this.deviceCodeTimeToLive;
+		}
+
+		public boolean isReuseRefreshTokens() {
+			return this.reuseRefreshTokens;
+		}
+
+		public Duration getRefreshTokenTimeToLive() {
+			return this.refreshTokenTimeToLive;
+		}
+
+		public SignatureAlgorithm getIdTokenSignatureAlgorithm() {
+			return this.idTokenSignatureAlgorithm;
+		}
+
+		public boolean isX509CertificateBoundAccessTokens() {
+			return this.x509CertificateBoundAccessTokens;
+		}
+
+	}
+	// @fold:off
+
+}

+ 29 - 0
docs/src/main/java/sample/redis/entity/OAuth2TokenExchangeGrantAuthorization.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020-2024 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.redis.entity;
+
+import java.util.Set;
+
+public class OAuth2TokenExchangeGrantAuthorization extends OAuth2AuthorizationGrantAuthorization {
+
+	// @fold:on
+	public OAuth2TokenExchangeGrantAuthorization(String id, String registeredClientId, String principalName,
+			Set<String> authorizedScopes, AccessToken accessToken) {
+		super(id, registeredClientId, principalName, authorizedScopes, accessToken, null);
+	}
+	// @fold:off
+
+}

+ 65 - 0
docs/src/main/java/sample/redis/entity/OAuth2UserConsent.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020-2024 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.redis.entity;
+
+import java.util.Set;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+import org.springframework.security.core.GrantedAuthority;
+
+@RedisHash("oauth2_authorization_consent")
+public class OAuth2UserConsent {
+
+	@Id
+	private final String id;
+
+	@Indexed
+	private final String registeredClientId;
+
+	@Indexed
+	private final String principalName;
+
+	private final Set<GrantedAuthority> authorities;
+
+	// @fold:on
+	public OAuth2UserConsent(String id, String registeredClientId, String principalName,
+			Set<GrantedAuthority> authorities) {
+		this.id = id;
+		this.registeredClientId = registeredClientId;
+		this.principalName = principalName;
+		this.authorities = authorities;
+	}
+
+	public String getId() {
+		return this.id;
+	}
+
+	public String getRegisteredClientId() {
+		return this.registeredClientId;
+	}
+
+	public String getPrincipalName() {
+		return this.principalName;
+	}
+
+	public Set<GrantedAuthority> getAuthorities() {
+		return this.authorities;
+	}
+	// @fold:off
+
+}

+ 59 - 0
docs/src/main/java/sample/redis/entity/OidcAuthorizationCodeGrantAuthorization.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020-2024 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.redis.entity;
+
+import java.security.Principal;
+import java.time.Instant;
+import java.util.Set;
+
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+
+public class OidcAuthorizationCodeGrantAuthorization extends OAuth2AuthorizationCodeGrantAuthorization {
+
+	private final IdToken idToken;
+
+	// @fold:on
+	public OidcAuthorizationCodeGrantAuthorization(String id, String registeredClientId, String principalName,
+			Set<String> authorizedScopes, AccessToken accessToken, RefreshToken refreshToken, Principal principal,
+			OAuth2AuthorizationRequest authorizationRequest, AuthorizationCode authorizationCode, String state,
+			IdToken idToken) {
+		super(id, registeredClientId, principalName, authorizedScopes, accessToken, refreshToken, principal,
+				authorizationRequest, authorizationCode, state);
+		this.idToken = idToken;
+	}
+
+	public IdToken getIdToken() {
+		return this.idToken;
+	}
+
+	public static class IdToken extends AbstractToken {
+
+		private final ClaimsHolder claims;
+
+		public IdToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated,
+				ClaimsHolder claims) {
+			super(tokenValue, issuedAt, expiresAt, invalidated);
+			this.claims = claims;
+		}
+
+		public ClaimsHolder getClaims() {
+			return this.claims;
+		}
+
+	}
+	// @fold:off
+
+}

+ 50 - 0
docs/src/main/java/sample/redis/repository/OAuth2AuthorizationGrantAuthorizationRepository.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020-2024 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.redis.repository;
+
+import sample.redis.entity.OAuth2AuthorizationCodeGrantAuthorization;
+import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
+import sample.redis.entity.OAuth2DeviceCodeGrantAuthorization;
+import sample.redis.entity.OidcAuthorizationCodeGrantAuthorization;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface OAuth2AuthorizationGrantAuthorizationRepository
+		extends CrudRepository<OAuth2AuthorizationGrantAuthorization, String> {
+
+	<T extends OAuth2AuthorizationCodeGrantAuthorization> T findByState(String token);
+
+	<T extends OAuth2AuthorizationCodeGrantAuthorization> T findByAuthorizationCode_TokenValue(String token);
+
+	<T extends OAuth2AuthorizationGrantAuthorization> T findByAccessToken_TokenValue(String token);
+
+	<T extends OAuth2AuthorizationGrantAuthorization> T findByRefreshToken_TokenValue(String token);
+
+	<T extends OidcAuthorizationCodeGrantAuthorization> T findByIdToken_TokenValue(String token);
+
+	<T extends OAuth2DeviceCodeGrantAuthorization> T findByDeviceState(String token);
+
+	<T extends OAuth2DeviceCodeGrantAuthorization> T findByDeviceCode_TokenValue(String token);
+
+	<T extends OAuth2DeviceCodeGrantAuthorization> T findByUserCode_TokenValue(String token);
+
+	<T extends OAuth2AuthorizationGrantAuthorization> T findByStateOrAuthorizationCode_TokenValueOrAccessToken_TokenValueOrRefreshToken_TokenValueOrIdToken_TokenValueOrDeviceStateOrDeviceCode_TokenValueOrUserCode_TokenValue(
+			String state, String authorizationCode, String accessToken, String refreshToken, String idToken,
+			String deviceState, String deviceCode, String userCode);
+
+}

+ 28 - 0
docs/src/main/java/sample/redis/repository/OAuth2RegisteredClientRepository.java

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020-2024 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.redis.repository;
+
+import sample.redis.entity.OAuth2RegisteredClient;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface OAuth2RegisteredClientRepository extends CrudRepository<OAuth2RegisteredClient, String> {
+
+	OAuth2RegisteredClient findByClientId(String clientId);
+
+}

+ 30 - 0
docs/src/main/java/sample/redis/repository/OAuth2UserConsentRepository.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020-2024 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.redis.repository;
+
+import sample.redis.entity.OAuth2UserConsent;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface OAuth2UserConsentRepository extends CrudRepository<OAuth2UserConsent, String> {
+
+	OAuth2UserConsent findByRegisteredClientIdAndPrincipalName(String registeredClientId, String principalName);
+
+	void deleteByRegisteredClientIdAndPrincipalName(String registeredClientId, String principalName);
+
+}

+ 498 - 0
docs/src/main/java/sample/redis/service/ModelMapper.java

@@ -0,0 +1,498 @@
+/*
+ * Copyright 2020-2024 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.redis.service;
+
+import java.security.Principal;
+
+import sample.redis.entity.OAuth2AuthorizationCodeGrantAuthorization;
+import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
+import sample.redis.entity.OAuth2ClientCredentialsGrantAuthorization;
+import sample.redis.entity.OAuth2DeviceCodeGrantAuthorization;
+import sample.redis.entity.OAuth2RegisteredClient;
+import sample.redis.entity.OAuth2TokenExchangeGrantAuthorization;
+import sample.redis.entity.OAuth2UserConsent;
+import sample.redis.entity.OidcAuthorizationCodeGrantAuthorization;
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2DeviceCode;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+final class ModelMapper {
+
+	static OAuth2RegisteredClient convertOAuth2RegisteredClient(RegisteredClient registeredClient) {
+		OAuth2RegisteredClient.ClientSettings clientSettings = new OAuth2RegisteredClient.ClientSettings(
+				registeredClient.getClientSettings().isRequireProofKey(),
+				registeredClient.getClientSettings().isRequireAuthorizationConsent(),
+				registeredClient.getClientSettings().getJwkSetUrl(),
+				registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm(),
+				registeredClient.getClientSettings().getX509CertificateSubjectDN());
+
+		OAuth2RegisteredClient.TokenSettings tokenSettings = new OAuth2RegisteredClient.TokenSettings(
+				registeredClient.getTokenSettings().getAuthorizationCodeTimeToLive(),
+				registeredClient.getTokenSettings().getAccessTokenTimeToLive(),
+				registeredClient.getTokenSettings().getAccessTokenFormat(),
+				registeredClient.getTokenSettings().getDeviceCodeTimeToLive(),
+				registeredClient.getTokenSettings().isReuseRefreshTokens(),
+				registeredClient.getTokenSettings().getRefreshTokenTimeToLive(),
+				registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm(),
+				registeredClient.getTokenSettings().isX509CertificateBoundAccessTokens());
+
+		return new OAuth2RegisteredClient(registeredClient.getId(), registeredClient.getClientId(),
+				registeredClient.getClientIdIssuedAt(), registeredClient.getClientSecret(),
+				registeredClient.getClientSecretExpiresAt(), registeredClient.getClientName(),
+				registeredClient.getClientAuthenticationMethods(), registeredClient.getAuthorizationGrantTypes(),
+				registeredClient.getRedirectUris(), registeredClient.getPostLogoutRedirectUris(),
+				registeredClient.getScopes(), clientSettings, tokenSettings);
+	}
+
+	static OAuth2UserConsent convertOAuth2UserConsent(OAuth2AuthorizationConsent authorizationConsent) {
+		String id = authorizationConsent.getRegisteredClientId()
+			.concat("-")
+			.concat(authorizationConsent.getPrincipalName());
+		return new OAuth2UserConsent(id, authorizationConsent.getRegisteredClientId(),
+				authorizationConsent.getPrincipalName(), authorizationConsent.getAuthorities());
+	}
+
+	static OAuth2AuthorizationGrantAuthorization convertOAuth2AuthorizationGrantAuthorization(
+			OAuth2Authorization authorization) {
+
+		if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorization.getAuthorizationGrantType())) {
+			OAuth2AuthorizationRequest authorizationRequest = authorization
+				.getAttribute(OAuth2AuthorizationRequest.class.getName());
+			return authorizationRequest.getScopes().contains(OidcScopes.OPENID)
+					? convertOidcAuthorizationCodeGrantAuthorization(authorization)
+					: convertOAuth2AuthorizationCodeGrantAuthorization(authorization);
+		}
+		else if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(authorization.getAuthorizationGrantType())) {
+			return convertOAuth2ClientCredentialsGrantAuthorization(authorization);
+		}
+		else if (AuthorizationGrantType.DEVICE_CODE.equals(authorization.getAuthorizationGrantType())) {
+			return convertOAuth2DeviceCodeGrantAuthorization(authorization);
+		}
+		else if (AuthorizationGrantType.TOKEN_EXCHANGE.equals(authorization.getAuthorizationGrantType())) {
+			return convertOAuth2TokenExchangeGrantAuthorization(authorization);
+		}
+		return null;
+	}
+
+	static OidcAuthorizationCodeGrantAuthorization convertOidcAuthorizationCodeGrantAuthorization(
+			OAuth2Authorization authorization) {
+		OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode authorizationCode = extractAuthorizationCode(
+				authorization);
+		OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
+		OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken = extractRefreshToken(authorization);
+		OidcAuthorizationCodeGrantAuthorization.IdToken idToken = extractIdToken(authorization);
+
+		return new OidcAuthorizationCodeGrantAuthorization(authorization.getId(), authorization.getRegisteredClientId(),
+				authorization.getPrincipalName(), authorization.getAuthorizedScopes(), accessToken, refreshToken,
+				authorization.getAttribute(Principal.class.getName()),
+				authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()), authorizationCode,
+				authorization.getAttribute(OAuth2ParameterNames.STATE), idToken);
+	}
+
+	static OAuth2AuthorizationCodeGrantAuthorization convertOAuth2AuthorizationCodeGrantAuthorization(
+			OAuth2Authorization authorization) {
+
+		OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode authorizationCode = extractAuthorizationCode(
+				authorization);
+		OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
+		OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken = extractRefreshToken(authorization);
+
+		return new OAuth2AuthorizationCodeGrantAuthorization(authorization.getId(),
+				authorization.getRegisteredClientId(), authorization.getPrincipalName(),
+				authorization.getAuthorizedScopes(), accessToken, refreshToken,
+				authorization.getAttribute(Principal.class.getName()),
+				authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()), authorizationCode,
+				authorization.getAttribute(OAuth2ParameterNames.STATE));
+	}
+
+	static OAuth2ClientCredentialsGrantAuthorization convertOAuth2ClientCredentialsGrantAuthorization(
+			OAuth2Authorization authorization) {
+
+		OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
+
+		return new OAuth2ClientCredentialsGrantAuthorization(authorization.getId(),
+				authorization.getRegisteredClientId(), authorization.getPrincipalName(),
+				authorization.getAuthorizedScopes(), accessToken);
+	}
+
+	static OAuth2DeviceCodeGrantAuthorization convertOAuth2DeviceCodeGrantAuthorization(
+			OAuth2Authorization authorization) {
+
+		OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
+		OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken = extractRefreshToken(authorization);
+		OAuth2DeviceCodeGrantAuthorization.DeviceCode deviceCode = extractDeviceCode(authorization);
+		OAuth2DeviceCodeGrantAuthorization.UserCode userCode = extractUserCode(authorization);
+
+		return new OAuth2DeviceCodeGrantAuthorization(authorization.getId(), authorization.getRegisteredClientId(),
+				authorization.getPrincipalName(), authorization.getAuthorizedScopes(), accessToken, refreshToken,
+				authorization.getAttribute(Principal.class.getName()), deviceCode, userCode,
+				authorization.getAttribute(OAuth2ParameterNames.SCOPE),
+				authorization.getAttribute(OAuth2ParameterNames.STATE));
+	}
+
+	static OAuth2TokenExchangeGrantAuthorization convertOAuth2TokenExchangeGrantAuthorization(
+			OAuth2Authorization authorization) {
+
+		OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
+
+		return new OAuth2TokenExchangeGrantAuthorization(authorization.getId(), authorization.getRegisteredClientId(),
+				authorization.getPrincipalName(), authorization.getAuthorizedScopes(), accessToken);
+	}
+
+	static OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode extractAuthorizationCode(
+			OAuth2Authorization authorization) {
+		OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode authorizationCode = null;
+		if (authorization.getToken(OAuth2AuthorizationCode.class) != null) {
+			OAuth2Authorization.Token<OAuth2AuthorizationCode> oauth2AuthorizationCode = authorization
+				.getToken(OAuth2AuthorizationCode.class);
+			authorizationCode = new OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode(
+					oauth2AuthorizationCode.getToken().getTokenValue(),
+					oauth2AuthorizationCode.getToken().getIssuedAt(), oauth2AuthorizationCode.getToken().getExpiresAt(),
+					oauth2AuthorizationCode.isInvalidated());
+		}
+		return authorizationCode;
+	}
+
+	static OAuth2AuthorizationGrantAuthorization.AccessToken extractAccessToken(OAuth2Authorization authorization) {
+		OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = null;
+		if (authorization.getAccessToken() != null) {
+			OAuth2Authorization.Token<OAuth2AccessToken> oauth2AccessToken = authorization.getAccessToken();
+			OAuth2TokenFormat tokenFormat = null;
+			if (OAuth2TokenFormat.SELF_CONTAINED.getValue()
+				.equals(oauth2AccessToken.getMetadata(OAuth2TokenFormat.class.getName()))) {
+				tokenFormat = OAuth2TokenFormat.SELF_CONTAINED;
+			}
+			else if (OAuth2TokenFormat.REFERENCE.getValue()
+				.equals(oauth2AccessToken.getMetadata(OAuth2TokenFormat.class.getName()))) {
+				tokenFormat = OAuth2TokenFormat.REFERENCE;
+			}
+			accessToken = new OAuth2AuthorizationGrantAuthorization.AccessToken(
+					oauth2AccessToken.getToken().getTokenValue(), oauth2AccessToken.getToken().getIssuedAt(),
+					oauth2AccessToken.getToken().getExpiresAt(), oauth2AccessToken.isInvalidated(),
+					oauth2AccessToken.getToken().getTokenType(), oauth2AccessToken.getToken().getScopes(), tokenFormat,
+					new OAuth2AuthorizationGrantAuthorization.ClaimsHolder(oauth2AccessToken.getClaims()));
+		}
+		return accessToken;
+	}
+
+	static OAuth2AuthorizationGrantAuthorization.RefreshToken extractRefreshToken(OAuth2Authorization authorization) {
+		OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken = null;
+		if (authorization.getRefreshToken() != null) {
+			OAuth2Authorization.Token<OAuth2RefreshToken> oauth2RefreshToken = authorization.getRefreshToken();
+			refreshToken = new OAuth2AuthorizationGrantAuthorization.RefreshToken(
+					oauth2RefreshToken.getToken().getTokenValue(), oauth2RefreshToken.getToken().getIssuedAt(),
+					oauth2RefreshToken.getToken().getExpiresAt(), oauth2RefreshToken.isInvalidated());
+		}
+		return refreshToken;
+	}
+
+	static OidcAuthorizationCodeGrantAuthorization.IdToken extractIdToken(OAuth2Authorization authorization) {
+		OidcAuthorizationCodeGrantAuthorization.IdToken idToken = null;
+		if (authorization.getToken(OidcIdToken.class) != null) {
+			OAuth2Authorization.Token<OidcIdToken> oidcIdToken = authorization.getToken(OidcIdToken.class);
+			idToken = new OidcAuthorizationCodeGrantAuthorization.IdToken(oidcIdToken.getToken().getTokenValue(),
+					oidcIdToken.getToken().getIssuedAt(), oidcIdToken.getToken().getExpiresAt(),
+					oidcIdToken.isInvalidated(),
+					new OAuth2AuthorizationGrantAuthorization.ClaimsHolder(oidcIdToken.getClaims()));
+		}
+		return idToken;
+	}
+
+	static OAuth2DeviceCodeGrantAuthorization.DeviceCode extractDeviceCode(OAuth2Authorization authorization) {
+		OAuth2DeviceCodeGrantAuthorization.DeviceCode deviceCode = null;
+		if (authorization.getToken(OAuth2DeviceCode.class) != null) {
+			OAuth2Authorization.Token<OAuth2DeviceCode> oauth2DeviceCode = authorization
+				.getToken(OAuth2DeviceCode.class);
+			deviceCode = new OAuth2DeviceCodeGrantAuthorization.DeviceCode(oauth2DeviceCode.getToken().getTokenValue(),
+					oauth2DeviceCode.getToken().getIssuedAt(), oauth2DeviceCode.getToken().getExpiresAt(),
+					oauth2DeviceCode.isInvalidated());
+		}
+		return deviceCode;
+	}
+
+	static OAuth2DeviceCodeGrantAuthorization.UserCode extractUserCode(OAuth2Authorization authorization) {
+		OAuth2DeviceCodeGrantAuthorization.UserCode userCode = null;
+		if (authorization.getToken(OAuth2UserCode.class) != null) {
+			OAuth2Authorization.Token<OAuth2UserCode> oauth2UserCode = authorization.getToken(OAuth2UserCode.class);
+			userCode = new OAuth2DeviceCodeGrantAuthorization.UserCode(oauth2UserCode.getToken().getTokenValue(),
+					oauth2UserCode.getToken().getIssuedAt(), oauth2UserCode.getToken().getExpiresAt(),
+					oauth2UserCode.isInvalidated());
+		}
+		return userCode;
+	}
+
+	static RegisteredClient convertRegisteredClient(OAuth2RegisteredClient oauth2RegisteredClient) {
+		ClientSettings.Builder clientSettingsBuilder = ClientSettings.builder()
+			.requireProofKey(oauth2RegisteredClient.getClientSettings().isRequireProofKey())
+			.requireAuthorizationConsent(oauth2RegisteredClient.getClientSettings().isRequireAuthorizationConsent());
+		if (StringUtils.hasText(oauth2RegisteredClient.getClientSettings().getJwkSetUrl())) {
+			clientSettingsBuilder.jwkSetUrl(oauth2RegisteredClient.getClientSettings().getJwkSetUrl());
+		}
+		if (oauth2RegisteredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() != null) {
+			clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(
+					oauth2RegisteredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm());
+		}
+		if (StringUtils.hasText(oauth2RegisteredClient.getClientSettings().getX509CertificateSubjectDN())) {
+			clientSettingsBuilder
+				.x509CertificateSubjectDN(oauth2RegisteredClient.getClientSettings().getX509CertificateSubjectDN());
+		}
+		ClientSettings clientSettings = clientSettingsBuilder.build();
+
+		TokenSettings.Builder tokenSettingsBuilder = TokenSettings.builder();
+		if (oauth2RegisteredClient.getTokenSettings().getAuthorizationCodeTimeToLive() != null) {
+			tokenSettingsBuilder.authorizationCodeTimeToLive(
+					oauth2RegisteredClient.getTokenSettings().getAuthorizationCodeTimeToLive());
+		}
+		if (oauth2RegisteredClient.getTokenSettings().getAccessTokenTimeToLive() != null) {
+			tokenSettingsBuilder
+				.accessTokenTimeToLive(oauth2RegisteredClient.getTokenSettings().getAccessTokenTimeToLive());
+		}
+		if (oauth2RegisteredClient.getTokenSettings().getAccessTokenFormat() != null) {
+			tokenSettingsBuilder.accessTokenFormat(oauth2RegisteredClient.getTokenSettings().getAccessTokenFormat());
+		}
+		if (oauth2RegisteredClient.getTokenSettings().getDeviceCodeTimeToLive() != null) {
+			tokenSettingsBuilder
+				.deviceCodeTimeToLive(oauth2RegisteredClient.getTokenSettings().getDeviceCodeTimeToLive());
+		}
+		tokenSettingsBuilder.reuseRefreshTokens(oauth2RegisteredClient.getTokenSettings().isReuseRefreshTokens());
+		if (oauth2RegisteredClient.getTokenSettings().getRefreshTokenTimeToLive() != null) {
+			tokenSettingsBuilder
+				.refreshTokenTimeToLive(oauth2RegisteredClient.getTokenSettings().getRefreshTokenTimeToLive());
+		}
+		if (oauth2RegisteredClient.getTokenSettings().getIdTokenSignatureAlgorithm() != null) {
+			tokenSettingsBuilder
+				.idTokenSignatureAlgorithm(oauth2RegisteredClient.getTokenSettings().getIdTokenSignatureAlgorithm());
+		}
+		tokenSettingsBuilder.x509CertificateBoundAccessTokens(
+				oauth2RegisteredClient.getTokenSettings().isX509CertificateBoundAccessTokens());
+		TokenSettings tokenSettings = tokenSettingsBuilder.build();
+
+		RegisteredClient.Builder registeredClientBuilder = RegisteredClient.withId(oauth2RegisteredClient.getId())
+				.clientId(oauth2RegisteredClient.getClientId())
+				.clientIdIssuedAt(oauth2RegisteredClient.getClientIdIssuedAt())
+				.clientSecret(oauth2RegisteredClient.getClientSecret())
+				.clientSecretExpiresAt(oauth2RegisteredClient.getClientSecretExpiresAt())
+				.clientName(oauth2RegisteredClient.getClientName())
+				.clientAuthenticationMethods((clientAuthenticationMethods) -> clientAuthenticationMethods
+						.addAll(oauth2RegisteredClient.getClientAuthenticationMethods()))
+				.authorizationGrantTypes((authorizationGrantTypes) -> authorizationGrantTypes
+						.addAll(oauth2RegisteredClient.getAuthorizationGrantTypes()))
+				.clientSettings(clientSettings)
+				.tokenSettings(tokenSettings);
+		if (!CollectionUtils.isEmpty(oauth2RegisteredClient.getRedirectUris())) {
+			registeredClientBuilder.redirectUris((redirectUris) -> redirectUris.addAll(oauth2RegisteredClient.getRedirectUris()));
+		}
+		if (!CollectionUtils.isEmpty(oauth2RegisteredClient.getPostLogoutRedirectUris())) {
+			registeredClientBuilder.postLogoutRedirectUris((postLogoutRedirectUris) ->
+					postLogoutRedirectUris.addAll(oauth2RegisteredClient.getPostLogoutRedirectUris()));
+		}
+		if (!CollectionUtils.isEmpty(oauth2RegisteredClient.getScopes())) {
+			registeredClientBuilder.scopes((scopes) -> scopes.addAll(oauth2RegisteredClient.getScopes()));
+		}
+
+		return registeredClientBuilder.build();
+	}
+
+	static OAuth2AuthorizationConsent convertOAuth2AuthorizationConsent(OAuth2UserConsent userConsent) {
+		return OAuth2AuthorizationConsent.withId(userConsent.getRegisteredClientId(), userConsent.getPrincipalName())
+			.authorities((authorities) -> authorities.addAll(userConsent.getAuthorities()))
+			.build();
+	}
+
+	static void mapOAuth2AuthorizationGrantAuthorization(
+			OAuth2AuthorizationGrantAuthorization authorizationGrantAuthorization,
+			OAuth2Authorization.Builder builder) {
+
+		if (authorizationGrantAuthorization instanceof OidcAuthorizationCodeGrantAuthorization authorizationGrant) {
+			mapOidcAuthorizationCodeGrantAuthorization(authorizationGrant, builder);
+		}
+		else if (authorizationGrantAuthorization instanceof OAuth2AuthorizationCodeGrantAuthorization authorizationGrant) {
+			mapOAuth2AuthorizationCodeGrantAuthorization(authorizationGrant, builder);
+		}
+		else if (authorizationGrantAuthorization instanceof OAuth2ClientCredentialsGrantAuthorization authorizationGrant) {
+			mapOAuth2ClientCredentialsGrantAuthorization(authorizationGrant, builder);
+		}
+		else if (authorizationGrantAuthorization instanceof OAuth2DeviceCodeGrantAuthorization authorizationGrant) {
+			mapOAuth2DeviceCodeGrantAuthorization(authorizationGrant, builder);
+		}
+		else if (authorizationGrantAuthorization instanceof OAuth2TokenExchangeGrantAuthorization authorizationGrant) {
+			mapOAuth2TokenExchangeGrantAuthorization(authorizationGrant, builder);
+		}
+	}
+
+	static void mapOidcAuthorizationCodeGrantAuthorization(
+			OidcAuthorizationCodeGrantAuthorization authorizationCodeGrantAuthorization,
+			OAuth2Authorization.Builder builder) {
+
+		mapOAuth2AuthorizationCodeGrantAuthorization(authorizationCodeGrantAuthorization, builder);
+		mapIdToken(authorizationCodeGrantAuthorization.getIdToken(), builder);
+	}
+
+	static void mapOAuth2AuthorizationCodeGrantAuthorization(
+			OAuth2AuthorizationCodeGrantAuthorization authorizationCodeGrantAuthorization,
+			OAuth2Authorization.Builder builder) {
+
+		builder.id(authorizationCodeGrantAuthorization.getId())
+			.principalName(authorizationCodeGrantAuthorization.getPrincipalName())
+			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+			.authorizedScopes(authorizationCodeGrantAuthorization.getAuthorizedScopes())
+			.attribute(Principal.class.getName(), authorizationCodeGrantAuthorization.getPrincipal())
+			.attribute(OAuth2AuthorizationRequest.class.getName(),
+					authorizationCodeGrantAuthorization.getAuthorizationRequest());
+		if (StringUtils.hasText(authorizationCodeGrantAuthorization.getState())) {
+			builder.attribute(OAuth2ParameterNames.STATE, authorizationCodeGrantAuthorization.getState());
+		}
+
+		mapAuthorizationCode(authorizationCodeGrantAuthorization.getAuthorizationCode(), builder);
+		mapAccessToken(authorizationCodeGrantAuthorization.getAccessToken(), builder);
+		mapRefreshToken(authorizationCodeGrantAuthorization.getRefreshToken(), builder);
+	}
+
+	static void mapOAuth2ClientCredentialsGrantAuthorization(
+			OAuth2ClientCredentialsGrantAuthorization clientCredentialsGrantAuthorization,
+			OAuth2Authorization.Builder builder) {
+
+		builder.id(clientCredentialsGrantAuthorization.getId())
+			.principalName(clientCredentialsGrantAuthorization.getPrincipalName())
+			.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+			.authorizedScopes(clientCredentialsGrantAuthorization.getAuthorizedScopes());
+
+		mapAccessToken(clientCredentialsGrantAuthorization.getAccessToken(), builder);
+	}
+
+	static void mapOAuth2DeviceCodeGrantAuthorization(OAuth2DeviceCodeGrantAuthorization deviceCodeGrantAuthorization,
+			OAuth2Authorization.Builder builder) {
+
+		builder.id(deviceCodeGrantAuthorization.getId())
+			.principalName(deviceCodeGrantAuthorization.getPrincipalName())
+			.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
+			.authorizedScopes(deviceCodeGrantAuthorization.getAuthorizedScopes());
+		if (deviceCodeGrantAuthorization.getPrincipal() != null) {
+			builder.attribute(Principal.class.getName(), deviceCodeGrantAuthorization.getPrincipal());
+		}
+		if (deviceCodeGrantAuthorization.getRequestedScopes() != null) {
+			builder.attribute(OAuth2ParameterNames.SCOPE, deviceCodeGrantAuthorization.getRequestedScopes());
+		}
+		if (StringUtils.hasText(deviceCodeGrantAuthorization.getDeviceState())) {
+			builder.attribute(OAuth2ParameterNames.STATE, deviceCodeGrantAuthorization.getDeviceState());
+		}
+
+		mapAccessToken(deviceCodeGrantAuthorization.getAccessToken(), builder);
+		mapRefreshToken(deviceCodeGrantAuthorization.getRefreshToken(), builder);
+		mapDeviceCode(deviceCodeGrantAuthorization.getDeviceCode(), builder);
+		mapUserCode(deviceCodeGrantAuthorization.getUserCode(), builder);
+	}
+
+	static void mapOAuth2TokenExchangeGrantAuthorization(
+			OAuth2TokenExchangeGrantAuthorization tokenExchangeGrantAuthorization,
+			OAuth2Authorization.Builder builder) {
+
+		builder.id(tokenExchangeGrantAuthorization.getId())
+			.principalName(tokenExchangeGrantAuthorization.getPrincipalName())
+			.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
+			.authorizedScopes(tokenExchangeGrantAuthorization.getAuthorizedScopes());
+
+		mapAccessToken(tokenExchangeGrantAuthorization.getAccessToken(), builder);
+	}
+
+	static void mapAuthorizationCode(OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode authorizationCode,
+			OAuth2Authorization.Builder builder) {
+		if (authorizationCode == null) {
+			return;
+		}
+		OAuth2AuthorizationCode oauth2AuthorizationCode = new OAuth2AuthorizationCode(authorizationCode.getTokenValue(),
+				authorizationCode.getIssuedAt(), authorizationCode.getExpiresAt());
+		builder.token(oauth2AuthorizationCode, (metadata) -> metadata
+			.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, authorizationCode.isInvalidated()));
+	}
+
+	static void mapAccessToken(OAuth2AuthorizationGrantAuthorization.AccessToken accessToken,
+			OAuth2Authorization.Builder builder) {
+		if (accessToken == null) {
+			return;
+		}
+		OAuth2AccessToken oauth2AccessToken = new OAuth2AccessToken(accessToken.getTokenType(),
+				accessToken.getTokenValue(), accessToken.getIssuedAt(), accessToken.getExpiresAt(),
+				accessToken.getScopes());
+		builder.token(oauth2AccessToken, (metadata) -> {
+			metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, accessToken.isInvalidated());
+			metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, accessToken.getClaims().getClaims());
+			metadata.put(OAuth2TokenFormat.class.getName(), accessToken.getTokenFormat().getValue());
+		});
+	}
+
+	static void mapRefreshToken(OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken,
+			OAuth2Authorization.Builder builder) {
+		if (refreshToken == null) {
+			return;
+		}
+		OAuth2RefreshToken oauth2RefreshToken = new OAuth2RefreshToken(refreshToken.getTokenValue(),
+				refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
+		builder.token(oauth2RefreshToken, (metadata) -> metadata
+			.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, refreshToken.isInvalidated()));
+	}
+
+	static void mapIdToken(OidcAuthorizationCodeGrantAuthorization.IdToken idToken,
+			OAuth2Authorization.Builder builder) {
+		if (idToken == null) {
+			return;
+		}
+		OidcIdToken oidcIdToken = new OidcIdToken(idToken.getTokenValue(), idToken.getIssuedAt(),
+				idToken.getExpiresAt(), idToken.getClaims().getClaims());
+		builder.token(oidcIdToken, (metadata) -> {
+			metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, idToken.isInvalidated());
+			metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims().getClaims());
+		});
+	}
+
+	static void mapDeviceCode(OAuth2DeviceCodeGrantAuthorization.DeviceCode deviceCode,
+			OAuth2Authorization.Builder builder) {
+		if (deviceCode == null) {
+			return;
+		}
+		OAuth2DeviceCode oauth2DeviceCode = new OAuth2DeviceCode(deviceCode.getTokenValue(), deviceCode.getIssuedAt(),
+				deviceCode.getExpiresAt());
+		builder.token(oauth2DeviceCode, (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME,
+				deviceCode.isInvalidated()));
+	}
+
+	static void mapUserCode(OAuth2DeviceCodeGrantAuthorization.UserCode userCode, OAuth2Authorization.Builder builder) {
+		if (userCode == null) {
+			return;
+		}
+		OAuth2UserCode oauth2UserCode = new OAuth2UserCode(userCode.getTokenValue(), userCode.getIssuedAt(),
+				userCode.getExpiresAt());
+		builder.token(oauth2UserCode, (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME,
+				userCode.isInvalidated()));
+	}
+
+}

+ 59 - 0
docs/src/main/java/sample/redis/service/RedisOAuth2AuthorizationConsentService.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020-2024 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.redis.service;
+
+import sample.redis.entity.OAuth2UserConsent;
+import sample.redis.repository.OAuth2UserConsentRepository;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.util.Assert;
+
+public class RedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
+
+	private final OAuth2UserConsentRepository userConsentRepository;
+
+	public RedisOAuth2AuthorizationConsentService(OAuth2UserConsentRepository userConsentRepository) {
+		Assert.notNull(userConsentRepository, "userConsentRepository cannot be null");
+		this.userConsentRepository = userConsentRepository;
+	}
+
+	@Override
+	public void save(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		OAuth2UserConsent oauth2UserConsent = ModelMapper.convertOAuth2UserConsent(authorizationConsent);
+		this.userConsentRepository.save(oauth2UserConsent);
+	}
+
+	@Override
+	public void remove(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		this.userConsentRepository.deleteByRegisteredClientIdAndPrincipalName(
+				authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
+	}
+
+	@Nullable
+	@Override
+	public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
+		Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
+		Assert.hasText(principalName, "principalName cannot be empty");
+		OAuth2UserConsent oauth2UserConsent = this.userConsentRepository
+			.findByRegisteredClientIdAndPrincipalName(registeredClientId, principalName);
+		return oauth2UserConsent != null ? ModelMapper.convertOAuth2AuthorizationConsent(oauth2UserConsent) : null;
+	}
+
+}

+ 122 - 0
docs/src/main/java/sample/redis/service/RedisOAuth2AuthorizationService.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020-2024 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.redis.service;
+
+import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
+import sample.redis.repository.OAuth2AuthorizationGrantAuthorizationRepository;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+
+public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
+
+	private final RegisteredClientRepository registeredClientRepository;
+
+	private final OAuth2AuthorizationGrantAuthorizationRepository authorizationGrantAuthorizationRepository;
+
+	public RedisOAuth2AuthorizationService(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationGrantAuthorizationRepository authorizationGrantAuthorizationRepository) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationGrantAuthorizationRepository,
+				"authorizationGrantAuthorizationRepository cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.authorizationGrantAuthorizationRepository = authorizationGrantAuthorizationRepository;
+	}
+
+	@Override
+	public void save(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		OAuth2AuthorizationGrantAuthorization authorizationGrantAuthorization = ModelMapper
+			.convertOAuth2AuthorizationGrantAuthorization(authorization);
+		this.authorizationGrantAuthorizationRepository.save(authorizationGrantAuthorization);
+	}
+
+	@Override
+	public void remove(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+		this.authorizationGrantAuthorizationRepository.deleteById(authorization.getId());
+	}
+
+	@Nullable
+	@Override
+	public OAuth2Authorization findById(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		return this.authorizationGrantAuthorizationRepository.findById(id)
+			.map(this::toOAuth2Authorization)
+			.orElse(null);
+	}
+
+	@Nullable
+	@Override
+	public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
+		Assert.hasText(token, "token cannot be empty");
+		OAuth2AuthorizationGrantAuthorization authorizationGrantAuthorization = null;
+		if (tokenType == null) {
+			authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository
+				.findByStateOrAuthorizationCode_TokenValueOrAccessToken_TokenValueOrRefreshToken_TokenValueOrIdToken_TokenValueOrDeviceStateOrDeviceCode_TokenValueOrUserCode_TokenValue(
+						token, token, token, token, token, token, token, token);
+		}
+		else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
+			authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository.findByState(token);
+			if (authorizationGrantAuthorization == null) {
+				authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository
+					.findByDeviceState(token);
+			}
+		}
+		else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
+			authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository
+				.findByAuthorizationCode_TokenValue(token);
+		}
+		else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
+			authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository
+				.findByAccessToken_TokenValue(token);
+		}
+		else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
+			authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository
+				.findByIdToken_TokenValue(token);
+		}
+		else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
+			authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository
+				.findByRefreshToken_TokenValue(token);
+		}
+		else if (OAuth2ParameterNames.USER_CODE.equals(tokenType.getValue())) {
+			authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository
+				.findByUserCode_TokenValue(token);
+		}
+		else if (OAuth2ParameterNames.DEVICE_CODE.equals(tokenType.getValue())) {
+			authorizationGrantAuthorization = this.authorizationGrantAuthorizationRepository
+				.findByDeviceCode_TokenValue(token);
+		}
+		return authorizationGrantAuthorization != null ? toOAuth2Authorization(authorizationGrantAuthorization) : null;
+	}
+
+	private OAuth2Authorization toOAuth2Authorization(
+			OAuth2AuthorizationGrantAuthorization authorizationGrantAuthorization) {
+		RegisteredClient registeredClient = this.registeredClientRepository
+			.findById(authorizationGrantAuthorization.getRegisteredClientId());
+		OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient);
+		ModelMapper.mapOAuth2AuthorizationGrantAuthorization(authorizationGrantAuthorization, builder);
+		return builder.build();
+	}
+
+}

+ 57 - 0
docs/src/main/java/sample/redis/service/RedisRegisteredClientRepository.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020-2024 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.redis.service;
+
+import sample.redis.entity.OAuth2RegisteredClient;
+import sample.redis.repository.OAuth2RegisteredClientRepository;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+
+public class RedisRegisteredClientRepository implements RegisteredClientRepository {
+
+	private final OAuth2RegisteredClientRepository registeredClientRepository;
+
+	public RedisRegisteredClientRepository(OAuth2RegisteredClientRepository registeredClientRepository) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+	}
+
+	@Override
+	public void save(RegisteredClient registeredClient) {
+		Assert.notNull(registeredClient, "registeredClient cannot be null");
+		OAuth2RegisteredClient oauth2RegisteredClient = ModelMapper.convertOAuth2RegisteredClient(registeredClient);
+		this.registeredClientRepository.save(oauth2RegisteredClient);
+	}
+
+	@Nullable
+	@Override
+	public RegisteredClient findById(String id) {
+		Assert.hasText(id, "id cannot be empty");
+		return this.registeredClientRepository.findById(id).map(ModelMapper::convertRegisteredClient).orElse(null);
+	}
+
+	@Nullable
+	@Override
+	public RegisteredClient findByClientId(String clientId) {
+		Assert.hasText(clientId, "clientId cannot be empty");
+		OAuth2RegisteredClient oauth2RegisteredClient = this.registeredClientRepository.findByClientId(clientId);
+		return oauth2RegisteredClient != null ? ModelMapper.convertRegisteredClient(oauth2RegisteredClient) : null;
+	}
+
+}