/images/resources/tutorials/migrations/tutorials-spring.jpg

Securing a Spring Boot API with JWTs

On this page

Spring Security has built-in support for implementing an OAuth 2.0 resource server. This tutorial shows how to let the framework validate a JWT and make use of claims in your API. Spring will validate the token and enforce the correct scope for specific endpoints. The API can also use the claims in the JWT to implement its business authorization.

Note

Curity Identity Server is used in this example, but other OAuth servers can also be used.

Prerequisites

This tutorial covers the API side, meaning that how to obtain a JWT is out of scope. Please look at the separate tutorial OIDC Client with Spring Security for information how to create a client using Spring Boot. This tutorial assumes that the JWT contains the following scope, services:read and the following claim, role=developer or role=something else.

Create a Spring Boot API

You can create a simple Spring Boot application from scratch using the Spring Initialzr website, which will also include the necessary dependencies. Open start.spring.io in your browser to access Spring Initialzr. In the configuration window that opens, select gradle, enter io.curity.example for the name of the group and call the artifact secureapi. Search for and add the following dependencies:

  • Spring Web
  • OAuth2 Resource Server
Spring Initializr

Generate the application. Spring Initializr creates an archive with a bootstrap application that includes the selected dependencies. Download and extract the archive, and import the project in an IDE of your choice.

Add a REST Controller

Next, create a class at src/main/java/io/curity/example/secureapi/ExampleController.java and paste in the following source code:

java
1234567891011121314151617181920212223242526272829
package io.curity.example.secureapi;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExampleController {
@GetMapping("/services")
public List<String> jwtProtected(@AuthenticationPrincipal Jwt jwt) {
String role = jwt.getClaimAsString("role");
return getServices("developer".equals(role));
}
private List<String> getServices(boolean developer) {
List<String> services = new ArrayList<>();
services.add("https://localhost/service/email");
services.add("https://localhost/service/support");
if (developer) {
services.add("https://localhost/service/devportal");
}
return services;
}
}

This creates an endpoint /services that returns a list of URLs. If your JWT has a claim role set to developer, an extra URL is returned.

Integrate Spring Security

For this tutorial, the OpenID Connect metadata of the Curity Identity Server must be published. The Spring API will connect to the metadata endpoint at {issuerUri}/.well-known/openid-configuration. In the Spring project, rename src/main/resources/application.properties to application.yml and paste in the following issuer URI. Update the value to match your own instance of the Curity Identity Server.

yaml
123456
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idsvr.example.com/oauth/v2/oauth-anonymous

The issuer URI is used by Spring Security to locate the Json Web Key Set (JWKS) endpoint of the Curity Identity Server. Token signing public keys are then downloaded, so that Spring can validate any JWT access tokens received.

Make sure Curity Identity Server is up and running

Your Spring application will fetch the public key when starting up and will fail if the endpoint is not reachable.

Next, create a class at src/main/java/io/curity/example/secureapi/SecurityConfiguration.java and paste in the following contents, to configure Spring as an OAuth resource server:

java
1234567891011121314151617181920212223242526272829303132
package io.curity.example.secureapi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
String issuerUri;
@Bean
public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("/services").hasAuthority("SCOPE_services:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(jwt ->
jwt.decoder(JwtDecoders.fromIssuerLocation(issuerUri))
)
).build();
}
}

This will ensure that all requests are authenticated. It will also verify that any calls to /services have the scope services:read. Spring appends SCOPE_ to all scopes and adds them as an authority.

Run the Demo API

Start the application in your IDE or by running ./gradlew bootRun. The default configuration will start a listener on localhost:8080.

Truststore Issues

The Curity Identity Server ssl certificate must be trusted, otherwise your API will fail to introspect the token. Look at the jvm arguments -Djavax.net.ssl.trustStore and -Djavax.net.ssl.trustStorePassword on how to configure your trust store.

Call the Demo API

Clients will call the API using HTTP requests, with an access token in the Authorization header. The way a client calls the API can be simulated using a cURL request:

bash
1
curl -i 'http://localhost:8080/services' -H 'Authorization: Bearer eyJraWQiOi...Gba'

If you fail to provide a valid JWT access token, a 401 unauthorized error response is returned in the www-authenticate header:

text
1
WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Jwt expired at 2023-06-13T14:16:00Z", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"

If you fail to provide a JWT with a valid scope of services:read, a 403 forbidden error response is instead returned:

text
1
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"

Otherwise, if you provide a valid JWT and scope, an array of URLs will be returned, with two elements. If the access token incluides a claim of role=developer, you will receive a third URL, for a developer portal. This demonstrates how dynamic access per user can be granted to your data, using claims:

json
12345
[
"https://localhost/service/email",
"https://localhost/service/support",
"https://localhost/service/devportal"
]

Conclusion

Spring Security provides a productive way to add OAuth security to APIs. Coarse-grained authorization decisions can be made using scopes, which are fixed at design time. Fine-grained authorization decisions can be made using claims, which are assigned to users dynamically at runtime.

For further examples and help regarding OAuth2 and Spring Security, see the Spring Security Reference. See also the following Curity tutorials on implementing OpenID Connect security into Spring websites:

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial