DEV Community

ReLive27
ReLive27

Posted on • Updated on

Using JWT with Spring Security OAuth2

Overview

OAuth 2.0 is the industry standard authorization protocol. OAuth 2.0 focuses on simplicity for client developers, while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices.

The OAuth authorization server is responsible for authenticating users and issuing access tokens containing user data and appropriate access policies.

Below we will use Spring Authorization Server to build a simple authorization server.

💡 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 authorization server implementation

Let's start with the OAuth2 authorization server configuration implementation.

Maven dependencies

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
   <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-authorization-server</artifactId>
  <version>0.3.1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Configuration

First let's configure the database connection information through application.yml.

spring:
  application:
    name: auth-server
  datasource:
    druid:
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/integrated_oauth?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: <<username>> # modify username
      password: <<password>> # change Password
Enter fullscreen mode Exit fullscreen mode

Then we create an AuthorizationServerConfig configuration class, in this class we will create the specific beans required by the OAuth2 authorization server. The first one will be the client service repository, where we create a client using the RegisteredClient builder type and persist it to the database.

 @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("relive-client")
                .clientSecret("{noop}relive-client")
                .clientAuthenticationMethods(s -> {
                    s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
                    s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
                })
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-client-authorization-code")
                .scope("message.read")
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(true)
                        .requireProofKey(false)
                        .build())
                .tokenSettings(TokenSettings.builder()
                        .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
                        .accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
                        .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
                        .reuseRefreshTokens(true)
                        .build())
                .build();

        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        registeredClientRepository.save(registeredClient);

        return registeredClientRepository;
    }
Enter fullscreen mode Exit fullscreen mode

The properties we configure are:

  • id--RegisteredClient unique id
  • clientId--client identifier
  • clientSecret--client secret
  • clientAuthenticationMethods--the authentication method the client may use. Supported values are client_secret_basic, client_secret_post, private_key_jwt, client_secret_jwt, and none
  • authorizationGrantTypes--the types of grants the client can use. Supported values are authorization_code, client_credentials and refresh_token
  • redirectUris--client has registered redirect URI
  • scopes--The ranges that clients are allowed to request
  • clientSettings--client Custom Settings

  • tokenSettings--custom settings for OAuth2 tokens issued to clients

Next let's configure the central component OAuth2AuthorizationService that stores new authorizations and queries existing ones.

   @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }
Enter fullscreen mode Exit fullscreen mode

For the authorization "consent" of an OAuth2 authorization request, Spring provides OAuth2AuthorizationConsentService components for storing new authorization consents and querying existing authorization consents.

   @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }
Enter fullscreen mode Exit fullscreen mode

Next let's create a bean, configure the OAuth2 authorization service with other default configurations, and use it to redirect the request to the login page for unauthenticated authorization requests.

   @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.exceptionHandling(exceptions -> exceptions.
                authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))).build();
    }
Enter fullscreen mode Exit fullscreen mode

Every authorization server needs a signing key for tokens, let's generate an RSA key:

final class KeyGeneratorUtils {

    private KeyGeneratorUtils() {
    }

    static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }
}
Enter fullscreen mode Exit fullscreen mode
public final class Jwks {

    private Jwks() {
    }

    public static RSAKey generateRsa() {
        KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode
 @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = Jwks.generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }
Enter fullscreen mode Exit fullscreen mode

After processing the signing key of the token, the authorization server also needs an issuer URL, which we can create through ProviderSettings:

    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder()
                .issuer("http://127.0.0.1:8080")
                .build();
    }
Enter fullscreen mode Exit fullscreen mode

Finally we will enable the Spring Security security configuration class to secure our service.

@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {


    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .formLogin(withDefaults())
        return http.build();
    }

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

Here authorizeRequests.anyRequest().authenticated() makes all requests require authentication and provides Form-based authentication.

We also need to define the user information used by the test, the following creates a memory-based user information repository.

    @Bean
    UserDetailsService users() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
Enter fullscreen mode Exit fullscreen mode

Resource server implementation

