PrepZone Logo
PrepZone

Stateless APIs with JWT / OAuth2

Token-based authentication, JWT structure and OAuth2 resource server setup.

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.
CorsFilter
JwtAuthFilter
AuthorizationFilter
Controller
Each filter handles one concern. Order matters — authentication before authorization.

JWT structure

A JSON Web Token has three Base64-encoded parts separated by dots:

Java
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

Java
@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);
    }
}
Java
@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

Java
@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):

Java
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Java
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.bookstore.example.com/realms/bookstore
Java
@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

Java
# 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:

Java
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.