8.3 Securing an API with a resource server
The resource server is actually just a filter that sits in front of an API, ensuring that requests for resources that require authorization include a valid access token with the required scope. Spring Security provides an OAuth2 resource server implementation that you can add to an existing API by adding the following dependency to the build for the project build as follows:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>You can also add the resource server dependency by selecting the “OAuth2 Resource Server” dependency from the Initializr when creating a project.
With the dependency in place, the next step is to declare that POST requests to /ingredients require the "writeIngredients" scope and that DELETE requests to /ingredients require the "deleteIngredients" scope. The following excerpt from the project’s SecurityConfig class shows how to do that:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
...
.antMatchers(HttpMethod.POST, "/api/ingredients")
.hasAuthority("SCOPE_writeIngredients")
.antMatchers(HttpMethod.DELETE, "/api//ingredients")
.hasAuthority("SCOPE_deleteIngredients")
...
}For each of the endpoints, the .hasAuthority() method is called to specify the required scope. Notice that the scopes are prefixed with "SCOPE_" to indicate that they should be matched against OAuth 2 scopes in the access token given on the request to those resources.
In that same configuration class, we’ll also need to enable the resource server, as shown next:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.and()
.oauth2ResourceServer(oauth2 -> oauth2.jwt())
...
}The oauth2ResourceServer() method here is given a lambda with which to configure the resource server. Here, it simply enables JWT tokens (as opposed to opaque tokens) so that the resource server can inspect the contents of the token to find what security claims it includes. Specifically, it will look to see that the token includes the "writeIngredients" and/or "deleteIngredients" scope for the two endpoints we’ve secured.
It won’t trust the token at face value, though. To be confident that the token was created by a trusted authorization server on behalf of a user, it will verify the token’s signature using the public key that matches the private key that was used to create the token’s signature. We’ll need to configure the resource server to know where to obtain the public key, though. The following property will specify the JWK set URL on the authorization server from which the resource server will fetch the public key:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:9000/oauth2/jwksAnd now our resource server is ready! Build the Taco Cloud application and start it up. Then you can try it out using curl like this:
$ curl localhost:8080/ingredients \
-H"Content-type: application/json" \
-d'{"id":"CRKT", "name":"Legless Crickets", "type":"PROTEIN"}'The request should fail with an HTTP 401 response code. That’s because we’ve configured the endpoint to require the "writeIngredients" scope for that endpoint, and we’ve not provided a valid access token with that scope on the request.
To make a successful request and add a new ingredient item, you’ll need to obtain an access token using the flow we used in the previous section, making sure that we request the "writeIngredients" and "deleteIngredients" scopes when directing the browser to the authorization server. Then, provide the access token in the "Authorization" header using curl like this (substituting "$token" for the actual access token):
$ curl localhost:8080/ingredients \
-H"Content-type: application/json" \
-d'{"id":"SHMP", "name":"Coconut Shrimp", "type":"PROTEIN"}' \
-H"Authorization: Bearer $token"This time the new ingredient should be created. You can verify that by using curl or your chosen HTTP client to perform a GET request to the /ingredients endpoint as follows:
$ curl localhost:8080/ingredients
[
{
"id": "FLTO",
"name": "Flour Tortilla",
"type": "WRAP"
},
...
{
"id": "SHMP",
"name": "Coconut Shrimp",
"type": "PROTEIN"
}
]Coconut Shrimp is now included at the end of the list of all of the ingredients returned from the /ingredients endpoint. Success!
Recall that the access token expires after 5 minutes. If you let the token expire, requests will start returning HTTP 401 responses again. But you can get a new access token by making a request to the authorization server using the refresh token that you got along with the access token (substituting the actual refresh token for "$refreshToken"), as shown here:
$ curl localhost:9000/oauth2/token \
-H"Content-type: application/x-www-form-urlencoded" \
-d"grant_type=refresh_token&refresh_token=$refreshToken" \
-u taco-admin-client:secretWith a newly created access token, you can keep on creating new ingredients to your heart’s content.
Now that we’ve secured the /ingredients endpoint, it’s probably a good idea to apply the same techniques to secure other potentially sensitive endpoints in our API. The /orders endpoint, for example, should probably not be open for any kind of request, even HTTP GET requests, because it would allow a hacker to easily grab customer information. I’ll leave it up to you to secure the /orders endpoint and the rest of the API as you see fit.
Administering the Taco Cloud application using curl works great for tinkering and getting to know how OAuth 2 tokens play a part in allowing access to a resource. But ultimately we want a real client application that can be used to manage ingredients. Let’s now turn our attention to creating an OAuth-enabled client that will obtain access tokens and make requests to the API.
