Generally, we’re used to understanding Spring Boot apps as services, but could they also take a client’s role? The answer is yes, and in this article, we discuss when and why sometimes you need to give a service the capability of acting as a client in an OAuth 2 system. You’ll learn as well to use Spring Security to implement your OAuth 2 client.
But I’d like to advise you from the beginning that without some basic knowledge of using Spring Security and implementing an OAuth 2 system with Spring Security, you might find this article cumbersome. If you don’t feel comfortable with your Spring Security skills yet, I recommend you first learn the basics by reading my book, Spring Security in Action (Manning, 2020). You could also supplement your learning by watching the Spring Security fundamentals playlist I share on my YouTube channel:
In the real world, systems out there are more complex than we usually see in theoretical examples (in books, articles, or tutorials). Sometimes, systems need to call external services to accomplish their responsibility (figure 1).
Suppose you implement an online shop. When users buy something from your shop, you want to give them the possibility to pay using their card. But definitely, it’s not your system that manages the payment process. In such a scenario, you’d need to delegate the payment responsibility to a separate system designed to process the payments – a payment service provider.
Now imagine this payment service provider is designed as an OAuth 2 system. It registers the other apps using its services as clients. The payment service provider allows them to authenticate and get authorized by following a client_credentials grant type flow. This is a good scenario where we would need to implement OAuth 2 client capabilities (figure 2).
So, how does Spring Security help us in adding this capability to our app? If you’ve already used Spring Security and you know how flexible and powerful this framework is, you won’t be so surprised to hear it offers you an easy way to implement a client as well. In the next section, we’ll take an example and implement an app that takes the OAuth 2 client responsibility using Spring Security and Spring Boot.
In this section, we implement an app acting as an OAuth 2 client using Spring Boot and Spring Security. Before we dive into writing code, there are some assumptions for our scenario (figure 3):
You find the demo authorization server and resource server we use in this article as well as a final solution of the OAuth 2 client in this GitHub repository:
https://github.com/lspil/oauth2_client_with_spring_security
Follow me with the development of the example presented in this article. You can start by downloading the GitHub repository content to access the authorization server and resource server. In this article, we don’t discuss the implementation of the authorization server and the resource server. Still, if you need to understand these better, you’ll find chapters 12 through 15 of Spring Security in Action (https://www.manning.com/books/spring-security-in-action) an excellent resource.
We’ll start with testing we can obtain an access token from the authorization server in section 1.2.1, and, in section 1.2.2, we’ll use this access token to call the /demo endpoint using a tool like Postman or cURL.
If you already know how to obtain an access token using the client_credentials grant type and how to use the access token to call an endpoint protected by the resource server, you can skip sections 1.2.1 and 1.2.2 and directly read section 1.2.3. In section 1.2.3, we start implementing the OAuth 2 client.
Once you downloaded the projects provided with this article, open the authorization server in your IDE, and start the app. Being a Maven project, you can easily open the app in any IDE. The authorization server starts running on port 6060. The next cURL command shows you the request you need to do to obtain an access token using the client_credentials grant type.
curl -X POST -u client1:secret1 ‘http://localhost:6060/oauth/token?grant_type=client_credentials&scope=read’
In response to this request, you get an access token. The HTTP response body looks like this (I truncated the token to make the response more comfortable to be read):
1 2 3 4 5 6 7 | { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6I…", "token_type": "bearer", "expires_in": 43199, "scope": "read", "jti": "3cc717b8-fe1e-47b3-b628-b6462bfdf242" } |
In the next section, we use this access token to call the /demo endpoint of the provided resource server.
In this section, we use an access token we obtained from the authorization server to call the resource server’s /demo endpoint. Once you downloaded the code provided with this article, you can import the resource server in your IDE and run it. Being a Maven project, you should easily import the resource server in any IDE. The resource server starts on port 7070. To call the demo endpoint, you use the following request (presented as a cURL command):
curl -H ‘Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI…’ ‘http://localhost:7070/demo’
Mind that I truncated the token in the command to make it easier to read. In response, you’ll get a 200 OK HTTP status and the body “Demo”. This behavior proves that the resource server works correctly.
Now that we know that both the authorization server and the resource server work and accept a client with the client_credentials grant type, it’s time to build our own client. In section 1.2.3, we build a Spring Boot service that acts as a client for the given authorization server and resource server.
In this section of the article, we implement a Spring Boot service to act as an OAuth 2 client for a given authorization server and resource server. The result is an app that successfully calls the /demo endpoint of the given resource server. To achieve this result, the client needs first to call the authorization server and obtain an access token. The client then uses the access token to call the endpoint protected by the resource server.
To test the functionality, we’ll make the client app expose its own (unprotected) /test endpoint. When we call this endpoint, the client calls further the resource server /demo endpoint, gets the response, and returns the response back to us.
Figure 4 presents an overview of the system. In green, you see the classes we’ll implement. The TestController is a simple REST controller we use to expose an unprotected /test endpoint. It uses a proxy class named ResourceServerProxy to call the /demo endpoint of the Resource Server. To call the /demo endpoint, the ResourceServerProxy needs an access token. The TokenManager class implements the responsibility of obtaining an access token from the authorization server.
Fortunately for us, Spring Security makes this functionality easy to implement. Let’s start with the needed dependencies. We create a Spring Boot project and make sure we have the following dependencies in the pom.xml file.
1 2 3 4 5 6 7 8 9 10 11 12 | <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> </dependency> |
Observe that besides Spring Security, we also added the Spring Security OAuth2 Client dependency.
We’ll now work on the configuration class. We start simply by extending WebSecurityConfigurerAdapter and making all endpoints accessible. Remember, we don’t want the /test endpoint to be protected. This is just to simplify our tests for this example.
But more importantly, we’ll use the oauth2Client() DSL method to add the OAuth 2 client responsibility to our implementation. The next listing presents this configuration.
1 2 3 4 5 6 7 8 9 10 11 12 | @Configuration public class ProjectConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.oauth2Client(); http.authorizeRequests() .anyRequest().permitAll(); } } |
We also need a RestTemplate to call the endpoint exposed by the resource server, isn’t it? Let’s add a RestTemplate bean to the Spring Context. The next listing shows you how to add a RestTemplate bean into the Spring context.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Configuration public class ProjectConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.oauth2Client(); http.authorizeRequests() .anyRequest().permitAll(); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } |
Finally, we’ll need our app to follow the OAuth 2 flow. The client first needs to obtain an access token from the authorization server before calling the resource server’s endpoints. Of course, we want this call to get the access token to be done automatically by our client. To achieve this, we need to add a bean of type OAuth2AuthorizedClientManager to the Spring Context. The next listing shows you how to add this bean to the Spring Context.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @Configuration public class ProjectConfiguration extends WebSecurityConfigurerAdapter { // Ommitted code @Bean public OAuth2AuthorizedClientManager authorizedClientManager( ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) { OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .build(); var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientRepository); authorizedClientManager .setAuthorizedClientProvider(authorizedClientProvider); return authorizedClientManager; } } |
The OAuth2AuthorizedClientManager needs some details to be able to complete the OAuth 2 flow. For example, how does this bean know what is the authorization server address? It needs to know where to find the authorization server to call it for obtaining the access token. We have to provide these details in the application.properties file. The next code snippet presents the contents of the application.properties file. In this file, we specify:
Next snippet presents the content of the application.properties file:
1 2 3 4 5 6 7 8 9 | client.registration.name=app spring.security.oauth2.client.registration.app.client-id=client1 spring.security.oauth2.client.registration.app.client-secret=secret1 spring.security.oauth2.client.registration.app.authorization-grant-type=client_credentials spring.security.oauth2.client.registration.app.scope=read spring.security.oauth2.client.provider.app.token-uri=http://localhost:6060/oauth/token resource.server.url=http://localhost:7070 |
With all these details configured, it should now be easy to use the OAuth2AuthorizedClientManager bean to get the access token we need to call the /demo endpoint. I’ll separate the code that gets the access token in a class named TokenManager. I start by creating the class and injecting the OAuth2AuthorizedClientManager bean and the values of the client name and client ID.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Component public class TokenManager { @Value("${client.registration.name}") private String clientRegistrationName; @Value("${spring.security.oauth2.client.registration.app.client-id}") private String clientId; private final OAuth2AuthorizedClientManager authorizedClientManager; public TokenManager( OAuth2AuthorizedClientManager authorizedClientManager) { this.authorizedClientManager = authorizedClientManager; } } |
I use the values I injected to write a method named getAccessToken() that calls the authorization server and returns the access token value. You find the implementation of this method in the next code snippet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @Component public class TokenManager { // Omitted code public String getAccessToken() { OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest .withClientRegistrationId(clientRegistrationName) .principal(clientId) .build(); OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager .authorize(authorizeRequest); OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); return accessToken.getTokenValue(); } } |
It’s now straightforward to use this bean to get an access token anywhere you need it. We’ll use the getAccessToken() method in a proxy class to call the /demo endpoint.
We start by implementing a class named ResourceServerProxy by injecting the TokenManager and RestTemplate beans from the context and the URL of the resource server from the project properties (see next listing).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @Component public class ResourceServerProxy { public static final String AUTHORIZATION = "Authorization"; @Value("${resource.server.url}") private String resourceServerURL; private final TokenManager tokenManager; private final RestTemplate restTemplate; public ResourceServerProxy( TokenManager tokenManager, RestTemplate restTemplate) { this.tokenManager = tokenManager; this.restTemplate = restTemplate; } } |
We add to this class a method that uses the RestTemplate bean to call the /demo endpoint as presented in the next listing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @Component public class ResourceServerProxy { // Ommitted code public String callDemo() { String token = tokenManager.getAccessToken(); String url = resourceServerURL + "/demo"; HttpHeaders headers = new HttpHeaders(); headers.add(AUTHORIZATION, "Bearer " + token); HttpEntity<Void> request = new HttpEntity<>(headers); var response = restTemplate.exchange(url, HttpMethod.GET, request, String.class); return response.getBody(); } } |
Finally, we only need a REST controller with a /test endpoint to validate the functionality we implemented. The /test endpoint uses the ResourceServerProxy bean we just created to call the /demo endpoint and returns the body of the resource server’s response.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @RestController public class TestController { private final ResourceServerProxy proxy; public TestController(ResourceServerProxy proxy) { this.proxy = proxy; } @GetMapping("/test") public String test() { return proxy.callDemo(); } } |
To test the implementation, start all three apps: the client, the authorization server, and the resource server. Then, you call the /test endpoint of the client. In the next snippet, you find the cURL command you can use to call the endpoint.
curl http://localhost:8080/test
If everything is fine, the response body contains the text “Demo”.
1 Comment
interesting