DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations

Trending

  • Scaling Site Reliability Engineering (SRE) Teams the Right Way
  • Replacing Apache Hive, Elasticsearch, and PostgreSQL With Apache Doris
  • Never Use Credentials in a CI/CD Pipeline Again
  • Transactional Outbox Patterns Step by Step With Spring and Kotlin

Trending

  • Scaling Site Reliability Engineering (SRE) Teams the Right Way
  • Replacing Apache Hive, Elasticsearch, and PostgreSQL With Apache Doris
  • Never Use Credentials in a CI/CD Pipeline Again
  • Transactional Outbox Patterns Step by Step With Spring and Kotlin
  1. DZone
  2. Coding
  3. Frameworks
  4. Use Both JWT and Opaque Access Tokens With Spring Boot

Use Both JWT and Opaque Access Tokens With Spring Boot

Brian Demers user avatar by
Brian Demers
·
Oct. 09, 20 · Tutorial
Like (5)
Save
Tweet
Share
12.92K Views

Join the DZone community and get the full member experience.

Join For Free

How can one validate OAuth 2.0 access tokens? This question frequently comes up — along with the topic of validating JSON Web Tokens (JWT) based access tokens— however, this is NOT part of the OAuth 2.0 specification. JWTs are used so commonly that Spring Security supported them before adding support for remotely validating tokens. 

This article will introduce how to build a simple application that utilizes both types of validation.

You can also follow along by watching videos on our YouTube channel. 


Prerequisites

  • Java 8+
  • A free Okta Developer account

Should I Validate Access Tokens Locally or Remote?

Whether you should validate access tokens locally (e.g., a JWT) or remotely (per spec) is a question of how much security you need. Often, people jump to, “I need all of the securities!” This statement simply isn’t true — how much security you need should be balanced with other factors like ease of use, cost, and performance.

There is no such thing as perfect security, only varying levels of insecurity.

Salman Rushdie

The biggest downside to validating a token locally is that your token is, by definition, stale. It is a snapshot of the moment in time when your identity provider (IdP) created the token. The further away you get from that moment, the more likely that token is no longer valid: it could have been revoked, the user could have logged out, or the application that created the token disabled.

Remotely validating tokens are not always ideal, either. Remote validation comes with the cost of adding latency in your application, as you need to add an HTTP request to a remote server every time you need to validate the token.

One way to reduce these concerns is to keep the lifetime of an access token short (say 5 minutes) and validate them locally; this limits the risk of using a revoked token.

There is another option: do both!

Validate Access Tokens Locally and Remotely!

By default, Spring Boot applications can be configured to use JWT validation OR opaque validation, simply by configuring a few properties. Using both types of validation in the same application requires a few extra lines of code.

Obviously, you wouldn’t do both on each request; you could validate more sensitive operations remotely and all other requests locally. For example, when updating a user’s contact information, you may want to validate the token remotely, but when viewing the user’s profile information, validate locally. This pattern keeps your application fast, as updating an address or an email happens less frequently than just viewing contact information.

To get started, you will need an OAuth Web application in Okta.

Login in to your Okta admin console, if you just created a new account, and have not logged in yet, follow the activation link in your inbox.

Make a note of the Org URL on the top right; I’ll refer to this as {yourOktaDomain} in the next section.

Once you are logged in, navigate to the top menu and select Applications -> Add Application. Select Web -> Next.

Give your application a name: “Spring Tokens Example”

Set the Login redirect URIs to https://oidcdebugger.com/debug

Check Implicit (Hybrid)

Click Done

application settings in Okta

Head over to start.spring.io and click the Generate button.

Note: The above link populates the following settings:

  • Group: com.okta.example
  • Artifact: spring-token-example
  • Package name: com.okta.example
  • Dependencies:
    • Spring Security (security)
    • Spring Web (web)

Unzip the project and open it in your favorite IDE.

Add two more dependencies to the Maven pom.xml file:

XML
xxxxxxxxxx
1
 
1
<dependency>
2
    <groupId>org.springframework.boot</groupId>
3
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
4
</dependency>
5
<dependency>
6
    <groupId>com.nimbusds</groupId>
7
    <artifactId>oauth2-oidc-sdk</artifactId>
8
    <version>7.3</version>
9
</dependency>


