The sec-auth topic covers authentication (who is the user?) and the models of authorization (which authorization framework are we using?). This topic is the implementation patterns beneath those models — what an RBAC system actually looks like as tables, how OAuth flows actually behave in edge cases, where JWT validation breaks, and how tokens get replayed when the lifecycle is sloppy.
Authorization Models
| Model | Decision rule | Scaling property | Typical fit |
|---|---|---|---|
| ACL (access control list) | Per-object list of permitted users | O(users × objects) — explodes past ~10k objects | Network filesystems; single-team wiki |
| RBAC (role-based) | User → role → permissions | O(users × roles + role × permissions) — scales with role count, not objects | The dominant model in business apps; SaaS multi-tenant |
| ABAC (attribute-based) | Decision over attributes of user, resource, action, environment | O(rules) — but the decision logic is programmable | Tight security zones; multi-jurisdictional healthcare |
| ReBAC (relationship-based) | Tuple-based / OPA-style relationships | O(relationships) for n-dimensional sharing | Google Drive / GitHub repo sharing; “user A is a viewer of team B which is a member of org C which owns doc D” |
The honest take: RBAC is the right default. ABAC surfaces in the rare systems that genuinely compute the decision (a patient’s doctor-can-see-this-record depends on state, age, and consent). ReBAC (à la Google Zanzibar / SpiceDB / OPA) is the answer to “I need to model organizational sharing relationships declaratively” — the auth system becomes a graph database of (subject, relation, object) tuples.
A platform with 500 employees, an HR system with 5 roles, and an SaaS app with 50 user roles: RBAC at every layer; the role hierarchy becomes the management currency. Reaching for ABAC at that scale is over-engineering; reaching for ReBAC is right when the permission graph is itself a graph.
OAuth 2.0: Grants and Scopes
OAuth 2.0 is the delegated authorization protocol: a user grants a third party access to act on their behalf, scoped to specific operations, without sharing their password.
| Grant type | Shape | Use case |
|---|---|---|
| Authorization Code (with PKCE) | Browser redirect → auth server → code → token exchange | Browser and mobile apps (PKCE mandatory for public clients since OAuth 2.1) |
| Client Credentials | Client authenticates with its secret; gets token for itself | Service-to-service (no user involved) |
| Device Code | User authorizes on a separate device | Smart TVs, IoT, CLI tools on headless boxes |
| Refresh Token | Exchange long-lived refresh token for a new short-lived access token | Standard across all flows; avoids re-prompting the user |
| Implicit grant (deprecated) | Token returned directly in the redirect | Removed in OAuth 2.1; never use |
A scope is the named permission the user grants (“read:profile”, “write:posts”). Two production rules:
- Scopes are additive and orthogonal — never
readwrite(a real bug seen in production: scope doesn’t separate read and write, so a read-only app gets write). - Scopes are user-consented at the time of authorisation — requesting 30 scopes panics the user; the trade-off of asking only for what the feature needs pays in completion rate.
OpenID Connect: The SSO Layer
OAuth 2.0 is authorization. OpenID Connect (OIDC) is authentication layered on top — the user proves who they are via an identity provider (Google, Okta, Auth0), and the relying party gets a verifiable ID token with their identity claims.
| OAuth 2.0 | OIDC |
|---|---|
| Authorises a client to act on behalf of a user | Authenticates a user to a client |
| Issues an access token (opaque, for the resource server) | Issues an ID token (a JWT containing user claims) |
| No defined user identity | Standardised user identity claims (sub, email, name) |
| No standard /userinfo endpoint | Standardised /userinfo endpoint |
The classic “Sign in with Google” button is OIDC: the browser redirects to Google, the user consents, Google returns an ID token that the application verifies, and the user is logged in. The access token from the same flow can also drive downstream API calls (“Sign in with Google and access my Drive”), which is why OIDC is layered on OAuth, not a separate protocol.
JWT Validation Pitfalls
A JWT is a JSON-signed token in three base64 parts: header.payload.signature. Validation is deceptively easy to get wrong. The standard bugs:
| Pitfall | What happens | The fix |
|---|---|---|
alg: none accepted | Token signed with no signature is accepted as valid | Whitelist the algorithm; reject none |
| RSA / HMAC confusion | Attacker signs an RSA-expected token with HMAC using the RSA public key as the secret | Bind algorithm to key type in validator; library choice: well-audited only |
| Wrong issuer / audience | A token minted for auth-A is presented to auth-B and accepted | Validate iss and aud against expected values |
| Expired token accepted | Library not enforcing exp | Reject if exp < now; check at request time, not just at token extraction |
| Wrong clock | Validator’s clock skew lets expired tokens pass | Bound exp - now to a clock-skew tolerance window; sync clocks via NTP |
| No key rotation | Key compromise invalidates all tokens; rotation is manual | Use a JWKS endpoint; rotate keys with overlap; old keys valid for exp_grace_period |
The deepest horror: the library that does “valid JWT?” without verifying alg, iss, aud, exp is itself a security bug. JWT validation is three nested checks; skipping any of one is the most common production vulnerability class for token-bearing systems.
Token Lifecycle and Replay
A token is a bearer instrument: whoever holds it can use it, exactly like cash. Three lifecycle properties:
- Short-lived access tokens — minutes, not days; the longer the access token’s TTL, the larger the replay-window damage if it is leaked.
- Rotating refresh tokens — every time a refresh token is used to mint a new access token, the old refresh token is invalidated and a new one is issued. A replayed refresh token detected (used twice in a row) is the signal that a token has been stolen; the session is terminated.
- Binding to client — tokens bound to a specific client via PoP (Proof-of-Possession) or DPoP (Demonstrating Proof-of-Possession) cannot be replayed from a different client; this is the upgrade path past bearer-token fragility.
The pattern of rotating refresh tokens (original introduced by OAuth 2.0, sharpened in OAuth 2.1) is the infrastructural defence against token leaks, and is the most-ignored single property in token-handling code.
Practice Trajectory
- Take an RBAC system you operate. List the roles; identify one role bloat case (a role permission that nobody actually needs). Remove it.
- Sketch the OAuth grant types appropriate for: (a) a CLI tool that connects to your SaaS; (b) a server-to-server worker calling an API; (c) a React SPA calling your own backend. Justify each.
- Audit a JWT validator in your codebase against the six pitfalls table. Patch any gap.
- Implement refresh-token rotation in your token endpoint. Trace what happens when a stolen refresh token is replayed (both attempts observed; session terminated).
- Pick an SSO flow you have used. Identify at which step the ID token is verified, and what fields must be checked for the user to be considered logged in.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Most business authorisation | RBAC is the right default; reach for ABAC / ReBAC only when RBAC genuinely can’t express the rule |
| Browser/mobile client → your API | OAuth Authorization Code with PKCE |
| Service-to-service | Client Credentials grant; mTLS or JWT for service identity |
| Single sign-on across apps | OIDC; the ID token’s sub is the cross-app user identity |
| Bearer tokens you cannot afford to leak | Add token binding (DPoP); rotate refresh tokens; keep access-token TTL in minutes |