Now we will create a resource server, the API interface in the service will only allow requests authenticated by the OAuth2 authorization server.

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-security</artifactId>
  <version>2.6.7</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
  <version>2.6.7</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Configuration

First let's configure the service port via application.yml.

server:
  port: 8090
Enter fullscreen mode Exit fullscreen mode

Next, for OAuth2 security configuration, we need to use the issuerUri set by the previous authorization server in ProviderSettings.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:8080
Enter fullscreen mode Exit fullscreen mode

The resource server will use this Uri to further configure itself, discover the public key of the authorization server, and pass in the JwtDecoder used to verify the JWT. A consequence of this process is that the authorization server must start and receive requests for the resource server to start successfully.

If the resource server must be able to start independently of the authorization server, then jwk-set-uri can be provided. This will be our further property to add in the OAuth2 security configuration:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:8080
          jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks

Enter fullscreen mode Exit fullscreen mode

Now that we can set up the Spring Security security configuration, every request to the service resource should be authorized and have the appropriate permissions:

@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.requestMatchers()
                .antMatchers("/resource/test/**")
                .and()
                .authorizeRequests()
                .mvcMatchers("/resource/test/**")
                .access("hasAuthority('SCOPE_message.read')")
                .and()
                .oauth2ResourceServer()
                .jwt();
        return http.build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we will create a REST controller that will return the jwt claims information.

@RestController
public class ResourceServerTestController {

    @GetMapping("/resource/test")
    public Map<String, Object> getArticles(@AuthenticationPrincipal Jwt jwt) {
        return jwt.getClaims();
    }
}
Enter fullscreen mode Exit fullscreen mode

OAuth2 client

Now we want to create a client, which first requests authorization from the authorization server to obtain an access token, and then accesses the corresponding resource on the resource server.

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

Configuration

First of all, we will configure the client's access port 8070 in application.yml.

server:
  port: 8070
Enter fullscreen mode Exit fullscreen mode

Next we'll define the configuration properties for the OAuth2 client:

spring:
  security:
    oauth2:
      client:
        registration:
          messaging-client-authorization-code:
            provider: client-provider
            client-id: relive-client
            client-secret: relive-client
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8070/login/oauth2/code/{registrationId}"
            scope: message.read
            client-name: messaging-client-authorization-code
        provider:
          client-provider:
            authorization-uri: http://127.0.0.1:8080/oauth2/authorize
            token-uri: http://127.0.0.1:8080/oauth2/token
Enter fullscreen mode Exit fullscreen mode

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

WebClient adds an OAuth2 authorization filter, which requires OAuth2AuthorizedClientManager as a dependency. Only the authorization code and refresh token are configured here, and other modes can be added if necessary:

@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

Finally, we'll configure the Spring Security security configuration:

 @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        //Easy to test, open permissions
                        authorizeRequests.anyRequest().permitAll()
                )
                .oauth2Client(withDefaults());
        return http.build();
    }
Enter fullscreen mode Exit fullscreen mode

Here we release all client API permissions, but in actual situations, client services require authentication. The OAuth2 protocol itself is an authorization protocol and does not care about the specific form of authentication. You can add simple forms authentication.

Access resource list

Finally, we create a controller where we will use the previously configured WebClient to make HTTP requests to our resource server:

@RestController
public class ClientTestController {
    @Autowired
    private WebClient webClient;

    @GetMapping(value = "/client/test")
    public Map<String, Object> getArticles(@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code") OAuth2AuthorizedClient authorizedClient) {
        return this.webClient
                .get()
                .uri("http://127.0.0.1:8090/resource/test")
                .attributes(oauth2AuthorizedClient(authorizedClient))
                .retrieve()
                .bodyToMono(Map.class)
                .block();
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we use the @RegisteredOAuth2AuthorizedClient annotation to bind OAuth2AuthorizedClient, and trigger the OAuth2 authorization code mode process to obtain an access token.

Conclusion

This example mainly demonstrates the secure communication between two services using the OAuth2 protocol, especially in complex Internet scenarios, where client services and resource services are provided by different platforms. OAuth2 is very good at obtaining the user's entrusted decision, In many ways, it is simpler and safer than other solutions.

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)