Why this matters
- Stateless APIs scale horizontally — no session store to replicate across BookStore pods.
- OAuth2 is the industry standard for third-party login (Google, GitHub) and service-to-service auth.
- JWT structure (header, payload, signature) is a frequent interview topic for backend roles.
JWT structure
A JSON Web Token has three Base64-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT parts
- Header — Algorithm (
HS256,RS256) and token type (JWT). - Payload — Claims:
sub(subject/user),exp(expiry),roles, custom fields. - Signature — HMAC or RSA signature proving the token was issued by a trusted authority.
Never put sensitive data (passwords, credit cards) in the payload — it is only encoded, not encrypted.
Issuing tokens on login
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder encoder;
private final JwtService jwtService;
public AuthResponse login(LoginRequest request) {
User user = userRepository.findByEmail(request.email())
.orElseThrow(() -> new BadCredentialsException("Invalid credentials"));
if (!encoder.matches(request.password(), user.getPasswordHash())) {
throw new BadCredentialsException("Invalid credentials");
}
String token = jwtService.generateToken(user);
return new AuthResponse(token, "Bearer", 3600);
}
}
@Service
public class JwtService {
@Value("${bookstore.jwt.secret}")
private String secret;
@Value("${bookstore.jwt.expiration-seconds:3600}")
private long expiration;
public String generateToken(User user) {
return Jwts.builder()
.subject(user.getEmail())
.claim("roles", user.getRole())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact();
}
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseSignedClaims(token)
.getPayload();
}
}
Validating tokens in the filter chain
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
Claims claims = jwtService.parseToken(token);
UserDetails user = userDetailsService
.loadUserByUsername(claims.getSubject());
var auth = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
chain.doFilter(request, response);
}
}
Register before UsernamePasswordAuthenticationFilter in the security chain.
OAuth2 resource server
For tokens issued by an external provider (Auth0, Keycloak, Google):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.bookstore.example.com/realms/bookstore
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/books/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
Spring validates the JWT signature against the issuer's public keys automatically.
Client usage
# Login
curl -X POST http://127.0.0.1:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@bookstore.com","password":"secret"}'
# {"token":"eyJ...","type":"Bearer","expiresIn":3600}
# Authenticated request
curl http://127.0.0.1:8080/api/orders \
-H "Authorization: Bearer eyJ..."
Token refresh
Short-lived access tokens (15–60 minutes) limit exposure. Issue refresh tokens with longer expiry for silent re-authentication:
public record AuthResponse(
String accessToken,
String refreshToken,
String tokenType,
long expiresIn
) {}
Refresh endpoint validates the refresh token and issues a new access token without re-entering credentials.
Quick recall
Everything you need if you only revisit this box.
- JWT carries signed claims (user identity, roles, expiry) without server-side sessions.
- Issue tokens on login; validate on every request via a servlet filter or OAuth2 resource server.
Authorization: Bearer <token>is the standard header for token-based APIs.- OAuth2 resource server mode validates tokens from external identity providers.
- Use short-lived access tokens with refresh tokens for secure session management.
- Never store JWTs in localStorage — prefer HttpOnly cookies for browser clients.
Test yourself
Answer these before moving on — recall is what makes it stick.