Add an OAuth2ClientProperties bean to the SpringTokenExampleApplication class to reuse the standard Spring Security OAuth properties.

Java
xxxxxxxxxx
1
 
1
@Bean
2
OAuth2ClientProperties oAuth2ClientProperties() {
3
    return new OAuth2ClientProperties();
4
}


Add a simple REST controller with a GET and a POST handler, in src/main/java/com/okta/example/SimpleController.java

Java
xxxxxxxxxx
1
20
 
1
package com.okta.example;
2
3
import org.springframework.web.bind.annotation.GetMapping;
4
import org.springframework.web.bind.annotation.PostMapping;
5
import org.springframework.web.bind.annotation.RequestParam;
6
import org.springframework.web.bind.annotation.RestController;
7
8
@RestController
9
public class SimpleController {
10
11
    @GetMapping("/")
12
    String hello() {
13
        return "Hello!";
14
    }
15
16
    @PostMapping("/")
17
    String helloPost(@RequestParam("message") String message) {
18
        return "hello: " + message;
19
    }
20
}


Create a new class that will map a RequestMatcher to the AuthenticationManager (more on this below) src/main/java/com/okta/example/RequestMatchingAuthenticationManagerResolver.java

NOTE: This class may be part of a future version of Spring Security.

Java
xxxxxxxxxx
1
48
 
1
package com.okta.example;
2
3
import java.util.LinkedHashMap;
4
import java.util.Map;
5
import javax.servlet.http.HttpServletRequest;
6
7
import org.springframework.security.authentication.AuthenticationManager;
8
import org.springframework.security.authentication.AuthenticationManagerResolver;
9
import org.springframework.security.authentication.AuthenticationServiceException;
10
import org.springframework.security.web.util.matcher.RequestMatcher;
11
import org.springframework.util.Assert;
12
13
/**  An {@link AuthenticationManagerResolver} that returns a {@link AuthenticationManager}
14
*  instances based upon the type of {@link HttpServletRequest} passed into
15
*  {@link #resolve(HttpServletRequest)}.
16
*  @author Josh Cummings
17
*/  @since 5.2
18
public class RequestMatchingAuthenticationManagerResolver
19
        implements AuthenticationManagerResolver<HttpServletRequest> {
20
21
    private final LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers;
22
23
    private AuthenticationManager defaultAuthenticationManager = authentication -> {
24
        throw new AuthenticationServiceException("Cannot authenticate " + authentication);
25
    };
26
27
    public RequestMatchingAuthenticationManagerResolver
28
    (LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers) {
29
        Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty");
30
        this.authenticationManagers = authenticationManagers;
31
    }
32
33
    @Override
34
    public AuthenticationManager resolve(HttpServletRequest context) {
35
        for (Map.Entry<RequestMatcher, AuthenticationManager> entry : this.authenticationManagers.entrySet()) {
36
            if (entry.getKey().matches(context)) {
37
                return entry.getValue();
38
            }
39
        }
40
41
        return this.defaultAuthenticationManager;
42
    }
43
44
    public void setDefaultAuthenticationManager(AuthenticationManager defaultAuthenticationManager) {
45
        Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
46
        this.defaultAuthenticationManager = defaultAuthenticationManager;
47
    }
48
}


Configure Spring Security to Validate JWTs and Opaque Tokens

Everything up until now has been boilerplate, now we get to the fun part!

Create a new ExampleWebSecurityConfigurer class which uses local JWT validation for GET requests, and remote “opaque” validation for all other requests (be sure to read the inline comments):

Java
xxxxxxxxxx
1
85
 
1
package com.okta.example;
2
3
import org.springframework.beans.factory.annotation.Autowired;
4
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
5
import org.springframework.context.annotation.Configuration;
6
import org.springframework.security.authentication.AuthenticationManager;
7
import org.springframework.security.authentication.AuthenticationManagerResolver;
8
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
9
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
10
import org.springframework.security.oauth2.jwt.JwtDecoder;
11
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
12
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
13
import org.springframework.security.oauth2.server.resource.authentication.JwtBearerTokenAuthenticationConverter;
14
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
15
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
16
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
17
import org.springframework.security.web.util.matcher.RequestMatcher;
18
19
import javax.servlet.http.HttpServletRequest;
20
import java.util.Arrays;
21
import java.util.LinkedHashMap;
22
import java.util.List;
23
24
@Configuration
25
public class ExampleWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
26
27
    // Inject the `OAuth2ClientProperties` we configured in the previous step
