JWT
Spring Security
token decoding
authentication
Java

JWT decoding with Spring Security

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

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

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.