8.4 Developing the client
In the OAuth 2 authorization dance, the client application’s role is to obtain an access token and make requests to the resource server on behalf of the user. Because we’re using OAuth 2’s authorization code flow, that means that when the client application determines that the user has not yet been authenticated, it should redirect the user’s browser to the authorization server to get consent from the user. Then, when the authorization server redirects control back to the client, the client must exchange the authorization code it receives for the access token.
First things first: the client will need Spring Security’s OAuth 2 client support in its classpath. The following starter dependency makes that happen:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>Not only does this give the application OAuth 2 client capabilities that we’ll exploit in a moment, but it also transitively brings in Spring Security itself. This enables us to write some security configuration for the application. The following SecurityFilterChain bean sets up Spring Security so that all requests require authentication:
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(
authorizeRequests -> authorizeRequests.anyRequest().authenticated()
)
.oauth2Login(
oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/taco-admin-client"))
.oauth2Client(withDefaults());
return http.build();
}What’s more, this SecurityFilterChain bean also enables the client-side bits of OAuth 2. Specifically, it sets up a login page at the path /oauth2/authorization/tacoadmin-client. But this is no ordinary login page that takes a username and password. Instead, it accepts an authorization code, exchanges it for an access token, and uses the access token to determine the identity of the user. Put another way, this is the path that the authorization server will redirect to after the user has granted permission.
We also need to configure details about the authorization server and our application’s OAuth 2 client details. That is done in configuration properties, such as in the following application.yml file, which configures a client named taco-admin-client:
spring:
security:
oauth2:
client:
registration:
taco-admin-client:
provider: tacocloud
client-id: taco-admin-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:9090/login/oauth2/code/{registrationId}"
scope: writeIngredients,deleteIngredients,openidThis registers a client with the Spring Security OAuth 2 client named taco-adminclient. The registration details include the client’s credentials (client-id and clientsecret), the grant type (authorization-grant-type), the scopes being requested (scope), and the redirect URI (redirect-uri). Notice that the value given to redirecturi has a placeholder that references the client’s registration ID, which is tacoadmin-client. Consequently, the redirect URI is set to http://127.0.0.1:9090/login/oauth2/code/taco-admin-client, which has the same path that we configured as the OAuth 2 login earlier.
But what about the authorization server itself? Where do we tell the client that it should redirect the user’s browser? That’s what the provider property does, albeit indirectly. The provider property is set to tacocloud, which is a reference to a separate set of configuration that describes the tacocloud provider’s authorization server. That provider configuration is configured in the same application.yml file like this:
spring:
security:
oauth2:
client:
...
provider:
tacocloud:
issuer-uri: http://authserver:9000The only property required for a provider configuration is issuer-uri. This property identifies the base URI for the authorization server. In this case, it refers to a server host whose name is authserver. Assuming that you are running these examples locally, this is just another alias for localhost. On most Unix-based operating systems, this can be added in your /etc/hosts file with the following line:
127.0.0.1 authserverRefer to documentation for your operating system for details on how to create custom host entries if /etc/hosts isn’t what works on your machine.
Building on the base URL, Spring Security’s OAuth 2 client will assume reasonable defaults for the authorization URL, token URL, and other authorization server specifics. But, if for some reason the authorization server you’re working with differs from those default values, you can explicitly configure authorization details like this:
spring:
security:
oauth2:
client:
provider:
tacocloud:
issuer-uri: http://authserver:9000
authorization-uri: http://authserver:9000/oauth2/authorize
token-uri: http://authserver:9000/oauth2/token
jwk-set-uri: http://authserver:9000/oauth2/jwks
user-info-uri: http://authserver:9000/userinfo
user-name-attribute: subWe’ve seen most of these URIs, such as the authorization, token, and JWK Set URIs already. The user-info-uri property is new, however. This URI is used by the client to obtain essential user information, most notably the user’s username. A request to that URI should return a JSON response that includes the property specified in username-attribute to identify the user. Note, however, when using Spring Authorization Server, you do not need to create the endpoint for that URI; Spring Authorization Server will expose the user-info endpoint automatically.
Now all of the pieces are in place for the application to authenticate and obtain an access token from the authorization server. Without doing anything more, you could fire up the application, make a request to any URL on that application, and be redirected to the authorization server for authorization. When the authorization server redirects back, then the inner workings of Spring Security’s OAuth 2 client library will exchange the code it receives in the redirect for an access token. Now, how can we use that token?
Let’s suppose that we have a service bean that interacts with the Taco Cloud API using RestTemplate. The following RestIngredientService implementation shows such a class that offers two methods: one for fetching a list of ingredients and another for saving a new ingredient:
package tacos;
import java.util.Arrays;
import org.springframework.web.client.RestTemplate;
public class RestIngredientService implements IngredientService {
private RestTemplate restTemplate;
public RestIngredientService() {
this.restTemplate = new RestTemplate();
}
@Override
public Iterable<Ingredient> findAll() {
return Arrays.asList(restTemplate.getForObject(
"http://localhost:8080/api/ingredients",
Ingredient[].class));
}
@Override
public Ingredient addIngredient(Ingredient ingredient) {
return restTemplate.postForObject(
"http://localhost:8080/api/ingredients",
ingredient,
Ingredient.class);
}
}The HTTP GET request for the /ingredients endpoint isn’t secured, so the findAll() method should work fine, as long as the Taco Cloud API is listening on localhost, port 8080. But the addIngredient() method is likely to fail with an HTTP 401 response because we’ve secured POST requests to /ingredients to require "writeIngredients" scope. The only way around that is to submit an access token with "writeIngredients" scope in the request’s Authorization header.
Fortunately, Spring Security’s OAuth 2 client should have the access token handy after completing the authorization code flow. All we need to do is make sure that the access token ends up in the request. To do that, let’s change the constructor to attach a request interceptor to the RestTemplate it creates as follows:
public RestIngredientService(String accessToken) {
this.restTemplate = new RestTemplate();
if (accessToken != null) {
this.restTemplate
.getInterceptors()
.add(getBearerTokenInterceptor(accessToken));
}
}
private ClientHttpRequestInterceptor
getBearerTokenInterceptor(String accessToken) {
ClientHttpRequestInterceptor interceptor =
new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] bytes,
ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add("Authorization", "Bearer " + accessToken);
return execution.execute(request, bytes);
}
};
return interceptor;
}The constructor now takes a String parameter that is the access token. Using this token, it attaches a client request interceptor that adds the Authorization header to every request made by the RestTemplate such that the header’s value is "Bearer" followed by the token value. In the interest of keeping the constructor tidy, the client interceptor is created in a separate private helper method.
Only one question remains: where does the access token come from? The following bean method is where the magic happens:
@Bean
@RequestScope
public IngredientService ingredientService(
OAuth2AuthorizedClientService clientService) {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
String accessToken = null;
if (authentication.getClass()
.isAssignableFrom(OAuth2AuthenticationToken.class)) {
OAuth2AuthenticationToken oauthToken =
(OAuth2AuthenticationToken) authentication;
String clientRegistrationId =
oauthToken.getAuthorizedClientRegistrationId();
if (clientRegistrationId.equals("taco-admin-client")) {
OAuth2AuthorizedClient client =
clientService.loadAuthorizedClient(
clientRegistrationId, oauthToken.getName());
accessToken = client.getAccessToken().getTokenValue();
}
}
return new RestIngredientService(accessToken);
}To start, notice that the bean is declared to be request-scoped using the @RequestScope annotation. This means that a new instance of the bean will be created on every request. The bean must be request-scoped because it needs to pull the authentication from the SecurityContext, which is populated on every request by one of Spring Security’s filters; there is no SecurityContext at application startup time when default-scoped beans are created.
Before returning a RestIngredientService instance, the bean method checks to see that the authentication is, in fact, implemented as OAuth2AuthenticationToken. If so, then that means it will have the token. It then verifies that the authentication token is for the client named taco-admin-client. If so, then it extracts the token from the authorized client and passes it through the constructor for RestIngredientService. With that token in hand, RestIngredientService will have no trouble making requests to the Taco Cloud API’s endpoints on behalf of the user who authorized the application.
