Skip to content

JWT Structure and Validation

Access Token (OIDC JWT)

Header: { "alg": "RS256", "typ": "JWT", "kid": "" }

Claims (example): { "iss": "https://sso.portugalodyssey.pt/realms/portugal-odyssey", "aud": "public-app", "sub": "1a2b3c4d-...", // Keycloak user id "exp": 1735689600, "iat": 1735688700, "azp": "public-app", "scope": "openid profile email", "email": "user@example.com", "email_verified": true, "preferred_username": "user", "tenant_id": "", // custom mapper (single) "tenant_ids": ["t1", "t2"], // custom mapper (array) "realm_access": { "roles": ["customer"] }, "resource_access": { "admin-app": { "roles": ["admin"] } } }

Refresh Token

Opaque string maintained by Keycloak. Rotating refresh tokens are enabled; reuse detection invalidates the session. The auth-service can store a refresh_token_id/jti in iam.sessions to help track sessions and revoke selectively.

Validation

  • Retrieve JWKS from https://sso.<env-domain>/realms/portugal-odyssey/protocol/openid-connect/certs.
  • Cache JWKS keys in Redis; rotate on kid misses.
  • Validate:
  • Signature (RS256)
  • iss exact match for environment
  • aud in allowed clients (public-app, admin-app, partner-console, api-gateway)
  • exp not expired; small clock skew allowed (<= 60s)
  • Optionally enforce azp == caller client id

Node/Nest Middleware

  • API Gateway: Express middleware verifies JWT via JWKS, then sets:

  • req.user = { sub, email, roles, tenant_id, tenant_ids }

  • Proxy adds headers x-user-id, x-tenant-id, x-roles

  • Services: NestJS AuthGuard + RolesGuard + TenantGuard consume the same claims.