Skip to content

Identity: OAuth 2.1, OIDC & JWT

BoxOffice launches its mobile app. Someone proposes the obvious design: a login screen in the app, the app sends e-mail and password to POST /login, keeps the password in the phone "so the user does not have to type it again", and sends it on every request. Within a week the product team also wants "Sign in with Google", a web checkout, a nightly reconciliation job in order that calls inventory with no user behind it, and an AI assistant that acts on the user's behalf. A homemade login cannot express any of this safely. This page is about the standards that can: sessions and tokens, JWT, OAuth 2.1 and OpenID Connect, and the authorization models that decide what an authenticated caller may do.


Authentication vs authorization

Two different questions are asked on every request:

Authentication (authN) Authorization (authZ)
Question Who are you? What are you allowed to do?
Input a credential: password, passkey, certificate, token the identity + a policy (roles, scopes, ownership)
Failure 401 Unauthorized 403 Forbidden (or 404 to hide existence)
In BoxOffice the auth service (an identity provider) logs the user in; the gateway validates the token order checks that order 1042 belongs to the caller; admin routes require the organizer role

Vocabulary used below: a principal is the authenticated entity (a user or a workload); a credential is what it presents to prove it; a token is what it receives after authenticating and presents instead of the credential; a claim is a statement inside a token ("sub": "acc-42"). A bearer token works for whoever holds it, like cash.


Sessions vs tokens

After login, the server must remember the user on the next request. Two families of designs:

flowchart LR
    subgraph s ["Server-side session"]
        B1(["Browser"]) -->|"Cookie: sid=9f2c…"| A1["app"]
        A1 -->|"GET session:9f2c"| R[("Redis")]
    end
    subgraph t ["Self-contained token (JWT)"]
        B2(["App"]) -->|"Authorization: Bearer eyJ…"| A2["gateway"]
        A2 -->|"verify signature locally<br/>(cached public key)"| A2
    end
Server-side session (opaque id β†’ store) Self-contained token (JWT)
What the client holds a random session id a signed document with the claims
Validation lookup in a shared store (Redis) signature check with a public key, no lookup
Revocation (logout, stolen) immediate: delete the key hard: valid until exp unless you add a denylist or introspection
Horizontal scaling needs a shared, highly available store stateless; any instance validates
Cross-service use services must call the session store any service with the public key can verify
Size per request ~32 bytes often 0.5–2 KB in every request header
Best for classic web apps, the BFF pattern APIs, mobile apps, service-to-service

Neither is "the modern one". Large systems combine them: a short-lived JWT (minutes) for APIs and a revocable refresh token or session stored server-side. Revocation latency then equals the access-token lifetime β€” a number you choose.

Think first: a user's phone is stolen at 14:00 and they log out from the web at 14:05. Access tokens live 15 minutes and are validated locally. Until when can the thief call the API?

The refresh token is revoked at 14:05, so the thief cannot get new access tokens. But the last access token issued β€” say at 13:58 β€” stays valid until 14:13, because the gateway validates it locally and cannot know it was "logged out". The exposure window is up to one access-token lifetime after revocation. Shorter tokens shrink it; introspection or a denylist (by jti, the token id) closes it at the cost of a lookup.


JSON Web Token (JWT)

A JWT (RFC 7519) is three Base64URL-encoded parts separated by dots: header.payload.signature. Decoded, a BoxOffice access token looks like this:

header:  { "alg": "RS256", "typ": "at+jwt", "kid": "2026-10-a" }
payload: {
  "iss": "https://auth.boxoffice.com/realms/boxoffice",
  "sub": "acc-42",
  "aud": "boxoffice-api",
  "client_id": "boxoffice-mobile",
  "scope": "orders:read orders:write",
  "iat": 1792310400,
  "exp": 1792311300,
  "jti": "5b0e7c1a-6d1f-4c1e-9a43-0d6c2f6c9e10"
}
signature: RSASSA-PKCS1-v1_5( SHA-256( base64url(header) + "." + base64url(payload) ), private key )

The payload is encoded, not encrypted β€” anyone can read it. Never put secrets or personal data there you would not show the user.

With asymmetric algorithms (RS256, ES256, EdDSA) the identity provider signs with a private key that never leaves it, and anyone verifies with the public key, published as a JWKS (JSON Web Key Set) at the provider's jwks_uri. With symmetric HMAC (HS256), every verifier holds the same secret and can therefore also forge tokens β€” acceptable only when issuer and verifier are the same service.

Validation checklist

RFC 8725 (JWT Best Current Practices) is the reference1. A resource server must check:

Check Why
Signature with an allow-listed algorithm (never alg: none, never let the token header choose) algorithm-confusion attacks forge tokens
Key selected by kid from your JWKS, re-fetched on unknown kid key rotation without downtime
iss equals your identity provider tokens from another issuer are rejected
aud contains this API a token minted for another API cannot be replayed here
exp and nbf, with a small clock skew (β‰ˆ 30–60 s) expired tokens rejected
typ is at+jwt for access tokens (RFC 9068) an ID token cannot be used as an access token
required scopes / roles for the endpoint least privilege

In the BoxOffice code: auth signs the tokens (lab 4)

In Hands-on 4 β€” auth, BoxOffice gets its front door: auth signs every token with RS256, the private key never leaves it, and the public key is served as a JWKS at /auth/.well-known/jwks.json, so the gateway can verify without sharing a secret that could also forge tokens. Compare the claims with the checklist: iss, aud, sub, exp, jti and the kid header are there, and the role becomes a scope claim (user or user admin). Three shortcuts, each with its price: typ is plain JWT (there are no ID tokens to confuse it with yet); the key pair is generated at every start, so restarting auth rejects every token issued before (unknown kid) β€” the lab's warning explains why real keys live in a KMS and rotate with two keys in the JWKS; and the lifetime is 1 hour (JWT_TTL) with no refresh token, so a stolen token works for up to an hour, not 15 minutes.

services/auth/src/main/java/boxoffice/auth/TokenService.java (signing a token)
public TokenOut issue(User user) {
    var now = Instant.now();
    var claims = new JWTClaimsSet.Builder()
            .issuer(issuer)
            .audience(audience)
            .subject(user.getId().toString())
            .claim("email", user.getEmail())
            .claim("name", user.getName())
            .claim("scope", user.getRole() == Role.ADMIN ? "user admin" : "user")
            .issueTime(Date.from(now))
            .expirationTime(Date.from(now.plus(ttl)))
            .jwtID(UUID.randomUUID().toString())
            .build();
    var header = new JWSHeader.Builder(JWSAlgorithm.RS256)
            .keyID(key.getKeyID())
            .type(JOSEObjectType.JWT)
            .build();
    try {
        var jwt = new SignedJWT(header, claims);
        jwt.sign(new RSASSASigner(key));
        return new TokenOut(jwt.serialize(), "Bearer", ttl.toSeconds());
    } catch (JOSEException e) {
        throw new IllegalStateException("could not sign the token", e);
    }
}

OAuth 2.1

OAuth is a framework for delegated authorization: a client application obtains a limited, short-lived access token to call an API on behalf of a user β€” or on its own behalf β€” without ever handling the user's password. The user authenticates only at the authorization server2.

Role Description In BoxOffice
Resource owner the user who owns the data Ana, a fan
Client the application requesting access mobile app, web checkout (BFF), reconciliation job, AI assistant
Authorization server (AS) authenticates the user, asks for consent, issues tokens the auth service (Keycloak in the course; Auth0, Cognito, Entra ID in industry)
Resource server (RS) API that validates tokens and serves data gateway + order, inventory, …

Clients are confidential (run on a server, can keep a secret: the BFF, a batch job) or public (run on the user's device, cannot keep a secret: the mobile app, a single-page app β€” anyone can decompile or read them).

What OAuth 2.1 changes

OAuth 2.0 (RFC 6749, 2012) accumulated a decade of security lessons in separate documents. OAuth 2.1 β€” an IETF draft that consolidates them, together with the Security Best Current Practice (RFC 9700) β€” is what new systems should follow34:

OAuth 2.0 allowed OAuth 2.1 requires
Authorization Code with or without PKCE PKCE for every client using the authorization code, including confidential ones
Implicit grant (token in the URL fragment) removed β€” tokens leaked through history, referrers, logs
Resource Owner Password Credentials (the app collects the password) removed β€” defeats delegation, blocks MFA, trains users to type passwords anywhere
redirect URIs matched by prefix or pattern exact string matching of redirect URIs
bearer tokens in query strings never in the URL; header or body only
long-lived refresh tokens for public clients refresh tokens sender-constrained or rotated on every use

Choosing the grant

flowchart TD
    q1{"Is a user<br/>involved?"} -->|no| cc["Client Credentials<br/>(reconciliation job)"]
    q1 -->|yes| q2{"Can the device<br/>open a browser?"}
    q2 -->|yes| pkce["Authorization Code + PKCE<br/>(mobile app, web BFF)"]
    q2 -->|no| dev["Device Authorization<br/>(smart TV, CLI)"]
    pkce --> q3{"A service must call another<br/>on the user's behalf?"}
    q3 -->|yes| tx["Token Exchange<br/>(assistant β†’ order)"]

Authorization Code + PKCE for the mobile app

The app never sees the password. It opens the system browser (not an embedded WebView, where the app could read keystrokes β€” RFC 82525) at the authorization server, the user logs in there (with MFA or a passkey), and the app receives a short-lived, single-use authorization code through a redirect. The app then exchanges the code for tokens in a direct back-channel request.

PKCE (Proof Key for Code Exchange, "pixy", RFC 76366) binds the code to the app instance that started the flow:

  1. The app generates a random code_verifier (43–128 characters) and keeps it in memory.
  2. It sends only code_challenge = BASE64URL(SHA-256(code_verifier)) in the authorization request.
  3. At the token exchange it sends the original code_verifier; the server hashes and compares.

A malicious app that registered the same redirect scheme and intercepts the code does not have the verifier, so the code is useless.

sequenceDiagram
    autonumber
    actor Ana
    participant App as BoxOffice app
    participant Br as System browser
    participant AS as auth (Keycloak)
    participant GW as API gateway
    participant O as order
    Ana->>App: tap "Log in"
    App->>App: create state, nonce, code_verifier, code_challenge (S256)
    App->>Br: open /authorize?response_type=code&client_id=boxoffice-mobile&scope=openid orders:write&code_challenge&state&nonce
    Br->>AS: GET /authorize
    AS->>Ana: login page (password + MFA, or passkey)
    Ana->>AS: credentials
    AS-->>Br: 302 https://app.boxoffice.com/cb?code=XYZ&state
    Br-->>App: redirect delivered to the app (claimed HTTPS link)
    App->>App: check state
    App->>AS: POST /token {code, code_verifier, redirect_uri}
    AS->>AS: SHA-256(code_verifier) == code_challenge?
    AS-->>App: access_token (15 min) + refresh_token (rotating) + id_token
    App->>GW: POST /orders  Authorization: Bearer access_token
    GW->>GW: verify signature (cached JWKS), iss, aud, exp, scope
    GW->>O: POST /orders + X-Account-Id: acc-42
    O-->>App: 201 Created

The state parameter prevents an attacker from injecting their code into Ana's session (CSRF on the callback); the nonce is echoed inside the ID token and prevents replaying an old one.

Computing a PKCE pair by hand
# 64 random URL-safe characters
verifier=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64)
# BASE64URL(SHA-256(verifier)), without '=' padding
challenge=$(printf '%s' "$verifier" | openssl dgst -sha256 -binary \
            | openssl base64 | tr '+/' '-_' | tr -d '=')
echo "verifier=$verifier"; echo "challenge=$challenge"

Where the mobile app keeps tokens. The access token lives in memory; the refresh token in the platform's secure storage (iOS Keychain, Android Keystore-backed storage). The authorization server rotates the refresh token on every use: if a stolen copy and the legitimate app both try to use the same one, the server detects the reuse and revokes the whole family.


OpenID Connect: who is the user?

OAuth answers "may this client call this API?". It deliberately does not tell the client who the user is β€” the access token is addressed to the API. OpenID Connect (OIDC) adds an identity layer on top of OAuth7:

  • the openid scope in the authorization request;
  • the ID token β€” a JWT addressed to the client (aud = its client_id) with sub, iss, exp, nonce, auth_time and, with the profile/email scopes, name and e-mail;
  • the UserInfo endpoint for more profile claims;
  • discovery at /.well-known/openid-configuration, which lists the endpoints and the jwks_uri, so clients and resource servers bootstrap from the issuer URL alone.
Token Audience Purpose Typical lifetime
ID token the client "the user who logged in is acc-42, at 14:02, with MFA" minutes, used once at login
Access token the API (resource server) "this client may orders:write for acc-42" 5–60 minutes
Refresh token the authorization server only obtain new access tokens without a new login hours to weeks, rotated

Web checkout: OIDC through a Backend-for-Frontend

For the browser, the current recommendation is to keep no tokens in JavaScript at all. A BFF (Backend for Frontend) is a confidential client on the server: it runs the OIDC flow, keeps the tokens, and gives the browser only an HttpOnly, Secure, SameSite session cookie8.

sequenceDiagram
    autonumber
    actor Ana
    participant Br as Browser (checkout SPA)
    participant BFF as web BFF (confidential client)
    participant AS as auth (OIDC provider)
    participant GW as API gateway
    Ana->>Br: click "Log in"
    Br->>BFF: GET /login
    BFF-->>Br: 302 to AS /authorize (code + PKCE, scope openid, nonce)
    Br->>AS: login (passkey)
    AS-->>Br: 302 to BFF /callback?code&state
    Br->>BFF: GET /callback?code&state
    BFF->>AS: POST /token {code, code_verifier, client secret or private_key_jwt}
    AS-->>BFF: id_token + access_token + refresh_token
    BFF->>BFF: validate id_token (sig, iss, aud, exp, nonce), store tokens server-side
    BFF-->>Br: Set-Cookie: session=… HttpOnly Secure SameSite=Lax
    Br->>BFF: GET /api/orders (cookie)
    BFF->>GW: GET /orders  Bearer access_token
    GW-->>BFF: 200 orders
    BFF-->>Br: 200 orders
Where the browser keeps credentials Readable by JS? Main risk
localStorage / sessionStorage yes any XSS (cross-site scripting) steals the token
HttpOnly cookie with the token no CSRF (cross-site request forgery); mitigated by SameSite + CSRF tokens
BFF session cookie, tokens on the server no token in the browser session theft; the BFF is a component to scale

Machines and delegation

Client Credentials: the reconciliation job

When no user is involved, the client authenticates as itself and receives a token that represents the service. The order reconciliation job asks for exactly the scope it needs:

curl -s -X POST https://auth.boxoffice.com/realms/boxoffice/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d client_id=order-reconciler \
  -d client_secret="$ORDER_RECONCILER_SECRET" \
  -d scope=inventory:read

Better than a client secret: authenticate the client with a signed JWT (private_key_jwt) or with its workload identity (mTLS certificate or the Kubernetes service-account token) β€” no shared secret to leak (Zero Trust).

Token Exchange: the assistant acting for the user

The AI assistant must call order on behalf of Ana, with less power than Ana's own token. Token Exchange (RFC 8693) lets a service trade the token it received for a new one with a different audience and narrower scope (aud=order, scope=orders:read), keeping sub=acc-42 and recording the actor (act: assistant). The audit log then shows "assistant, acting for acc-42, read order 1042".

Sender-constrained tokens

A bearer token works for anyone who steals it. Sender-constrained tokens are bound to a key the legitimate client holds: with DPoP (RFC 9449) the client signs a small proof for every request with a private key on the device; with mTLS-bound tokens (RFC 8705) the token is tied to the client certificate. A stolen token alone is then useless. Financial-grade APIs require them; for BoxOffice they are worth it on payment endpoints.

Passkeys

Passkeys (WebAuthn/FIDO2) replace passwords with a key pair per site, kept on the device and unlocked with biometrics. The private key never leaves the device and the signature is bound to the site's origin, so they resist phishing. They change how the user authenticates at the authorization server; the OAuth/OIDC flows after it stay the same.


Validating tokens at the gateway

The gateway is the first enforcement point: it rejects requests without a valid token and forwards a trusted identity downstream (see Edge β€” API gateway).

Local validation (JWKS) Remote validation (introspection, RFC 7662)
Extra hop none β€” signature checked with a cached public key one call to the AS per request (cacheable for seconds)
AS outage requests keep working every request fails (or uses the cache)
Revocation effective only at exp immediate
Token type JWT JWT or opaque
Typical use most API traffic sensitive operations, opaque tokens

With Spring Security (also in Spring Cloud Gateway), local validation is configuration:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.boxoffice.com/realms/boxoffice  # discovers jwks_uri, checks iss
          audiences: boxoffice-api                                 # rejects tokens for other APIs

Strip identity headers at the edge

If the gateway forwards X-Account-Id: acc-42 to services, it must remove any X-Account-Id sent by the client, and services must be unreachable except through the gateway (or must verify the token themselves). Otherwise anyone who reaches a service directly can become any user.

In the BoxOffice code: the gateway checks the token once (lab 4)

Hands-on 4 β€” gateway uses local validation: the decoder fetches the JWKS of auth and then checks the signature, exp/nbf, iss and aud of every request without calling auth. That is the reason for the choice: a login is expensive on purpose (BCrypt; the lab measures roughly 10–20 logins/s per CPU), so it happens once per session, while the 2 million requests of the on-sale cost only a signature check at the gateway β€” introspection would put auth in the path of every one of them. The price is revocation only at exp. UserHeadersFilter implements the warning above: it removes any X-User-* header sent by the client, then sets the identity taken from the validated token. The other half of the warning holds only by deployment: the catalog replicas publish no port, so from outside the gateway is the only way in β€” but any container on the same network can still send a forged X-User-Id, the gap that Zero Trust closes.

services/gateway/src/main/java/boxoffice/gateway/SecurityConfig.java (the JWT decoder)
/** Signature from the JWKS of auth, plus expiry, issuer and audience checks. */
@Bean
ReactiveJwtDecoder jwtDecoder(@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwksUri,
                              @Value("${jwt.issuer}") String issuer,
                              @Value("${jwt.audience}") String audience) {
    var decoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwksUri).build();
    decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
            JwtValidators.createDefaultWithIssuer(issuer),     // exp, nbf, iss
            new JwtClaimValidator<List<String>>("aud", aud -> aud != null && aud.contains(audience))));
    return decoder;
}
services/gateway/src/main/java/boxoffice/gateway/UserHeadersFilter.java (strip, then set)
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    var cleaned = exchange.mutate()
            .request(r -> r.headers(h -> {
                h.remove("X-User-Id");
                h.remove("X-User-Email");
                h.remove("X-User-Scope");
            }))
            .build();
    return ReactiveSecurityContextHolder.getContext()
            .map(SecurityContext::getAuthentication)
            .filter(JwtAuthenticationToken.class::isInstance)
            .cast(JwtAuthenticationToken.class)
            .map(auth -> cleaned.mutate()
                    .request(r -> r.headers(h -> {
                        var jwt = auth.getToken();
                        h.set("X-User-Id", jwt.getSubject());
                        h.set("X-User-Email", jwt.getClaimAsString("email"));
                        h.set("X-User-Scope", jwt.getClaimAsString("scope"));
                    }))
                    .build())
            .defaultIfEmpty(cleaned)          // anonymous request on an open route
            .flatMap(chain::filter);
}

Authorization models

Authentication produced an identity. Now: may this identity do this action on this object?

Model Decides by BoxOffice example Good for Weak at
RBAC (role-based) the user's roles organizer may create events; support may read any order coarse, stable permissions per-object rules ("only their events")
ABAC (attribute-based) attributes of user, resource, context refunds allowed if order.eventDate > now + 48h and user.country == order.country rich, contextual rules harder to audit "who can access X?"
ReBAC (relationship-based) a graph of relationships Ana may view order 1042 because she owns it; Bruno because Ana shared the ticket with him sharing, organizations, hierarchies needs a relationship store (Google Zanzibar, OpenFGA, SpiceDB)

OAuth scopes are not a replacement: a scope limits what the client may do (orders:read), not which objects the user may touch. The ownership check β€” the fix for OWASP API1: BOLA β€” always runs in the service that owns the data9:

@GetMapping("/orders/{id}")
@PreAuthorize("hasAuthority('SCOPE_orders:read')")
public OrderOut get(@PathVariable UUID id, @AuthenticationPrincipal Jwt jwt) {
    Order order = repository.findById(id)
        .filter(o -> o.getAccountId().equals(jwt.getSubject()))  // ownership (BOLA)
        .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    return OrderOut.from(order);                                 // explicit DTO (BOPLA)
}
package boxoffice.orders

import rego.v1

default allow := false

# owners can read their own orders
allow if {
    input.action == "read"
    input.resource.owner_id == input.user.sub
}

# support staff can read any order, never modify it
allow if {
    input.action == "read"
    "support" in input.user.roles
}
permit (
  principal,
  action == Action::"ReadOrder",
  resource
)
when { resource.owner == principal };

permit (
  principal in Role::"support",
  action == Action::"ReadOrder",
  resource
);

Policy engines (OPA, Cedar, OpenFGA) move the rules out of the code into a reviewable, testable artifact that several services share. The service still asks the question for every object.

In the interview

Whenever the design has users, say in one breath: "users log in through an OIDC provider; the mobile app uses Authorization Code with PKCE; the gateway validates short-lived JWT access tokens locally against the provider's JWKS; refresh tokens rotate; service-to-service calls use client credentials or the mesh identity; and every service checks object ownership." If they ask about logout or revocation, explain the trade-off between local validation and introspection β€” and the exposure window equal to the token lifetime.

Best practices

  • Issue access tokens that live 5–15 minutes, with rotating refresh tokens β€” revocation latency equals the token lifetime. In BoxOffice: lab 4 issues 1-hour tokens with no refresh token, a shortcut named in the auth box above.
  • Validate tokens locally against a cached JWKS, refetching it when an unknown kid arrives β€” no call to the identity provider per request, and key rotation needs no downtime. In BoxOffice: the gateway's NimbusReactiveJwtDecoder (gateway box).
  • Rotate signing keys on a schedule and publish the old and the new key in the JWKS during the overlap β€” tokens signed before the rotation stay valid until they expire. (Lab 4 makes a new key at every start instead, and every token dies with it.)
  • Strip client-supplied identity headers at the gateway before adding your own β€” in BoxOffice: UserHeadersFilter removes X-User-* first (gateway box).
  • Hash passwords with a slow, tunable function and rate-limit login β€” BCrypt with cost β‰₯ 10 or Argon2id; in lab 4, BCrypt cost 10 limits auth to roughly 10–20 logins/s per CPU, so logins must happen before 10:00, not at it.

