package com.example.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class JwtTokenService {
private final SecretKey key;
private final long ttlSeconds;
public JwtTokenService(@Value("${jwt.secret}") String secret,
@Value("${jwt.ttl-seconds:900}") long ttlSeconds) {
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
this.ttlSeconds = ttlSeconds;
}
public String generate(String username, List<String> roles) {
Instant now = Instant.now();
return Jwts.builder()
.subject(username)
.claim("roles", roles)
.issuedAt(Date.from(now))
.expiration(Date.from(now.plusSeconds(ttlSeconds)))
.signWith(key)
.compact();
}
public Claims parse(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
}
@SuppressWarnings("unchecked")
public Authentication authenticate(Claims claims) {
List<String> roles = claims.get("roles", List.class);
List<GrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(claims.getSubject(), null, authorities);
}
}
package com.example.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String HEADER = "Authorization";
private static final String PREFIX = "Bearer ";
private final JwtTokenService tokenService;
public JwtAuthenticationFilter(JwtTokenService tokenService) {
this.tokenService = tokenService;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
try {
Claims claims = tokenService.parse(token);
var authentication = (org.springframework.security.authentication.UsernamePasswordAuthenticationToken)
tokenService.authenticate(claims);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication((Authentication) authentication);
} catch (JwtException | IllegalArgumentException ex) {
SecurityContextHolder.clearContext();
}
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String header = request.getHeader(HEADER);
if (StringUtils.hasText(header) && header.startsWith(PREFIX)) {
return header.substring(PREFIX.length());
}
return null;
}
}
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login").permitAll()
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
This snippet shows how stateless authentication is wired together in a Spring Boot application: a service that signs and verifies JSON Web Tokens, and a servlet filter that pulls the token off each request and populates the security context. Because the tokens are self-contained, the server keeps no session state — every request carries its own proof of identity, which is what makes horizontal scaling and cross-service auth practical.
In JwtTokenService, the signing key is derived once from a configured secret with Keys.hmacShaKeyFor, and reused for both signing and verification. The generate method builds a compact HS256 token with the subject set to the username, a roles claim, and explicit issuedAt/expiration timestamps. Putting the expiry inside the signed payload is the whole point of statelessness: the server does not need to look anything up to know when a token dies. The parse method verifies the signature and expiry in one step via parseSignedClaims; any tampering, wrong key, or expired token raises a JwtException, which the caller treats as "not authenticated" rather than "error".
The authenticate method converts verified Claims into a Spring Authentication. It reads the roles claim, maps each to a SimpleGrantedAuthority, and returns a UsernamePasswordAuthenticationToken marked authenticated. Deriving authorities from the token — instead of re-querying the database — is a deliberate trade-off: it is fast and stateless, but it means role changes only take effect when a new token is issued, so short expiries matter.
In JwtAuthenticationFilter, extending OncePerRequestFilter guarantees the logic runs a single time per request even through forwards. It extracts the bearer token with resolveToken, and only sets SecurityContextHolder when parsing succeeds and nothing is already authenticated. Crucially, a failed parse does not reject the request here — it simply leaves the context empty and calls filterChain.doFilter, letting downstream authorization rules decide. This separation keeps the filter focused on identity, not policy.
A pitfall worth noting: the secret must be long enough for HS256 (256 bits), and the filter must be registered before the default UsernamePasswordAuthenticationFilter in the security config for the context to be populated in time.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post
data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)
HMAC signed API requests for webhook and partner integrity
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>
<section class="notifications">
<h1>Notifications</h1>
Turbo Streams + authorization: signed per-user stream name
Share this code
Here's the card — post it anywhere.