JWT
Spring Security
token decoding
authentication
Java

JWT decoding with Spring Security

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Spring Security provides built-in support for decoding and validating JSON Web Tokens (JWTs) through its OAuth2 Resource Server module. A JWT consists of three Base64URL-encoded parts — header, payload, and signature — separated by dots. Spring Security's JwtDecoder validates the signature, checks claims like expiration and issuer, and converts the token into an Authentication object. Configuration requires adding the spring-boot-starter-oauth2-resource-server dependency and setting the issuer URI or JWK Set endpoint.

JWT Structure

 
1eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsInNjb3BlIjoicmVhZCJ9.signature
2
3Header:  {"alg": "RS256", "typ": "JWT"}
4Payload: {"sub": "user1", "scope": "read", "exp": 1700000000, "iss": "https://auth.example.com"}
5Signature: RSASHA256(base64(header) + "." + base64(payload), privateKey)

Spring Boot Configuration

Add the dependency:

xml
1<dependency>
2    <groupId>org.springframework.boot</groupId>
3    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
4</dependency>

Configure the issuer URI in application.yml:

yaml
1spring:
2  security:
3    oauth2:
4      resourceserver:
5        jwt:
6          issuer-uri: https://auth.example.com
7          # Or specify the JWK Set URI directly:
8          # jwk-set-uri: https://auth.example.com/.well-known/jwks.json

Spring Boot auto-configures a JwtDecoder that fetches the public keys from the issuer's JWKS endpoint.

Security Configuration

java
1import org.springframework.context.annotation.Bean;
2import org.springframework.context.annotation.Configuration;
3import org.springframework.security.config.annotation.web.builders.HttpSecurity;
4import org.springframework.security.web.SecurityFilterChain;
5
6@Configuration
7public class SecurityConfig {
8
9    @Bean
10    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
11        http
12            .authorizeHttpRequests(auth -> auth
13                .requestMatchers("/public/**").permitAll()
14                .requestMatchers("/admin/**").hasAuthority("SCOPE_admin")
15                .anyRequest().authenticated()
16            )
17            .oauth2ResourceServer(oauth2 -> oauth2
18                .jwt(jwt -> jwt
19                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
20                )
21            );
22        return http.build();
23    }
24
25    // Custom converter to extract authorities from JWT claims
26    @Bean
27    public JwtAuthenticationConverter jwtAuthenticationConverter() {
28        JwtGrantedAuthoritiesConverter grantedAuthorities = new JwtGrantedAuthoritiesConverter();
29        grantedAuthorities.setAuthoritiesClaimName("roles");  // Custom claim name
30        grantedAuthorities.setAuthorityPrefix("ROLE_");       // Prefix for Spring Security
31
32        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
33        converter.setJwtGrantedAuthoritiesConverter(grantedAuthorities);
34        return converter;
35    }
36}

Custom JwtDecoder Bean

For scenarios where you need a custom decoder (symmetric keys, custom validation):

java
1import org.springframework.security.oauth2.jwt.JwtDecoder;
2import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
3
4@Bean
5public JwtDecoder jwtDecoder() {
6    // RSA public key
7    return NimbusJwtDecoder.withPublicKey(rsaPublicKey).build();
8
9    // Or HMAC secret key
10    // SecretKey key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
11    // return NimbusJwtDecoder.withSecretKey(key).build();
12
13    // Or JWK Set URI
14    // return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/.well-known/jwks.json").build();
15}

Adding Custom Claim Validation

java
1@Bean
2public JwtDecoder jwtDecoder() {
3    NimbusJwtDecoder decoder = NimbusJwtDecoder
4        .withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
5        .build();
6
7    // Add validators
8    OAuth2TokenValidator<Jwt> issuerValidator =
9        JwtValidators.createDefaultWithIssuer("https://auth.example.com");
10
11    OAuth2TokenValidator<Jwt> audienceValidator = token -> {
12        if (token.getAudience().contains("my-api")) {
13            return OAuth2TokenValidatorResult.success();
14        }
15        return OAuth2TokenValidatorResult.failure(
16            new OAuth2Error("invalid_audience", "Expected audience 'my-api'", null)
17        );
18    };
19
20    OAuth2TokenValidator<Jwt> combined = new DelegatingOAuth2TokenValidator<>(
21        issuerValidator, audienceValidator
22    );
23
24    decoder.setJwtValidator(combined);
25    return decoder;
26}

Accessing JWT Claims in Controllers

java
1@RestController
2@RequestMapping("/api")
3public class UserController {
4
5    @GetMapping("/profile")
6    public Map<String, Object> getProfile(@AuthenticationPrincipal Jwt jwt) {
7        return Map.of(
8            "subject", jwt.getSubject(),
9            "email", jwt.getClaimAsString("email"),
10            "roles", jwt.getClaimAsStringList("roles"),
11            "issuedAt", jwt.getIssuedAt(),
12            "expiresAt", jwt.getExpiresAt()
13        );
14    }
15
16    // Or via SecurityContextHolder
17    @GetMapping("/me")
18    public String getCurrentUser() {
19        JwtAuthenticationToken auth = (JwtAuthenticationToken)
20            SecurityContextHolder.getContext().getAuthentication();
21        Jwt jwt = auth.getToken();
22        return jwt.getSubject();
23    }
24}

Manual JWT Decoding (Without Spring Security)

For decoding without the full security framework:

java
1import java.util.Base64;
2import com.fasterxml.jackson.databind.ObjectMapper;
3
4public class JwtUtil {
5    private static final ObjectMapper mapper = new ObjectMapper();
6
7    public static Map<String, Object> decodePayload(String token) throws Exception {
8        String[] parts = token.split("\\.");
9        if (parts.length != 3) throw new IllegalArgumentException("Invalid JWT");
10
11        String payload = new String(Base64.getUrlDecoder().decode(parts[1]));
12        return mapper.readValue(payload, Map.class);
13    }
14}
15
16// Usage
17Map<String, Object> claims = JwtUtil.decodePayload(token);
18String subject = (String) claims.get("sub");

This does NOT verify the signature. Only use for debugging or when the signature is verified elsewhere.

Common Pitfalls

  • Not validating the signature: Decoding the payload with Base64 is trivial — anyone can read JWT claims. The signature validation is what proves the token is authentic and untampered. Always use JwtDecoder with proper key configuration.
  • Wrong issuer or audience claim: If the iss or aud claim in the token does not match your validation config, Spring Security rejects the token with a generic 401. Check both the token and the config for exact string matches.
  • Clock skew causing expiration failures: Tokens that expire right at the boundary may fail due to clock differences between the auth server and your application. Configure clock skew tolerance with JwtTimestampValidator(Duration.ofSeconds(60)).
  • Confusing scope authorities prefix: Spring Security prefixes JWT scopes with SCOPE_ by default. A JWT with "scope": "read write" maps to authorities SCOPE_read and SCOPE_write, not read and write. Use hasAuthority("SCOPE_read") or customize the prefix.
  • JWKS endpoint caching: NimbusJwtDecoder caches the JWK Set. If the auth server rotates keys, existing tokens may fail until the cache refreshes. Configure cache TTL or implement a JWKSetCache with appropriate expiration.

Summary

  • Add spring-boot-starter-oauth2-resource-server and set issuer-uri for automatic JWT decoding
  • Use JwtAuthenticationConverter to map JWT claims to Spring Security authorities
  • Access JWT claims in controllers with @AuthenticationPrincipal Jwt jwt
  • Add custom validators for audience, issuer, or domain-specific claims
  • Never decode JWTs by Base64 alone in production — always validate the signature

Course illustration
Course illustration

All Rights Reserved.