28
    @Autowired
29
    private OAuth2ClientProperties oAuth2ClientProperties;
30
31
    @Override
32
    protected void configure(HttpSecurity http) throws Exception {
33
34
        // All routes require authentication
35
        http.authorizeRequests().anyRequest().authenticated();
36
37
        // Configure a custom `AuthenticationManager` to determine 
38
        // if JWT or opaque token validation should be used
39
        http.oauth2ResourceServer().authenticationManagerResolver(customAuthenticationManager());
40
    }
41
42
    AuthenticationManagerResolver<HttpServletRequest> customAuthenticationManager() {
43
        LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers = new LinkedHashMap<>();
44
45
        // USE JWT tokens (locally validated) to validate HEAD, GET, and OPTIONS requests
46
        List<String> readMethod = Arrays.asList("HEAD", "GET", "OPTIONS");
47
        RequestMatcher readMethodRequestMatcher = request -> readMethod.contains(request.getMethod());
48
        authenticationManagers.put(readMethodRequestMatcher, jwt());
49
50
        // all other requests will use opaque tokens (remotely validated)
51
        RequestMatchingAuthenticationManagerResolver authenticationManagerResolver 
52
            = new RequestMatchingAuthenticationManagerResolver(authenticationManagers);
53
54
        // Use opaque tokens (remotely validated) for all other requests
55
        authenticationManagerResolver.setDefaultAuthenticationManager(opaque());
56
        return authenticationManagerResolver;
57
    }
58
59
    // Mimic the default configuration for JWT validation.
60
    AuthenticationManager jwt() {
61
        // this is the keys endpoint for okta
62
        String issuer = oAuth2ClientProperties.getProvider().get("okta").getIssuerUri();
63
        String jwkSetUri = issuer + "/v1/keys";
64
65
        // This is basically the default jwt logic
66
        JwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
67
        JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(jwtDecoder);
68
        authenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter());
69
        return authenticationProvider::authenticate;
70
    }
71
72
    // Mimic the default configuration for opaque token validation
73
    AuthenticationManager opaque() {
74
        String issuer = oAuth2ClientProperties.getProvider().get("okta").getIssuerUri();
75
        String introspectionUri = issuer + "/v1/introspect";
76
77
        // The default opaque token logic
78
        OAuth2ClientProperties.Registration oktaRegistration = oAuth2ClientProperties.getRegistration().get("okta");
79
        OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(
80
                introspectionUri,
81
                oktaRegistration.getClientId(),
82
                oktaRegistration.getClientSecret());
83
        return new OpaqueTokenAuthenticationProvider(introspectionClient)::authenticate;
84
    }
85
}


Better JWT Validation

Out of the box, Spring Security does minimal validation of the JWT because this is a vendor-specific detail. In addition to the standard JWT validation, Okta recommends validating the issuer and audience claims: iss and aud.

Update the above jwt() method to look like the following:

Java
xxxxxxxxxx
1
33
 
1
AuthenticationManager jwt() {
2
    // this is the keys endpoint for okta
3
    String issuer = oAuth2ClientProperties.getProvider().get("okta").getIssuerUri();
4
    String jwkSetUri = issuer + "/v1/keys";
5
6
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
7
8
    // okta recommends validating the `iss` and `aud` claims
9
    // see: https://developer.okta.com/docs/guides/validate-access-tokens/java/overview/
10
    List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
11
    validators.add(new JwtTimestampValidator());
12
    // Add validation of the issuer claim
13
    validators.add(new JwtIssuerValidator(issuer));
14
    validators.add(token -> {
15
        Set<String> expectedAudience = new HashSet<>();
16
        // Add validation of the audience claim
17
        expectedAudience.add("api://default");
18
        // For new Okta orgs, the default audience is `api://default`, 
19
        // if you have changed this from the default update this value
20
        return !Collections.disjoint(token.getAudience(), expectedAudience)
21
            ? OAuth2TokenValidatorResult.success()
22
            : OAuth2TokenValidatorResult.failure(new OAuth2Error(
23
                OAuth2ErrorCodes.INVALID_REQUEST,
24
                "This aud claim is not equal to the configured audience",
25
                "https://tools.ietf.org/html/rfc6750#section-3.1"));
26
    });
27
    OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(validators);
28
    jwtDecoder.setJwtValidator(validator);
29
30
    JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(jwtDecoder);
31
    authenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter());
