The most common diagram in auth systems is the sequence diagram. This is the PlantUML sequence template for four major auth protocols (JWT, OAuth2 Authorization Code, OAuth2 PKCE, SAML 2.0, OpenID Connect), plus the token lifecycle bugs you’ll hit.
actor "User" as User participant "Web App" as App participant "Auth Service" as Auth database "DB" as DB
User -> App : ① enter username/password App -> Auth : ② POST /login {username, password} Auth -> DB : ③ SELECT user WHERE username = ? DB --> Auth : ④ user record (hashed password)
alt password correct Auth -> Auth : ⑤ verify bcrypt(password) Auth -> Auth : ⑥ sign JWT {sub: user_id, exp: now+1h, role} Auth --> App : ⑦ 200 {access_token: <JWT>, refresh_token: <opaque>} App -> App : ⑧ store access_token in memory, refresh_token in HttpOnly cookie App --> User : ⑨ redirect to home else wrong password Auth --> App : 401 Unauthorized App --> User : show "username or password wrong" end
note over App : subsequent requests App -> App : ① check if access_token expired before each call alt token not expired App -> Auth : GET /api/users/me (Authorization: Bearer ... Auth -> Auth : ② verify JWT signature Auth --> App : 200 user info else token expired App -> Auth : POST /refresh (refresh_token) Auth -> DB : ③ check if refresh_token revoked alt refresh_token valid Auth -> Auth : ④ sign new access_token Auth --> App : 200 {access_token: <new JWT>} else refresh_token revoked or expired Auth --> App : 401 → App redirects to login end end @enduml
Key decisions:
access_token short (15min - 1h) — expires fast if leaked
refresh_token long (7d - 30d) — but revocable (DB-tracked state)
refresh_token must NOT live in localStorage — XSS steals it. HttpOnly cookie + SameSite=Strict.
access_token must NOT live in localStorage either — store in memory (lost on refresh, requires silent refresh)
actor "User" as User participant "Web App\n(Client)" as App participant "Authorization\nServer" as AuthServer participant "Resource\nServer" as API
== Step 1: redirect to auth page == App -> User : ① 302 https://auth.com/authorize?\nresponse_type=code&\nclient_id=abc&\nredirect_uri=https://app.com/cb&\nscope=read:profile&\nstate=xyz User -> AuthServer : ② GET /authorize (User-Agent follows redirect) AuthServer -> User : ③ render login page
== Step 2: user grants == User -> AuthServer : ④ enter credentials + click "Consent" AuthServer -> AuthServer : ⑤ verify credentials AuthServer -> User : ⑥ 302 https://app.com/cb?code=AUTH_CODE&state=xyz
actor "User" as User participant "Service\nProvider (SP)" as App participant "Identity\nProvider (IdP)" as IdP
== Step 1: user accesses SP == User -> App : ① GET /dashboard App -> App : ② user not logged in App -> User : ③ 302 https://idp.com/sso?SAMLRequest=<base64 XML>
== Step 2: IdP auth == User -> IdP : ④ GET /sso (User-Agent follows) IdP -> User : ⑤ render login page User -> IdP : ⑥ enter enterprise creds + MFA
actor "User" as User participant "Web App\n(Relying Party)" as App participant "OpenID\nProvider" as OP
User -> App : ① login App -> OP : ② 302 /authorize?\nresponse_type=code&\nscope=openid+profile+email User -> OP : ③ login + grant OP -> User : ④ redirect_uri?code=...&state=... User -> App : ⑤ GET /cb App -> OP : ⑥ POST /token (code + client_secret) OP -> App : ⑦ {access_token, id_token, refresh_token}
App -> App : ⑧ parse id_token (JWT)\nverify iss/aud/exp/nonce App -> App : ⑨ extract user info {sub, email, name, picture} App -> User : ⑩ login success
note over App id_token = identity info (JWT) access_token = API access credential refresh_token = refresh end note @enduml
OIDC vs OAuth2:
OAuth2 cares only about authorization
OIDC adds ID Token, cares about authentication
Anyone saying “login with OAuth2” is actually using OIDC
Token lifecycle management
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
@startuml title Token Refresh Race Condition
participant "Client" as C participant "API" as API database "Token Store" as Store
== Scenario: two clients refresh simultaneously == C -> API : ① refresh_token = abc C -> API : ② refresh_token = abc (concurrent request) API -> Store : ③ look up abc API -> Store : ④ UPDATE abc SET used=true API --> C : ⑤ new token + new refresh_token
API -> Store : ⑥ look up abc (second request) Store --> API : ⑦ used=true → reject API --> C : ⑧ 401 → trigger logout on all devices @enduml
@startuml title Refresh Token Rotation (RFC 6819 §5.2.2.3)
participant "Client" as C participant "Auth Server" as Auth database "Token Store" as Store
C -> Auth : ① refresh_token = abc Auth -> Store : ② look up abc
alt abc valid and unused Auth -> Store : ③ mark abc as used Auth -> Store : ④ store family ID of abc Auth --> C : ⑤ new access_token + new refresh_token = def\nsame family_id C -> Auth : ⑥ use def to refresh Auth -> Store : ⑦ look up def (family_id=family1) Auth --> C : ⑧ new token + new refresh = ghi (family1) else abc already used (stolen) Auth -> Store : ⑨ detect reuse → revoke entire family1 tokens Auth --> C : ⑩ 401 → force re-login end @enduml
Reuse detection triggers whole-family revocation — attacker uses the stolen refresh once, the real user’s token also expires, user gets “login from new device” alert.
Field foot-guns
access_token in localStorage — XSS steals it in one keystroke. Use memory + silent refresh.
refresh_token not HttpOnly — same risk. Must be HttpOnly + Secure + SameSite=Strict.
state not verified — CSRF can pre-generate code then impersonate user. state must be crypto random + verify echoed value.
SAML Response signature not verified — attacker forges SAML Response to log in. Must verify signature.
JWT in cookie — CSRF can use the cookie directly. Authorization: Bearer header or double-submit cookie.
Decision tree
1 2 3 4 5 6 7
What do you need? ├─ Your own user system → JWT + refresh token ├─ Third-party login (WeChat / Google / WeCom) → OAuth2 Authorization Code + PKCE ├─ Enterprise SSO (legacy big co) → SAML 2.0 ├─ Modern SaaS / new enterprise → OpenID Connect ├─ Service-to-service (no user) → OAuth2 Client Credentials └─ IoT / TV / CLI → OAuth2 Device Code
Minimum implementation — JWT self-signed + refresh token rotation. Once you scale — add OAuth2 server, let others plug in. Enterprise customer demands SAML — add SAML SP. Never store plaintext passwords — bcrypt/argon2 hash only.