1. Overview
In this tutorial, we’ll learn how to set up an OAuth 2.0 resource server using Spring Security 5.
We’ll do this using JWTs, as well as opaque tokens, the two kinds of bearer tokens supported by Spring Security.
Before we jump in to the implementation and code samples, we’ll first establish some background.
2. A Little Background
2.1. What Are JWTs and Opaque Tokens?
JWT, or JSON Web Token, is a way to transfer sensitive information securely in the widely-accepted JSON format. The contained information could be about the user, or about the token itself, such as its expiry and issuer.
On the other hand, an opaque token, as the name suggests, is opaque in terms of the information it carries. The token is just an identifier that points to the information stored at the authorization server; it gets validated via introspection at the server’s end.
2.2. What Is a Resource Server?
In the context of OAuth 2.0, a resource server is an application that protects resources via OAuth tokens. These tokens are issued by an authorization server, typically to a client application. The job of the resource server is to validate the token before serving a resource to the client.
A token’s validity is determined by several things:
- Did this token come from the configured authorization server?
- Is it unexpired?
- Is this resource server its intended audience?
- Does the token have the required authority to access the requested resource?
To visualize this, let’s look at a sequence diagram for the authorization code flow, and see all the actors in action:
As we can see in step 8, when the client application calls the resource server’s API to access a protected resource, it first goes to the authorization server to validate the token contained in the request’s Authorization: Bearer header, and then responds to the client.
Step 9 is what we’ll focus on in this tutorial.
So now let’s jump into the code part. We’ll set up an authorization server using Keycloak, a resource server validating JWT tokens, another resource server validating opaque tokens, and a couple of JUnit tests to simulate client apps and verify responses.
3. Authorization Server
First, we’ll set up an authorization server, the thing that issues tokens.
For this, we’ll use Keycloak embedded in a Spring Boot Application. Keycloak is an open-source identity and access management solution. Since we’re focusing on the resource server in this tutorial, we won’t delve any deeper into it.
Our embedded Keycloak Server has two clients defined, fooClient and barClient, corresponding to our two resource server applications.
4. Resource Server – Using JWTs
Our resource server will have four main components:
- Model – the resource to protect
- API – a REST controller to expose the resource
- Security Configuration – a class to define access control for the protected resource that the API exposes
- application.yml – a config file to declare properties, including information about the authorization server
After we take a quick look at the dependencies, we’ll go through these components one by one for our resource server handling JWT tokens.
4.1. Maven Dependencies
Mainly, we’ll need the spring-boot-starter-oauth2-resource-server, Spring Boot’s starter for resource server support. This starter includes Spring Security by default, so we don’t need to add it explicitly:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
Apart from that, we’ll also add web support.
For our demonstration purposes, we’ll generate resources randomly, instead of getting them from a database, with some help from Apache’s commons-lang3 library.
4.2. Model
Keeping it simple, we’ll use Foo, a POJO, as our protected resource:
public class Foo {
private long id;
private String name;
// constructor, getters and setters
}
4.3. API
Here’s our rest controller to make Foo available for manipulation:
@RestController
@RequestMapping(value = "/foos")
public class FooController {
@GetMapping(value = "/{id}")
public Foo findOne(@PathVariable Long id) {
return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
}
@GetMapping
public List findAll() {
List fooList = new ArrayList();
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
return fooList;
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public void create(@RequestBody Foo newFoo) {
logger.info("Foo created");
}
}
As is evident, we have the provision to GET all Foos, GET a Foo by id, and POST a Foo.
4.4. Security Configuration
In this configuration class, we’ll define access levels for our resource:
@Configuration
public class JWTSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authz -> authz.antMatchers(HttpMethod.GET, "/foos/**")
.hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/foos")
.hasAuthority("SCOPE_write")
.anyRequest()
.authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
}
Anyone with an access token having the read scope can get Foos. In order to POST a new Foo, their token should have a write scope.
Additionally, we’ll add a call to jwt() using the oauth2ResourceServer() DSL to indicate the type of tokens supported by our server here.
4.5. application.yml
In the application properties, in addition to the usual port number and context-path, we need to define the path to our authorization server’s issuer URI so that the resource server can discover its provider configuration:
server:
port: 8081
servlet:
context-path: /resource-server-jwt
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8083/auth/realms/baeldung
The resource server uses this information to validate the JWT tokens coming in from the client application, as per Step 9 of our sequence diagram.
For this validation to work using the issuer-uri property, the authorization server must be up and running. Otherwise, the resource server won’t start.
If we need to start it independently, then we can supply the jwk-set-uri property instead to point to the authorization server’s endpoint exposing public keys:
jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs
And that’s all we need to get our server to validate JWT tokens.
4.6. Testing
For testing, we’ll set up a JUnit. In order to execute this test, we need the authorization server, as well as the resource server, up and running.
Let’s verify that we can get Foos from resource-server-jwt with a read scoped token in our test:
@Test
public void givenUserWithReadScope_whenGetFooResource_thenSuccess() {
String accessToken = obtainAccessToken("read");
Response response = RestAssured.given()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.get("http://localhost:8081/resource-server-jwt/foos");
assertThat(response.as(List.class)).hasSizeGreaterThan(0);
}
In the above code, at Line #3, we obtain an access token with a read scope from the authorization server, covering Steps 1 through 7 of our sequence diagram.
Step 8 is performed by the RestAssured‘s get() call. Step 9 is performed by the resource server with the configurations we saw, and is transparent to us as users.
5. Resource Server – Using Opaque Tokens
Next, let’s see the same components for our resource server handling opaque tokens.
5.1. Maven Dependencies
To support opaque tokens, we’ll need the additional oauth2-oidc-sdk dependency:
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>8.19</version>
<scope>runtime</scope>
</dependency>
5.2. Model and Controller
For this one, we’ll add a Bar resource:
public class Bar {
private long id;
private String name;
// constructor, getters and setters
}
We’ll also have a BarController, with endpoints similar to our FooController before, to dish out Bars.
5.3. application.yml
In the application.yml here, we’ll need to add an introspection-uri corresponding to our authorization server’s introspection endpoint. As mentioned before, this is how an opaque token gets validated:
server:
port: 8082
servlet:
context-path: /resource-server-opaque
spring:
security:
oauth2:
resourceserver:
opaque:
introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
introspection-client-id: barClient
introspection-client-secret: barClientSecret
5.4. Security Configuration
Keeping access levels similar to that of Foo for the Bar resource as well, this configuration class also makes a call to opaqueToken() using the oauth2ResourceServer() DSL to indicate the use of the opaque token type:
@Configuration
public class OpaqueSecurityConfig {
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}")
String introspectionUri;
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}")
String clientId;
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}")
String clientSecret;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authz -> authz.antMatchers(HttpMethod.GET, "/bars/**")
.hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/bars")
.hasAuthority("SCOPE_write")
.anyRequest()
.authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.opaqueToken
(token -> token.introspectionUri(this.introspectionUri)
.introspectionClientCredentials(this.clientId, this.clientSecret)));
return http.build();
}
}
Here we’ll also specify the client credentials corresponding to the authorization server’s client that we’ll be using. We defined these earlier in our application.yml.
5.5. Testing
We’ll set up a JUnit for our opaque token-based resource server, similar to what we did for the JWT one.
In this case, we’ll check if a write scoped access token can POST a Bar to resource-server-opaque:
@Test
public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() {
String accessToken = obtainAccessToken("read write");
Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
Response response = RestAssured.given()
.contentType(ContentType.JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(newBar)
.log()
.all()
.post("http://localhost:8082/resource-server-opaque/bars");
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value());
}
If we get a status of CREATED back, it means the resource server successfully validated the opaque token and created the Bar for us.
6. Conclusion
In this article, we learned how to configure a Spring Security based resource server application for validating JWTs, as well as opaque tokens.
As we saw, with minimal setup, Spring made it possible to seamlessly validate the tokens with an issuer and send resources to the requesting party (in our case, a JUnit test).
As always, the source code is available over on GitHub.