32
    return authenticationProvider::authenticate;
33
}


Configure and Run Your OAuth 2.0 Application

We are in the home stretch; the last thing needed for our application is a little configuration.

Update your src/main/resources/application.properties file to include the properties from the previous steps.

Properties files
xxxxxxxxxx
1
 
1
spring.security.oauth2.client.provider.okta.issuer-uri = {yourOktaDomain}/oauth2/default
2
spring.security.oauth2.client.registration.okta.client-id = {clientId}
3
spring.security.oauth2.client.registration.okta.client-secret = {clientSecret}


Run the application on the command line:

Shell
 




xxxxxxxxxx
1


 
1
./mvnw spring-boot:run



Make a curl request to the server:

Shell
xxxxxxxxxx
1
 
1
curl localhost:8080/ -v


This will return something like:

Plain Text
xxxxxxxxxx
1
 
1
HTTP/1.1 401
2
WWW-Authenticate: Bearer


A 401 is expected here, as we did not provide an access token to the request. There are a few ways to get an access token — which option is right for you depends on where and how you access your REST application. Usually, another application is calling your REST API and that application already has an access token. For testing purposes, we will set up the OIDC Debugger.

Get a Token With the OIDC Debugger

Head over to https://oidcdebugger.com/ and populate the form with the following values:

  • Authorize URI - {yourOktaDomain}/oauth2/default/v1/authorize
  • Client ID - {clientId} from the previous step
  • State - this is a test (this can be any value)
  • Response type - select token
  • Use defaults for all other fields

Press the Send Request button.

If you are using an incognito/private browser, this may prompt you to login again. Once the Success page loads, copy the Access token and create an environment variable:

Shell
 




xxxxxxxxxx
1


 
1
export TOKEN="<your-access-token-here>"



Now that you have a token, you can make another request to your REST API:

Shell
xxxxxxxxxx
1
 
1
curl localhost:8080/ -H "Authorization: Bearer $TOKEN"
2
3
> Hello!


Similarly, we can call the POST endpoint:

Shell
xxxxxxxxxx
1
 
1
curl -X POST -F 'message=there' localhost:8080/ -H "Authorization: Bearer ${TOKEN}"
2
3
> hello: there


We can perform a simple (unscientific) performance test using the time utility, by prefixing the above commands with time:

Shell
xxxxxxxxxx
1
 
1
time curl localhost:8080/ -H "Authorization: Bearer ${TOKEN}"
2
time curl -X POST -F 'message=there' localhost:8080/ -H "Authorization: Bearer ${TOKEN}"


This data isn’t a great benchmark, as both the client and server are running on the same machine, but you can see the first one returned faster.

Plain Text
xxxxxxxxxx
1
 
1
0.00s user 0.01s system 65% cpu 0.013 total
2
0.00s user 0.01s system 4% cpu 0.210 total


NOTE: The increased CPU usage is caused by the JWT signature validation.

Learn More About Secure Applications

In this post, I’ve discussed the different ways to validate access tokens and provided a simple example that shows how you can use both options. As always, this code is available on GitHub.

Check out these related blog posts to learn more about building secure web applications.

  • A Quick Guide to Spring Boot Login Options
  • OpenID Connect Logout Options with Spring Boot
  • Secure Legacy Apps with Spring Cloud Gateway

If you like this blog post and want to see more like it, follow @oktadev on Twitter, subscribe to our YouTube channel, or follow us on LinkedIn. As always, please leave a comment below if you have any questions.

Spring Framework Spring Boot JWT (JSON Web Token) Spring Security Web application Requests

Published at DZone with permission of Brian Demers, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Trending

  • Scaling Site Reliability Engineering (SRE) Teams the Right Way
  • Replacing Apache Hive, Elasticsearch, and PostgreSQL With Apache Doris
  • Never Use Credentials in a CI/CD Pipeline Again
  • Transactional Outbox Patterns Step by Step With Spring and Kotlin

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com

Let's be friends: