Java: Spring Security OAuth2/OIDC protecting Client App and Resource Server

The Spring Security framework provides a robust and customizable framework for authentication and authorization for Spring based applications.

Using Spring Security, a Spring developer can add OIDC authentication and OAuth2 protection of resources by including the libraries in the build, configuring the Spring application.yml, and enabling various component configurations and annotations.

In this article, I will exercise the OAuth2 Authorization Code flow.  This allows an application to access a 3rd party API on behalf of the end user as illustrated by the diagram below.

These are the entities we will deploy:

  • Authentication Server a local Windows 2019 ADFS server, configured per my article here
  • Client Application a Spring Boot web app that orchestrates with the ADFS Authentication Server to authenticate, and trade a code for an Access Token
  • Resource Server a Spring Boot web app that validates the JWT Access Token and provides access to its services according to scope/role/claim

Prerequisites

A Windows 2019 Domain Controller, and a Windows 2019 ADFS server configured with an Application Group for OAuth2 access.

I have articles on creating and configuring these in previous articles:

For the local solution validation, you also need a local Docker daemon to run the images.

And the following groups in Active Directory:

  • Group=engineers
  • Group=managers

If you followed along with my previous articles on AD/ADFS, then you have run create-test-users-2019.ps1 and have these AD groups with members added to the group.  The group membership is inserted as a custom claim into the Access Token JWT by ADFS as described in my previous article.

The REST methods on the Resource Server check the JWT for these group claims when evaluating permission.

Bring up OAuth2 entities (Auth Server,Client App,Resource Server)

Configure “Authentication Server”

You should have already configured Windows 2019 ADFS with an OAuth2 Application Group per the prerequisites.

You will need to know the ADFS server name, ADFS_CLIENT_ID, and ADFS_CLIENT_SECRET before continuing.

Bring up local “Client Application”

In one console, bring up the Spring Boot Client Application which runs at http://localhost:8080

This Client Application is the one that negotiates the authentication flow between the end user and the Authentication Server.

# your ADFS server hostname
export ADFS=win2k19-adfs1.fabian.lee

# OAuth2 client-id and client-secret from ADFS
export ADFS_CLIENT_ID=<the oauth2 client id>
export ADFS_CLIENT_SECRET=<the oauth2 client secret>
export ADFS_SCOPE="openid allatclaims api_delete"

# clear out any older runs
docker rm spring-security5-oauth2-client-app

# run docker image locally, listening on localhost:8080
docker run \
--network host \
-p 8080:8080 \
--name spring-security5-oauth2-client-app \
-e ADFS_CLIENT_ID=$ADFS_CLIENT_ID \
-e ADFS_CLIENT_SECRET=$ADFS_CLIENT_SECRET \
-e ADFS=$ADFS \
-e ADFS_SCOPE="$ADFS_SCOPE" \
fabianlee/spring-security5-oauth2-client-app:1.0

This app is now ready to accept a code back from ADFS at “http://localhost:8080/login/oauth2/adfs” that will opaquely be exchanged for an Access Token.

Bring up local “Resource Server”

In another console, bring up the Spring Boot Resource Server which runs at http://localhost:8081

This Spring Boot Resource Server represents the 3rd party microservice API whose services are consumed on the end user’s behalf.

# your ADFS server hostname
export ADFS=win2k19-adfs1.fabian.lee

# clear out any older runs
docker rm spring-security5-oauth2-resource-server

# run docker image locally, listening on localhost:8080
docker run \
--network host \
-p 8081:8081 \
--name spring-security5-oauth2-resource-server \
-e ADFS=$ADFS \
fabianlee/spring-security5-oauth2-resource-server:1.0

This application will be sent requests with the HTTP header “Authorization: Bearer <jwtToken>”.  The app is responsible for checking the JWT is valid, then evaluating the claims/authority/scope information to return a response.

Use browser to test OAuth2 Authorization Code flow

I would recommend you enable the developer mode of your browser (F12 on Firefox and Chrome), so that you can follow along with the network requests being made from the browser.

Orchestrate login between Client App and Auth Server

Pointing your browser at http://localhost:8080 will show a simple web page with a login button.

Press “Default Login” and it will take you to the the Spring Security “localhost:8080/login” page, where you have a list of available OAuth2 Authorization servers.  Click “ADFS” (it is the only Auth Server configured for this example).

If you are watching the developer console of the browser, you see it silently redirect to the ADFS server at “/adfs/oauth2/authorize” with parameters for client_id, scope, and redirect_uri.

ADFS will then display a login form where you can enter in the credentials of a valid Active Directory user.  Login as a member of the Active Directory “engineers” group, which is “engineer1” in our case (since we used create-test-users-2019.ps1 when setting up AD).

Press “Sign in” and from the browser developer console you will see a GET response back to the “localhost:8080/login/oauth2/code/adfs” (the redirect URL) with a code parameter.

Opaque to the end user, the Client web application then takes this code and exchanges it for an ID and Access Token via POST to ADFS at https://ADFS/adfs/oauth2/token.

It will then redirect you to the main page, and this time you will see basic user info, instead of a login button.  There are two links available, one to view the OIDC id token used for first-party authentication, and the other to view the OAuth2 Access token used for access to 3rd party resource server API.

OIDC ID Token used for first party authentication

If you click on “view id token”, you will see the essential claims contained in Spring’s OidcUser object which represents the the ID token.

The raw token is in JWT format, and can be decoded by any of the online JWT decoders.  There are many overlapping claims between the ID token and access token, this is normal.

OAuth2 Access Token used to call 3rd party Resource Server on behalf of user

But a direct login to the Client Application is not the end of the Authorization Code flow purpose.  This Access Token JWT is meant to be used as authentication into a service on an separate Resource Server.

