Add How-to: Implement core services with Redis
Closes gh-1019
This commit is contained in:
@@ -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[]
|
||||
|
||||
225
docs/modules/ROOT/pages/guides/how-to-redis.adoc
Normal file
225
docs/modules/ROOT/pages/guides/how-to-redis.adoc
Normal file
@@ -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`.
|
||||
@@ -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"
|
||||
|
||||
85
docs/src/main/java/sample/redis/config/RedisConfig.java
Normal file
85
docs/src/main/java/sample/redis/config/RedisConfig.java
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.config;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import sample.redis.convert.BytesToClaimsHolderConverter;
|
||||
import sample.redis.convert.BytesToOAuth2AuthorizationRequestConverter;
|
||||
import sample.redis.convert.BytesToUsernamePasswordAuthenticationTokenConverter;
|
||||
import sample.redis.convert.ClaimsHolderToBytesConverter;
|
||||
import sample.redis.convert.OAuth2AuthorizationRequestToBytesConverter;
|
||||
import sample.redis.convert.UsernamePasswordAuthenticationTokenToBytesConverter;
|
||||
import sample.redis.repository.OAuth2AuthorizationGrantAuthorizationRepository;
|
||||
import sample.redis.repository.OAuth2RegisteredClientRepository;
|
||||
import sample.redis.repository.OAuth2UserConsentRepository;
|
||||
import sample.redis.service.RedisOAuth2AuthorizationConsentService;
|
||||
import sample.redis.service.RedisOAuth2AuthorizationService;
|
||||
import sample.redis.service.RedisRegisteredClientRepository;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.convert.RedisCustomConversions;
|
||||
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
|
||||
@EnableRedisRepositories("sample.redis.repository") // <1>
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
return new JedisConnectionFactory(); // <2>
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisCustomConversions redisCustomConversions() { // <3>
|
||||
return new RedisCustomConversions(Arrays.asList(new UsernamePasswordAuthenticationTokenToBytesConverter(),
|
||||
new BytesToUsernamePasswordAuthenticationTokenConverter(),
|
||||
new OAuth2AuthorizationRequestToBytesConverter(), new BytesToOAuth2AuthorizationRequestConverter(),
|
||||
new ClaimsHolderToBytesConverter(), new BytesToClaimsHolderConverter()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisRegisteredClientRepository registeredClientRepository(
|
||||
OAuth2RegisteredClientRepository registeredClientRepository) {
|
||||
return new RedisRegisteredClientRepository(registeredClientRepository); // <4>
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisOAuth2AuthorizationService authorizationService(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationGrantAuthorizationRepository authorizationGrantAuthorizationRepository) {
|
||||
return new RedisOAuth2AuthorizationService(registeredClientRepository,
|
||||
authorizationGrantAuthorizationRepository); // <5>
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisOAuth2AuthorizationConsentService authorizationConsentService(
|
||||
OAuth2UserConsentRepository userConsentRepository) {
|
||||
return new RedisOAuth2AuthorizationConsentService(userConsentRepository); // <6>
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.convert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
|
||||
|
||||
@ReadingConverter
|
||||
public class BytesToClaimsHolderConverter
|
||||
implements Converter<byte[], OAuth2AuthorizationGrantAuthorization.ClaimsHolder> {
|
||||
|
||||
private final Jackson2JsonRedisSerializer<OAuth2AuthorizationGrantAuthorization.ClaimsHolder> serializer;
|
||||
|
||||
public BytesToClaimsHolderConverter() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper
|
||||
.registerModules(SecurityJackson2Modules.getModules(BytesToClaimsHolderConverter.class.getClassLoader()));
|
||||
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
|
||||
objectMapper.addMixIn(OAuth2AuthorizationGrantAuthorization.ClaimsHolder.class, ClaimsHolderMixin.class);
|
||||
this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper,
|
||||
OAuth2AuthorizationGrantAuthorization.ClaimsHolder.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationGrantAuthorization.ClaimsHolder convert(byte[] value) {
|
||||
return this.serializer.deserialize(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.convert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
|
||||
|
||||
@ReadingConverter
|
||||
public class BytesToOAuth2AuthorizationRequestConverter implements Converter<byte[], OAuth2AuthorizationRequest> {
|
||||
|
||||
private final Jackson2JsonRedisSerializer<OAuth2AuthorizationRequest> serializer;
|
||||
|
||||
public BytesToOAuth2AuthorizationRequestConverter() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModules(
|
||||
SecurityJackson2Modules.getModules(BytesToOAuth2AuthorizationRequestConverter.class.getClassLoader()));
|
||||
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
|
||||
this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper, OAuth2AuthorizationRequest.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest convert(byte[] value) {
|
||||
return this.serializer.deserialize(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.convert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
|
||||
@ReadingConverter
|
||||
public class BytesToUsernamePasswordAuthenticationTokenConverter
|
||||
implements Converter<byte[], UsernamePasswordAuthenticationToken> {
|
||||
|
||||
private final Jackson2JsonRedisSerializer<UsernamePasswordAuthenticationToken> serializer;
|
||||
|
||||
public BytesToUsernamePasswordAuthenticationTokenConverter() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModules(SecurityJackson2Modules
|
||||
.getModules(BytesToUsernamePasswordAuthenticationTokenConverter.class.getClassLoader()));
|
||||
this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper, UsernamePasswordAuthenticationToken.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsernamePasswordAuthenticationToken convert(byte[] value) {
|
||||
return this.serializer.deserialize(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.convert;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
abstract class ClaimsHolderMixin {
|
||||
|
||||
@JsonCreator
|
||||
ClaimsHolderMixin(@JsonProperty("claims") Map<String, Object> claims) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.convert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
|
||||
|
||||
@WritingConverter
|
||||
public class ClaimsHolderToBytesConverter
|
||||
implements Converter<OAuth2AuthorizationGrantAuthorization.ClaimsHolder, byte[]> {
|
||||
|
||||
private final Jackson2JsonRedisSerializer<OAuth2AuthorizationGrantAuthorization.ClaimsHolder> serializer;
|
||||
|
||||
public ClaimsHolderToBytesConverter() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper
|
||||
.registerModules(SecurityJackson2Modules.getModules(ClaimsHolderToBytesConverter.class.getClassLoader()));
|
||||
objectMapper.registerModules(new OAuth2AuthorizationServerJackson2Module());
|
||||
objectMapper.addMixIn(OAuth2AuthorizationGrantAuthorization.ClaimsHolder.class, ClaimsHolderMixin.class);
|
||||
this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper,
|
||||
OAuth2AuthorizationGrantAuthorization.ClaimsHolder.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] convert(OAuth2AuthorizationGrantAuthorization.ClaimsHolder value) {
|
||||
return this.serializer.serialize(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.convert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
|
||||
|
||||
@WritingConverter
|
||||
public class OAuth2AuthorizationRequestToBytesConverter implements Converter<OAuth2AuthorizationRequest, byte[]> {
|
||||
|
||||
private final Jackson2JsonRedisSerializer<OAuth2AuthorizationRequest> serializer;
|
||||
|
||||
public OAuth2AuthorizationRequestToBytesConverter() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModules(
|
||||
SecurityJackson2Modules.getModules(OAuth2AuthorizationRequestToBytesConverter.class.getClassLoader()));
|
||||
objectMapper.registerModules(new OAuth2AuthorizationServerJackson2Module());
|
||||
this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper, OAuth2AuthorizationRequest.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] convert(OAuth2AuthorizationRequest value) {
|
||||
return this.serializer.serialize(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.convert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
|
||||
@WritingConverter
|
||||
public class UsernamePasswordAuthenticationTokenToBytesConverter
|
||||
implements Converter<UsernamePasswordAuthenticationToken, byte[]> {
|
||||
|
||||
private final Jackson2JsonRedisSerializer<UsernamePasswordAuthenticationToken> serializer;
|
||||
|
||||
public UsernamePasswordAuthenticationTokenToBytesConverter() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModules(SecurityJackson2Modules
|
||||
.getModules(BytesToUsernamePasswordAuthenticationTokenConverter.class.getClassLoader()));
|
||||
this.serializer = new Jackson2JsonRedisSerializer<>(objectMapper, UsernamePasswordAuthenticationToken.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] convert(UsernamePasswordAuthenticationToken value) {
|
||||
return this.serializer.serialize(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.entity;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
|
||||
public class OAuth2AuthorizationCodeGrantAuthorization extends OAuth2AuthorizationGrantAuthorization {
|
||||
|
||||
private final Principal principal;
|
||||
|
||||
private final OAuth2AuthorizationRequest authorizationRequest;
|
||||
|
||||
private final AuthorizationCode authorizationCode;
|
||||
|
||||
@Indexed
|
||||
private final String state; // Used to correlate the request during the authorization
|
||||
// consent flow
|
||||
|
||||
// @fold:on
|
||||
public OAuth2AuthorizationCodeGrantAuthorization(String id, String registeredClientId, String principalName,
|
||||
Set<String> authorizedScopes, AccessToken accessToken, RefreshToken refreshToken, Principal principal,
|
||||
OAuth2AuthorizationRequest authorizationRequest, AuthorizationCode authorizationCode, String state) {
|
||||
super(id, registeredClientId, principalName, authorizedScopes, accessToken, refreshToken);
|
||||
this.principal = principal;
|
||||
this.authorizationRequest = authorizationRequest;
|
||||
this.authorizationCode = authorizationCode;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Principal getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
public OAuth2AuthorizationRequest getAuthorizationRequest() {
|
||||
return this.authorizationRequest;
|
||||
}
|
||||
|
||||
public AuthorizationCode getAuthorizationCode() {
|
||||
return this.authorizationCode;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public static class AuthorizationCode extends AbstractToken {
|
||||
|
||||
public AuthorizationCode(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
|
||||
super(tokenValue, issuedAt, expiresAt, invalidated);
|
||||
}
|
||||
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.entity;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
|
||||
@RedisHash("oauth2_authorization")
|
||||
public abstract class OAuth2AuthorizationGrantAuthorization {
|
||||
|
||||
@Id
|
||||
private final String id;
|
||||
|
||||
private final String registeredClientId;
|
||||
|
||||
private final String principalName;
|
||||
|
||||
private final Set<String> authorizedScopes;
|
||||
|
||||
private final AccessToken accessToken;
|
||||
|
||||
private final RefreshToken refreshToken;
|
||||
|
||||
// @fold:on
|
||||
protected OAuth2AuthorizationGrantAuthorization(String id, String registeredClientId, String principalName,
|
||||
Set<String> authorizedScopes, AccessToken accessToken, RefreshToken refreshToken) {
|
||||
this.id = id;
|
||||
this.registeredClientId = registeredClientId;
|
||||
this.principalName = principalName;
|
||||
this.authorizedScopes = authorizedScopes;
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getRegisteredClientId() {
|
||||
return this.registeredClientId;
|
||||
}
|
||||
|
||||
public String getPrincipalName() {
|
||||
return this.principalName;
|
||||
}
|
||||
|
||||
public Set<String> getAuthorizedScopes() {
|
||||
return this.authorizedScopes;
|
||||
}
|
||||
|
||||
public AccessToken getAccessToken() {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
public RefreshToken getRefreshToken() {
|
||||
return this.refreshToken;
|
||||
}
|
||||
|
||||
protected abstract static class AbstractToken {
|
||||
|
||||
@Indexed
|
||||
private final String tokenValue;
|
||||
|
||||
private final Instant issuedAt;
|
||||
|
||||
private final Instant expiresAt;
|
||||
|
||||
private final boolean invalidated;
|
||||
|
||||
protected AbstractToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
|
||||
this.tokenValue = tokenValue;
|
||||
this.issuedAt = issuedAt;
|
||||
this.expiresAt = expiresAt;
|
||||
this.invalidated = invalidated;
|
||||
}
|
||||
|
||||
public String getTokenValue() {
|
||||
return this.tokenValue;
|
||||
}
|
||||
|
||||
public Instant getIssuedAt() {
|
||||
return this.issuedAt;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return this.expiresAt;
|
||||
}
|
||||
|
||||
public boolean isInvalidated() {
|
||||
return this.invalidated;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class ClaimsHolder {
|
||||
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
public ClaimsHolder(Map<String, Object> claims) {
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
public Map<String, Object> getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class AccessToken extends AbstractToken {
|
||||
|
||||
private final OAuth2AccessToken.TokenType tokenType;
|
||||
|
||||
private final Set<String> scopes;
|
||||
|
||||
private final OAuth2TokenFormat tokenFormat;
|
||||
|
||||
private final ClaimsHolder claims;
|
||||
|
||||
public AccessToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated,
|
||||
OAuth2AccessToken.TokenType tokenType, Set<String> scopes, OAuth2TokenFormat tokenFormat,
|
||||
ClaimsHolder claims) {
|
||||
super(tokenValue, issuedAt, expiresAt, invalidated);
|
||||
this.tokenType = tokenType;
|
||||
this.scopes = scopes;
|
||||
this.tokenFormat = tokenFormat;
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
public OAuth2AccessToken.TokenType getTokenType() {
|
||||
return this.tokenType;
|
||||
}
|
||||
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
public OAuth2TokenFormat getTokenFormat() {
|
||||
return this.tokenFormat;
|
||||
}
|
||||
|
||||
public ClaimsHolder getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class RefreshToken extends AbstractToken {
|
||||
|
||||
public RefreshToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
|
||||
super(tokenValue, issuedAt, expiresAt, invalidated);
|
||||
}
|
||||
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.entity;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class OAuth2ClientCredentialsGrantAuthorization extends OAuth2AuthorizationGrantAuthorization {
|
||||
|
||||
// @fold:on
|
||||
public OAuth2ClientCredentialsGrantAuthorization(String id, String registeredClientId, String principalName,
|
||||
Set<String> authorizedScopes, AccessToken accessToken) {
|
||||
super(id, registeredClientId, principalName, authorizedScopes, accessToken, null);
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.entity;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
|
||||
public class OAuth2DeviceCodeGrantAuthorization extends OAuth2AuthorizationGrantAuthorization {
|
||||
|
||||
private final Principal principal;
|
||||
|
||||
private final DeviceCode deviceCode;
|
||||
|
||||
private final UserCode userCode;
|
||||
|
||||
private final Set<String> requestedScopes;
|
||||
|
||||
@Indexed
|
||||
private final String deviceState; // Used to correlate the request during the
|
||||
// authorization consent flow
|
||||
|
||||
// @fold:on
|
||||
public OAuth2DeviceCodeGrantAuthorization(String id, String registeredClientId, String principalName,
|
||||
Set<String> authorizedScopes, AccessToken accessToken, RefreshToken refreshToken, Principal principal,
|
||||
DeviceCode deviceCode, UserCode userCode, Set<String> requestedScopes, String deviceState) {
|
||||
super(id, registeredClientId, principalName, authorizedScopes, accessToken, refreshToken);
|
||||
this.principal = principal;
|
||||
this.deviceCode = deviceCode;
|
||||
this.userCode = userCode;
|
||||
this.requestedScopes = requestedScopes;
|
||||
this.deviceState = deviceState;
|
||||
}
|
||||
|
||||
public Principal getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
public DeviceCode getDeviceCode() {
|
||||
return this.deviceCode;
|
||||
}
|
||||
|
||||
public UserCode getUserCode() {
|
||||
return this.userCode;
|
||||
}
|
||||
|
||||
public Set<String> getRequestedScopes() {
|
||||
return this.requestedScopes;
|
||||
}
|
||||
|
||||
public String getDeviceState() {
|
||||
return this.deviceState;
|
||||
}
|
||||
|
||||
public static class DeviceCode extends AbstractToken {
|
||||
|
||||
public DeviceCode(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
|
||||
super(tokenValue, issuedAt, expiresAt, invalidated);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class UserCode extends AbstractToken {
|
||||
|
||||
public UserCode(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated) {
|
||||
super(tokenValue, issuedAt, expiresAt, invalidated);
|
||||
}
|
||||
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.entity;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
|
||||
@RedisHash("oauth2_registered_client")
|
||||
public class OAuth2RegisteredClient {
|
||||
|
||||
@Id
|
||||
private final String id;
|
||||
|
||||
@Indexed
|
||||
private final String clientId;
|
||||
|
||||
private final Instant clientIdIssuedAt;
|
||||
|
||||
private final String clientSecret;
|
||||
|
||||
private final Instant clientSecretExpiresAt;
|
||||
|
||||
private final String clientName;
|
||||
|
||||
private final Set<ClientAuthenticationMethod> clientAuthenticationMethods;
|
||||
|
||||
private final Set<AuthorizationGrantType> authorizationGrantTypes;
|
||||
|
||||
private final Set<String> redirectUris;
|
||||
|
||||
private final Set<String> postLogoutRedirectUris;
|
||||
|
||||
private final Set<String> scopes;
|
||||
|
||||
private final ClientSettings clientSettings;
|
||||
|
||||
private final TokenSettings tokenSettings;
|
||||
|
||||
// @fold:on
|
||||
public OAuth2RegisteredClient(String id, String clientId, Instant clientIdIssuedAt, String clientSecret,
|
||||
Instant clientSecretExpiresAt, String clientName,
|
||||
Set<ClientAuthenticationMethod> clientAuthenticationMethods,
|
||||
Set<AuthorizationGrantType> authorizationGrantTypes, Set<String> redirectUris,
|
||||
Set<String> postLogoutRedirectUris, Set<String> scopes, ClientSettings clientSettings,
|
||||
TokenSettings tokenSettings) {
|
||||
this.id = id;
|
||||
this.clientId = clientId;
|
||||
this.clientIdIssuedAt = clientIdIssuedAt;
|
||||
this.clientSecret = clientSecret;
|
||||
this.clientSecretExpiresAt = clientSecretExpiresAt;
|
||||
this.clientName = clientName;
|
||||
this.clientAuthenticationMethods = clientAuthenticationMethods;
|
||||
this.authorizationGrantTypes = authorizationGrantTypes;
|
||||
this.redirectUris = redirectUris;
|
||||
this.postLogoutRedirectUris = postLogoutRedirectUris;
|
||||
this.scopes = scopes;
|
||||
this.clientSettings = clientSettings;
|
||||
this.tokenSettings = tokenSettings;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
public Instant getClientIdIssuedAt() {
|
||||
return this.clientIdIssuedAt;
|
||||
}
|
||||
|
||||
public String getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
public Instant getClientSecretExpiresAt() {
|
||||
return this.clientSecretExpiresAt;
|
||||
}
|
||||
|
||||
public String getClientName() {
|
||||
return this.clientName;
|
||||
}
|
||||
|
||||
public Set<ClientAuthenticationMethod> getClientAuthenticationMethods() {
|
||||
return this.clientAuthenticationMethods;
|
||||
}
|
||||
|
||||
public Set<AuthorizationGrantType> getAuthorizationGrantTypes() {
|
||||
return this.authorizationGrantTypes;
|
||||
}
|
||||
|
||||
public Set<String> getRedirectUris() {
|
||||
return this.redirectUris;
|
||||
}
|
||||
|
||||
public Set<String> getPostLogoutRedirectUris() {
|
||||
return this.postLogoutRedirectUris;
|
||||
}
|
||||
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
public ClientSettings getClientSettings() {
|
||||
return this.clientSettings;
|
||||
}
|
||||
|
||||
public TokenSettings getTokenSettings() {
|
||||
return this.tokenSettings;
|
||||
}
|
||||
|
||||
public static class ClientSettings {
|
||||
|
||||
private final boolean requireProofKey;
|
||||
|
||||
private final boolean requireAuthorizationConsent;
|
||||
|
||||
private final String jwkSetUrl;
|
||||
|
||||
private final JwsAlgorithm tokenEndpointAuthenticationSigningAlgorithm;
|
||||
|
||||
private final String x509CertificateSubjectDN;
|
||||
|
||||
public ClientSettings(boolean requireProofKey, boolean requireAuthorizationConsent, String jwkSetUrl,
|
||||
JwsAlgorithm tokenEndpointAuthenticationSigningAlgorithm, String x509CertificateSubjectDN) {
|
||||
this.requireProofKey = requireProofKey;
|
||||
this.requireAuthorizationConsent = requireAuthorizationConsent;
|
||||
this.jwkSetUrl = jwkSetUrl;
|
||||
this.tokenEndpointAuthenticationSigningAlgorithm = tokenEndpointAuthenticationSigningAlgorithm;
|
||||
this.x509CertificateSubjectDN = x509CertificateSubjectDN;
|
||||
}
|
||||
|
||||
public boolean isRequireProofKey() {
|
||||
return this.requireProofKey;
|
||||
}
|
||||
|
||||
public boolean isRequireAuthorizationConsent() {
|
||||
return this.requireAuthorizationConsent;
|
||||
}
|
||||
|
||||
public String getJwkSetUrl() {
|
||||
return this.jwkSetUrl;
|
||||
}
|
||||
|
||||
public JwsAlgorithm getTokenEndpointAuthenticationSigningAlgorithm() {
|
||||
return this.tokenEndpointAuthenticationSigningAlgorithm;
|
||||
}
|
||||
|
||||
public String getX509CertificateSubjectDN() {
|
||||
return this.x509CertificateSubjectDN;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TokenSettings {
|
||||
|
||||
private final Duration authorizationCodeTimeToLive;
|
||||
|
||||
private final Duration accessTokenTimeToLive;
|
||||
|
||||
private final OAuth2TokenFormat accessTokenFormat;
|
||||
|
||||
private final Duration deviceCodeTimeToLive;
|
||||
|
||||
private final boolean reuseRefreshTokens;
|
||||
|
||||
private final Duration refreshTokenTimeToLive;
|
||||
|
||||
private final SignatureAlgorithm idTokenSignatureAlgorithm;
|
||||
|
||||
private final boolean x509CertificateBoundAccessTokens;
|
||||
|
||||
public TokenSettings(Duration authorizationCodeTimeToLive, Duration accessTokenTimeToLive,
|
||||
OAuth2TokenFormat accessTokenFormat, Duration deviceCodeTimeToLive, boolean reuseRefreshTokens,
|
||||
Duration refreshTokenTimeToLive, SignatureAlgorithm idTokenSignatureAlgorithm,
|
||||
boolean x509CertificateBoundAccessTokens) {
|
||||
this.authorizationCodeTimeToLive = authorizationCodeTimeToLive;
|
||||
this.accessTokenTimeToLive = accessTokenTimeToLive;
|
||||
this.accessTokenFormat = accessTokenFormat;
|
||||
this.deviceCodeTimeToLive = deviceCodeTimeToLive;
|
||||
this.reuseRefreshTokens = reuseRefreshTokens;
|
||||
this.refreshTokenTimeToLive = refreshTokenTimeToLive;
|
||||
this.idTokenSignatureAlgorithm = idTokenSignatureAlgorithm;
|
||||
this.x509CertificateBoundAccessTokens = x509CertificateBoundAccessTokens;
|
||||
}
|
||||
|
||||
public Duration getAuthorizationCodeTimeToLive() {
|
||||
return this.authorizationCodeTimeToLive;
|
||||
}
|
||||
|
||||
public Duration getAccessTokenTimeToLive() {
|
||||
return this.accessTokenTimeToLive;
|
||||
}
|
||||
|
||||
public OAuth2TokenFormat getAccessTokenFormat() {
|
||||
return this.accessTokenFormat;
|
||||
}
|
||||
|
||||
public Duration getDeviceCodeTimeToLive() {
|
||||
return this.deviceCodeTimeToLive;
|
||||
}
|
||||
|
||||
public boolean isReuseRefreshTokens() {
|
||||
return this.reuseRefreshTokens;
|
||||
}
|
||||
|
||||
public Duration getRefreshTokenTimeToLive() {
|
||||
return this.refreshTokenTimeToLive;
|
||||
}
|
||||
|
||||
public SignatureAlgorithm getIdTokenSignatureAlgorithm() {
|
||||
return this.idTokenSignatureAlgorithm;
|
||||
}
|
||||
|
||||
public boolean isX509CertificateBoundAccessTokens() {
|
||||
return this.x509CertificateBoundAccessTokens;
|
||||
}
|
||||
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.entity;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class OAuth2TokenExchangeGrantAuthorization extends OAuth2AuthorizationGrantAuthorization {
|
||||
|
||||
// @fold:on
|
||||
public OAuth2TokenExchangeGrantAuthorization(String id, String registeredClientId, String principalName,
|
||||
Set<String> authorizedScopes, AccessToken accessToken) {
|
||||
super(id, registeredClientId, principalName, authorizedScopes, accessToken, null);
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.entity;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
@RedisHash("oauth2_authorization_consent")
|
||||
public class OAuth2UserConsent {
|
||||
|
||||
@Id
|
||||
private final String id;
|
||||
|
||||
@Indexed
|
||||
private final String registeredClientId;
|
||||
|
||||
@Indexed
|
||||
private final String principalName;
|
||||
|
||||
private final Set<GrantedAuthority> authorities;
|
||||
|
||||
// @fold:on
|
||||
public OAuth2UserConsent(String id, String registeredClientId, String principalName,
|
||||
Set<GrantedAuthority> authorities) {
|
||||
this.id = id;
|
||||
this.registeredClientId = registeredClientId;
|
||||
this.principalName = principalName;
|
||||
this.authorities = authorities;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getRegisteredClientId() {
|
||||
return this.registeredClientId;
|
||||
}
|
||||
|
||||
public String getPrincipalName() {
|
||||
return this.principalName;
|
||||
}
|
||||
|
||||
public Set<GrantedAuthority> getAuthorities() {
|
||||
return this.authorities;
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.entity;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
|
||||
public class OidcAuthorizationCodeGrantAuthorization extends OAuth2AuthorizationCodeGrantAuthorization {
|
||||
|
||||
private final IdToken idToken;
|
||||
|
||||
// @fold:on
|
||||
public OidcAuthorizationCodeGrantAuthorization(String id, String registeredClientId, String principalName,
|
||||
Set<String> authorizedScopes, AccessToken accessToken, RefreshToken refreshToken, Principal principal,
|
||||
OAuth2AuthorizationRequest authorizationRequest, AuthorizationCode authorizationCode, String state,
|
||||
IdToken idToken) {
|
||||
super(id, registeredClientId, principalName, authorizedScopes, accessToken, refreshToken, principal,
|
||||
authorizationRequest, authorizationCode, state);
|
||||
this.idToken = idToken;
|
||||
}
|
||||
|
||||
public IdToken getIdToken() {
|
||||
return this.idToken;
|
||||
}
|
||||
|
||||
public static class IdToken extends AbstractToken {
|
||||
|
||||
private final ClaimsHolder claims;
|
||||
|
||||
public IdToken(String tokenValue, Instant issuedAt, Instant expiresAt, boolean invalidated,
|
||||
ClaimsHolder claims) {
|
||||
super(tokenValue, issuedAt, expiresAt, invalidated);
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
public ClaimsHolder getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.repository;
|
||||
|
||||
import sample.redis.entity.OAuth2AuthorizationCodeGrantAuthorization;
|
||||
import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
|
||||
import sample.redis.entity.OAuth2DeviceCodeGrantAuthorization;
|
||||
import sample.redis.entity.OidcAuthorizationCodeGrantAuthorization;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface OAuth2AuthorizationGrantAuthorizationRepository
|
||||
extends CrudRepository<OAuth2AuthorizationGrantAuthorization, String> {
|
||||
|
||||
<T extends OAuth2AuthorizationCodeGrantAuthorization> T findByState(String token);
|
||||
|
||||
<T extends OAuth2AuthorizationCodeGrantAuthorization> T findByAuthorizationCode_TokenValue(String token);
|
||||
|
||||
<T extends OAuth2AuthorizationGrantAuthorization> T findByAccessToken_TokenValue(String token);
|
||||
|
||||
<T extends OAuth2AuthorizationGrantAuthorization> T findByRefreshToken_TokenValue(String token);
|
||||
|
||||
<T extends OidcAuthorizationCodeGrantAuthorization> T findByIdToken_TokenValue(String token);
|
||||
|
||||
<T extends OAuth2DeviceCodeGrantAuthorization> T findByDeviceState(String token);
|
||||
|
||||
<T extends OAuth2DeviceCodeGrantAuthorization> T findByDeviceCode_TokenValue(String token);
|
||||
|
||||
<T extends OAuth2DeviceCodeGrantAuthorization> T findByUserCode_TokenValue(String token);
|
||||
|
||||
<T extends OAuth2AuthorizationGrantAuthorization> T findByStateOrAuthorizationCode_TokenValueOrAccessToken_TokenValueOrRefreshToken_TokenValueOrIdToken_TokenValueOrDeviceStateOrDeviceCode_TokenValueOrUserCode_TokenValue(
|
||||
String state, String authorizationCode, String accessToken, String refreshToken, String idToken,
|
||||
String deviceState, String deviceCode, String userCode);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.repository;
|
||||
|
||||
import sample.redis.entity.OAuth2RegisteredClient;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface OAuth2RegisteredClientRepository extends CrudRepository<OAuth2RegisteredClient, String> {
|
||||
|
||||
OAuth2RegisteredClient findByClientId(String clientId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.repository;
|
||||
|
||||
import sample.redis.entity.OAuth2UserConsent;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface OAuth2UserConsentRepository extends CrudRepository<OAuth2UserConsent, String> {
|
||||
|
||||
OAuth2UserConsent findByRegisteredClientIdAndPrincipalName(String registeredClientId, String principalName);
|
||||
|
||||
void deleteByRegisteredClientIdAndPrincipalName(String registeredClientId, String principalName);
|
||||
|
||||
}
|
||||
498
docs/src/main/java/sample/redis/service/ModelMapper.java
Normal file
498
docs/src/main/java/sample/redis/service/ModelMapper.java
Normal file
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.redis.service;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import sample.redis.entity.OAuth2AuthorizationCodeGrantAuthorization;
|
||||
import sample.redis.entity.OAuth2AuthorizationGrantAuthorization;
|
||||
import sample.redis.entity.OAuth2ClientCredentialsGrantAuthorization;
|
||||
import sample.redis.entity.OAuth2DeviceCodeGrantAuthorization;
|
||||
import sample.redis.entity.OAuth2RegisteredClient;
|
||||
import sample.redis.entity.OAuth2TokenExchangeGrantAuthorization;
|
||||
import sample.redis.entity.OAuth2UserConsent;
|
||||
import sample.redis.entity.OidcAuthorizationCodeGrantAuthorization;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2DeviceCode;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2UserCode;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
final class ModelMapper {
|
||||
|
||||
static OAuth2RegisteredClient convertOAuth2RegisteredClient(RegisteredClient registeredClient) {
|
||||
OAuth2RegisteredClient.ClientSettings clientSettings = new OAuth2RegisteredClient.ClientSettings(
|
||||
registeredClient.getClientSettings().isRequireProofKey(),
|
||||
registeredClient.getClientSettings().isRequireAuthorizationConsent(),
|
||||
registeredClient.getClientSettings().getJwkSetUrl(),
|
||||
registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm(),
|
||||
registeredClient.getClientSettings().getX509CertificateSubjectDN());
|
||||
|
||||
OAuth2RegisteredClient.TokenSettings tokenSettings = new OAuth2RegisteredClient.TokenSettings(
|
||||
registeredClient.getTokenSettings().getAuthorizationCodeTimeToLive(),
|
||||
registeredClient.getTokenSettings().getAccessTokenTimeToLive(),
|
||||
registeredClient.getTokenSettings().getAccessTokenFormat(),
|
||||
registeredClient.getTokenSettings().getDeviceCodeTimeToLive(),
|
||||
registeredClient.getTokenSettings().isReuseRefreshTokens(),
|
||||
registeredClient.getTokenSettings().getRefreshTokenTimeToLive(),
|
||||
registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm(),
|
||||
registeredClient.getTokenSettings().isX509CertificateBoundAccessTokens());
|
||||
|
||||
return new OAuth2RegisteredClient(registeredClient.getId(), registeredClient.getClientId(),
|
||||
registeredClient.getClientIdIssuedAt(), registeredClient.getClientSecret(),
|
||||
registeredClient.getClientSecretExpiresAt(), registeredClient.getClientName(),
|
||||
registeredClient.getClientAuthenticationMethods(), registeredClient.getAuthorizationGrantTypes(),
|
||||
registeredClient.getRedirectUris(), registeredClient.getPostLogoutRedirectUris(),
|
||||
registeredClient.getScopes(), clientSettings, tokenSettings);
|
||||
}
|
||||
|
||||
static OAuth2UserConsent convertOAuth2UserConsent(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
String id = authorizationConsent.getRegisteredClientId()
|
||||
.concat("-")
|
||||
.concat(authorizationConsent.getPrincipalName());
|
||||
return new OAuth2UserConsent(id, authorizationConsent.getRegisteredClientId(),
|
||||
authorizationConsent.getPrincipalName(), authorizationConsent.getAuthorities());
|
||||
}
|
||||
|
||||
static OAuth2AuthorizationGrantAuthorization convertOAuth2AuthorizationGrantAuthorization(
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorization.getAuthorizationGrantType())) {
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization
|
||||
.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
return authorizationRequest.getScopes().contains(OidcScopes.OPENID)
|
||||
? convertOidcAuthorizationCodeGrantAuthorization(authorization)
|
||||
: convertOAuth2AuthorizationCodeGrantAuthorization(authorization);
|
||||
}
|
||||
else if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(authorization.getAuthorizationGrantType())) {
|
||||
return convertOAuth2ClientCredentialsGrantAuthorization(authorization);
|
||||
}
|
||||
else if (AuthorizationGrantType.DEVICE_CODE.equals(authorization.getAuthorizationGrantType())) {
|
||||
return convertOAuth2DeviceCodeGrantAuthorization(authorization);
|
||||
}
|
||||
else if (AuthorizationGrantType.TOKEN_EXCHANGE.equals(authorization.getAuthorizationGrantType())) {
|
||||
return convertOAuth2TokenExchangeGrantAuthorization(authorization);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static OidcAuthorizationCodeGrantAuthorization convertOidcAuthorizationCodeGrantAuthorization(
|
||||
OAuth2Authorization authorization) {
|
||||
OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode authorizationCode = extractAuthorizationCode(
|
||||
authorization);
|
||||
OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
|
||||
OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken = extractRefreshToken(authorization);
|
||||
OidcAuthorizationCodeGrantAuthorization.IdToken idToken = extractIdToken(authorization);
|
||||
|
||||
return new OidcAuthorizationCodeGrantAuthorization(authorization.getId(), authorization.getRegisteredClientId(),
|
||||
authorization.getPrincipalName(), authorization.getAuthorizedScopes(), accessToken, refreshToken,
|
||||
authorization.getAttribute(Principal.class.getName()),
|
||||
authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()), authorizationCode,
|
||||
authorization.getAttribute(OAuth2ParameterNames.STATE), idToken);
|
||||
}
|
||||
|
||||
static OAuth2AuthorizationCodeGrantAuthorization convertOAuth2AuthorizationCodeGrantAuthorization(
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode authorizationCode = extractAuthorizationCode(
|
||||
authorization);
|
||||
OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
|
||||
OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken = extractRefreshToken(authorization);
|
||||
|
||||
return new OAuth2AuthorizationCodeGrantAuthorization(authorization.getId(),
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName(),
|
||||
authorization.getAuthorizedScopes(), accessToken, refreshToken,
|
||||
authorization.getAttribute(Principal.class.getName()),
|
||||
authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()), authorizationCode,
|
||||
authorization.getAttribute(OAuth2ParameterNames.STATE));
|
||||
}
|
||||
|
||||
static OAuth2ClientCredentialsGrantAuthorization convertOAuth2ClientCredentialsGrantAuthorization(
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
|
||||
|
||||
return new OAuth2ClientCredentialsGrantAuthorization(authorization.getId(),
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName(),
|
||||
authorization.getAuthorizedScopes(), accessToken);
|
||||
}
|
||||
|
||||
static OAuth2DeviceCodeGrantAuthorization convertOAuth2DeviceCodeGrantAuthorization(
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
|
||||
OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken = extractRefreshToken(authorization);
|
||||
OAuth2DeviceCodeGrantAuthorization.DeviceCode deviceCode = extractDeviceCode(authorization);
|
||||
OAuth2DeviceCodeGrantAuthorization.UserCode userCode = extractUserCode(authorization);
|
||||
|
||||
return new OAuth2DeviceCodeGrantAuthorization(authorization.getId(), authorization.getRegisteredClientId(),
|
||||
authorization.getPrincipalName(), authorization.getAuthorizedScopes(), accessToken, refreshToken,
|
||||
authorization.getAttribute(Principal.class.getName()), deviceCode, userCode,
|
||||
authorization.getAttribute(OAuth2ParameterNames.SCOPE),
|
||||
authorization.getAttribute(OAuth2ParameterNames.STATE));
|
||||
}
|
||||
|
||||
static OAuth2TokenExchangeGrantAuthorization convertOAuth2TokenExchangeGrantAuthorization(
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = extractAccessToken(authorization);
|
||||
|
||||
return new OAuth2TokenExchangeGrantAuthorization(authorization.getId(), authorization.getRegisteredClientId(),
|
||||
authorization.getPrincipalName(), authorization.getAuthorizedScopes(), accessToken);
|
||||
}
|
||||
|
||||
static OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode extractAuthorizationCode(
|
||||
OAuth2Authorization authorization) {
|
||||
OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode authorizationCode = null;
|
||||
if (authorization.getToken(OAuth2AuthorizationCode.class) != null) {
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> oauth2AuthorizationCode = authorization
|
||||
.getToken(OAuth2AuthorizationCode.class);
|
||||
authorizationCode = new OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode(
|
||||
oauth2AuthorizationCode.getToken().getTokenValue(),
|
||||
oauth2AuthorizationCode.getToken().getIssuedAt(), oauth2AuthorizationCode.getToken().getExpiresAt(),
|
||||
oauth2AuthorizationCode.isInvalidated());
|
||||
}
|
||||
return authorizationCode;
|
||||
}
|
||||
|
||||
static OAuth2AuthorizationGrantAuthorization.AccessToken extractAccessToken(OAuth2Authorization authorization) {
|
||||
OAuth2AuthorizationGrantAuthorization.AccessToken accessToken = null;
|
||||
if (authorization.getAccessToken() != null) {
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> oauth2AccessToken = authorization.getAccessToken();
|
||||
OAuth2TokenFormat tokenFormat = null;
|
||||
if (OAuth2TokenFormat.SELF_CONTAINED.getValue()
|
||||
.equals(oauth2AccessToken.getMetadata(OAuth2TokenFormat.class.getName()))) {
|
||||
tokenFormat = OAuth2TokenFormat.SELF_CONTAINED;
|
||||
}
|
||||
else if (OAuth2TokenFormat.REFERENCE.getValue()
|
||||
.equals(oauth2AccessToken.getMetadata(OAuth2TokenFormat.class.getName()))) {
|
||||
tokenFormat = OAuth2TokenFormat.REFERENCE;
|
||||
}
|
||||
accessToken = new OAuth2AuthorizationGrantAuthorization.AccessToken(
|
||||
oauth2AccessToken.getToken().getTokenValue(), oauth2AccessToken.getToken().getIssuedAt(),
|
||||
oauth2AccessToken.getToken().getExpiresAt(), oauth2AccessToken.isInvalidated(),
|
||||
oauth2AccessToken.getToken().getTokenType(), oauth2AccessToken.getToken().getScopes(), tokenFormat,
|
||||
new OAuth2AuthorizationGrantAuthorization.ClaimsHolder(oauth2AccessToken.getClaims()));
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
static OAuth2AuthorizationGrantAuthorization.RefreshToken extractRefreshToken(OAuth2Authorization authorization) {
|
||||
OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken = null;
|
||||
if (authorization.getRefreshToken() != null) {
|
||||
OAuth2Authorization.Token<OAuth2RefreshToken> oauth2RefreshToken = authorization.getRefreshToken();
|
||||
refreshToken = new OAuth2AuthorizationGrantAuthorization.RefreshToken(
|
||||
oauth2RefreshToken.getToken().getTokenValue(), oauth2RefreshToken.getToken().getIssuedAt(),
|
||||
oauth2RefreshToken.getToken().getExpiresAt(), oauth2RefreshToken.isInvalidated());
|
||||
}
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
static OidcAuthorizationCodeGrantAuthorization.IdToken extractIdToken(OAuth2Authorization authorization) {
|
||||
OidcAuthorizationCodeGrantAuthorization.IdToken idToken = null;
|
||||
if (authorization.getToken(OidcIdToken.class) != null) {
|
||||
OAuth2Authorization.Token<OidcIdToken> oidcIdToken = authorization.getToken(OidcIdToken.class);
|
||||
idToken = new OidcAuthorizationCodeGrantAuthorization.IdToken(oidcIdToken.getToken().getTokenValue(),
|
||||
oidcIdToken.getToken().getIssuedAt(), oidcIdToken.getToken().getExpiresAt(),
|
||||
oidcIdToken.isInvalidated(),
|
||||
new OAuth2AuthorizationGrantAuthorization.ClaimsHolder(oidcIdToken.getClaims()));
|
||||
}
|
||||
return idToken;
|
||||
}
|
||||
|
||||
static OAuth2DeviceCodeGrantAuthorization.DeviceCode extractDeviceCode(OAuth2Authorization authorization) {
|
||||
OAuth2DeviceCodeGrantAuthorization.DeviceCode deviceCode = null;
|
||||
if (authorization.getToken(OAuth2DeviceCode.class) != null) {
|
||||
OAuth2Authorization.Token<OAuth2DeviceCode> oauth2DeviceCode = authorization
|
||||
.getToken(OAuth2DeviceCode.class);
|
||||
deviceCode = new OAuth2DeviceCodeGrantAuthorization.DeviceCode(oauth2DeviceCode.getToken().getTokenValue(),
|
||||
oauth2DeviceCode.getToken().getIssuedAt(), oauth2DeviceCode.getToken().getExpiresAt(),
|
||||
oauth2DeviceCode.isInvalidated());
|
||||
}
|
||||
return deviceCode;
|
||||
}
|
||||
|
||||
static OAuth2DeviceCodeGrantAuthorization.UserCode extractUserCode(OAuth2Authorization authorization) {
|
||||
OAuth2DeviceCodeGrantAuthorization.UserCode userCode = null;
|
||||
if (authorization.getToken(OAuth2UserCode.class) != null) {
|
||||
OAuth2Authorization.Token<OAuth2UserCode> oauth2UserCode = authorization.getToken(OAuth2UserCode.class);
|
||||
userCode = new OAuth2DeviceCodeGrantAuthorization.UserCode(oauth2UserCode.getToken().getTokenValue(),
|
||||
oauth2UserCode.getToken().getIssuedAt(), oauth2UserCode.getToken().getExpiresAt(),
|
||||
oauth2UserCode.isInvalidated());
|
||||
}
|
||||
return userCode;
|
||||
}
|
||||
|
||||
static RegisteredClient convertRegisteredClient(OAuth2RegisteredClient oauth2RegisteredClient) {
|
||||
ClientSettings.Builder clientSettingsBuilder = ClientSettings.builder()
|
||||
.requireProofKey(oauth2RegisteredClient.getClientSettings().isRequireProofKey())
|
||||
.requireAuthorizationConsent(oauth2RegisteredClient.getClientSettings().isRequireAuthorizationConsent());
|
||||
if (StringUtils.hasText(oauth2RegisteredClient.getClientSettings().getJwkSetUrl())) {
|
||||
clientSettingsBuilder.jwkSetUrl(oauth2RegisteredClient.getClientSettings().getJwkSetUrl());
|
||||
}
|
||||
if (oauth2RegisteredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() != null) {
|
||||
clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(
|
||||
oauth2RegisteredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm());
|
||||
}
|
||||
if (StringUtils.hasText(oauth2RegisteredClient.getClientSettings().getX509CertificateSubjectDN())) {
|
||||
clientSettingsBuilder
|
||||
.x509CertificateSubjectDN(oauth2RegisteredClient.getClientSettings().getX509CertificateSubjectDN());
|
||||
}
|
||||
ClientSettings clientSettings = clientSettingsBuilder.build();
|
||||
|
||||
TokenSettings.Builder tokenSettingsBuilder = TokenSettings.builder();
|
||||
if (oauth2RegisteredClient.getTokenSettings().getAuthorizationCodeTimeToLive() != null) {
|
||||
tokenSettingsBuilder.authorizationCodeTimeToLive(
|
||||
oauth2RegisteredClient.getTokenSettings().getAuthorizationCodeTimeToLive());
|
||||
}
|
||||
if (oauth2RegisteredClient.getTokenSettings().getAccessTokenTimeToLive() != null) {
|
||||
tokenSettingsBuilder
|
||||
.accessTokenTimeToLive(oauth2RegisteredClient.getTokenSettings().getAccessTokenTimeToLive());
|
||||
}
|
||||
if (oauth2RegisteredClient.getTokenSettings().getAccessTokenFormat() != null) {
|
||||
tokenSettingsBuilder.accessTokenFormat(oauth2RegisteredClient.getTokenSettings().getAccessTokenFormat());
|
||||
}
|
||||
if (oauth2RegisteredClient.getTokenSettings().getDeviceCodeTimeToLive() != null) {
|
||||
tokenSettingsBuilder
|
||||
.deviceCodeTimeToLive(oauth2RegisteredClient.getTokenSettings().getDeviceCodeTimeToLive());
|
||||
}
|
||||
tokenSettingsBuilder.reuseRefreshTokens(oauth2RegisteredClient.getTokenSettings().isReuseRefreshTokens());
|
||||
if (oauth2RegisteredClient.getTokenSettings().getRefreshTokenTimeToLive() != null) {
|
||||
tokenSettingsBuilder
|
||||
.refreshTokenTimeToLive(oauth2RegisteredClient.getTokenSettings().getRefreshTokenTimeToLive());
|
||||
}
|
||||
if (oauth2RegisteredClient.getTokenSettings().getIdTokenSignatureAlgorithm() != null) {
|
||||
tokenSettingsBuilder
|
||||
.idTokenSignatureAlgorithm(oauth2RegisteredClient.getTokenSettings().getIdTokenSignatureAlgorithm());
|
||||
}
|
||||
tokenSettingsBuilder.x509CertificateBoundAccessTokens(
|
||||
oauth2RegisteredClient.getTokenSettings().isX509CertificateBoundAccessTokens());
|
||||
TokenSettings tokenSettings = tokenSettingsBuilder.build();
|
||||
|
||||
RegisteredClient.Builder registeredClientBuilder = RegisteredClient.withId(oauth2RegisteredClient.getId())
|
||||
.clientId(oauth2RegisteredClient.getClientId())
|
||||
.clientIdIssuedAt(oauth2RegisteredClient.getClientIdIssuedAt())
|
||||
.clientSecret(oauth2RegisteredClient.getClientSecret())
|
||||
.clientSecretExpiresAt(oauth2RegisteredClient.getClientSecretExpiresAt())
|
||||
.clientName(oauth2RegisteredClient.getClientName())
|
||||
.clientAuthenticationMethods((clientAuthenticationMethods) -> clientAuthenticationMethods
|
||||
.addAll(oauth2RegisteredClient.getClientAuthenticationMethods()))
|
||||
.authorizationGrantTypes((authorizationGrantTypes) -> authorizationGrantTypes
|
||||
.addAll(oauth2RegisteredClient.getAuthorizationGrantTypes()))
|
||||
.clientSettings(clientSettings)
|
||||
.tokenSettings(tokenSettings);
|
||||
if (!CollectionUtils.isEmpty(oauth2RegisteredClient.getRedirectUris())) {
|
||||
registeredClientBuilder.redirectUris((redirectUris) -> redirectUris.addAll(oauth2RegisteredClient.getRedirectUris()));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(oauth2RegisteredClient.getPostLogoutRedirectUris())) {
|
||||
registeredClientBuilder.postLogoutRedirectUris((postLogoutRedirectUris) ->
|
||||
postLogoutRedirectUris.addAll(oauth2RegisteredClient.getPostLogoutRedirectUris()));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(oauth2RegisteredClient.getScopes())) {
|
||||
registeredClientBuilder.scopes((scopes) -> scopes.addAll(oauth2RegisteredClient.getScopes()));
|
||||
}
|
||||
|
||||
return registeredClientBuilder.build();
|
||||
}
|
||||
|
||||
static OAuth2AuthorizationConsent convertOAuth2AuthorizationConsent(OAuth2UserConsent userConsent) {
|
||||
return OAuth2AuthorizationConsent.withId(userConsent.getRegisteredClientId(), userConsent.getPrincipalName())
|
||||
.authorities((authorities) -> authorities.addAll(userConsent.getAuthorities()))
|
||||
.build();
|
||||
}
|
||||
|
||||
static void mapOAuth2AuthorizationGrantAuthorization(
|
||||
OAuth2AuthorizationGrantAuthorization authorizationGrantAuthorization,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
|
||||
if (authorizationGrantAuthorization instanceof OidcAuthorizationCodeGrantAuthorization authorizationGrant) {
|
||||
mapOidcAuthorizationCodeGrantAuthorization(authorizationGrant, builder);
|
||||
}
|
||||
else if (authorizationGrantAuthorization instanceof OAuth2AuthorizationCodeGrantAuthorization authorizationGrant) {
|
||||
mapOAuth2AuthorizationCodeGrantAuthorization(authorizationGrant, builder);
|
||||
}
|
||||
else if (authorizationGrantAuthorization instanceof OAuth2ClientCredentialsGrantAuthorization authorizationGrant) {
|
||||
mapOAuth2ClientCredentialsGrantAuthorization(authorizationGrant, builder);
|
||||
}
|
||||
else if (authorizationGrantAuthorization instanceof OAuth2DeviceCodeGrantAuthorization authorizationGrant) {
|
||||
mapOAuth2DeviceCodeGrantAuthorization(authorizationGrant, builder);
|
||||
}
|
||||
else if (authorizationGrantAuthorization instanceof OAuth2TokenExchangeGrantAuthorization authorizationGrant) {
|
||||
mapOAuth2TokenExchangeGrantAuthorization(authorizationGrant, builder);
|
||||
}
|
||||
}
|
||||
|
||||
static void mapOidcAuthorizationCodeGrantAuthorization(
|
||||
OidcAuthorizationCodeGrantAuthorization authorizationCodeGrantAuthorization,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
|
||||
mapOAuth2AuthorizationCodeGrantAuthorization(authorizationCodeGrantAuthorization, builder);
|
||||
mapIdToken(authorizationCodeGrantAuthorization.getIdToken(), builder);
|
||||
}
|
||||
|
||||
static void mapOAuth2AuthorizationCodeGrantAuthorization(
|
||||
OAuth2AuthorizationCodeGrantAuthorization authorizationCodeGrantAuthorization,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
|
||||
builder.id(authorizationCodeGrantAuthorization.getId())
|
||||
.principalName(authorizationCodeGrantAuthorization.getPrincipalName())
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizedScopes(authorizationCodeGrantAuthorization.getAuthorizedScopes())
|
||||
.attribute(Principal.class.getName(), authorizationCodeGrantAuthorization.getPrincipal())
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(),
|
||||
authorizationCodeGrantAuthorization.getAuthorizationRequest());
|
||||
if (StringUtils.hasText(authorizationCodeGrantAuthorization.getState())) {
|
||||
builder.attribute(OAuth2ParameterNames.STATE, authorizationCodeGrantAuthorization.getState());
|
||||
}
|
||||
|
||||
mapAuthorizationCode(authorizationCodeGrantAuthorization.getAuthorizationCode(), builder);
|
||||
mapAccessToken(authorizationCodeGrantAuthorization.getAccessToken(), builder);
|
||||
mapRefreshToken(authorizationCodeGrantAuthorization.getRefreshToken(), builder);
|
||||
}
|
||||
|
||||
static void mapOAuth2ClientCredentialsGrantAuthorization(
|
||||
OAuth2ClientCredentialsGrantAuthorization clientCredentialsGrantAuthorization,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
|
||||
builder.id(clientCredentialsGrantAuthorization.getId())
|
||||
.principalName(clientCredentialsGrantAuthorization.getPrincipalName())
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.authorizedScopes(clientCredentialsGrantAuthorization.getAuthorizedScopes());
|
||||
|
||||
mapAccessToken(clientCredentialsGrantAuthorization.getAccessToken(), builder);
|
||||
}
|
||||
|
||||
static void mapOAuth2DeviceCodeGrantAuthorization(OAuth2DeviceCodeGrantAuthorization deviceCodeGrantAuthorization,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
|
||||
builder.id(deviceCodeGrantAuthorization.getId())
|
||||
.principalName(deviceCodeGrantAuthorization.getPrincipalName())
|
||||
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
||||
.authorizedScopes(deviceCodeGrantAuthorization.getAuthorizedScopes());
|
||||
if (deviceCodeGrantAuthorization.getPrincipal() != null) {
|
||||
builder.attribute(Principal.class.getName(), deviceCodeGrantAuthorization.getPrincipal());
|
||||
}
|
||||
if (deviceCodeGrantAuthorization.getRequestedScopes() != null) {
|
||||
builder.attribute(OAuth2ParameterNames.SCOPE, deviceCodeGrantAuthorization.getRequestedScopes());
|
||||
}
|
||||
if (StringUtils.hasText(deviceCodeGrantAuthorization.getDeviceState())) {
|
||||
builder.attribute(OAuth2ParameterNames.STATE, deviceCodeGrantAuthorization.getDeviceState());
|
||||
}
|
||||
|
||||
mapAccessToken(deviceCodeGrantAuthorization.getAccessToken(), builder);
|
||||
mapRefreshToken(deviceCodeGrantAuthorization.getRefreshToken(), builder);
|
||||
mapDeviceCode(deviceCodeGrantAuthorization.getDeviceCode(), builder);
|
||||
mapUserCode(deviceCodeGrantAuthorization.getUserCode(), builder);
|
||||
}
|
||||
|
||||
static void mapOAuth2TokenExchangeGrantAuthorization(
|
||||
OAuth2TokenExchangeGrantAuthorization tokenExchangeGrantAuthorization,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
|
||||
builder.id(tokenExchangeGrantAuthorization.getId())
|
||||
.principalName(tokenExchangeGrantAuthorization.getPrincipalName())
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.authorizedScopes(tokenExchangeGrantAuthorization.getAuthorizedScopes());
|
||||
|
||||
mapAccessToken(tokenExchangeGrantAuthorization.getAccessToken(), builder);
|
||||
}
|
||||
|
||||
static void mapAuthorizationCode(OAuth2AuthorizationCodeGrantAuthorization.AuthorizationCode authorizationCode,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
if (authorizationCode == null) {
|
||||
return;
|
||||
}
|
||||
OAuth2AuthorizationCode oauth2AuthorizationCode = new OAuth2AuthorizationCode(authorizationCode.getTokenValue(),
|
||||
authorizationCode.getIssuedAt(), authorizationCode.getExpiresAt());
|
||||
builder.token(oauth2AuthorizationCode, (metadata) -> metadata
|
||||
.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, authorizationCode.isInvalidated()));
|
||||
}
|
||||
|
||||
static void mapAccessToken(OAuth2AuthorizationGrantAuthorization.AccessToken accessToken,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
OAuth2AccessToken oauth2AccessToken = new OAuth2AccessToken(accessToken.getTokenType(),
|
||||
accessToken.getTokenValue(), accessToken.getIssuedAt(), accessToken.getExpiresAt(),
|
||||
accessToken.getScopes());
|
||||
builder.token(oauth2AccessToken, (metadata) -> {
|
||||
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, accessToken.isInvalidated());
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, accessToken.getClaims().getClaims());
|
||||
metadata.put(OAuth2TokenFormat.class.getName(), accessToken.getTokenFormat().getValue());
|
||||
});
|
||||
}
|
||||
|
||||
static void mapRefreshToken(OAuth2AuthorizationGrantAuthorization.RefreshToken refreshToken,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
if (refreshToken == null) {
|
||||
return;
|
||||
}
|
||||
OAuth2RefreshToken oauth2RefreshToken = new OAuth2RefreshToken(refreshToken.getTokenValue(),
|
||||
refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
|
||||
builder.token(oauth2RefreshToken, (metadata) -> metadata
|
||||
.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, refreshToken.isInvalidated()));
|
||||
}
|
||||
|
||||
static void mapIdToken(OidcAuthorizationCodeGrantAuthorization.IdToken idToken,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
if (idToken == null) {
|
||||
return;
|
||||
}
|
||||
OidcIdToken oidcIdToken = new OidcIdToken(idToken.getTokenValue(), idToken.getIssuedAt(),
|
||||
idToken.getExpiresAt(), idToken.getClaims().getClaims());
|
||||
builder.token(oidcIdToken, (metadata) -> {
|
||||
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, idToken.isInvalidated());
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims().getClaims());
|
||||
});
|
||||
}
|
||||
|
||||
static void mapDeviceCode(OAuth2DeviceCodeGrantAuthorization.DeviceCode deviceCode,
|
||||
OAuth2Authorization.Builder builder) {
|
||||
if (deviceCode == null) {
|
||||
return;
|
||||
}
|
||||
OAuth2DeviceCode oauth2DeviceCode = new OAuth2DeviceCode(deviceCode.getTokenValue(), deviceCode.getIssuedAt(),
|
||||
deviceCode.getExpiresAt());
|
||||
builder.token(oauth2DeviceCode, (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME,
|
||||
deviceCode.isInvalidated()));
|
||||
}
|
||||
|
||||
static void mapUserCode(OAuth2DeviceCodeGrantAuthorization.UserCode userCode, OAuth2Authorization.Builder builder) {
|
||||
if (userCode == null) {
|
||||
return;
|
||||
}
|
||||
OAuth2UserCode oauth2UserCode = new OAuth2UserCode(userCode.getTokenValue(), userCode.getIssuedAt(),
|
||||
userCode.getExpiresAt());
|
||||
builder.token(oauth2UserCode, (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME,
|
||||
userCode.isInvalidated()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user