Spring Boot App to App OAuth2 with Spring Security

Overview

This tutorial will show how to call OAuth 2.0 secured resource servers from within a secured Spring Boot app. Spring security and spring OAuth client provide a way to make requests to secure resource servers quickly. We will use an OKTA authorization server and a Spring Boot resource server in this example to test the implementation of the OAuth client.

OAuth 2.0 Client Credentials Flow

OAuth 2.0 is an authorization framework that protects endpoints with access tokens that you access through various flows depending on the desired use case. The OAuth 2.0 client credentials flow retrieves an access token using a client id and client secret issued to the trusted application. The manual way to make this request is to send a POST request to the /token endpoint with the client id and client secret as basic authentication credentials and the grant type and any scopes as form parameters. The following WebClient code snippet will fetch an access token from the OKTA /token endpoint:

 1    private final WebClient webClient = WebClient.builder().build();
 2
 3    public Mono<JsonNode> fetchOktaToken() {
 4        return webClient.post()
 5                .uri("https://dev-75166832.okta.com/oauth2/default/v1/token")
 6                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
 7                .accept(MediaType.APPLICATION_JSON)
 8                .headers(headers -> headers.setBasicAuth("0oa5a815y0AtNAPNI5d7", "IZjsoXulj146gd-1gDe3bOdS6t67Z-E4I--M_uwP"))
 9                .body(BodyInserters.fromFormData("grant_type", "client_credentials")
10                .with("scope", "Custom_Scope"))
11                .retrieve()
12                .bodyToMono(JsonNode.class);
13    }

That request returns the access token as a JsonNode object.

1{
2	"token_type": "Bearer",
3	"expires_in": 3600,
4	"access_token": "eyJraWQiOiJ2Vk9TMC12ZFUyNldSR043Zm5jWUIzQ25fUTVkVGhpMWhISWhacjhIcjlZIiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmUyR2RnZk9nVFN1RU5Bay1TNjR1TzUtMFdDQWt1bWRCdWxVLTJxOU5haVUiLCJpc3MiOiJodHRwczovL2Rldi03NTE2NjgzMi5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2NTQ1NTMzNzIsImV4cCI6MTY1NDU1Njk3MiwiY2lkIjoiMG9hNWE4MTV5MEF0TkFQTkk1ZDciLCJzY3AiOlsiQ3VzdG9tX1Njb3BlIl0sInN1YiI6IjBvYTVhODE1eTBBdE5BUE5JNWQ3In0.Th0HqjbkKt0qoB7DmDxyhXwgsPt3nRsmh5DypX3vwpyHZcdWwJLdpd_lcWKnZXKTKbUKYTH_SbYqk7M6FCZDiBBNXaSd6e1yJtCik1XnIAN-VEn7xEtSmSxUlK1QMSehdaYrvhQhYlta6E0U2Oql1lfVa9PddskFdk_peTOH7b68NppM7momUOKcCfbPgiZnlRvMh6cmEhVVF5uxnZwQqwEy6cp4TmrnIZZ0oaVhyKMwOt6tWfMG5P_9yow5jtsJfKQ8sdS6SWDnbYdiQxsLxuA-ScyIDvfyr1AsWFqCCupAb1F_K8xARCJXC5DfWOV8xMoQACJdSlPLSi6ZHP7bWw",
5	"scope": "Custom_Scope"
6}

The access token can then be added as a Bearer token to a request to a secured Resource server to authorize a request successfully. Actual implementations of fetching the token will store the currently alive access token. They will re-request the token before it expires, and if the authorization fails, it will request a new token.

Using Spring Security to Manage Adding the Token to WebClient Requests

Spring Security provides some beans you use to configure an OAuth client in the project, fetching the access token and adding it to requests. Configure a ReactiveClientRegistrationRepository with the appropriate values for your authorization server. Add the properties to your configuration file to be able to configure the RegistrationRepository properly.

 1spring:
 2  security:
 3    oauth2:
 4      client:
 5        provider:
 6          okta:
 7            token-uri: https://dev-75166832.okta.com/oauth2/default/v1/token
 8        registration:
 9          okta:
10            client-id: 0oa5a815y0AtNAPNI5d7
11            client-secret: IZjsoXulj146gd-1gDe3bOdS6t67Z-E4I--M_uwP
12            scope: Custom_Scope
13            authorization-grant-type: client_credentials

After adding the properties to your configuration file, you can implement the bean in a configuration class. Create an InMemoryRepository, which will store the associated client registrations in memory and is used to manage those clients.

 1@Configuration
 2public class WebConfig {
 3
 4    @Bean
 5    ReactiveClientRegistrationRepository clientRegistrations(@Value("${spring.security.oauth2.client.provider.okta.token-uri}") String tokenUri,
 6                                                             @Value("${spring.security.oauth2.client.registration.okta.client-id}") String clientId,
 7                                                             @Value("${spring.security.oauth2.client.registration.okta.client-secret}") String clientSecret,
 8                                                             @Value("${spring.security.oauth2.client.registration.okta.scope}") String scope,
 9                                                             @Value("${spring.security.oauth2.client.registration.okta.authorization-grant-type}") String authGrantType) {
10        ClientRegistration registration = ClientRegistration
11                .withRegistrationId("okta")
12                .tokenUri(tokenUri)
13                .clientId(clientId)
14                .clientSecret(clientSecret)
15                .scope(scope)
16                .authorizationGrantType(new AuthorizationGrantType(authGrantType))
17                .build();
18        return new InMemoryReactiveClientRegistrationRepository(registration);
19    }
20

This bean can be used along with some other OAuth related classes to configure a WebClient bean that will add a filter to automatically add the access token to all WebClient requests.

 1 @Bean
 2    WebClient webClient(ReactiveClientRegistrationRepository clientRegistrationRepository) {
 3        InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
 4        AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, clientService);
 5        configureHttpProxy(authorizedClientManager);
 6        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
 7        oauth.setDefaultClientRegistrationId("okta");
 8        return WebClient.builder()
 9                .filter(oauth)
10                .build();
11    }

Proxy Configuration

If you need to go through a proxy to make your WebClient request fetch your access token, you will need to replace the HttpClient that the WebClient uses. The following proxyHttpClient() method can fetch the proxy properties from the environment variables and create an HttpClient that uses that proxy information. Then the new proxy-aware HttpClient is added to the ClientCredentialsReactiveOAuth2AuthorizedClientProvider class that fetches the token using Client Credentials.

The HttpClient proxy configuration

 1public HttpClient proxyHttpClient() {
 2        String proxyHost = System.getProperty("https.proxyHost");
 3        String proxyPort = System.getProperty("https.proxyPort");
 4
 5        if (proxyHost == null && proxyPort == null) {
 6            return HttpClient.create();
 7        }
 8
 9        return HttpClient.create()
10                .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP).host(proxyHost).port(Integer.parseInt(proxyPort)));
11    }

The method that adds the proxyHttpClient to the ClientCredentialsReactiveOAuth2AuthorizedClientProvider

 1private void configureHttpProxy(AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
 2        WebClientReactiveClientCredentialsTokenResponseClient tokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
 3        tokenResponseClient.setWebClient(WebClient.builder()
 4            .clientConnector(new ReactorClientHttpConnector(proxyHttpClient()))
 5            .build());
 6
 7        ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
 8        authorizedClientProvider.setAccessTokenResponseClient(tokenResponseClient);
 9        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
10    }

Calling a Resource Server

After configuring the WebClient in the configuration class, you can inject an instance of WebClient and make a request to the desired resource server just like any other WebClient call. Inject the WebClient through constructor injection from the Spring application context. The WebClient instance passed to the constructor will be the version defined in the configuration with the OAuth filter.

1private final WebClient secureWebClient;
2
3    public FetchOktaTokenDto(WebClient secureWebClient) {
4        this.secureWebClient = secureWebClient;
5    }

