Validating a token, step by step

JWT Authentication Course · lesson 9 of 13 · 6 min read

Seven checks, and what skipping each one lets an attacker do.

Open this lesson in the learning hub

Key points

  • Verify the signature first, using a key you chose - never one named in the token. A token you have not verified is attacker-controlled input, including its header.
  • Pin the algorithm. Accepting whatever alg says enables the historic none attack and the RS256-to-HS256 confusion where your public key is used as an HMAC secret.
  • Check exp, and allow a small clock skew - thirty seconds is typical. Without skew you get intermittent 401s that nobody can reproduce.
  • Check iss against a pinned value. Otherwise anyone who can run an identity provider can mint tokens you accept.
  • Check aud. This is the one most often skipped, and skipping it means a token issued for another service in the same estate works here.
  • Then, and only then, read the claims. Authorisation decisions come after validation, never from an unverified token.

Example

// The order matters. Each step gates the next.
//
//   1. parse the structure           header.payload.signature
//   2. select the key                by kid, from YOUR trusted JWKS
//   3. verify the signature          with a PINNED algorithm
//   4. check exp / nbf               with ~30s clock skew
//   5. check iss                     against a pinned value
//   6. check aud                     is this token FOR me?
//   7. only now read the claims

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder decoder = NimbusJwtDecoder
            .withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
            .jwsAlgorithm(SignatureAlgorithm.RS256)   // PINNED - not from the token
            .build();

    decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
            new JwtIssuerValidator("https://auth.example.com"),
            new JwtTimestampValidator(Duration.ofSeconds(30)),
            audienceValidator("orders-api")));
    return decoder;
}

static OAuth2TokenValidator<Jwt> audienceValidator(String expected) {
    return jwt -> jwt.getAudience().contains(expected)
            ? OAuth2TokenValidatorResult.success()
            : OAuth2TokenValidatorResult.failure(
                    new OAuth2Error("invalid_token", "Wrong audience", null));
}

/*
 * WHAT EACH SKIPPED CHECK COSTS:
 *
 *   signature      anyone forges any token                    critical
 *   pinned alg     alg:none, or RS256 -> HS256 confusion       critical
 *   exp            a leaked token never expires               high
 *   iss            any issuer you can reach is trusted        critical
 *   aud            a token for service B is accepted by A     high
 *   clock skew     intermittent 401s (not a hole, just pain)
 */

Verify the signature with a pinned algorithm before reading anything - an unverified token is attacker-controlled input.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the JWT Authentication Course course, and every lesson in it is listed on the JWT Authentication Course contents page.