If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:
>> LEARN SPRING SECURITYMocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely โ and automatically โ with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatโs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions โ one for newcomers and one for experienced users. Youโll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
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:
๐ AuthCodeFlowSequenceDiagram
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).