The following code snippet shows a get request to a local resource server that returns some test JSON data.

1public Mono<ObjectNode> fetchSecureData() {
2        return secureWebClient.get()
3                .uri("http://localhost:8080/testData")
4                .retrieve()
5                .bodyToMono(ObjectNode.class);
6    }

That request returns the following JSON:

1{
2	"name": "Johnathan",
3	"email": "johnathan@jommer.chat",
4	"id": "123456",
5	"display_name": "HaZe"
6}

When a WebClient instance is used without the filter an authorization error is returned and a WebClientResponseException will be thrown.

1org.springframework.web.reactive.function.client.WebClientResponseException$Unauthorized: 401 Unauthorized from GET http://localhost:8080/testData

Directly Accessing the Access Token

Suppose you need direct access to the OAuth access token. In that case, you can set up an additional bean that you can inject into a class to fetch the token and access the value directly, rather than automatically letting the filter control the addition of the token.

Configure a AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager bean using the existing ClientRegistrationRepository bean and an instance of the InMemoryReactiveOAuth2AuthorizedClientService. Inject the ClientManager bean into the class where you want to fetch the access token. Create an OAuth2AuthorizeRequest with the appropriate client registration id and principal for your client and authorize a request on the ClientManager. The client returned from the authorized can be used to get the access token value.

Example GET endpoint fetches the access token and returns it as a String.

 1@Autowired
 2    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager;
 3    
 4    @GetMapping("fetchTokenDirectly")
 5    public ResponseEntity<String> fetchTokenDirectly() {
 6        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
 7                .principal("TestResourceServer")
 8                .build();
 9        OAuth2AuthorizedClient auth2AuthorizedClient = this.authorizedClientManager.authorize(oAuth2AuthorizeRequest).block();
10        return new ResponseEntity<>(auth2AuthorizedClient.getAccessToken().getTokenValue(), HttpStatus.OK);
11    }

The result of calling that endpoint returns the access token as a String

1eyJraWQiOiJ2Vk9TMC12ZFUyNldSR043Zm5jWUIzQ25fUTVkVGhpMWhISWhacjhIcjlZIiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULl9UMUN4VUJKeW00RDFhT0VGdnEwZms3ZW4wOEJqQTF4eHI3X25rVVo4MEUiLCJpc3MiOiJodHRwczovL2Rldi03NTE2NjgzMi5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2NTQ5NTcxMjYsImV4cCI6MTY1NDk2MDcyNiwiY2lkIjoiMG9hNWE4MTV5MEF0TkFQTkk1ZDciLCJzY3AiOlsiQ3VzdG9tX1Njb3BlIl0sInN1YiI6IjBvYTVhODE1eTBBdE5BUE5JNWQ3In0.XoHQJeTNecq0qKgyFKsOrWRMj0GNnpgUt5iyGhCiCN4dky4KX2ULBV3erW7kWiA0nlYww8LbGwoNA_eiFs-427SNG3WA5wTpubNdNyGLbjBFpFwP_ZXnJjmzZeDYU5D6EyPtO7SUsE33xG09WQ-3Y_kj39m-eror_f5AuV0qmYbsIlFsfV1v5O8nDbexrG_a2ipqtAyyh7Z2_J8DhwLryG5szzZiG2XkNZOe-flso3gRn2Ln9-2C9Ty24DViTf8GQE1WLBwl8yXGUQ8SX6THwMd-VIo9VruhYXPPMccnlEB2IJVL0r9cSszR1AWhyIOv3xtOtLgCRHqAyfBk8SeAgQ

Summary

We have covered how to automatically add an OAuth token to WebClient requests and use the Spring Security OAuth client to fetch an access token from a ClientManager instance. You should now know how to call OAuth resource servers using the OAuth client credentials flow from a secure Spring Boot application. The code used for the examples in this post is available on GitHub.

comments powered by Disqus