JWT
Authentication
Spring
Principal Conversion
Usability

Convert JWT Authentication Principal to something more usable in spring

Master System Design with Codemia

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

Introduction

Spring Security can authenticate a request from a JWT quickly, but the default authenticated object is often awkward to use in application code. Many teams do not want controllers and services digging through raw claims such as sub, preferred_username, or scope on every request. A better approach is to convert the JWT into a small application-specific principal as part of authentication.

What Spring Gives You by Default

With Spring Security resource server support, the authenticated object is commonly a JwtAuthenticationToken, and its principal is a Jwt. That is useful infrastructure data, but not a great domain API.

A controller that uses the raw token often looks like this:

java
1import org.springframework.security.core.annotation.AuthenticationPrincipal;
2import org.springframework.security.oauth2.jwt.Jwt;
3import org.springframework.web.bind.annotation.GetMapping;
4import org.springframework.web.bind.annotation.RestController;
5
6@RestController
7class ProfileController {
8    @GetMapping("/me")
9    public String me(@AuthenticationPrincipal Jwt jwt) {
10        return jwt.getClaimAsString("preferred_username");
11    }
12}

This works, but it spreads JWT claim names throughout the codebase. It also makes refactoring harder if the token format changes.

Map Claims to a Real Principal Type

Create a small principal type that matches what the application actually needs.

java
1import java.util.List;
2
3public record CurrentUser(
4    String userId,
5    String username,
6    String email,
7    List<String> roles
8) {
9}

Now create an authentication token that stores CurrentUser as the principal.

java
1import java.util.Collection;
2import org.springframework.security.authentication.AbstractAuthenticationToken;
3import org.springframework.security.core.GrantedAuthority;
4import org.springframework.security.oauth2.jwt.Jwt;
5
6public class CurrentUserAuthenticationToken extends AbstractAuthenticationToken {
7    private final CurrentUser principal;
8    private final Jwt jwt;
9
10    public CurrentUserAuthenticationToken(
11        CurrentUser principal,
12        Jwt jwt,
13        Collection<? extends GrantedAuthority> authorities
14    ) {
15        super(authorities);
16        this.principal = principal;
17        this.jwt = jwt;
18        setAuthenticated(true);
19    }
20
21    @Override
22    public Object getCredentials() {
23        return jwt.getTokenValue();
24    }
25
26    @Override
27    public CurrentUser getPrincipal() {
28        return principal;
29    }
30}

The application can now work with CurrentUser instead of raw claim lookups.

Plug the Converter into Spring Security

Spring Security lets you provide a converter from Jwt to AbstractAuthenticationToken.

java
1import java.util.List;
2import java.util.stream.Collectors;
3import org.springframework.core.convert.converter.Converter;
4import org.springframework.security.core.GrantedAuthority;
5import org.springframework.security.core.authority.SimpleGrantedAuthority;
6import org.springframework.security.oauth2.jwt.Jwt;
7
8public class CurrentUserJwtConverter implements Converter<Jwt, CurrentUserAuthenticationToken> {
9    @Override
10    public CurrentUserAuthenticationToken convert(Jwt jwt) {
11        List<String> roles = jwt.getClaimAsStringList("roles");
12        if (roles == null) {
13            roles = List.of();
14        }
15
16        List<GrantedAuthority> authorities = roles.stream()
17            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
18            .collect(Collectors.toList());
19
20        CurrentUser principal = new CurrentUser(
21            jwt.getSubject(),
22            jwt.getClaimAsString("preferred_username"),
23            jwt.getClaimAsString("email"),
24            roles
25        );
26
27        return new CurrentUserAuthenticationToken(principal, jwt, authorities);
28    }
29}

Register that converter in the 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
7class SecurityConfig {
8    @Bean
9    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
10        http
11            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
12            .oauth2ResourceServer(oauth2 -> oauth2
13                .jwt(jwt -> jwt.jwtAuthenticationConverter(new CurrentUserJwtConverter()))
14            );
15
16        return http.build();
17    }
18}

At that point, the authentication pipeline produces your application-specific principal automatically.

Use the Converted Principal in Controllers and Services

Now the controller code becomes much cleaner:

java
1import org.springframework.security.core.annotation.AuthenticationPrincipal;
2import org.springframework.web.bind.annotation.GetMapping;
3import org.springframework.web.bind.annotation.RestController;
4
5@RestController
6class ProfileController {
7    @GetMapping("/me")
8    public String me(@AuthenticationPrincipal CurrentUser user) {
9        return user.username() + " <" + user.email() + ">";
10    }
11}

If you do not want angle brackets in a response string, return a DTO instead. The key improvement is that application code now sees a stable model instead of token internals.

You can also access the converted principal from services via SecurityContextHolder, but injecting it into controllers or method arguments is usually clearer and easier to test.

Keep Claim Mapping Conservative

JWT claims are untrusted until signature validation and issuer checks succeed, and even then they still represent external data. Convert only the claims you actually need. Avoid using the converter as a place for database lookups, because that turns authentication into a slow network-dependent step. If you need richer user state, map the minimum identity data from the JWT and load the rest later in the request flow.

Common Pitfalls

  • Reading raw claim names in controllers and services instead of centralizing the mapping in one converter.
  • Assuming a claim such as roles always exists and failing with a null value when a token shape changes.
  • Doing expensive database work inside the JWT converter instead of keeping authentication lightweight.
  • Mapping claims to authorities inconsistently, which breaks authorization checks later in the request.
  • Returning the raw Jwt everywhere and missing the chance to define a stable application-level principal.

Summary

  • Spring can authenticate JWTs out of the box, but the default principal is often too low-level for application code.
  • Create a small principal type that reflects the user data your application actually needs.
  • Implement a Converter<Jwt, AbstractAuthenticationToken> to build that principal once during authentication.
  • Register the converter in the resource server configuration.
  • Keep the mapping small, explicit, and focused on stable claims.

Course illustration
Course illustration

All Rights Reserved.