Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ricardomb-tech/auth-service/llms.txt

Use this file to discover all available pages before exploring further.

Auth Service operates on a deny-all model: every route is private by default, and public endpoints must be explicitly listed. Downstream microservices that accept tokens issued by Auth Service should follow the same posture — treat all incoming requests as unauthenticated until a valid, non-expired JWT is presented, then authorize based on the claims embedded in the token. Because the access token is stateless, validation in a downstream service requires only the shared JWT secret, the expected issuer string, and a standard JWT library — no call back to Auth Service is needed per request.

Sending the token

Clients pass the access token in the Authorization header using the Bearer scheme:
curl https://your-downstream-service/api/some-resource \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
Access tokens have a 15-minute TTL (expiresInSeconds: 900). A downstream service must always validate the exp claim and reject expired tokens with 401 Unauthorized. Clients should proactively refresh tokens before expiry — see Token Management for the rotation flow.

Validating the JWT

Auth Service signs tokens with HS256 using the secret from JWT_SECRET_CURRENT. To validate a token in a downstream service:
  1. Verify the signature using the shared HS256 secret (JWT_SECRET_CURRENT).
  2. Check the exp claim — reject tokens where exp is in the past.
  3. Check the iss claim — reject tokens where iss is not "auth-service".
  4. Extract claims (sub, email, roles) for authorization decisions.
If the token is missing, malformed, expired, or has an invalid signature or issuer, return 401 Unauthorized and do not process the request.

Available claims for authorization

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "user@example.com",
  "roles": ["USER"],
  "iat": 1700000000,
  "exp": 1700000900,
  "iss": "auth-service"
}
ClaimUse in downstream services
subStable UUID identifying the account. Use as the user foreign-key in your data model.
emailThe verified email address. May be used for display or audit logging; not guaranteed to be unique across time if accounts are deleted and re-registered.
rolesArray of role names. Use for role-based access control.
issMust equal "auth-service". Always validate this to prevent token confusion attacks.

Roles and access control

Auth Service defines two roles:
RoleTypical use
USERGranted to every newly registered account. Allows access to standard user-facing endpoints.
ADMINElevated privileges for administrative operations. Assigned manually or via provisioning configuration.
Roles are returned as a string array in the roles claim. An admin account will have "roles": ["USER", "ADMIN"] — the USER role is always included. Your authorization logic should check for the presence of the required role within the array.

Extracting roles in a Java downstream service

The following example shows how to parse the roles claim and build Spring Security GrantedAuthority objects — mirroring exactly how JwtAuthenticationFilter does it in Auth Service itself:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class JwtValidator {

    private final String jwtSecret; // inject JWT_SECRET_CURRENT

    public List<GrantedAuthority> validateAndExtractAuthorities(String rawToken) {
        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));

        Claims claims = Jwts.parser()
                .verifyWith(key)
                .requireIssuer("auth-service")
                .build()
                .parseSignedClaims(rawToken)
                .getPayload();

        List<String> roles = claims.get("roles", List.class);
        if (roles == null) {
            return List.of();
        }
        // Auth Service stores role names without the "ROLE_" prefix in the JWT;
        // Spring Security's hasRole() expects it to be present, so add it here.
        return roles.stream()
                .map(role -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + role))
                .toList();
    }
}

Extracting roles in other languages (pseudocode)

token    = request.headers["Authorization"].removePrefix("Bearer ")
payload  = jwt.verify(token, secret=JWT_SECRET_CURRENT, algorithms=["HS256"])

assert payload["iss"] == "auth-service"
assert payload["exp"] > current_unix_time()

user_id = payload["sub"]       # UUID string
email   = payload["email"]
roles   = payload["roles"]     # e.g. ["USER"] or ["USER", "ADMIN"]

if "ADMIN" not in roles:
    return 403 Forbidden

How JwtAuthenticationFilter works (Auth Service reference)

JwtAuthenticationFilter in Auth Service is a OncePerRequestFilter that runs before Spring Security’s UsernamePasswordAuthenticationFilter. Its behavior is a useful reference model:
  1. Reads the Authorization header. If absent or not starting with "Bearer ", the filter does nothing and passes the request on unauthenticated.
  2. Attempts to parse the token with JWT_SECRET_CURRENT. If parsing fails with ExpiredJwtException, the token is silently dropped (the request proceeds unauthenticated).
  3. If parsing fails for any other reason, it retries with JWT_SECRET_PREVIOUS (to support zero-downtime secret rotation). If the token is expired against the previous key as well, it is silently dropped. If it fails for any other reason (bad signature, malformed token), the token is dropped and a warning is logged.
  4. On success, builds a UsernamePasswordAuthenticationToken from the sub claim (as the principal) and the roles claim (as GrantedAuthority objects, prefixed with ROLE_), and sets it on the SecurityContextHolder.
  5. The filter never writes an error response itself. A missing or invalid token simply leaves the request unauthenticated; the downstream AuthenticationEntryPoint issues the 401 if the matched route requires authentication.

Public vs. protected routes

SecurityConfig defines the list of endpoints that are accessible without a token. All other routes require authentication by default.
private static final String[] PUBLIC_ENDPOINTS = {
        "/error",
        "/swagger-ui.html",
        "/swagger-ui/**",
        "/v3/api-docs",
        "/v3/api-docs/**",
        "/auth/**",
        "/oauth2/**",
        "/login/oauth2/**"
};
The deny-all default means every new route you add to Auth Service (or a downstream service following the same pattern) is automatically private. A route becomes public only when explicitly added to the allow list. This prevents accidental exposure of new endpoints before authorization is wired up.
Unauthenticated requests to protected routes receive:
{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authentication required."
}
Authenticated requests that lack the required role receive:
{
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403,
  "detail": "Access denied."
}
Both responses use Content-Type: application/problem+json (RFC 9457).

Build docs developers (and LLMs) love