Security · identity
Everything security and identity for a system design, on one page, with the flows drawn out. Authentication vs authorization; how identity is carried (server sessions, self-contained JWTs, reference tokens); the protocols (OAuth 2.1, OpenID Connect, SAML, passkeys); how a request's identity context propagates across microservices (mTLS, SPIFFE, token exchange); the authorization models (RBAC, ABAC, ReBAC); plus password storage, encryption in transit and at rest, and the common web attacks (injection, XSS, CSRF, SSRF, IDOR) with their defenses. Sequence diagrams for each. Companion to S5 · API Design & Networking.
Authentication proves who you are; authorization decides what you may do; and a session or token carries that proof to every service that needs it. The whole subject is one question repeated at every hop: "is this proof valid, unexpired, meant for me, and does it grant this action?" Keep the proof short-lived, bound to its holder, and verified at every boundary, never trusted just because it arrived.
Two different questions people constantly merge. Authentication (authN) establishes who the caller is. Authorization (authZ) decides what that caller may do. Identity is proven once, then carried as a session or token and re-checked on every request.
| Term | Is | In practice |
|---|---|---|
| Principal / subject | the entity making the request | a user, a device, or a service (a workload) |
| Credential | the secret that proves identity | password, private key, client secret, passkey |
| Session | server-side record of a logged-in principal | a row keyed by a cookie id |
| Token | a portable, verifiable proof of identity + rights | JWT (self-contained) or opaque (a reference) |
| Claim | a fact asserted inside a token | sub, email, roles, scope |
| Scope | a coarse permission granted to a client | read:orders, openid |
Solving authZ with authN. "The user is logged in" does not mean "the user may delete this record." A valid token answers who; you still owe a separate what check (roles, scopes, ownership) on every sensitive action. IDOR bugs are exactly this gap: authenticated, but never authorized for that specific object.
The central design axis. Once a principal is authenticated, something must carry that fact to the next request and the next service. There are three shapes, and the whole rest of this sheet is variations on them.
| Shape | State lives | Validated by | Revoke | Cost |
|---|---|---|---|---|
| Server session | server (session store) | look up the id | instant, delete the row | a store read per request; needs shared store to scale |
| Self-contained (JWT) | the token itself | verify signature locally | hard, valid until it expires | no lookup; larger payload; keep TTL short |
| Reference / opaque | server (auth server) | introspect at the issuer | instant, revoke at issuer | a network call to validate, or cache it |
It is stateful vs stateless. Sessions and opaque tokens keep state on the server, so you can revoke instantly but you pay a lookup and need a shared store. JWTs push the state into the token, so validation is a fast local signature check with no lookup, but you cannot easily revoke, which is why access tokens are kept short-lived and paired with a revocable refresh token. Many real systems combine them: an opaque token to the outside world, a JWT inside (the phantom-token pattern, section 6).
The classic web model. Log in once, the server stores a session and hands back a cookie holding only an opaque session id. Every later request carries the cookie; the server looks up the session. Simple, instantly revocable, and the default for server-rendered apps.
sequenceDiagram
autonumber
actor U as User
participant B as Browser
participant S as Server
participant DB as Session store
U->>B: enter credentials
B->>S: POST /login with username and password
S->>S: verify password with argon2 or bcrypt
S->>DB: create session, store server-side
S-->>B: 200, Set-Cookie sid HttpOnly Secure SameSite
Note over B,S: every later request carries the cookie
B->>S: GET /account with Cookie sid
S->>DB: look up session by sid
DB-->>S: session gives user id and roles
S-->>B: 200 protected data
HttpOnly (JavaScript can't read it, blunts XSS theft), Secure (HTTPS only), SameSite=Lax/Strict (blunts CSRF), and the __Host- prefix (locks it to the exact origin). To scale beyond one server, the session store must be shared (Redis), or you sign a stateless session cookie and accept you cannot revoke it before expiry.
A JSON Web Token is three base64url parts joined by dots: a header, a payload of claims, and a signature. Anyone can read it; only the issuer can sign it. Validation is a local check, which is what makes JWTs fast and stateless.
header.payload.signature # three base64url parts, dot-separated
header { "alg": "RS256", "kid": "k1", "typ": "JWT" }
payload { "iss": issuer, "sub": user id, "aud": your api,
"exp": expiry, "iat": issued, "scope": "read:orders",
"jti": token id } # claims, readable by anyone
signature = sign( base64(header) + "." + base64(payload), key )
flowchart TB
T["incoming request with a JWT"] --> A["fetch signing key from JWKS by kid"]
A --> B["verify signature, RS256 or ES256"]
B --> C{signature valid?}
C -->|no| X["401 reject"]
C -->|yes| D["check iss, aud, exp, nbf"]
D --> E{claims ok?}
E -->|no| X
E -->|yes| F["check scope or roles for this route"]
F --> G["allow, set security context"]
classDef good fill:#D6F5E3,stroke:#1B7F46,color:#09244B
classDef bad fill:#FEE4E2,stroke:#C8102E,color:#09244B
class G good
class X bad
1. A JWT is signed, not encrypted. The payload is plain base64, readable by anyone, so never put secrets or PII in it. 2. Prefer asymmetric signing (RS256 / ES256): the auth server holds the private key, every service verifies with the public key from the JWKS endpoint, so no shared secret is spread around. Pin the algorithm on verify, never trust the token's own alg (that is the alg=none and RS/HS confusion attack).
Because a JWT can't be revoked mid-life, you keep the access token short-lived (minutes) and pair it with a long-lived, revocable refresh token that mints new access tokens. Rotating the refresh token on every use, and detecting reuse, is how you make a stateless scheme safely revocable.
sequenceDiagram
autonumber
participant C as Client
participant AS as Auth server
participant API as Resource server
C->>API: request with access token
API-->>C: 401, access token expired
C->>AS: POST /token grant_type refresh_token
AS->>AS: validate refresh token, check for reuse
AS-->>C: new short access token and rotated refresh token
Note over AS: the old refresh token is now invalid
C->>API: retry with the new access token
API-->>C: 200 data
The access token is the day pass: short TTL, sent to every API, sacrificed if leaked because it expires fast. The refresh token is the master key: long-lived, sent only to the auth server, stored carefully, rotated on use, and revocable. Never send a refresh token to a resource API, and never store it where XSS can reach it (see the BFF pattern, section 13).
An opaque token is a random string that means nothing on its own; the resource server asks the issuer to introspect it. That gives instant revocation and leaks no data outward, at the cost of a network call. The phantom-token pattern gets both: an opaque token outside, a JWT inside.
sequenceDiagram
autonumber
participant C as Client
participant GW as API gateway
participant AS as Auth server
participant SVC as Microservice
C->>GW: request with an opaque token
GW->>AS: introspect or exchange the opaque token
AS-->>GW: valid plus claims, issue a short-lived JWT
GW->>SVC: forward request with the JWT
SVC->>SVC: verify the JWT locally, no network call
SVC-->>C: response
Opaque outward means no claims leak to the browser and you can revoke on logout instantly. JWT inward means internal services validate with a local signature check and never call the auth server on the hot path. You spend one exchange at the edge to buy privacy plus revocation outside and speed inside.
OAuth is a delegated authorization framework: it lets an app act on a resource on your behalf without ever seeing your password. Four roles, and a small set of grant types (flows). OAuth 2.1 is the consolidation that bakes in a decade of security lessons.
The four roles
| Grant | For |
|---|---|
| Auth code + PKCE | web, SPA, mobile, all interactive logins |
| Client credentials | service-to-service, no user present |
| Device code | TVs, CLIs, input-constrained devices |
| Implicit | removed in 2.1 (token leaked in URL) |
| Password (ROPC) | removed in 2.1 (app sees the password) |
The net: one flow, authorization code + PKCE, for all interactive clients. Learn that one well.
This is the canonical OAuth flow and the single most asked-about diagram. PKCE (Proof Key for Code Exchange) binds the token request to the same client that started the flow, so a stolen authorization code is useless to an attacker.
sequenceDiagram
autonumber
actor U as User
participant App as Client app
participant AS as Auth server
participant API as Resource API
App->>App: generate code_verifier and code_challenge S256
U->>App: click log in
App->>AS: /authorize response_type code, code_challenge, redirect_uri
AS->>U: show login and consent
U->>AS: authenticate and approve
AS-->>App: redirect back with an authorization code
App->>AS: POST /token with code and code_verifier
AS->>AS: check that hash of verifier equals the challenge
AS-->>App: access token, refresh token, id token if OIDC
App->>API: call API with the access token
API-->>App: 200 data
code_verifier never leaves the client until the final token call; the auth server only saw its hash. A stolen code cannot be exchanged without the original verifier, which defeats code-interception attacks.The authorization code is a short-lived, one-time voucher returned through the browser (a less trusted channel); the actual tokens come back over a direct, back-channel POST. PKCE ties the two legs together. That indirection is exactly what the removed implicit flow skipped, and why it was insecure.
OAuth alone answers "may this app access this API?" It does not, by itself, tell the app who logged in. OpenID Connect is a thin identity layer over OAuth 2 that adds an ID token (a JWT about the user) and a standard /userinfo endpoint. "Sign in with Google" is OIDC.
sequenceDiagram
autonumber
actor U as User
participant RP as App, the relying party
participant OP as Identity provider
U->>RP: click sign in
RP->>OP: /authorize scope openid profile email, code plus PKCE
U->>OP: authenticate
OP-->>RP: authorization code
RP->>OP: exchange code at /token
OP-->>RP: id_token the who, access_token the what
RP->>RP: verify id_token, establish local user session
RP->>OP: optional /userinfo with the access token
OP-->>RP: profile claims
| OAuth 2.x | OpenID Connect | |
|---|---|---|
| Question | what may this app do? | who is this user? |
| Answer | access token (for APIs) | id token (for the app) |
| Use | delegated authorization | authentication, login, SSO |
SAML is the older, XML-based SSO standard that still runs most corporate and B2B logins. A Service Provider (the app) redirects you to an Identity Provider (Okta, Entra ID, the corporate IdP), which returns a signed assertion vouching for you. Same idea as OIDC, different wire format.
sequenceDiagram
autonumber
actor U as User
participant SP as App, service provider
participant IdP as Identity provider
U->>SP: open the protected app
SP-->>U: redirect with a SAML AuthnRequest
U->>IdP: authenticate with corporate login
IdP-->>U: signed SAML assertion in a POST form
U->>SP: browser posts the assertion to the ACS endpoint
SP->>SP: verify signature, read the user attributes
SP-->>U: application session established
| SAML | OpenID Connect | |
|---|---|---|
| Format | XML assertions | JSON / JWT |
| Best for | enterprise, workforce SSO, legacy | web, mobile, APIs, modern apps |
| Transport | browser POST / redirect | REST + JSON, mobile-friendly |
Single sign-on works because the session lives at the identity provider, not the app. Log in once at the IdP; every other app that trusts that IdP gets you silently, because your IdP session is already established. Federation extends this across organizations and IdPs.
sequenceDiagram
autonumber
actor U as User
participant A as App A
participant B as App B
participant IdP as Identity provider
U->>A: open App A
A->>IdP: redirect to authenticate
U->>IdP: sign in, IdP creates its own session
IdP-->>A: token, App A session created
Note over U,IdP: later, a different app
U->>B: open App B
B->>IdP: redirect to authenticate
IdP-->>B: existing IdP session, silent token, no new login
B-->>U: signed in automatically
Plain SSO uses one IdP for many apps. Federation links separate identity domains: your app trusts a partner's IdP (via OIDC or SAML) so their employees log in with their own corporate identity, no new account. The trust is configured once between the identity providers; users just log in where they always do.
Passkeys replace the password with a device-held key pair. The private key never leaves your phone or laptop; the site stores only the public key. Because the signature is bound to the site's origin, passkeys are phishing-resistant in a way passwords and OTP codes are not.
sequenceDiagram
autonumber
actor U as User
participant B as Browser and authenticator
participant RP as Server, relying party
RP-->>B: challenge, a random nonce plus the site id
B->>U: verify with biometric or PIN
U-->>B: local gesture, the private key stays on device
B->>B: sign the challenge with the private key
B-->>RP: signed assertion plus credential id
RP->>RP: verify the signature with the stored public key
RP-->>U: authenticated, phishing-resistant and per-origin
Multi-factor combines categories: a password (know), a phone or security key (have), a fingerprint (are). Strength order, roughly: SMS OTP (weak, SIM-swappable) < TOTP app < push approval < WebAuthn / passkey (phishing-resistant). Use step-up auth to demand a stronger factor only for sensitive actions (a payment, a settings change), not on every request.
Where a single-page app keeps its tokens is a genuine security decision. localStorage is readable by any XSS; a cookie is sent automatically and invites CSRF. The modern answer is to keep tokens out of the browser entirely with a Backend-for-Frontend.
| Where | Risk | Verdict |
|---|---|---|
| localStorage | any XSS reads the token | avoid for real tokens |
| JS-readable cookie | XSS + CSRF | avoid |
| HttpOnly cookie | CSRF (mitigate with SameSite) | ok for a session id |
| Not in the browser (BFF) | tokens never exposed to JS | the recommendation |
sequenceDiagram
autonumber
actor U as User
participant SPA as Browser SPA
participant BFF as BFF, a confidential client
participant AS as Auth server
participant API as API
U->>SPA: click log in
SPA->>BFF: start login
BFF->>AS: authorization code plus PKCE, server-side
AS-->>BFF: tokens, kept server-side only
BFF-->>SPA: Set-Cookie session, HttpOnly SameSite
Note over SPA,BFF: the browser never sees a token
SPA->>BFF: /api/orders with the session cookie
BFF->>API: forward the request with the access token
API-->>BFF: data
BFF-->>SPA: data
Inside a system, two questions run in parallel: which service is calling (workload identity) and on whose behalf (the user context). You carry both, and you often mint a fresh, narrowly-scoped token for each hop rather than forwarding the original everywhere.
sequenceDiagram
autonumber
participant GW as API gateway
participant AS as Auth server
participant A as Service A
participant B as Service B
GW->>GW: validate the user JWT, iss aud exp signature
GW->>A: forward request plus user context over mTLS
A->>AS: token exchange RFC 8693, user token to downstream token
AS-->>A: new token scoped for Service B, on behalf of the user
A->>B: call B with the exchanged token over mTLS
B->>B: verify the token and the caller identity, SPIFFE SVID
B-->>A: response
Do coarse checks once at the edge, fine-grained checks where the context lives, and defense-in-depth all the way down. Centralize the policy, distribute the enforcement.
flowchart LR C["client"] --> GW["gateway: authenticate, coarse checks, signature iss exp aud"] GW --> SVC["service: fine-grained authZ, RBAC ABAC or ReBAC"] SVC --> DATA["data layer: row and field checks, the last line"] GW -. mTLS plus SPIFFE .-> SVC classDef edge fill:#DCEBFE,stroke:#09244B,color:#09244B classDef good fill:#D6F5E3,stroke:#1B7F46,color:#09244B class GW edge class DATA good
Do not scatter authorization logic as ad-hoc if checks. Externalize the policy (who may do what) to one place, an engine like OPA / Rego or a Zanzibar-style service, and let every service ask it, often via a sidecar in a service mesh. You get one auditable source of truth and consistent decisions, without a single network chokepoint on the hot path.
How the "what may you do" decision is actually modelled. Pick by how fine-grained and relationship-heavy your permissions are.
| Model | Decides by | Example | When |
|---|---|---|---|
| ACL | a list per object of who may do what | this file: alice=read, bob=write | few objects, simple sharing |
| RBAC | the role assigned to the user | admins may delete | most apps; simple, coarse |
| ABAC | attributes of user, resource, context | managers in EU during work hours | policy-rich, contextual rules |
| ReBAC | relationships between entities | editors of a doc's parent folder | fine-grained sharing at scale |
| Scopes | coarse grants on a token (OAuth) | read:orders | what a client app may do, not a user |
flowchart LR U["user alice"] -->|member| G["group eng"] G -->|editor| D["doc design"] U -->|owner| F["folder specs"] F -->|parent of| D Q["can alice edit doc design?"] --> R["yes, via group editor or folder ownership"] classDef good fill:#D6F5E3,stroke:#1B7F46,color:#09244B class R good
Start with RBAC: it covers most apps and is easy to reason about. Add ABAC rules when decisions depend on context (time, location, resource attributes). Reach for ReBAC / Zanzibar only when you have Google-Docs-style nested sharing where permissions flow through relationships. Do not build a graph database of permissions for an app that needs three roles.
A token is a claim you sign and cannot easily take back before it expires, so encode only what is stable, non-sensitive, and needed at every hop. Everything else, look up at request time. This one decision drives token size, staleness, and blast radius.
| Put in the token | Leave out, look it up instead |
|---|---|
stable subject id (sub), tenant / org id | fast-changing data (balances, feature flags, live status) |
issuer, audience, expiry (iss, aud, exp, iat, jti) | fine-grained per-object permissions (use a lookup / ReBAC) |
| a few coarse, stable scopes or roles | large role or group lists (token bloat, header limits) |
auth context: how and when (amr, acr, auth_time) | secrets, passwords, keys (the payload is readable) |
| just enough to authorize this request | PII beyond the minimum (tokens land in logs and caches) |
flowchart TB
A["a piece of data"] --> B{stable for the whole token lifetime?}
B -->|no| L["look it up at request time"]
B -->|yes| C{sensitive, PII, or a secret?}
C -->|yes| L
C -->|no| D{needed to authorize this request?}
D -->|no| L
D -->|yes| T["encode it as a claim"]
classDef good fill:#D6F5E3,stroke:#1B7F46,color:#09244B
classDef alt fill:#DCEBFE,stroke:#4A90D9,color:#09244B
class T good
class L alt
In the token (embed roles / scopes): fast and stateless, every service decides locally with no call, but the data goes stale the moment you change a role, and it inflates every request. By lookup (call an authZ service or ReBAC store): always fresh and instantly revocable, at the cost of a call per decision (cache it). Rule of thumb: put coarse, slow-changing grants in the token (is this an admin?); resolve fine-grained, fast-changing ones by lookup (may alice edit this doc right now?).
The failure modes are well known and mostly cheap to prevent. Catch these in a design review.
Do
aud): a token for service A must not work on service B.kid.Never
alg: the alg=none and RS/HS confusion attacks.localStorage: one XSS and they are gone. Use a BFF.state / PKCE check: that is how CSRF and code interception land.Two places data must be protected, two different mechanisms. Both are table stakes, and both are distinct from password hashing (which is one-way, not encryption).
Encryption in transit stops eavesdropping; at rest stops a stolen disk or backup. Neither stops an attacker who already has app access, that is what authz and least-privilege are for. Hashing (passwords) is one-way and is not encryption; encryption (reversible) is for data you must read back, and then the key management is the real problem.
The handful that come up constantly. Each has a standard defense; naming both is the interview win.
| Attack | What it is | Defense |
|---|---|---|
| SQL injection | untrusted input becomes SQL | parameterized queries / prepared statements; never string-concat SQL |
| XSS | injected script runs in a victim's browser | output-encode, a Content-Security-Policy, HttpOnly cookies |
| CSRF | a site makes the victim's browser send an authed request | SameSite cookies, CSRF tokens, check Origin |
| SSRF | server tricked into requesting an internal URL | allowlist outbound hosts; block link-local/metadata IPs |
| Broken object authz | change an id, read another user's data (IDOR) | authorize every object access against the caller |
Never trust input, and never skip the per-object authorization check. Injection (SQL, XSS, SSRF) is untrusted input crossing a boundary unescaped; the fix is always to treat input as data, not code (parameterize, encode, allowlist). IDOR is a missing authz check, the same gap section 1 warns about. These two ideas cover most of the OWASP Top 10.
Start from the caller and the trust boundary, then pick the mechanism.
| Context | Reach for | Not |
|---|---|---|
| Server-rendered web app | session cookie | a JWT you can't revoke |
| Single-page app (browser) | BFF + auth code / PKCE | tokens in localStorage |
| Mobile / native app | auth code + PKCE | password grant |
| Customer / social login | OpenID Connect | rolling your own |
| Enterprise / workforce SSO | OIDC or SAML | local accounts |
| Service-to-service, same mesh | mTLS + SPIFFE | a shared API key |
| Service on behalf of a user | token exchange (8693) | forwarding the raw user token |
| Machine / CLI / cron | client credentials | a human's token |
| Fine-grained resource sharing | ReBAC / Zanzibar | roles bolted on |
| Passwordless, phishing-resistant | passkeys (WebAuthn) | SMS OTP |
| Store a password | argon2 + unique salt | md5/sha, encryption, plaintext |
| Protect data on the wire | TLS 1.3 + HSTS | plain HTTP, self-signed in prod |
| Protect sensitive data at rest | field encryption + KMS | app-readable plaintext |
| Stop injection | parameterized queries | string-concatenated SQL |
The security and auth questions that come up in a system-design round, and the sentence or two that shows you understand the trade-off.
Q · Sessions or JWTs?
Sessions are stateful (instant revoke, needs a shared store); JWTs are stateless (fast local check, hard to revoke). Use sessions for server-rendered apps, short-lived JWTs plus a refresh token for APIs and mobile.
Q · How do you revoke a JWT?
You mostly don't; you keep the TTL short and revoke the refresh token, or maintain a small deny-list of jtis, or use opaque/phantom tokens if instant revocation is a hard requirement.
Q · Why is PKCE mandatory now?
It binds the token exchange to the client that started the flow, so an intercepted authorization code can't be redeemed. OAuth 2.1 requires it for all clients and drops the implicit flow entirely.
Q · ID token vs access token?
The ID token (OIDC) tells the app who logged in; verify it and drop it. The access token is for calling APIs. Don't identify users with an access token, and don't call APIs with an ID token.
Q · OAuth vs OIDC vs SAML?
OAuth is delegated authorization (access to APIs). OIDC adds authentication (login) on top, in JSON/JWT. SAML is the older XML equivalent of OIDC login, still dominant in enterprise SSO.
Q · Where do you store tokens in a browser?
Ideally nowhere: use a BFF so tokens stay server-side and the browser holds only an HttpOnly session cookie. If you must, an HttpOnly cookie beats localStorage, which any XSS can read.
Q · How does identity cross microservices?
The gateway validates the user token once; workloads authenticate to each other with mTLS/SPIFFE; and you mint a per-hop token with token exchange (RFC 8693) so each service gets a narrowly-scoped, on-behalf-of token.
Q · RBAC vs ABAC vs ReBAC?
RBAC decides by role (simple, coarse). ABAC by attributes and context (flexible policy). ReBAC by relationships between entities (Zanzibar), for Google-Docs-style nested sharing at scale. Start with RBAC.
Q · mTLS or a bearer token?
They answer different questions and pair up: mTLS proves which workload is calling; the token proves on whose behalf. A bearer token alone is stealable, so sender-constrain it with mTLS or DPoP.
Q · What makes passkeys phishing-resistant?
The private key never leaves the device and the authenticator only signs for the real origin, so a look-alike phishing site gets nothing. There is no shared secret to type, replay, or leak in a breach.
Q · How do you store passwords?
A slow, salted, one-way hash, argon2 or bcrypt, with a unique salt per user. Never plaintext, never reversible encryption, never a fast hash like SHA-256, so a breach leaks only hashes that are expensive to crack.
Q · How do you prevent SQL injection?
Parameterized queries / prepared statements, so input is always data, never concatenated into SQL. The same "treat input as data" idea defends XSS (encode output) and SSRF (allowlist hosts).
Q · What is IDOR / broken object authz?
A logged-in user reads or edits another's data by changing an id in the request, because the server authenticated them but never checked they own that object. Fix: authorize every object access against the caller.
Q · Encryption in transit vs at rest?
In transit (TLS) stops eavesdropping on the wire; at rest stops a stolen disk or backup. Neither stops an attacker with app access, that is authz and least-privilege. And hashing is not encryption: it is one-way.
The ways security and auth go wrong, and the habit that catches all of them.
The traps
alg: the classic JWT forgery.The repeatable move
1. separate authN (who) from authZ (what)
2. pick how identity is carried: session / JWT / opaque
3. use auth code + PKCE for every interactive login
4. keep access tokens short, rotate refresh tokens
5. re-check authz per object, at every hop, on the server
6. never trust input; encrypt in transit + at rest
The tell in a design review: a token that never expires, an authZ check missing on a sensitive route, a raw user token forwarded across services, a password hashed with SHA-256, or a token in localStorage. Name the missing check and push back.
Prove identity once, carry it as a short-lived token, and re-check "is this valid and allowed" on the server at every boundary; hash passwords slowly, encrypt in transit and at rest, and never trust input. Authentication is a moment; authorization is a habit you repeat on every request, and most breaches are a missing one of these, not broken cryptography.
Companion to S5 · API Design & Networking. Further reading: OWASP Top 10 · OAuth 2.1 · RFC 9700 · OAuth Security BCP · OpenID Connect · RFC 8693 · Token Exchange · SPIFFE · FIDO passkeys · Google Zanzibar / ReBAC · OWASP · password storage.