In previous article, it is introduced that the client requests authorization from the authorization server (using Spring Authorization Server) and accesses the protected resources of the resource server. When creating an OAuth2 client service, the client registration is usually autoloaded from the application.yml
file, Spring auto-configuration uses OAuth2ClientProperties
to create a ClientRegistration
and instantiates a ClientRegistrationRepository
.
The following Spring auto-configuration OAuth2ClientRegistrationRepositoryConfiguration
code is as follows:
@Configuration(
proxyBeanMethods = false
)
@EnableConfigurationProperties({OAuth2ClientProperties.class})
@Conditional({ClientsConfiguredCondition.class})
class OAuth2ClientRegistrationRepositoryConfiguration {
OAuth2ClientRegistrationRepositoryConfiguration() {
}
@Bean
@ConditionalOnMissingBean({ClientRegistrationRepository.class})
InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList(OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
return new InMemoryClientRegistrationRepository(registrations);
}
}
As you can see, ClientRegistrationRepository
is implemented by default and only one implementation class is InMemoryClientRegistrationRepository
, which stores ClientRegistration in memory, and this method may have certain limitations in a production environment.
In this article you will learn how to implement OAuth2 client persistence by extending ClientRegistrationRepository.
💡 Note: If you don’t want to read till the end, you can view the source code here.Don’t forget to give a star to the project if you like it!
OAuth2 client service implementation
In this section, you will create a simple OAuth2 client service and store OAuth2 client information through the database, now look at the code!
Maven dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
...
Configuration
First, let us configure service port information and database connection information through application.yml
:
server:
port: 8070
spring:
datasource:
druid:
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/persistence_oauth2_client?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: <<username>> # Remember to change the username
password: <<password>> # Remember to change the password
Next, we create a database table based on ClientRegistration
to store OAuth2 client information:
CREATE TABLE `oauth2_registered_client`
(
`registration_id` varchar(100) NOT NULL,
`client_id` varchar(100) NOT NULL,
`client_secret` varchar(200) DEFAULT NULL,
`client_authentication_method` varchar(100) NOT NULL,
`authorization_grant_type` varchar(100) NOT NULL,
`client_name` varchar(200) DEFAULT NULL,
`redirect_uri` varchar(1000) NOT NULL,
`scopes` varchar(1000) NOT NULL,
`authorization_uri` varchar(1000) DEFAULT NULL,
`token_uri` varchar(1000) NOT NULL,
`jwk_set_uri` varchar(1000) DEFAULT NULL,
`issuer_uri` varchar(1000) DEFAULT NULL,
`user_info_uri` varchar(1000) DEFAULT NULL,
`user_info_authentication_method` varchar(100) DEFAULT NULL,
`user_name_attribute_name` varchar(100) DEFAULT NULL,
`configuration_metadata` varchar(2000) DEFAULT NULL,
PRIMARY KEY (`registration_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Below we implement the JdbcClientRegistrationRepository that ClientRegistrationRepository extends:
public class JdbcClientRegistrationRepository implements ClientRegistrationRepository {
private static final String COLUMN_NAMES = "registration_id,client_id,client_secret,client_authentication_method,authorization_grant_type,client_name,redirect_uri,scopes,authorization_uri,token_uri,jwk_set_uri,issuer_uri,user_info_uri,user_info_authentication_method,user_name_attribute_name,configuration_metadata";
private static final String TABLE_NAME = "oauth2_registered_client";
private static final String LOAD_CLIENT_REGISTERED_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE ";
private static final String INSERT_CLIENT_REGISTERED_SQL = "INSERT INTO " + TABLE_NAME + "(" + COLUMN_NAMES + ") VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
private static final String UPDATE_CLIENT_REGISTERED_SQL = "UPDATE " + TABLE_NAME + " SET client_id = ?,client_secret = ?,client_authentication_method = ?,authorization_grant_type = ?,client_name = ?,redirect_uri = ?,scopes = ?,authorization_uri = ?,token_uri = ?,jwk_set_uri = ?,issuer_uri = ?,user_info_uri = ?,user_info_authentication_method = ?,user_name_attribute_name = ? WHERE registration_id = ?";
private final JdbcOperations jdbcOperations;
private RowMapper<ClientRegistration> clientRegistrationRowMapper;
private Function<ClientRegistration, List<SqlParameterValue>> clientRegistrationListParametersMapper;
public JdbcClientRegistrationRepository(JdbcOperations jdbcOperations) {
Assert.notNull(jdbcOperations, "JdbcOperations can not be null");
this.jdbcOperations = jdbcOperations;
this.clientRegistrationRowMapper = new ClientRegistrationRowMapper();
this.clientRegistrationListParametersMapper = new ClientRegistrationParametersMapper();
}
@Override
public ClientRegistration findByRegistrationId(String registrationId) {
Assert.hasText(registrationId, "registrationId cannot be empty");
return this.findBy("registration_id = ?", registrationId);
}
private ClientRegistration findBy(String filter, Object... args) {
List<ClientRegistration> result = this.jdbcOperations.query(LOAD_CLIENT_REGISTERED_SQL + filter, this.clientRegistrationRowMapper, args);
return !result.isEmpty() ? result.get(0) : null;
}
public void save(ClientRegistration clientRegistration) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
ClientRegistration existingClientRegistration = this.findByRegistrationId(clientRegistration.getRegistrationId());
if (existingClientRegistration != null) {
this.updateRegisteredClient(clientRegistration);
} else {
this.insertClientRegistration(clientRegistration);
}
}
private void updateRegisteredClient(ClientRegistration clientRegistration) {
List<SqlParameterValue> parameterValues = this.clientRegistrationListParametersMapper.apply(clientRegistration);
PreparedStatementSetter statementSetter = new ArgumentPreparedStatementSetter(parameterValues.toArray());
this.jdbcOperations.update(UPDATE_CLIENT_REGISTERED_SQL, statementSetter);
}
private void insertClientRegistration(ClientRegistration clientRegistration) {
List<SqlParameterValue> parameterValues = this.clientRegistrationListParametersMapper.apply(clientRegistration);
PreparedStatementSetter statementSetter = new ArgumentPreparedStatementSetter(parameterValues.toArray());
this.jdbcOperations.update(INSERT_CLIENT_REGISTERED_SQL, statementSetter);
}
//...
}
Afterwards we will create the SecurityConfig
security configuration class, in which we will create the specific beans required by the OAuth2 Client. First we will instantiate the above custom JdbcClientRegistrationRepository:
@Bean
public ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
return new JdbcClientRegistrationRepository(jdbcTemplate);
}
ClientRegistration: Indicates a client registered using OAuth 2.0 or OpenID Connect (OIDC). It contains all basic information about the client, such as client ID, client secret, authorization type and various URIs.
ClientRegistrationRepository: This is a repository that contains ClientRegistrations and is responsible for persistence.
Next configure the OAuth2AuthorizedClient management class OAuth2AuthorizedClientService:
@Bean
public OAuth2AuthorizedClientService authorizedClientService(
JdbcTemplate jdbcTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
}
OAuth2AuthorizedClient: Indicates the authorized client. This is a composite class that contains client registration but adds authentication information.
OAuth2AuthorizedClientService: Responsible for persisting OAuth2AuthorizedClient
between web requests.
To define JdbcOAuth2AuthorizedClientService, you need to create the required data tables, you can find them in OAuth2 Client Schema to get the table definition:
CREATE TABLE oauth2_authorized_client
(
client_registration_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
access_token_type varchar(100) NOT NULL,
access_token_value blob NOT NULL,
access_token_issued_at timestamp NOT NULL,
access_token_expires_at timestamp NOT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (client_registration_id, principal_name)
);
Next configure the OAuth2AuthorizedClientRepository container class:
@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository(
OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
OAuth2AuthorizedClientRepository: is a container class used to save and persist authorized clients between requests. Here, the client is stored in the database through JdbcOAuth2AuthorizedClientService.
Next instantiate the manager class that contains the logic for the authorization flow:
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
.builder()
.authorizationCode()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
OAuth2AuthorizedClientManager: is the manager class that contains the logic for handling the authorization process. Most importantly, it uses OAuth2AuthorizedClientProvider
to handle the actual request logic for different grant types and OAuth 2.0 providers. It also delegates to OAuth2AuthorizedClientRepository
to call success or failure handlers when client authorization succeeds or fails.
Now let's create a WebClient instance to perform HTTP requests to the resource server:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
Finally, we'll configure the Spring Security security configuration:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(login -> {
login.loginPage("/login").permitAll();
})
.oauth2Client(withDefaults());
return http.build();
}
Configure all requests here to require authentication and authorization, provide Form form authentication methods, and customize the login template through thymeleaf. The code here is not within the scope of this article, and the following will not Show specific details.
Access resource list
We will create a PersistenceClientController and use WebClient to make an HTTP request to the resource server:
@RestController
public class PersistenceClientController {
@Autowired
private WebClient webClient;
@GetMapping(value = "/client/test")
public List<String> getArticles(@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code") OAuth2AuthorizedClient authorizedClient) {
return this.webClient
.get()
.uri("http://127.0.0.1:8090/resource/article")
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(List.class)
.block();
}
}
In this article, you have seen the implementation method of OAuth2 client service persistence to the database. I will not explain the configuration of other authorization servers and resource servers. If you are interested, you can refer to this article Combining JWT with Spring Security OAuth2.
Conclusion
As always, the source code used in this article is available on GitHub.
You might want to read on to the next one:
- Spring Security OAuth2 Client Credentials Grant
- Authorization Code Flow with Proof Key for Code Exchange (PKCE)
- Spring Security OAuth2 Login
Thanks for reading!
Top comments (2)
anuragmishra1539@gmail.com my email please mail i want one help
i want to connect can we?