Common pitfalls

  • Implicit or password (ROPC) grants in new code β€” removed in OAuth 2.1; use Authorization Code + PKCE.
  • Skipping aud β€” a token issued for another API is accepted by yours.
  • Long-lived access tokens (24 h) β€” a stolen bearer token is a 24-hour master key.
  • Tokens in localStorage or in URLs and logs β€” XSS and log leaks.
  • Calling APIs with the ID token β€” it is for the client; APIs accept access tokens only.
  • Authorization only at the gateway β€” BOLA: GET /orders/{id} returns any order to any logged-in user.
  • Sharing an HMAC secret across services β€” every verifier can forge tokens.

How it connects

flowchart LR
    AS["auth: OIDC provider<br/>(authorization server)"] --> PKCE["Authorization Code + PKCE<br/>mobile app, web BFF"]
    AS --> CC["Client Credentials<br/>reconciliation job"]
    AS --> TX["Token Exchange<br/>assistant β†’ order"]
    PKCE --> JWT["short-lived JWT access token<br/>+ rotating refresh token"]
    PKCE --> SESS["BFF session cookie<br/>shared store β†’ Caching"]
    JWT --> GW["gateway validates via JWKS<br/>iss, aud, exp β†’ Edge"]
    GW --> SVC["each service checks ownership<br/>RBAC / ABAC / ReBAC"]
    SVC --> BOLA["fixes OWASP API1 (BOLA)"]
    CC --> WI["workload identity<br/>β†’ Zero Trust"]
    TX --> AI["agents acting for users<br/>β†’ AI systems"]

Palette components: auth Β· apigw (the first enforcement point) Β· client Β· mobile Β· cache (server-side sessions) Β· orchestrator (the assistant acting on the user's behalf). Scenarios: rate-limiter, flash-sale, ai-assistant. Continues in: Edge β€” API gateway, Zero Trust, Caching β€” Redis, AI systems β€” agents, Hands-on 4 β€” Gateway & identity.

Key takeaways

  • AuthN (who) β‰  authZ (what). The gateway authenticates; each service still authorizes access to its own objects.
  • Sessions revoke instantly but need a shared store; JWTs scale statelessly but stay valid until exp. Combine short access tokens with revocable refresh tokens.
  • Validate JWTs properly: allow-listed algorithm, kid from your JWKS, iss, aud, exp, typ, scopes.
  • OAuth 2.1: Authorization Code + PKCE for every user-facing client (system browser for mobile, BFF for web), Client Credentials for jobs, Token Exchange for delegation; no Implicit, no ROPC.
  • OIDC adds the ID token, UserInfo and discovery. Choose RBAC, ABAC or ReBAC for authorization, and always check ownership (BOLA).


  1. SHEFFER, Y.; HARDT, D.; JONES, M. RFC 8725 β€” JSON Web Token Best Current Practices. IETF, 2020. See also RFC 9068 β€” JWT Profile for OAuth 2.0 Access Tokens. ↩

  2. HARDT, D. (ed.). RFC 6749 β€” The OAuth 2.0 Authorization Framework. IETF, 2012. ↩

  3. HARDT, D.; PARECKI, A.; LODDERSTEDT, T. The OAuth 2.1 Authorization Framework. IETF Internet-Draft. ↩

  4. LODDERSTEDT, T. et al. RFC 9700 β€” Best Current Practice for OAuth 2.0 Security. IETF, 2025. ↩

  5. DENNISS, W.; BRADLEY, J. RFC 8252 β€” OAuth 2.0 for Native Apps. IETF, 2017. ↩

  6. SAKIMURA, N.; BRADLEY, J.; AGARWAL, N. RFC 7636 β€” Proof Key for Code Exchange by OAuth Public Clients. IETF, 2015. ↩

  7. SAKIMURA, N. et al. OpenID Connect Core 1.0. OpenID Foundation. ↩

  8. PARECKI, A.; DE RYCK, P. OAuth 2.0 for Browser-Based Applications. IETF Internet-Draft. ↩

  9. OWASP. API1:2023 β€” Broken Object Level Authorization. ↩