JWT Testing Server

Creating a testing environment is useful to perform

  • Performance & Load Tests

  • Security Tests

  • Other User Acceptance tests

  • ...

By using OAuth2/OIDC you have an additional external component in place that is required to run your clients and server applications.

To support testing environments without having external dependencies you have several possibilities:

  1. Spring Integration Tests for JWT (see last lab)

  2. Test using your own self-signed JWT tokens

  3. Test using the TestContainers library using a dockerized Keycloak instance

Option 3 is a bit out of focus of this workshop, so we will go for option 2 as part of this workshop.

Lab Contents

The Keycloak identity provider is not required anymore for this lab.

Learning Targets

In this fourth lab you will see how you can configure the resource server from Lab 1 with a custom static private/public key pair and create an application to generate your own JWT tokens using the corresponding self-signing private key.

This is quite helpful in testing environments, e.g. doing load/performance testing and preventing from load testing the identity server as well.

This lab is actually split into three steps:

  1. Look into a resource server with static public key to verify JWT tokens

  2. Generate custom JWT tokens for different user identities to be used at the resource server of step 1

  3. Make requests to the resource server of step 1 with generated JWT from step 2

Folder Contents

In the lab 5 folder you find 3 applications:

  • library-server-static-complete: This application is the complete static resource server

  • jwt-generator: This application is the JWT generator to generate custom JWT tokens

Start the Lab

In this lab you will not really implement anything yourself, but you will see how to use such static resource server with custom generated JWt tokens. So let's start.

Step 1: Resource server with static token validation

Now, let's start with step 1 of this lab. Here we will have a look into the required changes we need compared to the resource server of Lab 1 to support static public keys for token signature validation.

In Lab 1 we have seen how Spring security 5 uses the OpenID Connect Discovery specification to completely configure the resource server to use our keycloak instance.

As we will now locally validate the incoming JWT access tokens using a static public key we do not need the discovery entries (especially the JWKS uri) anymore.

You can see the changes in application.yml, here no issuer uri property is required anymore. Instead, we specify a location reference to a file containing a public key to verify JWT tokens.

This looks like this:

spring:
  jpa:
    open-in-view: false
  jackson:
    date-format: com.fasterxml.jackson.databind.util.StdDateFormat
    default-property-inclusion: non_null
  security:
    oauth2:
      resourceserver:
        jwt:
          publicKeyLocation: classpath:library_server.pub

Now we have to use this public key to configure the JwtDecoder to use this for validating JWT tokens instead of contacting keycloak.

This requires a small change in the class com.example.library.server.config.WebSecurityConfiguration:

Open the class com.example.library.server.config.WebSecurityConfiguration and look at the changes:

package com.example.library.server.config;

import com.example.library.server.security.AudienceValidator;
import com.example.library.server.security.LibraryUserDetailsService;
import com.example.library.server.security.LibraryUserJwtAuthenticationConverter;
import com.example.library.server.security.LibraryUserRolesJwtAuthenticationConverter;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

import java.security.interfaces.RSAPublicKey;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

  private final LibraryUserDetailsService libraryUserDetailsService;

  @Value("${spring.security.oauth2.resourceserver.jwt.publicKeyLocation}")
  private RSAPublicKey key;

  public WebSecurityConfiguration(LibraryUserDetailsService libraryUserDetailsService) {
    this.libraryUserDetailsService = libraryUserDetailsService;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .csrf()
        .disable()
        .authorizeRequests()
        .anyRequest()
        .fullyAuthenticated()
        .and()
        .oauth2ResourceServer()
        .jwt()
        .jwtAuthenticationConverter(libraryUserJwtAuthenticationConverter());
  }

  @Bean
  JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder =
            NimbusJwtDecoder.withPublicKey(this.key).build();

    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer("test_issuer");
    OAuth2TokenValidator<Jwt> withAudience =
        new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
  }

  @Bean
  LibraryUserJwtAuthenticationConverter libraryUserJwtAuthenticationConverter() {
    return new LibraryUserJwtAuthenticationConverter(libraryUserDetailsService);
  }
}

This configuration above looks like the one as in Lab 1 with one important change:

@Value("${spring.security.oauth2.resourceserver.jwt.publicKeyLocation}")
private RSAPublicKey key;

...

