Spring Security Persistent OAuth2 Client
Tags: java, security, oauth2, springsecurity
Published: 2023-02-19 12:35:28


In [previous article](https://dev.to/relive27/using-jwt-with-spring-security-oauth2-41jp), it is introduced that the client requests authorization from the authorization server (using [Spring Authorization Server](https://spring.io/projects/spring-authorization-server#learn)) 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:

```java
@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](https://github.com/ReLive27/spring-security-oauth2-sample/tree/main/oauth2-persistence-client) 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

```xml
<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`:

```yaml
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:

```sql
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:

```java
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*:

```java
@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*:

```java
@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](https://docs.spring.io/spring-security/site/docs/5.4.5/reference/html5/#dbschema-oauth2 -client) to get the table definition:

```sql
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:

```java
@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:

```java
@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](https://www.thymeleaf.org/). 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:

```java
@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] (https://dev.to/relive27/using-jwt-with-spring-security-oauth2-41jp).

### Conclusion

As always, the source code used in this article is available [on GitHub](https://github.com/ReLive27/spring-security-oauth2-sample/tree/main/oauth2-persistence-client).

You might want to read on to the next one:
- [Spring Security OAuth2 Client Credentials Grant](https://dev.to/relive27/spring-security-oauth2-client-credentials-grant-fj5)
- [Authorization Code Flow with Proof Key for Code Exchange (PKCE)](https://dev.to/relive27/authorization-code-flow-with-proof-key-for-code-exchange-pkce-4h22)
- [Spring Security OAuth2 Login](https://dev.to/relive27/spring-security-oauth2-login-51lj)

Thanks for reading!