Implementing OAuth 2.0 access token validation with Spring Security

Would you like to know some basic concepts of Spring Security that can be implemented in a modern, micorservice application? If so this article is for you! In it I’ll guide you how to add step-by-step OAuth 2.0 access token validation to REST API endpoints of your Spring Boot application.

Movies backend application

To make it simple I’ve prepared a small Spring Boot application which servers two REST endpoints to the outside world:

  • its signature is correct (it assures that nobody has changed a content of a token),
  • it’s not expired,
  • it contains roles and scopes information.

Access token validation with Spring Security

Like many other problems this one could be resolved in many ways. For instance it would be possible to not use Spring Security at all and implement everything from the scratch. Another approach would be to use some additional library, built on top of the Spring Security which would magicaly do most of the job, but possibly it won’t allow to do some custom stuff.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<!-- OAuth 2.0 dependencies -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.15.0</version>
</dependency>
<!-- Utils dependencies -->
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXYmdnRkNBNzJ4UG5KYWNUZTRHMzdEN1NDWFpJOW8wQVZMTWd0d0tfamhRIn0.eyJleHAiOjE2MTM2Mjc1NzYsImlhdCI6MTYxMzYyNzI3NiwianRpIjoiZjkzODVlMDMtYzRjOC00NzY0LWIyMDgtYzBhYWFiMjIyODEyIiwiaXNzIjoiaHR0cDovL2tleWNsb2FrOjgwODAvYXV0aC9yZWFsbXMvdGVzdCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIwODhjNTdiMy1mNGJlLTQwOTQtOGQzNC00Y2UyNWQ1OGUzY2IiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0ZXN0X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiJlMTYxMTNjZi0wZGI5LTRlZDMtOGJjMC0yNWRmNDY4MDU2NjkiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJ0ZXN0X2NsaWVudCI6eyJyb2xlcyI6WyJ1bWFfcHJvdGVjdGlvbiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SWQiOiJ0ZXN0X2NsaWVudCIsImNsaWVudEhvc3QiOiIxNzIuMTguMC4xIiwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LXRlc3RfY2xpZW50IiwiY2xpZW50QWRkcmVzcyI6IjE3Mi4xOC4wLjEifQ.ihCQGFWRdr1NR7el7zsnnXZFwonOpFgEE_MHBlYSq07s2JhjsLJauvQ9erTM5YV_gmY-Q-QpmpRI-qq4miF9hem8qxXzxBkei7cXyYYQeiz44MwkusUW75VChfsYljgRNUSJM5G4_O636xWQNc2ET5v8508CPpgVIvl4QFKYQui3J2BADH8AyDihgaOcF5hfrutuEpH6AINvBmUebUKOLG3ZyST81Q3GjmSZmBaL7RK29uTm94i1HVqfXgRLIuf5zxLucq_gU4KM8c3mB5d8ZfJOMl8nOnYDbEbiGVEP_coz9x3iF2Lf3Fp8K2zez59w4yvGTy39Whns-KhtX02yAQ
{keycloak_base_url}/auth/realms/{realm_name}/protocol/openid-connect/certs
{
"keys": [{
"kid": "WbggFCA72xPnJacTe4G37D7SCXZI9o0AVLMgtwK_jhQ",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "k96-hliB-mYJx47V4wPqnWi89IsSYfmCXjiWgHG4e4VAEcLqZ33V9aQ3nConMf0AWbG9zcQZUtqSTtXVKeUvtx_7QH9Rgzwoj0XDOU3K21hcZCs7ShMK-2kROk2jnZpM5I6r7GeloR52fPXIck-sXcR3acmeUgZyR_y7TtlYYCn4GUt2kjrwyL_qdNdvQoDXvHTnxv4wk3_-Zff7I3xK3lnPE8uIz6gxjFMZ20-ruU_5-umpUvF9B6NUPf3LQTlMm864MBw1narN6QrWTp9c9DF0SeGcN8r_XMo0jDkMDjTWwrHiumnA8Il50hbmcp_WdMV8ivuVoONGLc_5Q6-uUQ",
"e": "AQAB",
"x5c": [
"MIIClzCCAX8CBgF3GcqCYDANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTIxMDExOTA4MzUzOFoXDTMxMDExOTA4MzcxOFowDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJPevoZYgfpmCceO1eMD6p1ovPSLEmH5gl44loBxuHuFQBHC6md91fWkN5wqJzH9AFmxvc3EGVLakk7V1SnlL7cf+0B/UYM8KI9FwzlNyttYXGQrO0oTCvtpETpNo52aTOSOq+xnpaEednz1yHJPrF3Ed2nJnlIGckf8u07ZWGAp+BlLdpI68Mi/6nTXb0KA17x058b+MJN//mX3+yN8St5ZzxPLiM+oMYxTGdtPq7lP+frpqVLxfQejVD39y0E5TJvOuDAcNZ2qzekK1k6fXPQxdEnhnDfK/1zKNIw5DA401sKx4rppwPCJedIW5nKf1nTFfIr7laDjRi3P+UOvrlECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAN+QZvSPXUP6Q9JxpYOzl6U7TXLAWmQPznf+gdSc4rmHQD5gKr2s8zeNuJrx+cIIxPGsMk04VGdNjKf8bRYvB7ta+POxQ5VRnHZS5XuEyGBFeSWbbzwZnQY+cWjY3AmyReenJ4QKal4iFZgNvkYbVaw9SX1qDXTXQTn6OXyz/Cp8SbMUW8fy7GqdGFLGKYhX1Irq4Reiidkw4pKVgGHblcjB2hIRfpC7TYnv0jL9RK8iymvPGYRQYka2ks8J/+HNudKO8MoOwghtP9jwTHEygioc9gAyf8DMS9cGnsFnisCz4B/etTyvyzuhyBgTili5WzXjOk1Y0NqaxY50618BvPg=="],
"x5t": "KQ4zgovMGPVesO36mzxvI9UTfdQ",
"x5t#S256": "F4xuZVX7WsxcdXAetwwTOOI2ElV-hH9Gl_G25hANAaE"
}]
}
  • @EnableWebSecurity annotation - this annotation tells Spring that this is a configuration class and enables security features defined by WebSecurityConfigurerAdapter superclass,
  • String nonSecureUrl field - holds information which of served URLs by the application should be excluded from checking the access token. Those values (comma seperated) can be passed to application.properties or application.yaml file under spring.security.ignored key. For my application I've decided to exclude all actuator endpoints to allow external services to collect metrics.
  • Url values defined in above variable are than used in the configure(WebSecurity web) method, where we configure Spring Security to ignore validation for provided endpoints
  • String jwkProviderUrl field - this holds a base URL of the authorization server, Keycloak in our case, that provides a public key needed for validating signature of an access token,
  • value that is holded by above variable is used to create a Spring bean in JwkProvider keycloakJwkProvider(),
  • configure(HttpSecurity http) - this is the most important method in this class, because here we're applying the AccessTokenFilter to the filter chain with addFilterBefore method. Apart from that I've also decided to add following configs:
  • .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) - I would like to have a stateless application, which means that user info are not stored in-memory between requests and prevents Spring from creating HTTP sessions,
  • .csrf().disable() - because I want to have a stateless and relatively simple application I decided to disable CSRF protection, if you need to know more about protecting from this kind of attacks go check the Spring official docs
  • cors() - with this config we will allow other origin (domain, server, port) to use endpoints; in simplier words if we would not add it our frontend app (that will be build in upcoming post) would not be able to consume REST API because it'll be running on a different port (when I'll start it locally),
  • AuthenticationManager authenticationManagerBean() - here we're defining the AuthenticationManager bean,
  • configure(AuthenticationManagerBuilder auth) - having already the AuthenticationManager we need to register an AuthenticationProvider, which is defined as a bean in following config method,
  • AuthenticationProvider authenticationProvider() - in our case we would like to define KeycloakAuthenticationProvider as a provider,
  • JwtTokenValidator jwtTokenValidator(JwkProvider jwkProvider) - here we're defining the class responsible for token validation.

Manual testing

In order to test E2E our solution we will need a Keycloak instance up and running. In my previous blog post I’ve described how it can be achieved using Docker, but if you don’t want go back here, in short are what we need to have.

> docker-compose up -d keycloak
Starting postgres ... done
Starting keycloak ... done
127.0.0.1	keycloak
client_id       test_client
client_secret 8ac27a39-fa84-46b9-8c30-b485056e0cea
username luke
password password

Handling AuthenticationException

In the test case when we haven’t provided an access token our application returns only the HTTP code 401, which is kind of an information but still it’s not really verbose. It doesn’t tell why request was not authorized.

Protecting REST API endpoints by user role

Next step into full-secured application is to allow only selected users to use certain REST API endpoints. In order to define those special users we need to assign roles to their accounts. In my previous article about Keycloak set up I’ve created two roles and assign each on of them to different user. Now looking at the movie backend application let’s say that an endpoint /movies can be used only by a user with ADMIN role, and an endpoint /movies/{id} only by the users with VISITOR role. That's the target.

Unit and E2E tests

That could be the end of this blog post, but as it’s a good practice to have tests for a code we write let’s do some of these.

Java Software Developer, DevOps newbie, constant learner, podcast enthusiast.