OAuth2 and JWT Authentication
JWT is the passport for microservices—stateless, self-contained, and enabling cross-service authentication—putting an end to the challenges of session sharing.
1. What You'll Learn
- Comparison of the OAuth 2.0 Authorization Code Flow and the Password Grant
- JWT Token Structure: Header / Payload / Signature
spring-boot-starter-oauth2-resource-serverConfiguration and JWT Decoding- Mapping Custom JWT Claims to User Roles
- Alice implements JWT issuance and API token authentication for OrderFlow
2. A True Story of an Architect
(1) Pain Point: Sessions Expire in Microservices
After Alice scaled OrderFlow from a single instance to three instances, a session issue arose: when a user logged in on Instance A and the request was load-balanced to Instance B, the session did not exist, and the user was logged out. Bob tried using Redis to share sessions, but this introduced new dependencies and complexity. Charlie requested support for third-party login (Google/GitHub), which the traditional session solution was completely unable to accommodate.
(2) JWT Solution
JWT tokens contain all authentication information, so the server does not need to store sessions:
// Issue JWT on login
String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
// Verify JWT on each request (stateless, no session needed)
// Spring Security auto-extracts roles from JWT claims
(3) Revenue
After Alice replaced sessions with JWTs: the three instances no longer need to share a session, enabling zero-cost horizontal scaling; tokens are stored on the client side, making the server stateless; and third-party OAuth 2.0 login is supported.
3. OAuth 2.0 and JWT Concepts
(1) OAuth 2.0 Authorization Flow
sequenceDiagram
participant U as User Agent<br/>(Browser)
participant C as Client<br/>(OrderFlow SPA)
participant AS as Authorization<br/>Server
participant RS as Resource<br/>Server (API)
U->>C: Click "Login with GitHub"
C->>AS: Redirect to /authorize
AS->>U: Login + Consent page
U->>AS: Approve
AS->>C: Redirect back with auth code
C->>AS: POST /token (auth code + client_secret)
AS->>C: Access Token (JWT)
C->>RS: GET /api/orders (Bearer token)
RS->>RS: Verify JWT signature
RS->>C: 200 OK + Data
| Authorization Model | Use Cases | Client Types |
|---|---|---|
| Authorization Code Flow | Web Applications / SPAs | Backend Services + Frontend |
| Client Credentials | Inter-service Calls | Machine-to-Machine |
| Resource Owner Password Credentials (ROPC) | Deprecated, for testing only | First-party apps |
| Implicit Mode | Deprecated | Legacy SPA |
(2) JWT Token Structure
A JWT consists of three parts, separated by .:
Header.Payload.Signature
eyJhbGciOiJS256.eyJzdWIiOiJib2IiLCJyb2xlIjoiQ1VTVU9NRVIifQ.signature
| Section | Content | Example |
|---|---|---|
| Header | Algorithm + Token Type | {"alg":"RS256","typ":"JWT"} |
| Payload | Claims | {"sub":"bob","role":"CUSTOMER","exp":1705312000} |
| Signature | Signature of Header+Payload | RS256 Signature Integrity Verification |
| Standard Claim | Meaning |
|---|---|
sub |
Subject (User ID) |
iss |
Issuer |
aud |
Audience |
exp |
Expiration |
iat |
Issued At (Issuance Date) |
scope |
Scope of Permissions |
4. OAuth 2.0 Resource Server Configuration
(1) Dependencies and Configuration
(1) ▶ Example: pom.xml dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Output:
// Execution Successful
(2) ▶ Example: SecurityFilterChain JWT Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s ->
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login").permitAll()
.requestMatchers("/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/orders/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
String role = jwt.getClaimAsString("role");
if (role == null) return List.of();
return List.of(new SimpleGrantedAuthority("ROLE_" + role));
});
return converter;
}
}
Output:
// Execution Successful
5. JWT Issuance and Verification
(1) RSA Key Pair Configuration
(1) ▶ Example: Generating an RSA key pair
@Configuration
public class JwtConfig {
@Bean
public KeyPair keyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
return generator.generateKeyPair();
}
@Bean
public JwtEncoder jwtEncoder(KeyPair keyPair) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSource<SecurityContext> jwkSource =
new ImmutableJWKSet<>(new JWKSet(rsaKey));
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
return JwtDecoders.withPublicKey((RSAPublicKey) keyPair.getPublic());
}
}
Output:
// Execution Successful
(2) ▶ Example: Issuing a JWT upon login
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
private final JwtEncoder jwtEncoder;
private final AuthenticationManager authenticationManager;
public AuthController(JwtEncoder jwtEncoder,
AuthenticationManager authenticationManager) {
this.jwtEncoder = jwtEncoder;
this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public Map<String, String> login(@RequestBody LoginRequest request) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(), request.password()));
Instant now = Instant.now();
Instant expiry = now.plus(1, ChronoUnit.HOURS);
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("orderflow-service")
.subject(auth.getName())
.issuedAt(now)
.expiresAt(expiry)
.claim("role", auth.getAuthorities().stream()
.filter(a -> a.getAuthority().startsWith("ROLE_"))
.map(a -> a.getAuthority().replace("ROLE_", ""))
.findFirst().orElse("CUSTOMER"))
.build();
String token = jwtEncoder
.encode(JwtEncoderParameters.from(claims))
.getTokenValue();
return Map.of("accessToken", token);
}
public record LoginRequest(String username, String password) {}
}
Output:
// Execution Successful
6. Custom JWT Claims and Role Mappings
(1) Role Mapping Strategy
| Claim Field | Value | Mapped to Authority |
|---|---|---|
role: "ADMIN" |
Single-character string | ROLE_ADMIN |
roles: ["ADMIN","CUSTOMER"] |
Character Array | ROLE_ADMIN, ROLE_CUSTOMER |
scope: "read write" |
OAuth2 scope | SCOPE_read, SCOPE_write |
(1) ▶ Example: Custom JwtAuthenticationConverter
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
// Support both "role" (single) and "roles" (array) claims
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
String role = jwt.getClaimAsString("role");
if (role != null) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
List<String> roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.forEach(r -> authorities.add(
new SimpleGrantedAuthority("ROLE_" + r)));
}
return authorities;
});
return converter;
}
Output:
// Execution Successful
(2) ▶ Example: Using JWT Information in a Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@GetMapping("/my")
public List<OrderResponse> getMyOrders(
@AuthenticationPrincipal Jwt jwt) {
String username = jwt.getClaimAsString("sub");
String role = jwt.getClaimAsString("role");
return orderService.getOrdersByUsername(username);
}
}
Output:
// Execution Successful
| Method | Obtaining User Information | Applicable Scenarios |
|---|---|---|
@AuthenticationPrincipal Jwt jwt |
Complete JWT Claims | Custom Claim Required |
@AuthenticationPrincipal UserDetails user |
UserDetails | Standard user information required |
Principal principal |
getName() | Username only |
7. Comprehensive Example: Complete Implementation of OrderFlow JWT Authentication
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/orders/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt ->
Optional.ofNullable(jwt.getClaimAsString("role"))
.map(role -> List.of(new SimpleGrantedAuthority("ROLE_" + role)))
.orElse(List.of()));
return converter;
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService, PasswordEncoder encoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(encoder);
return new ProviderManager(provider);
}
}
// AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
private final JwtEncoder jwtEncoder;
private final AuthenticationManager authManager;
public AuthController(JwtEncoder jwtEncoder, AuthenticationManager authManager) {
this.jwtEncoder = jwtEncoder;
this.authManager = authManager;
}
@PostMapping("/login")
public Map<String, String> login(@RequestBody LoginRequest req) {
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(req.username(), req.password()));
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("orderflow").subject(auth.getName())
.issuedAt(now).expiresAt(now.plus(1, ChronoUnit.HOURS))
.claim("role", auth.getAuthorities().stream()
.map(a -> a.getAuthority().replace("ROLE_", ""))
.findFirst().orElse("CUSTOMER"))
.build();
String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
return Map.of("accessToken", token);
}
public record LoginRequest(String username, String password) {}
}
# 1. Login and get JWT
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"bob","password":"pass123"}' | jq -r '.accessToken')
# 2. Access protected API with JWT
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/orders/my
# 3. Admin endpoint with admin JWT
ADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"admin123"}' | jq -r '.accessToken')
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:8080/api/v1/admin/dashboard
❓ FAQ
📖 Summary
- The three parts of a JWT: Header (algorithm), Payload (claims), and Signature (signature)
- The OAuth2 Resource Server verifies the JWT signature and extracts roles from the claims to grant authorization
- RSA asymmetric encryption: private key issuance, public key verification, and more secure key management
JwtAuthenticationConverterCustom mapping logic from Claims to Authority- The login API issues a JWT; other APIs use Bearer Token authentication
- JWT is stateless and well-suited for microservices; its drawback is that it cannot be actively revoked.
📝 Exercises
-
Basic Exercise (Difficulty ⭐): Implement JWT login and API authentication for OrderFlow, replacing httpBasic. Use curl to test the login process and obtain a token → Use the token to access the protected API.
-
Advanced Exercise (Difficulty: ⭐⭐): Implement the Refresh Token mechanism—the Access Token is valid for 15 minutes, the Refresh Token is valid for 7 days, and the Refresh Token is used to obtain a new Access Token.
-
Challenge (Difficulty: ⭐⭐⭐): Integrate Keycloak as an external authorization server, with OrderFlow acting solely as a resource server to validate JWTs issued by Keycloak. Consider how to establish trust relationships between the services.



