Auth protocol sequence diagrams: JWT / OAuth2 / SAML / OpenID Connect

puml.online

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.

Protocol landscape

1
2
3
4
5
6
7
8
9
10
11
12
Auth
├── stateless
│ └── JWT (JSON Web Token)
├── delegated authorization
│ └── OAuth 2.0
│ ├── Authorization Code
│ ├── PKCE (mobile / SPA)
│ ├── Client Credentials (service-to-service)
│ └── Device Code (IoT / TV)
└── federated identity
├── SAML 2.0 (legacy enterprise SSO)
└── OpenID Connect (OAuth2 + ID Token)

Protocol 1: JWT self-signed auth

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@startuml
title JWT Authentication Flow

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 eitherstore in memory (lost on refresh, requires silent refresh)

Protocol 2: OAuth 2.0 Authorization Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@startuml
title OAuth 2.0 Authorization Code Flow

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

== Step 3: backend exchanges code ==
User -> App : ⑦ GET /cb (User-Agent follows redirect)
App -> App : ⑧ verify state == xyz (CSRF defense)
App -> AuthServer : ⑨ POST /token {grant_type: authorization_code, code, client_id, client_secret, redirect_uri}
AuthServer -> AuthServer : ⑩ verify client_secret + code unused
AuthServer -> App : ⑪ 200 {access_token, refresh_token, expires_in, id_token?}

== Step 4: API call ==
App -> API : GET /api/data (Authorization: Bearer access...nAPI -> API : verify token
API --> App : 200 data

@enduml

Notes:

  • Step ⑨ must happen server-side — client_secret cannot leak to frontend
  • Step ⑧ state must be verified — without it, CSRF is possible
  • Code single-use — used codes invalidate immediately
  • Redirect URI strict match — prevents authorization code injection

Protocol 3: OAuth 2.0 PKCE (mobile / SPA)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@startuml
title OAuth 2.0 with PKCE (Mobile/SPA)

actor "User" as User
participant "Mobile App\n(Public Client)" as App
participant "Auth Server" as AuthServer

== Step 1: generate PKCE params ==
App -> App : ① generate code_verifier (random 43-128 chars)
App -> App : ② code_challenge = SHA256(code_verifier) base64url
App -> App : ③ code_challenge_method = S256

== Step 2: redirect ==
App -> User : ④ open browser https://auth.com/authorize?\nresponse_type=code&\nclient_id=mobile_app&\ncode_challenge=...&\ncode_challenge_method=S256
User -> AuthServer : ⑤ user logs in + grants
AuthServer -> AuthServer : ⑥ store code_challenge
AuthServer -> User : ⑦ redirect_uri?code=AUTH_CODE

== Step 3: exchange token (verifier proves identity) ==
User -> App : ⑧ receive redirect + code
App -> AuthServer : ⑨ POST /token {grant_type: authorization_code, code, client_id, code_verifier}
AuthServer -> AuthServer : ⑩ verify SHA256(code_verifier) == code_challenge
AuthServer --> App : ⑪ 200 {access_token, refresh_token}

note over App
PKCE replaces client_secret —
mobile has no backend to store secret,
PKCE uses code_verifier to prove same-app
end note
@enduml

Key: PKCE solves the “public client (no secret)” security problem. SPA, mobile apps, desktop apps MUST use PKCE.

Protocol 4: SAML 2.0 (enterprise SSO)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@startuml
title SAML 2.0 SSO (SP-Initiated)

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

== Step 3: IdP returns SAML Response ==
IdP -> IdP : ⑦ generate SAML Response (XML, signed)\ncontains Assertion {Subject, Attribute}
IdP -> User : ⑧ 302 https://app.com/sso/acs?\nSAMLResponse=<base64 XML>
User -> App : ⑨ POST /sso/acs (form auto-submit)

== Step 4: SP verifies ==
App -> App : ⑩ decode + verify SAML Response signature
App -> App : ⑪ verify Audience == self
App -> App : ⑫ verify NotOnOrAfter > now
App -> App : ⑬ create session cookie
App -> User : ⑭ 200 /dashboard
@enduml

SAML vs OAuth/OIDC:

  • SAML: XML format, enterprise IT knows it, legacy (2005)
  • OAuth/OIDC: JSON format, dev-friendly, modern (2010+)
  • New projects use OIDC, legacy enterprise IT integration uses SAML

Protocol 5: OpenID Connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@startuml
title OpenID Connect (OIDC) Login

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

Fix — refresh token rotation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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.
  • PKCE verifier < 43 chars — RFC 7636 requires 43-128 chars. Shorter → 400.
  • 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.

  • Title: Auth protocol sequence diagrams: JWT / OAuth2 / SAML / OpenID Connect
  • Author: puml.online
  • Created at : 2026-07-30 17:30:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-jwt-oauth-sequence-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.