If you click on “view access token”, you will see the raw JWT as contained in Spring’s OAuth2AuthorizedClient object which represents the the Access token.  This can be copied-pasted into any of the online JWT decoders for a full dump of the claims.

Below this are Javascript buttons that will invoke the remote REST microservice endpoints at http://localhost:8081

This page uses Javascript Fetch to send the JWT in the ‘Authorization’ header and make the calls to the Resource Server.  CORS is enabled so these cross-site calls can be made.

  • GET /infojson – no authentication necessary, returns app info
  • GET /api/user/me – returns logged in user info (callee must be logged in)
  • GET /api/user – returns list of users (callee must be logged in)
  • DELETE /api/user – deletes last user in list (callee must be in managers group and have ‘api_delete’ scope)
  • GET /api/user/engineer – returns list of engineers (callee must be in engineers or managers group)
  • GET /api/user/manager – returns list of managers (callee must be in managers group)

Press each of these buttons to view the result coming back from the microservice.  Your browser’s developer console will show each of these Javascript network calls also.

Expected permission 403 errors for “engineers”

Note that as “engineer1” you will get 403 errors for two of the operations:

  • when attempting to DELETE a user at “/api/user”
  • when attempting to GET a list of managers at “/api/user/manager”

This is expected because of the Spring Security PreAuthorize expressions in UserController.java that limit access to these two microservice methods.  Below is a snippet of the code.

@DeleteMapping
@PreAuthorize("hasRole('managers') && hasAuthority('SCOPE_api_delete')")
public Iterable deleteUser() {
...

@GetMapping(value="/manager")
@PreAuthorize("hasRole('managers')")
public ResponseEntity<List> listManagers() {
...

The “engineer1” user token does not have a ‘managers’ role claim, so these calls correctly throw a 403 forbidden.

Access Token used to make Resource Server calls as member of ‘managers’

If you open a new private browser and go through the login process at http://localhost:8080 again, but this time with a user in the “managers” AD group (such as “managers1”), then when you navigate to the “view access token” page and test each call you will see success to every function.

These will all be successful because the “managers” custom group claim is embedded in the JWT.

Building and Running the project code

If you want to make modifications to either of these projects, then you need to checkout my spring-boot-security5-oauth2-oidc project from github and build and run it using gradle.

Prerequisite OpenJDK 17+

# refresh package repos
sudo apt update

# show available openjdk versions
sudo apt search openjdk-* | grep -P '^openjdk-1\d-jdk/'

# pick latest (which is 17 as of this writing)
sudo apt install openjdk-17-jdk curl git -y

# validate version reported is the one just installed
java --version

Fetch project from github

git clone https://github.com/fabianlee/spring-boot-security5-oauth2-oidc.git
cd spring-boot-security5-oauth2-oidc

BASEDIR=$(realpath .)
echo "Base directory of project: $BASEDIR"

Run Client Application from one console on port 8080

# your ADFS server hostname
export ADFS=win2k19-adfs1.fabian.lee

# OAuth2 client-id and client-secret from ADFS
export ADFS_CLIENT_ID=<the oauth2 client id>
export ADFS_CLIENT_SECRET=<the oauth2 client secret>
export ADFS_SCOPE="openid allatclaims api_delete"

cd $BASEDIR/spring-security5-oauth2-client-app
./gradlew bootRun

Run Resource Server from another console on port 8081

# your ADFS server hostname
export ADFS=win2k19-adfs1.fabian.lee

cd $BASEDIR/spring-security5-oauth2-resource-server
./gradlew bootRun

Access Token used to make Resource Server calls using curl

If you would prefer to use the console to make calls to the microservice on the Resource Server, you can use my Bash script.

cd $BASEDIR/spring-security5-oauth2-resource-server

export JWT=<token from access page>
./test-jwt-auth.sh

 

REFERENCES

fabianlee github, code for this article at ‘spring-boot-security5-oauth2-oidc’

public docker hub images, spring-security5-oauth2-client-app and spring-security5-oauth2-resource-server

Spring.io, Spring Boot and OAuth2 intro, simple example project

Spring docs, OAuth2 client

Spring guide and sample app, Spring Boot OAuth2 web client

github spring-security examples

BezKoder, Spring Boot Security example

okta, what the heck is OAuth2

okta, getting started with Spring Security 5 and OIDC example code

github, Spring with Google OIDC

github, Spring with onegini OIDC (based on Google OIDC project above)

Aaron Parecki, OAuth2 simplified

postman.com, OAuth2 flow diagrams and PXCE

github eugenep, Spring Security and OAuth2 with articles on Baeldung

Baeldung, custom endpoints for Spring Security and OAuth2

Baeldung, customizing authorization and token requests with Spring Security

okta.com, create Spring Boot OAuth2 Resource Server and then Client web app

dzone.com, differences between newer Spring Security 5 and older versions

hellokoding.com, Spring Security and OAuth2 and OIDC with links to github source

Spring, WebSecurityConfigurerAdapter deprecated in Spring 5.7

LogicBig.com, understanding Spring Security UserDetailsService

Spring blog, Spring Security alternative to WebSecurityConfigurerAdapter with InMemoryUserDetailsManager

github chuuks, Spring example using OAuth2 and repo, domain, controller layers

sultanov.dev, enhancing JWT claims in Spring Security OAuth2

spring docs, PreAuthorize expressions

Mozilla developer, CORS protocol and header descriptions

spring.io, Spring Boot with OAuth2

NOTES

Doing direct curl and untar from Spring Starter

$ mkdir ui && cd ui
$ curl https://start.spring.io/starter.tgz -d style=web -d name=simple | tar -xzvf -

JWT Decoding

There are many online JWT token decoders, but here is the access token pasted into http://calebb.net, The Resource Server uses this for evaluating scope/role for permission to resources.