NimbusJwtDecoder jwtDecoder =
            NimbusJwtDecoder.withPublicKey(this.key).build();

Here we use the public key (using RSA crypto algorithm) we read from the publicKeyLocation and create a NimbusJwtDecoder using this public key instead of configuring a JwtDecoder from issuer uri.

With this configuration in place we have already a working resource server that can handle JWt access tokens transmitted via http bearer token header. Spring Security also validates by default:

  • the JWT signature against the given static public key

  • the JWT iss claim against the configured issuer uri

  • that the JWT is not expired, if the JWT contains such entry

Step 2: Run JWT generator web application

Please navigate your Java IDE to the lab4/jwt-generator project. Then start the application by running the class com.example.jwt.generator.Lab5JwtGeneratorApplication.

After starting navigate your browser to localhost:9093.

Then you should see a screen like the following one.

To generate an JWT access token with the correct user identity and role information please fill the shown form with one of the following users and roles:

Username

Email

Role

bwayne

bruce.wayne@example.com

library_user

bbanner

bruce.banner@example.com

library_user

pparker

peter.parker@example.com

library_curator

ckent

clark.kent@example.com

library_admin

After filling the form click on the button Generate JWT then you should get another web page with the generate access token. This should look like this one.

To continue with this lab copy the contents of the JWT and use this JWT as access token to make a request to the resource server in the next step.

Step 3: Run and test static resource server

Please navigate your Java IDE to the lab5/library-server-static-complete project and at first explore this project a bit. Then start the application by running the class com.example.library.server.Lab5CompleteStaticLibraryServerApplication.

Same as in Lab 1 we require bearer tokens in JWT format to authenticate at our resource server.

To do this we will need to run the copied access token from the JWT generator web application in the previous step.

To make a request for a list of users we have to specify the access token as part of a Authorization header of type Bearer like this:

httpie:

http localhost:9091/library-server/users \
'Authorization: Bearer [access_token]'

curl:

curl -H 'Authorization: Bearer [access_token]' \
-v http://localhost:9091/library-server/users | jq

You have to replace [access_token] with the one you have obtained from the JWt generator application.

Navigate your web browser to jwt.io and paste your access token into the Encoded text field.

If you scroll down a bit on the right hand side then you will see the following block (depending on which user you have specified when generating a JWT):

{
  "scope": "library_admin email profile",
  "email_verified": true,
  "name": "Clark Kent",
  "groups": [
    "library_admin"
  ],
  "preferred_username": "ckent",
  "given_name": "Clark",
  "family_name": "Kent",
  "email": "clark.kent@example.com"
}

As you can see our user has the scopes library_admin, email and profile. These scopes are now mapped to the Spring Security authorities SCOPE_library_admin, SCOPE_email and SCOPE_profile.

This request should succeed with an '200' OK status and return a list of users.

Step 4: Use new approach for JWT based authorization tests

As of version 5.2 of spring security new support for JWT based authorization tests is provided.

To see the new approach please have a look into the test class com.example.library.server.api.BookApiJwtAuthorizationTests.

Here you see that you may configure either default or customized JWT tokens to test different authorization scenarios.

...
@Test
  @DisplayName("get list of books")
  void verifyGetBooks() throws Exception {

    this.mockMvc
        .perform(get("/books").with(jwt()))
        .andExpect(status().isOk());
  }

  @Test
  @DisplayName("get single book")
  void verifyGetBook() throws Exception {

    Jwt jwt = Jwt.withTokenValue("token")
            .header("alg", "none")
            .claim("sub", "bwanye")
            .claim("groups", new String[] {"library_user"}).build();

    this.mockMvc
        .perform(
            get("/books/{bookId}", DataInitializer.BOOK_CLEAN_CODE_IDENTIFIER)
                .with(jwt(jwt)))
        .andExpect(status().isOk());
  }

  @Test
  @DisplayName("delete a book")
  void verifyDeleteBook() throws Exception {
    this.mockMvc
        .perform(
            delete("/books/{bookId}", DataInitializer.BOOK_DEVOPS_IDENTIFIER)
                .with(jwt().authorities(new SimpleGrantedAuthority("ROLE_LIBRARY_CURATOR"))))
        .andExpect(status().isNoContent());
  }
...

Details on JWT testing support can be found in the corresponding spring security reference documentation section.

This concludes the Lab 5.

Last updated