diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index cf3f176f..a99eaa91 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -11,5 +11,6 @@ ** xref:guides/how-to-multitenancy.adoc[] ** xref:guides/how-to-userinfo.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-dynamic-client-registration.adoc[] diff --git a/docs/modules/ROOT/pages/guides/how-to-redis.adoc b/docs/modules/ROOT/pages/guides/how-to-redis.adoc new file mode 100644 index 00000000..eda071a8 --- /dev/null +++ b/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`. diff --git a/docs/spring-authorization-server-docs.gradle b/docs/spring-authorization-server-docs.gradle index 0527a06d..c68bce29 100644 --- a/docs/spring-authorization-server-docs.gradle +++ b/docs/spring-authorization-server-docs.gradle @@ -12,6 +12,10 @@ java { sourceCompatibility = JavaVersion.VERSION_17 } +compileJava { + options.compilerArgs << '-parameters' +} + antora { options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] environment = [ @@ -57,6 +61,10 @@ dependencies { 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-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 project(":spring-security-oauth2-authorization-server") runtimeOnly "com.h2database:h2" diff --git a/docs/src/main/java/sample/redis/config/RedisConfig.java b/docs/src/main/java/sample/redis/config/RedisConfig.java new file mode 100644 index 00000000..c4b0c552 --- /dev/null +++ b/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 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> + } + +} diff --git a/docs/src/main/java/sample/redis/convert/BytesToClaimsHolderConverter.java b/docs/src/main/java/sample/redis/convert/BytesToClaimsHolderConverter.java new file mode 100644 index 00000000..328443d2 --- /dev/null +++ b/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 { + + private final Jackson2JsonRedisSerializer 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); + } + +} diff --git a/docs/src/main/java/sample/redis/convert/BytesToOAuth2AuthorizationRequestConverter.java b/docs/src/main/java/sample/redis/convert/BytesToOAuth2AuthorizationRequestConverter.java new file mode 100644 index 00000000..b0f330a7 --- /dev/null +++ b/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 { + + private final Jackson2JsonRedisSerializer 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); + } + +} diff --git a/docs/src/main/java/sample/redis/convert/BytesToUsernamePasswordAuthenticationTokenConverter.java b/docs/src/main/java/sample/redis/convert/BytesToUsernamePasswordAuthenticationTokenConverter.java new file mode 100644 index 00000000..6620aeda --- /dev/null +++ b/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 { + + private final Jackson2JsonRedisSerializer 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); + } + +} diff --git a/docs/src/main/java/sample/redis/convert/ClaimsHolderMixin.java b/docs/src/main/java/sample/redis/convert/ClaimsHolderMixin.java new file mode 100644 index 00000000..e1373acc --- /dev/null +++ b/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 claims) { + } + +} diff --git a/docs/src/main/java/sample/redis/convert/ClaimsHolderToBytesConverter.java b/docs/src/main/java/sample/redis/convert/ClaimsHolderToBytesConverter.java new file mode 100644 index 00000000..c2aa6b5f --- /dev/null +++ b/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 { + + private final Jackson2JsonRedisSerializer 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); + } + +} diff --git a/docs/src/main/java/sample/redis/convert/OAuth2AuthorizationRequestToBytesConverter.java b/docs/src/main/java/sample/redis/convert/OAuth2AuthorizationRequestToBytesConverter.java new file mode 100644 index 00000000..05947d7f --- /dev/null +++ b/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 { + + private final Jackson2JsonRedisSerializer 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); + } + +} diff --git a/docs/src/main/java/sample/redis/convert/UsernamePasswordAuthenticationTokenToBytesConverter.java b/docs/src/main/java/sample/redis/convert/UsernamePasswordAuthenticationTokenToBytesConverter.java new file mode 100644 index 00000000..f332d088 --- /dev/null +++ b/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 { + + private final Jackson2JsonRedisSerializer 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); + } + +} diff --git a/docs/src/main/java/sample/redis/entity/OAuth2AuthorizationCodeGrantAuthorization.java b/docs/src/main/java/sample/redis/entity/OAuth2AuthorizationCodeGrantAuthorization.java new file mode 100644 index 00000000..fbf84d6a --- /dev/null +++ b/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 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 + +} diff --git a/docs/src/main/java/sample/redis/entity/OAuth2AuthorizationGrantAuthorization.java b/docs/src/main/java/sample/redis/entity/OAuth2AuthorizationGrantAuthorization.java new file mode 100644 index 00000000..b9cd2b5a --- /dev/null +++ b/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 authorizedScopes; + + private final AccessToken accessToken; + + private final RefreshToken refreshToken; + + // @fold:on + protected OAuth2AuthorizationGrantAuthorization(String id, String registeredClientId, String principalName, + Set 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 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 claims; + + public ClaimsHolder(Map claims) { + this.claims = claims; + } + + public Map getClaims() { + return this.claims; + } + + } + + public static class AccessToken extends AbstractToken { + + private final OAuth2AccessToken.TokenType tokenType; + + private final Set scopes; + + private final OAuth2TokenFormat tokenFormat; + + private final ClaimsHolder claims; + + public AccessToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated, + OAuth2AccessToken.TokenType tokenType, Set 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 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 + +} diff --git a/docs/src/main/java/sample/redis/entity/OAuth2ClientCredentialsGrantAuthorization.java b/docs/src/main/java/sample/redis/entity/OAuth2ClientCredentialsGrantAuthorization.java new file mode 100644 index 00000000..7714320c --- /dev/null +++ b/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 authorizedScopes, AccessToken accessToken) { + super(id, registeredClientId, principalName, authorizedScopes, accessToken, null); + } + // @fold:off + +} diff --git a/docs/src/main/java/sample/redis/entity/OAuth2DeviceCodeGrantAuthorization.java b/docs/src/main/java/sample/redis/entity/OAuth2DeviceCodeGrantAuthorization.java new file mode 100644 index 00000000..a238e6be --- /dev/null +++ b/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 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 authorizedScopes, AccessToken accessToken, RefreshToken refreshToken, Principal principal, + DeviceCode deviceCode, UserCode userCode, Set 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 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 + +} diff --git a/docs/src/main/java/sample/redis/entity/OAuth2RegisteredClient.java b/docs/src/main/java/sample/redis/entity/OAuth2RegisteredClient.java new file mode 100644 index 00000000..4e09b584 --- /dev/null +++ b/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 clientAuthenticationMethods; + + private final Set authorizationGrantTypes; + + private final Set redirectUris; + + private final Set postLogoutRedirectUris; + + private final Set 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 clientAuthenticationMethods, + Set authorizationGrantTypes, Set redirectUris, + Set postLogoutRedirectUris, Set 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 getClientAuthenticationMethods() { + return this.clientAuthenticationMethods; + } + + public Set getAuthorizationGrantTypes() { + return this.authorizationGrantTypes; + } + + public Set getRedirectUris() { + return this.redirectUris; + } + + public Set getPostLogoutRedirectUris() { + return this.postLogoutRedirectUris; + } + + public Set 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 + +} diff --git a/docs/src/main/java/sample/redis/entity/OAuth2TokenExchangeGrantAuthorization.java b/docs/src/main/java/sample/redis/entity/OAuth2TokenExchangeGrantAuthorization.java new file mode 100644 index 00000000..405dc5e5 --- /dev/null +++ b/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 authorizedScopes, AccessToken accessToken) { + super(id, registeredClientId, principalName, authorizedScopes, accessToken, null); + } + // @fold:off + +} diff --git a/docs/src/main/java/sample/redis/entity/OAuth2UserConsent.java b/docs/src/main/java/sample/redis/entity/OAuth2UserConsent.java new file mode 100644 index 00000000..3900a902 --- /dev/null +++ b/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 authorities; + + // @fold:on + public OAuth2UserConsent(String id, String registeredClientId, String principalName, + Set 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 getAuthorities() { + return this.authorities; + } + // @fold:off + +} diff --git a/docs/src/main/java/sample/redis/entity/OidcAuthorizationCodeGrantAuthorization.java b/docs/src/main/java/sample/redis/entity/OidcAuthorizationCodeGrantAuthorization.java new file mode 100644 index 00000000..eba45b87 --- /dev/null +++ b/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 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 + +} diff --git a/docs/src/main/java/sample/redis/repository/OAuth2AuthorizationGrantAuthorizationRepository.java b/docs/src/main/java/sample/redis/repository/OAuth2AuthorizationGrantAuthorizationRepository.java new file mode 100644 index 00000000..91e4b5f0 --- /dev/null +++ b/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 { + + T findByState(String token); + + T findByAuthorizationCode_TokenValue(String token); + + T findByAccessToken_TokenValue(String token); + + T findByRefreshToken_TokenValue(String token); + + T findByIdToken_TokenValue(String token); + + T findByDeviceState(String token); + + T findByDeviceCode_TokenValue(String token); + + T findByUserCode_TokenValue(String token); + + T findByStateOrAuthorizationCode_TokenValueOrAccessToken_TokenValueOrRefreshToken_TokenValueOrIdToken_TokenValueOrDeviceStateOrDeviceCode_TokenValueOrUserCode_TokenValue( + String state, String authorizationCode, String accessToken, String refreshToken, String idToken, + String deviceState, String deviceCode, String userCode); + +} diff --git a/docs/src/main/java/sample/redis/repository/OAuth2RegisteredClientRepository.java b/docs/src/main/java/sample/redis/repository/OAuth2RegisteredClientRepository.java new file mode 100644 index 00000000..291d907c --- /dev/null +++ b/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 findByClientId(String clientId); + +} diff --git a/docs/src/main/java/sample/redis/repository/OAuth2UserConsentRepository.java b/docs/src/main/java/sample/redis/repository/OAuth2UserConsentRepository.java new file mode 100644 index 00000000..858b67fa --- /dev/null +++ b/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 findByRegisteredClientIdAndPrincipalName(String registeredClientId, String principalName); + + void deleteByRegisteredClientIdAndPrincipalName(String registeredClientId, String principalName); + +} diff --git a/docs/src/main/java/sample/redis/service/ModelMapper.java b/docs/src/main/java/sample/redis/service/ModelMapper.java new file mode 100644 index 00000000..ee05666c --- /dev/null +++ b/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 = 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 = 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 = 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 = 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 = 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 = 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())); + } + +} diff --git a/docs/src/main/java/sample/redis/service/RedisOAuth2AuthorizationConsentService.java b/docs/src/main/java/sample/redis/service/RedisOAuth2AuthorizationConsentService.java new file mode 100644 index 00000000..e311939a --- /dev/null +++ b/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; + } + +} diff --git a/docs/src/main/java/sample/redis/service/RedisOAuth2AuthorizationService.java b/docs/src/main/java/sample/redis/service/RedisOAuth2AuthorizationService.java new file mode 100644 index 00000000..3ec9396d --- /dev/null +++ b/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(); + } + +} diff --git a/docs/src/main/java/sample/redis/service/RedisRegisteredClientRepository.java b/docs/src/main/java/sample/redis/service/RedisRegisteredClientRepository.java new file mode 100644 index 00000000..2e92e5f5 --- /dev/null +++ b/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; + } + +}