security:sso-web
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revision | |||
| security:sso-web [2026/06/16 06:30] – phong2018 | security:sso-web [2026/06/16 06:38] (current) – phong2018 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== OIDC SSO System — Multi-Application (Browser + WebA + WebB + ServerSSO + JWT + JWKS) ====== | ====== OIDC SSO System — Multi-Application (Browser + WebA + WebB + ServerSSO + JWT + JWKS) ====== | ||
| - | **Document Version: | + | **Document Version: |
| **Last Updated:** 2026-06-16 | **Last Updated:** 2026-06-16 | ||
| + | **Changes in v2.0:** Added full RSA Key Usage section; annotated every step in both scenarios | ||
| + | with which key (private / public) is used and by which component. | ||
| **Scope:** Single Sign-On across two independent traditional web applications (WebA and WebB), | **Scope:** Single Sign-On across two independent traditional web applications (WebA and WebB), | ||
| sharing one ServerSSO. A user authenticates once and gains access to both apps | sharing one ServerSSO. A user authenticates once and gains access to both apps | ||
| Line 14: | Line 16: | ||
| - [[# | - [[# | ||
| - [[# | - [[# | ||
| + | - [[# | ||
| - [[# | - [[# | ||
| - [[# | - [[# | ||
| Line 133: | Line 136: | ||
| | SSO Session TTL | 8 hours (inactivity); | | SSO Session TTL | 8 hours (inactivity); | ||
| | Registered clients | web-a-001, web-b-001 (and others) | | Registered clients | web-a-001, web-b-001 (and others) | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== RSA Key Usage — Private Key vs Public Key ===== | ||
| + | |||
| + | ==== The Asymmetric Key Pair ==== | ||
| + | |||
| + | ServerSSO uses **RS256** (RSA + SHA-256), an asymmetric signing algorithm. | ||
| + | This means two mathematically linked keys exist with strictly separate roles: | ||
| + | |||
| + | < | ||
| + | ┌─────────────────────────────────────────────────────────────────────────────┐ | ||
| + | │ RSA KEY PAIR — WHO HOLDS WHAT │ | ||
| + | │ │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | └─────────────────────────────────────────────────────────────────────────────┘ | ||
| + | </ | ||
| + | |||
| + | ==== Fundamental Rule ==== | ||
| + | |||
| + | > **SIGN with PRIVATE KEY → VERIFY with PUBLIC KEY** | ||
| + | > | ||
| + | > Only ServerSSO can create valid JWT signatures (it alone holds the private key). | ||
| + | > Anyone with the public key (WebA, WebB, or any other party) can verify those signatures | ||
| + | > but can NEVER forge a new one. | ||
| + | |||
| + | ==== Key Usage Master Table ==== | ||
| + | |||
| + | ^ Component | ||
| + | | ServerSSO | ||
| + | | ServerSSO | ||
| + | | ServerSSO | ||
| + | | ServerSSO | ||
| + | | WebA | 🔓 Public | ||
| + | | WebA | 🔓 Public | ||
| + | | WebB | 🔓 Public | ||
| + | | WebB | 🔓 Public | ||
| + | |||
| + | > **Note on access_token: | ||
| + | > server-side and forward it to resource APIs. If WebA/WebB also need to inspect the | ||
| + | > access_token' | ||
| + | > use the public key to verify it. The public key is fetched once and cached per app. | ||
| + | |||
| + | ==== Where the Private Key Lives ==== | ||
| + | |||
| + | < | ||
| + | ServerSSO key storage options (most secure first): | ||
| + | |||
| + | 1. Hardware Security Module (HSM) | ||
| + | → Key material never exists in software memory | ||
| + | → Signing operation performed inside HSM hardware | ||
| + | → Best for production | ||
| + | |||
| + | 2. Cloud KMS (AWS KMS, GCP Cloud KMS, Azure Key Vault) | ||
| + | → Private key stored in managed key store | ||
| + | → Signing done via API call to KMS | ||
| + | → Key never exported to disk | ||
| + | |||
| + | 3. Encrypted key file (minimum acceptable) | ||
| + | → PEM file encrypted with passphrase | ||
| + | → Loaded into process memory at startup | ||
| + | → Passphrase injected via secrets manager at deploy time | ||
| + | → Never committed to source control | ||
| + | |||
| + | ❌ NEVER: | ||
| + | → Hardcode in source code | ||
| + | → Store in environment variable in plaintext | ||
| + | → Check into git | ||
| + | → Log to any output | ||
| + | → Transmit over the network | ||
| + | </ | ||
| + | |||
| + | ==== Where the Public Key Travels ==== | ||
| + | |||
| + | < | ||
| + | Public key distribution path: | ||
| + | |||
| + | ServerSSO private key | ||
| + | │ | ||
| + | │ RSA key generation (one-time) | ||
| + | ▼ | ||
| + | ServerSSO public key ←── stored alongside private key | ||
| + | │ | ||
| + | │ Exposed at JWKS endpoint (intentionally public) | ||
| + | ▼ | ||
| + | GET https:// | ||
| + | │ | ||
| + | | ||
| + | │ | ||
| + | | ||
| + | |||
| + | The public key contains ONLY: | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | |||
| + | The public key does NOT contain: | ||
| + | " | ||
| + | " | ||
| + | </ | ||
| + | |||
| + | ==== Step-by-Step Key Operations During JWT Lifecycle ==== | ||
| + | |||
| + | < | ||
| + | STEP 1 — JWT Creation (ServerSSO, private key) | ||
| + | ────────────────────────────────────────────── | ||
| + | ServerSSO assembles: | ||
| + | header | ||
| + | payload = { " | ||
| + | |||
| + | signing_input = base64url(header) + " | ||
| + | |||
| + | 🔐 signature = RSA_PKCS1v15_SIGN( | ||
| + | key = private_key, | ||
| + | hash = SHA256(signing_input) | ||
| + | ) | ||
| + | |||
| + | JWT = signing_input + " | ||
| + | |||
| + | STEP 2 — JWT Transmission | ||
| + | ────────────────────────────────────────────── | ||
| + | ServerSSO → WebA/WebB: JWT returned in /token response (HTTPS) | ||
| + | WebA/WebB stores JWT in Redis session — browser never sees it | ||
| + | WebA/WebB sends JWT to resource APIs as Bearer token (HTTPS) | ||
| + | |||
| + | STEP 3 — JWT Verification (WebA or WebB, public key) | ||
| + | ────────────────────────────────────────────────────── | ||
| + | Receiver (WebA/WebB) performs: | ||
| + | |||
| + | 1. Split JWT into header, payload, signature | ||
| + | 2. Decode header → extract " | ||
| + | 3. Fetch public key from JWKS cache matching kid | ||
| + | 4. Reconstruct signing_input = header + " | ||
| + | |||
| + | 🔓 valid = RSA_PKCS1v15_VERIFY( | ||
| + | key = public_key, | ||
| + | hash = SHA256(signing_input), | ||
| + | signature = base64url_decode(signature_part) | ||
| + | ) | ||
| + | |||
| + | 5. If valid == true → signature is authentic → trust payload claims | ||
| + | 6. Validate iss, aud, exp, nbf, iat, nonce, scope | ||
| + | </ | ||
| + | |||
| + | ==== Key Rotation ==== | ||
| + | |||
| + | RSA keys should be rotated periodically (every 90 days recommended). | ||
| + | ServerSSO supports multiple active keys in JWKS simultaneously during rotation: | ||
| + | |||
| + | < | ||
| + | During key rotation, JWKS contains TWO keys: | ||
| + | |||
| + | GET / | ||
| + | { | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ] | ||
| + | } | ||
| + | |||
| + | Rotation process: | ||
| + | 1. Generate new RSA key pair on ServerSSO | ||
| + | 2. Add new public key to JWKS (both keys now published) | ||
| + | 3. Start signing NEW tokens with new private key | ||
| + | 4. Old tokens (signed with old key) still verifiable via old public key in JWKS | ||
| + | 5. Wait for all old tokens to expire (max access_token TTL = 15 min) | ||
| + | 6. Remove old public key from JWKS | ||
| + | 7. Decommission old private key | ||
| + | |||
| + | WebA and WebB handle this automatically: | ||
| + | - If JWT header.kid not found in cache → re-fetch JWKS → find new key → verify | ||
| + | - No restart, no config change required | ||
| + | </ | ||
| + | |||
| + | ==== Summary: Who Uses Which Key and Why ==== | ||
| + | |||
| + | < | ||
| + | ╔══════════════╦════════════════╦══════════════════════════════════╦══════════════════════════╗ | ||
| + | ║ Component | ||
| + | ╠══════════════╬════════════════╬══════════════════════════════════╬══════════════════════════╣ | ||
| + | ║ ServerSSO | ||
| + | ║ ║ ║ (RSA-SHA256 signature) | ||
| + | ║ ║ ║ ║ logout_token | ||
| + | ╠══════════════╬════════════════╬══════════════════════════════════╬══════════════════════════╣ | ||
| + | ║ ServerSSO | ||
| + | ║ ║ ║ for consumers to fetch | ||
| + | ╠══════════════╬════════════════╬══════════════════════════════════╬══════════════════════════╣ | ||
| + | ║ WebA ║ 🔓 PUBLIC KEY ║ Verifies id_token signature | ||
| + | ║ (fetched | ||
| + | ║ and cached)║ | ||
| + | ╠══════════════╬════════════════╬══════════════════════════════════╬══════════════════════════╣ | ||
| + | ║ WebB ║ 🔓 PUBLIC KEY ║ Verifies id_token signature | ||
| + | ║ (fetched | ||
| + | ║ and cached)║ | ||
| + | ╚══════════════╩════════════════╩══════════════════════════════════╩══════════════════════════╝ | ||
| + | |||
| + | KEY RULE: ServerSSO is the ONLY component that ever touches the private key. | ||
| + | WebA and WebB ONLY ever hold and use the public key. | ||
| + | The private key NEVER travels over a network. | ||
| + | The public key is INTENTIONALLY distributed to all verifiers. | ||
| + | </ | ||
| ---- | ---- | ||
| Line 219: | Line 461: | ||
| ==== Different Tokens for Different Apps ==== | ==== Different Tokens for Different Apps ==== | ||
| - | When a user logs into WebA and WebB, ServerSSO issues **different JWTs** to each: | + | When a user logs into WebA and WebB, ServerSSO issues **different JWTs** to each. |
| + | Both are signed with the **same RSA private key** but carry different audience and scope claims: | ||
| < | < | ||
| JWT issued to WebA (via web-a-001 client): | JWT issued to WebA (via web-a-001 client): | ||
| + | 🔐 SIGNED with ServerSSO RSA PRIVATE KEY (kid: " | ||
| { | { | ||
| " | " | ||
| Line 236: | Line 480: | ||
| JWT issued to WebB (via web-b-001 client): | JWT issued to WebB (via web-b-001 client): | ||
| + | 🔐 SIGNED with ServerSSO RSA PRIVATE KEY (kid: " | ||
| { | { | ||
| " | " | ||
| Line 247: | Line 492: | ||
| " | " | ||
| } | } | ||
| + | |||
| + | Verification by WebA or WebB: | ||
| + | 🔓 VERIFY with ServerSSO RSA PUBLIC KEY (fetched from JWKS, cached) | ||
| + | Both apps use the same public key from the same JWKS endpoint. | ||
| </ | </ | ||
| Line 470: | Line 719: | ||
| === Step A11 — ServerSSO Returns Tokens to WebA === | === Step A11 — ServerSSO Returns Tokens to WebA === | ||
| + | |||
| + | > 🔐 **PRIVATE KEY USED HERE — ServerSSO signs both JWTs with RSA private key** | ||
| + | > '' | ||
| <code json> | <code json> | ||
| Line 482: | Line 734: | ||
| " | " | ||
| } | } | ||
| + | |||
| + | JWT header (both tokens): | ||
| + | Signature computed with: 🔐 ServerSSO RSA PRIVATE KEY | ||
| </ | </ | ||
| Line 491: | Line 746: | ||
| === Step A12 — WebA Stores Session and Sets Cookie === | === Step A12 — WebA Stores Session and Sets Cookie === | ||
| + | |||
| + | > 🔓 **PUBLIC KEY USED HERE — WebA fetches JWKS and verifies id_token signature** | ||
| + | > WebA calls '' | ||
| + | > matching '' | ||
| + | > If verification passes, WebA trusts the claims (sub, email, nonce, aud). | ||
| < | < | ||
| Line 498: | Line 758: | ||
| sub: " | sub: " | ||
| email: | email: | ||
| - | access_token: | + | access_token: |
| access_token_exp: | access_token_exp: | ||
| refresh_token: | refresh_token: | ||
| Line 708: | Line 968: | ||
| === Step B9 — ServerSSO Returns WebB-Specific Tokens === | === Step B9 — ServerSSO Returns WebB-Specific Tokens === | ||
| + | |||
| + | > 🔐 **PRIVATE KEY USED HERE — ServerSSO signs new JWTs for WebB with the same RSA private key** | ||
| + | > JWT-B is a brand-new token with different '' | ||
| + | > by the same private key (identified by '' | ||
| <code json> | <code json> | ||
| Line 720: | Line 984: | ||
| " | " | ||
| } | } | ||
| + | |||
| + | JWT header (both tokens): | ||
| + | Signature computed with: 🔐 ServerSSO RSA PRIVATE KEY (same key as JWT-A) | ||
| </ | </ | ||
| Line 726: | Line 993: | ||
| === Step B10 — WebB Creates Session, Sets Cookie, Redirects === | === Step B10 — WebB Creates Session, Sets Cookie, Redirects === | ||
| + | |||
| + | > 🔓 **PUBLIC KEY USED HERE — WebB verifies id_token signature** | ||
| + | > WebB fetches JWKS (or uses its own cache), finds '' | ||
| + | > verifies the id_token RSA signature, then checks '' | ||
| + | > and '' | ||
| + | > both share the same JWKS endpoint. | ||
| < | < | ||
| Line 733: | Line 1006: | ||
| sub: " | sub: " | ||
| email: | email: | ||
| - | access_token: | + | access_token: |
| access_token_exp: | access_token_exp: | ||
| refresh_token: | refresh_token: | ||
| Line 909: | Line 1182: | ||
| === Step 3 — ServerSSO Returns New Tokens === | === Step 3 — ServerSSO Returns New Tokens === | ||
| + | |||
| + | > 🔐 **PRIVATE KEY USED HERE — ServerSSO signs fresh JWTs with the RSA private key** | ||
| + | > The new access_token is a freshly minted JWT with updated '' | ||
| + | > signed again with the private key. The '' | ||
| + | > (unless key rotation just happened). | ||
| <code json> | <code json> | ||
| Line 917: | Line 1195: | ||
| " | " | ||
| } | } | ||
| + | |||
| + | Signature computed with: 🔐 ServerSSO RSA PRIVATE KEY | ||
| </ | </ | ||
| Line 1043: | Line 1323: | ||
| 11. WebA → ServerSSO | 11. WebA → ServerSSO | ||
| 12. ServerSSO | 12. ServerSSO | ||
| - | 13. ServerSSO | + | 13. ServerSSO |
| - | 14. WebA → WebA : | + | 14. ServerSSO |
| - | 15. WebA → Redis : store SESS-A { JWT-A, refresh-A, sso_sid=SSO-XYZ } | + | 15. WebA → WebA : |
| - | 16. WebA → Browser | + | 16. WebA → WebA : |
| - | 17. Browser | + | 17. WebA → Redis : store SESS-A { JWT-A, refresh-A, sso_sid=SSO-XYZ } |
| - | 18. WebA → Browser | + | 18. WebA → Browser |
| + | 19. Browser | ||
| + | 20. WebA → Browser | ||
| ── Alice now opens WebB ── | ── Alice now opens WebB ── | ||
| - | 19. Browser | + | 21. Browser |
| - | 20. WebB → WebB : | + | 22. WebB → WebB : |
| - | 21. WebB → Browser | + | 23. WebB → Browser |
| - | 22. Browser | + | 24. Browser |
| - | 23. ServerSSO | + | 25. ServerSSO |
| - | 24. ServerSSO | + | 26. ServerSSO |
| - | 25. Browser | + | 27. Browser |
| - | 26. WebB → WebB : | + | 28. WebB → WebB : |
| - | 27. WebB → ServerSSO | + | 29. WebB → ServerSSO |
| - | 28. ServerSSO | + | 30. ServerSSO |
| - | 29. WebB → WebB : | + | 31. ServerSSO |
| - | 30. WebB → Redis : store SESS-B { JWT-B, refresh-B, sso_sid=SSO-XYZ } | + | 32. WebB → WebB : |
| - | 31. WebB → Browser | + | 33. WebB → WebB : |
| - | 32. Browser | + | 34. WebB → Redis : store SESS-B { JWT-B, refresh-B, sso_sid=SSO-XYZ } |
| - | 33. WebB → Browser | + | 35. WebB → Browser |
| + | 36. Browser | ||
| + | 37. WebB → Browser | ||
| + | |||
| + | KEY USAGE SUMMARY FOR THIS FLOW: | ||
| + | Steps 13, 30 → 🔐 ServerSSO RSA PRIVATE KEY | ||
| + | Steps 15, 32 → 🔓 RSA PUBLIC KEY from JWKS | ||
| </ | </ | ||
| Line 1142: | Line 1430: | ||
| ==== WebA & WebB Backchannel Logout Handler ==== | ==== WebA & WebB Backchannel Logout Handler ==== | ||
| + | |||
| + | > 🔓 **PUBLIC KEY USED HERE — WebA and WebB verify the logout_token signature before acting on it** | ||
| + | > This is critical: without signature verification, | ||
| + | > and force-terminate user sessions. The public key guarantees the logout_token genuinely | ||
| + | > came from ServerSSO. | ||
| <code python> | <code python> | ||
| Line 1148: | Line 1441: | ||
| logout_token = request.POST.get(' | logout_token = request.POST.get(' | ||
| - | # Validate signature via JWKS | + | # 🔓 Validate signature via JWKS (public key) |
| payload = verify_jwt( | payload = verify_jwt( | ||
| token = logout_token, | token = logout_token, | ||
| Line 1438: | Line 1731: | ||
| ╔═════════════════════════════════════════════════════════════════════════╗ | ╔═════════════════════════════════════════════════════════════════════════╗ | ||
| ║ OIDC MULTI-APP SSO — QUICK REFERENCE | ║ OIDC MULTI-APP SSO — QUICK REFERENCE | ||
| + | ╠═════════════════════════════════════════════════════════════════════════╣ | ||
| + | ║ RSA KEY USAGE — WHO USES WHAT ║ | ||
| + | ║ ║ | ||
| + | ║ 🔐 PRIVATE KEY → ServerSSO ONLY ║ | ||
| + | ║ Used to SIGN: access_token, | ||
| + | ║ | ||
| + | ║ NEVER shared, NEVER leaves ServerSSO | ||
| + | ║ ║ | ||
| + | ║ 🔓 PUBLIC KEY | ||
| + | ║ Used to VERIFY: id_token signature (after /token exchange) | ||
| + | ║ | ||
| + | ║ | ||
| + | ║ | ||
| + | ║ kid " | ||
| + | ╠═════════════════════════════════════════════════════════════════════════╣ | ||
| + | ║ WHEN EACH KEY IS USED (step reference) | ||
| + | ║ | ||
| + | ║ | ||
| + | ║ | ||
| + | ║ | ||
| + | ║ | ||
| + | ║ | ||
| ╠═════════════════════════════════════════════════════════════════════════╣ | ╠═════════════════════════════════════════════════════════════════════════╣ | ||
| ║ THREE-LAYER COOKIE MODEL ║ | ║ THREE-LAYER COOKIE MODEL ║ | ||
| Line 1456: | Line 1771: | ||
| ║ | ║ | ||
| ║ | ║ | ||
| + | ║ Both signed by same 🔐 private key; both verified by same 🔓 pub key ║ | ||
| ╠═════════════════════════════════════════════════════════════════════════╣ | ╠═════════════════════════════════════════════════════════════════════════╣ | ||
| ║ CLIENT CREDENTIALS (CONFIDENTIAL) | ║ CLIENT CREDENTIALS (CONFIDENTIAL) | ||
| Line 1469: | Line 1785: | ||
| ║ GLOBAL LOGOUT CHAIN ║ | ║ GLOBAL LOGOUT CHAIN ║ | ||
| ║ User logs out WebA → WebA calls SSO /logout → SSO terminates session ║ | ║ User logs out WebA → WebA calls SSO /logout → SSO terminates session ║ | ||
| - | ║ → SSO sends backchannel logout to WebA + WebB ║ | + | ║ → SSO signs logout_token (🔐 private key) for each registered client ║ |
| - | ║ → Both app sessions deleted | + | ║ |
| + | ║ → User fully logged out everywhere | ||
| ╠═════════════════════════════════════════════════════════════════════════╣ | ╠═════════════════════════════════════════════════════════════════════════╣ | ||
| ║ TOKEN LIFETIMES | ║ TOKEN LIFETIMES | ||
| Line 1486: | Line 1803: | ||
| //Document maintained by: Platform Security Team// | //Document maintained by: Platform Security Team// | ||
| //Format: DokuWiki// | //Format: DokuWiki// | ||
| + | //Version: 2.0 — Added RSA Key Usage section (private key / public key per component and step)// | ||
| //Standard: OpenID Connect Core 1.0 · OpenID Connect Back-Channel Logout 1.0 · RFC 6749 · RFC 7519 · RFC 7517// | //Standard: OpenID Connect Core 1.0 · OpenID Connect Back-Channel Logout 1.0 · RFC 6749 · RFC 7519 · RFC 7517// | ||
security/sso-web.1781591435.txt.gz · Last modified: by phong2018
