User Tools

Site Tools


security:sso-web

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
security:sso-web [2026/06/16 06:30] phong2018security: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:** 1.0+**Document Version:** 2.0
 **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:
   - [[#what-makes-this-different|What Makes This Different]]   - [[#what-makes-this-different|What Makes This Different]]
   - [[#components|Components]]   - [[#components|Components]]
 +  - [[#rsa-key-usage-private-key-vs-public-key|RSA Key Usage — Private Key vs Public Key]]
   - [[#sso-session-vs-app-session|SSO Session vs Application Session]]   - [[#sso-session-vs-app-session|SSO Session vs Application Session]]
   - [[#jwt--jwks-in-multi-app-sso|JWT & JWKS in Multi-App SSO]]   - [[#jwt--jwks-in-multi-app-sso|JWT & JWKS in Multi-App SSO]]
Line 133: Line 136:
 | SSO Session TTL  | 8 hours (inactivity); 24 hours (absolute)          | | SSO Session TTL  | 8 hours (inactivity); 24 hours (absolute)          |
 | 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:
 +
 +<code>
 +┌─────────────────────────────────────────────────────────────────────────────┐
 +│                     RSA KEY PAIR — WHO HOLDS WHAT                          │
 +│                                                                             │
 +│   ┌───────────────────────────────────────────────────────┐                │
 +│   │              ServerSSO                                │                │
 +│   │  ┌─────────────────────────────────────────────────┐  │                │
 +│   │  │  🔐 PRIVATE KEY  (RSA-2048 or RSA-4096)         │  │                │
 +│   │  │  kid: "key-2024-01"                             │  │                │
 +│   │  │  Stored in: HSM / KMS / encrypted key store     │  │                │
 +│   │  │  NEVER leaves ServerSSO — never shared          │  │                │
 +│   │  │  Used to: SIGN JWTs (access_token, id_token,    │  │                │
 +│   │  │                       logout_token)             │  │                │
 +│   │  └─────────────────────────────────────────────────┘  │                │
 +│   │  ┌─────────────────────────────────────────────────┐  │                │
 +│   │  │  🔓 PUBLIC KEY  (exposed via JWKS endpoint)     │  │                │
 +│   │  │  kid: "key-2024-01"                             │  │                │
 +│   │  │  Available at: /.well-known/jwks.json           │  │                │
 +│   │  │  Anyone can fetch it — it is intentionally      │  │                │
 +│   │  │  public (n + e RSA components only)             │  │                │
 +│   │  │  Used to: VERIFY JWT signatures                 │  │                │
 +│   │  └─────────────────────────────────────────────────┘  │                │
 +│   └───────────────────────────────────────────────────────┘                │
 +│                                                                             │
 +│   ┌────────────────────┐        ┌────────────────────┐                     │
 +│   │       WebA         │        │       WebB         │                     │
 +│   │  🔓 PUBLIC KEY     │        │  🔓 PUBLIC KEY     │                     │
 +│   │  (fetched via JWKS │        │  (fetched via JWKS │                     │
 +│   │   and cached)      │        │   and cached)      │                     │
 +│   │  Used to: VERIFY   │        │  Used to: VERIFY   │                     │
 +│   │  id_token sig      │        │  id_token sig      │                     │
 +│   │  logout_token sig  │        │  logout_token sig  │                     │
 +│   └────────────────────┘        └────────────────────┘                     │
 +└─────────────────────────────────────────────────────────────────────────────┘
 +</code>
 +
 +==== 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   ^ Key Type    ^ Key ID        ^ Operation              ^ Token / Object           ^ When                                  ^
 +| ServerSSO   | 🔐 Private  | key-2024-01   | **SIGN**               | access_token (JWT)       | Every /token response                 |
 +| ServerSSO   | 🔐 Private  | key-2024-01   | **SIGN**               | id_token (JWT)           | Every /token response                 |
 +| ServerSSO   | 🔐 Private  | key-2024-01   | **SIGN**               | logout_token (JWT)       | Every backchannel logout call         |
 +| ServerSSO   | 🔓 Public   | key-2024-01   | **EXPOSE** via JWKS    | n, e (RSA components)    | GET /.well-known/jwks.json (always)   |
 +| WebA        | 🔓 Public   | key-2024-01   | **VERIFY signature**   | id_token received        | Step A11 — after /token exchange      |
 +| WebA        | 🔓 Public   | key-2024-01   | **VERIFY signature**   | logout_token received    | Every backchannel logout              |
 +| WebB        | 🔓 Public   | key-2024-01   | **VERIFY signature**   | id_token received        | Step B9 — after /token exchange       |
 +| WebB        | 🔓 Public   | key-2024-01   | **VERIFY signature**   | logout_token received    | Every backchannel logout              |
 +
 +> **Note on access_token:** In this architecture, WebA and WebB store the access_token
 +> server-side and forward it to resource APIs. If WebA/WebB also need to inspect the
 +> access_token's claims (e.g. to check roles before calling an API), they would also
 +> use the public key to verify it. The public key is fetched once and cached per app.
 +
 +==== Where the Private Key Lives ====
 +
 +<code>
 +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
 +</code>
 +
 +==== Where the Public Key Travels ====
 +
 +<code>
 +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://sso.example.com/.well-known/jwks.json
 +       │
 +       ├──────────────────────────────▶  WebA caches public key (1 hour TTL)
 +       │
 +       └──────────────────────────────▶  WebB caches public key (1 hour TTL)
 +
 +The public key contains ONLY:
 +  "n": RSA modulus  (large integer — public component)
 +  "e": RSA exponent (usually 65537 — public component)
 +  "kid": key ID
 +  "alg": RS256
 +  "use": sig
 +
 +The public key does NOT contain:
 +  "d": private exponent  (this stays on ServerSSO only)
 +  "p", "q": prime factors (this stays on ServerSSO only)
 +</code>
 +
 +==== Step-by-Step Key Operations During JWT Lifecycle ====
 +
 +<code>
 +STEP 1 — JWT Creation (ServerSSO, private key)
 +──────────────────────────────────────────────
 +ServerSSO assembles:
 +  header  = { "alg": "RS256", "kid": "key-2024-01", "typ": "JWT" }
 +  payload = { "sub": "user-uid-456", "iss": "https://sso.example.com", ... }
 +
 +  signing_input = base64url(header) + "." + base64url(payload)
 +
 +  🔐 signature = RSA_PKCS1v15_SIGN(
 +        key   = private_key,
 +        hash  = SHA256(signing_input)
 +     )
 +
 +  JWT = signing_input + "." + base64url(signature)
 +
 +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 "kid" = "key-2024-01"
 +  3. Fetch public key from JWKS cache matching kid
 +  4. Reconstruct signing_input = header + "." + payload
 +
 +  🔓 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
 +</code>
 +
 +==== Key Rotation ====
 +
 +RSA keys should be rotated periodically (every 90 days recommended).
 +ServerSSO supports multiple active keys in JWKS simultaneously during rotation:
 +
 +<code>
 +During key rotation, JWKS contains TWO keys:
 +
 +GET /.well-known/jwks.json
 +{
 +  "keys": [
 +    {
 +      "kid": "key-2024-01",   ← OLD key (still valid, being phased out)
 +      "kty": "RSA", "alg": "RS256", "use": "sig",
 +      "n": "old-modulus...", "e": "AQAB"
 +    },
 +    {
 +      "kid": "key-2025-01",   ← NEW key (now used for signing)
 +      "kty": "RSA", "alg": "RS256", "use": "sig",
 +      "n": "new-modulus...", "e": "AQAB"
 +    }
 +  ]
 +}
 +
 +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
 +</code>
 +
 +==== Summary: Who Uses Which Key and Why ====
 +
 +<code>
 +╔══════════════╦════════════════╦══════════════════════════════════╦══════════════════════════╗
 +║  Component   ║   Key Used     ║         Operation                ║   Tokens Affected        ║
 +╠══════════════╬════════════════╬══════════════════════════════════╬══════════════════════════╣
 +║  ServerSSO   ║ 🔐 PRIVATE KEY ║ Signs JWT payload + header       ║ access_token             ║
 +║              ║                ║ (RSA-SHA256 signature)           ║ id_token                 ║
 +║              ║                ║                                  ║ logout_token             ║
 +╠══════════════╬════════════════╬══════════════════════════════════╬══════════════════════════╣
 +║  ServerSSO   ║ 🔓 PUBLIC KEY  ║ Publishes n+e in JWKS endpoint   ║ (key material, not token)║
 +║              ║                ║ for consumers to fetch           ║                          ║
 +╠══════════════╬════════════════╬══════════════════════════════════╬══════════════════════════╣
 +║  WebA        ║ 🔓 PUBLIC KEY  ║ Verifies id_token signature      ║ id_token (from SSO)      ║
 +║  (fetched    ║ (from JWKS)    ║ Verifies logout_token signature  ║ logout_token (from SSO)  ║
 +║   and cached)║                ║ Optionally verifies access_token ║ access_token (optional) 
 +╠══════════════╬════════════════╬══════════════════════════════════╬══════════════════════════╣
 +║  WebB        ║ 🔓 PUBLIC KEY  ║ Verifies id_token signature      ║ id_token (from SSO)      ║
 +║  (fetched    ║ (from JWKS)    ║ Verifies logout_token signature  ║ logout_token (from SSO)  ║
 +║   and cached)║                ║ Optionally verifies access_token ║ access_token (optional) 
 +╚══════════════╩════════════════╩══════════════════════════════════╩══════════════════════════╝
 +
 +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.
 +</code>
  
 ---- ----
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:
  
 <code> <code>
 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: "key-2024-01")
 { {
   "iss":   "https://sso.example.com",   "iss":   "https://sso.example.com",
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: "key-2024-01") ← same private key
 { {
   "iss":   "https://sso.example.com",   "iss":   "https://sso.example.com",
Line 247: Line 492:
   "sid":   "SSO-XYZ-789"                          ← same SSO session reference   "sid":   "SSO-XYZ-789"                          ← same SSO session reference
 } }
 +
 +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.
 </code> </code>
  
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**
 +> ''kid: "key-2024-01"'' is embedded in each JWT header so verifiers know which public key to use.
  
 <code json> <code json>
Line 482: Line 734:
     "scope":         "openid profile email api:resourceA"     "scope":         "openid profile email api:resourceA"
   }   }
 +
 +  JWT header (both tokens):  { "alg": "RS256", "kid": "key-2024-01", "typ": "JWT" }
 +  Signature computed with:   🔐 ServerSSO RSA PRIVATE KEY
 </code> </code>
  
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 ''GET https://sso.example.com/.well-known/jwks.json'', finds the key
 +> matching ''kid: "key-2024-01"'', then verifies the RSA signature on the id_token.
 +> If verification passes, WebA trusts the claims (sub, email, nonce, aud).
  
 <code> <code>
Line 498: Line 758:
     sub:              "user-uid-456",     sub:              "user-uid-456",
     email:            "alice@example.com",     email:            "alice@example.com",
-    access_token:     "JWT-A",+    access_token:     "JWT-A",              ← signed by 🔐 private key
     access_token_exp: now + 900,     access_token_exp: now + 900,
     refresh_token:    "rtA-xyz",     refresh_token:    "rtA-xyz",
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 ''aud'' and ''scope'' from JWT-A, but signed
 +> by the same private key (identified by ''kid: "key-2024-01"'' in the header).
  
 <code json> <code json>
Line 720: Line 984:
     "scope":         "openid profile email api:resourceB"     "scope":         "openid profile email api:resourceB"
   }   }
 +
 +  JWT header (both tokens):  { "alg": "RS256", "kid": "key-2024-01", "typ": "JWT" }
 +  Signature computed with:   🔐 ServerSSO RSA PRIVATE KEY  (same key as JWT-A)
 </code> </code>
  
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 ''kid: "key-2024-01"'',
 +> verifies the id_token RSA signature, then checks ''aud == "web-b-001"''
 +> and ''nonce == stored nonce''. WebB uses the **same public key** as WebA —
 +> both share the same JWKS endpoint.
  
 <code> <code>
Line 733: Line 1006:
     sub:              "user-uid-456",     sub:              "user-uid-456",
     email:            "alice@example.com",     email:            "alice@example.com",
-    access_token:     "JWT-B",+    access_token:     "JWT-B",              ← signed by 🔐 private key
     access_token_exp: now + 900,     access_token_exp: now + 900,
     refresh_token:    "rtB-uvw",     refresh_token:    "rtB-uvw",
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 ''iat'' and ''exp'' claims,
 +> signed again with the private key. The ''kid'' in the header is unchanged
 +> (unless key rotation just happened).
  
 <code json> <code json>
Line 917: Line 1195:
   "scope":         "openid profile email api:resourceA"   "scope":         "openid profile email api:resourceA"
 } }
 +
 +  Signature computed with:  🔐 ServerSSO RSA PRIVATE KEY
 </code> </code>
  
Line 1043: Line 1323:
 11.  WebA        → ServerSSO    :  POST /token (code=CODE-A, client_secret-A)  [s2s] 11.  WebA        → ServerSSO    :  POST /token (code=CODE-A, client_secret-A)  [s2s]
 12.  ServerSSO   → ServerSSO    :  verify client, code, redirect_uri 12.  ServerSSO   → ServerSSO    :  verify client, code, redirect_uri
-13.  ServerSSO   → WebA         :  { JWT-A, id_token-A, refresh-A } +13.  ServerSSO   → ServerSSO    :  🔐 SIGN JWT-A + id_token-A with RSA PRIVATE KEY 
-14.  WebA        → WebA         :  validate id_token-A (aud=web-a-001, nonce=nonce-A) +14.  ServerSSO   → WebA         :  { JWT-A, id_token-A, refresh-A } 
-15.  WebA        → Redis        :  store SESS-A { JWT-A, refresh-A, sso_sid=SSO-XYZ } +15.  WebA        → WebA         :  🔓 VERIFY id_token-A signature with RSA PUBLIC KEY (JWKS) 
-16.  WebA        → Browser      :  Set-Cookie: session_a + 302 → /page +16.  WebA        → WebA         :  validate id_token-A claims (aud=web-a-001, nonce=nonce-A) 
-17.  Browser     → WebA         :  GET /page  (Cookie: session_a=SESS-A) +17.  WebA        → Redis        :  store SESS-A { JWT-A, refresh-A, sso_sid=SSO-XYZ } 
-18.  WebA        → Browser      :  200 HTML+18.  WebA        → Browser      :  Set-Cookie: session_a + 302 → /page 
 +19.  Browser     → WebA         :  GET /page  (Cookie: session_a=SESS-A) 
 +20.  WebA        → Browser      :  200 HTML
  
 ── Alice now opens WebB ── ── Alice now opens WebB ──
  
-19.  Browser     → WebB         :  GET /page  (no session_b) +21.  Browser     → WebB         :  GET /page  (no session_b) 
-20.  WebB        → WebB         :  generate state-B, nonce-B; store in Redis +22.  WebB        → WebB         :  generate state-B, nonce-B; store in Redis 
-21.  WebB        → Browser      :  302 → /authorize?client_id=web-b-001&state=state-B +23.  WebB        → Browser      :  302 → /authorize?client_id=web-b-001&state=state-B 
-22.  Browser     → ServerSSO    :  GET /authorize  (Cookie: sso_session=SSO-XYZ ✅) +24.  Browser     → ServerSSO    :  GET /authorize  (Cookie: sso_session=SSO-XYZ ✅) 
-23.  ServerSSO   → ServerSSO    :  SSO session valid → skip login → issue CODE-B +25.  ServerSSO   → ServerSSO    :  SSO session valid → skip login → issue CODE-B 
-24.  ServerSSO   → Browser      :  302 → WebB/callback?code=CODE-B  (no login page!) +26.  ServerSSO   → Browser      :  302 → WebB/callback?code=CODE-B  (no login page!) 
-25.  Browser     → WebB         :  GET /callback?code=CODE-B&state=state-B +27.  Browser     → WebB         :  GET /callback?code=CODE-B&state=state-B 
-26.  WebB        → WebB         :  validate state-B == stored; retrieve nonce-B +28.  WebB        → WebB         :  validate state-B == stored; retrieve nonce-B 
-27.  WebB        → ServerSSO    :  POST /token (code=CODE-B, client_secret-B)  [s2s] +29.  WebB        → ServerSSO    :  POST /token (code=CODE-B, client_secret-B)  [s2s] 
-28.  ServerSSO   → WebB         :  { JWT-B, id_token-B, refresh-B } +30.  ServerSSO   → ServerSSO    :  🔐 SIGN JWT-B + id_token-B with RSA PRIVATE KEY (same key) 
-29.  WebB        → WebB         :  validate id_token-B (aud=web-b-001, nonce=nonce-B) +31.  ServerSSO   → WebB         :  { JWT-B, id_token-B, refresh-B } 
-30.  WebB        → Redis        :  store SESS-B { JWT-B, refresh-B, sso_sid=SSO-XYZ } +32.  WebB        → WebB         :  🔓 VERIFY id_token-B signature with RSA PUBLIC KEY (JWKS) 
-31.  WebB        → Browser      :  Set-Cookie: session_b + 302 → /page +33.  WebB        → WebB         :  validate id_token-B claims (aud=web-b-001, nonce=nonce-B) 
-32.  Browser     → WebB         :  GET /page  (Cookie: session_b=SESS-B) +34.  WebB        → Redis        :  store SESS-B { JWT-B, refresh-B, sso_sid=SSO-XYZ } 
-33.  WebB        → Browser      :  200 HTML+35.  WebB        → Browser      :  Set-Cookie: session_b + 302 → /page 
 +36.  Browser     → WebB         :  GET /page  (Cookie: session_b=SESS-B) 
 +37.  WebB        → Browser      :  200 HTML 
 + 
 +KEY USAGE SUMMARY FOR THIS FLOW: 
 +  Steps 13, 30  →  🔐 ServerSSO RSA PRIVATE KEY   (signing) 
 +  Steps 15, 32  →  🔓 RSA PUBLIC KEY from JWKS     (verification)
 </code> </code>
  
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, any party could forge a logout request
 +> 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')     logout_token = request.POST.get('logout_token')
  
-    # 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, id_token, logout_token                ║
 +║     Stored in:     HSM / KMS / encrypted store on ServerSSO            ║
 +║     NEVER shared, NEVER leaves ServerSSO                               ║
 +║                                                                         ║
 +║  🔓 PUBLIC KEY   →  WebA, WebB (fetched from JWKS, cached 1 hr)        ║
 +║     Used to VERIFY: id_token signature (after /token exchange)         ║
 +║                     logout_token signature (backchannel logout)        ║
 +║                     access_token signature (optional, if inspected)    ║
 +║     Fetched from: GET /sso.example.com/.well-known/jwks.json           ║
 +║     kid "key-2024-01" links JWT header → correct key in JWKS           ║
 +╠═════════════════════════════════════════════════════════════════════════╣
 +║ WHEN EACH KEY IS USED (step reference)                                  ║
 +║   Scenario 1, Step A11 → 🔐 SSO signs JWT-A + id_token-A              ║
 +║   Scenario 1, Step A12 → 🔓 WebA verifies id_token-A                  ║
 +║   Scenario 1, Step B9  → 🔐 SSO signs JWT-B + id_token-B              ║
 +║   Scenario 1, Step B10 → 🔓 WebB verifies id_token-B                  ║
 +║   Scenario 2B,  Step 3 → 🔐 SSO signs new JWT-A (refresh)             ║
 +║   Logout, Step 7       → 🔓 WebB verifies logout_token                ║
 ╠═════════════════════════════════════════════════════════════════════════╣ ╠═════════════════════════════════════════════════════════════════════════╣
 ║ THREE-LAYER COOKIE MODEL                                                ║ ║ THREE-LAYER COOKIE MODEL                                                ║
Line 1456: Line 1771:
 ║   id_token-A: aud=web-a-001 → WebA accepts; WebB rejects               ║ ║   id_token-A: aud=web-a-001 → WebA accepts; WebB rejects               ║
 ║   id_token-B: aud=web-b-001 → WebB accepts; WebA rejects               ║ ║   id_token-B: aud=web-b-001 → WebB accepts; WebA rejects               ║
 +║   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        +║   → WebA + WebB verify logout_token (🔓 public key) then delete sessions║ 
 +║   → 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