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.
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:
- The app generates a random
code_verifier(43β128 characters) and keeps it in memory. - It sends only
code_challenge = BASE64URL(SHA-256(code_verifier))in the authorization request. - 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
openidscope in the authorization request; - the ID token β a JWT addressed to the client (
aud= itsclient_id) withsub,iss,exp,nonce,auth_timeand, with theprofile/emailscopes, name and e-mail; - the UserInfo endpoint for more profile claims;
- discovery at
/.well-known/openid-configuration, which lists the endpoints and thejwks_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.
/** 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;
}
@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)
}
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
authbox above. - Validate tokens locally against a cached JWKS, refetching it when an unknown
kidarrives β no call to the identity provider per request, and key rotation needs no downtime. In BoxOffice: the gateway'sNimbusReactiveJwtDecoder(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:
UserHeadersFilterremovesX-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
authto 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
localStorageor 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,
kidfrom 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).
-
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.Β β©
-
HARDT, D. (ed.). RFC 6749 β The OAuth 2.0 Authorization Framework. IETF, 2012.Β β©
-
HARDT, D.; PARECKI, A.; LODDERSTEDT, T. The OAuth 2.1 Authorization Framework. IETF Internet-Draft.Β β©
-
LODDERSTEDT, T. et al. RFC 9700 β Best Current Practice for OAuth 2.0 Security. IETF, 2025.Β β©
-
DENNISS, W.; BRADLEY, J. RFC 8252 β OAuth 2.0 for Native Apps. IETF, 2017.Β β©
-
SAKIMURA, N.; BRADLEY, J.; AGARWAL, N. RFC 7636 β Proof Key for Code Exchange by OAuth Public Clients. IETF, 2015.Β β©
-
SAKIMURA, N. et al. OpenID Connect Core 1.0. OpenID Foundation.Β β©
-
PARECKI, A.; DE RYCK, P. OAuth 2.0 for Browser-Based Applications. IETF Internet-Draft.Β β©
-
OWASP. API1:2023 β Broken Object Level Authorization.Β β©