The topic of validating an OAuth 2.0 access tokens comes up frequently on the Okta developer blog. Often we talk about how to validate JSON Web Token (JWT) based access tokens; however, this is NOT part of the OAuth 2.0 specification. JWTs are so commonly used that Spring Security supported them before adding support for remotely validating tokens (which is part of the OAuth 2.0 specification.)

Table of Contents

In this post, you will build a simple application that takes advantage of both types of validation.

Prerequisites

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!

React LogoReact Logo

Go from vanilla JavaScript 👉 React

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

Make a note of the Client ID, and Client secret you will need them for the next steps.

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:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency> <groupId>com.nimbusds</groupId> <artifactId>oauth2-oidc-sdk</artifactId> <version>7.3</version>
</dependency>

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

@Bean
OAuth2ClientProperties oAuth2ClientProperties() { return new OAuth2ClientProperties();
}

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

package com.okta.example; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; @RestController
public class SimpleController { @GetMapping("/") String hello() { return "Hello!"; } @PostMapping("/") String helloPost(@RequestParam("message") String message) { return "hello: " + message; }
}

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.

package com.okta.example; import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert; /_*
_ An {@link AuthenticationManagerResolver} that returns a {@link AuthenticationManager}
_ instances based upon the type of {@link HttpServletRequest} passed into
_ {@link #resolve(HttpServletRequest)}.
_ @author Josh Cummings
_ @since 5.2
*/
public class RequestMatchingAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> { private final LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers; private AuthenticationManager defaultAuthenticationManager = authentication -> { throw new AuthenticationServiceException("Cannot authenticate " + authentication); }; public RequestMatchingAuthenticationManagerResolver (LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers) { Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty"); this.authenticationManagers = authenticationManagers; } @Override public AuthenticationManager resolve(HttpServletRequest context) { for (Map.Entry<RequestMatcher, AuthenticationManager> entry : this.authenticationManagers.entrySet()) { if (entry.getKey().matches(context)) { return entry.getValue(); } } return this.defaultAuthenticationManager; } public void setDefaultAuthenticationManager(AuthenticationManager defaultAuthenticationManager) { Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null"); this.defaultAuthenticationManager = defaultAuthenticationManager; }
}

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):

package com.okta.example; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.JwtBearerTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.web.util.matcher.RequestMatcher; import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List; @Configuration
public class ExampleWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Autowired private OAuth2ClientProperties oAuth2ClientProperties; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); http.oauth2ResourceServer().authenticationManagerResolver(customAuthenticationManager()); } AuthenticationManagerResolver<HttpServletRequest> customAuthenticationManager() { LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers = new LinkedHashMap<>(); List<String> readMethod = Arrays.asList("HEAD", "GET", "OPTIONS"); RequestMatcher readMethodRequestMatcher = request -> readMethod.contains(request.getMethod()); authenticationManagers.put(readMethodRequestMatcher, jwt()); RequestMatchingAuthenticationManagerResolver authenticationManagerResolver = new RequestMatchingAuthenticationManagerResolver(authenticationManagers); authenticationManagerResolver.setDefaultAuthenticationManager(opaque()); return authenticationManagerResolver; } AuthenticationManager jwt() { String issuer = oAuth2ClientProperties.getProvider().get("okta").getIssuerUri(); String jwkSetUri = issuer + "/v1/keys"; JwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(jwtDecoder); authenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter()); return authenticationProvider::authenticate; } AuthenticationManager opaque() { String issuer = oAuth2ClientProperties.getProvider().get("okta").getIssuerUri(); String introspectionUri = issuer + "/v1/introspect"; OAuth2ClientProperties.Registration oktaRegistration = oAuth2ClientProperties.getRegistration().get("okta"); OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector( introspectionUri, oktaRegistration.getClientId(), oktaRegistration.getClientSecret()); return new OpaqueTokenAuthenticationProvider(introspectionClient)::authenticate; }
}

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:

AuthenticationManager jwt() { String issuer = oAuth2ClientProperties.getProvider().get("okta").getIssuerUri(); String jwkSetUri = issuer + "/v1/keys"; NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>(); validators.add(new JwtTimestampValidator()); validators.add(new JwtIssuerValidator(issuer)); validators.add(token -> { Set<String> expectedAudience = new HashSet<>(); expectedAudience.add("api://default"); return !Collections.disjoint(token.getAudience(), expectedAudience) ? OAuth2TokenValidatorResult.success() : OAuth2TokenValidatorResult.failure(new OAuth2Error( OAuth2ErrorCodes.INVALID_REQUEST, "This aud claim is not equal to the configured audience", "https://tools.ietf.org/html/rfc6750#section-3.1")); }); OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(validators); jwtDecoder.setJwtValidator(validator); JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(jwtDecoder); authenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter()); return authenticationProvider::authenticate;
}

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.

spring.security.oauth2.client.provider.okta.issuer-uri = {yourOktaDomain}/oauth2/default
spring.security.oauth2.client.registration.okta.client-id = {clientId}
spring.security.oauth2.client.registration.okta.client-secret = {clientSecret}

Run the application on the command line:

./mvnw spring-boot:run

Make a curl request to the server:

curl localhost:8080/ -v

This will return something like:

HTTP/1.1 401
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.

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
  • Statethis 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:

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

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

curl localhost:8080/ -H "Authorization: Bearer $TOKEN" > Hello!

Similarly, we can call the POST endpoint:

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

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

time curl localhost:8080/ -H "Authorization: Bearer ${TOKEN}"
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.

0.00s user 0.01s system 65% cpu 0.013 total
0.00s user 0.01s system 4% cpu 0.210 total

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

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.

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.

Like this article? Follow @chrisoncode on Twitter