Table of Contents
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 JWT as well as opaque tokens, the two kinds of bearer tokens supported by Spring Security.
Before jumping on to the implementation and code samples, we’ll 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, 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’re focusing on in this tutorial.
All right, 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, or the thing that issues tokens.
For that, we’ll be using 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’ll not 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
Let’s see them one by one for our resource server handling JWT tokens, after taking a peek at the dependencies.
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.2.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> <version>2.2.6.RELEASE</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version> </dependency>
Apart from that, we’ve also added 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 define access levels for our resource:
@Configuration public class JWTSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(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()); } }
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 added 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/maixuanviet
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 wouldn’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/maixuanviet/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 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 read scope from the authorization server, covering Steps from 1 through 7 of our sequence diagram.
Step 8 is performed by 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 additionally need the 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/maixuanviet/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 extends WebSecurityConfigurerAdapter { @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; @Override protected void configure(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))); } }
Here we’re also specifying the client credentials corresponding to the authorization server’s client 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 how we did it for the JWT one.
In this case, let’s 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 tutorial, we saw how to configure a Spring Security based resource server application for validating JWT 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, source code is available over on GitHub.