DEV Community

ReLive27
ReLive27

Posted on • Updated on

Spring Security Persistent OAuth2 Client

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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>

...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
    }

  //...
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

Next configure the OAuth2AuthorizedClientRepository container class:

@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository(
  OAuth2AuthorizedClientService authorizedClientService) {
  return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Thanks for reading!

Top comments (0)