User Tools

Site Tools


security:sso-mobile

Differences

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

Link to this comparison view

Next revision
Previous revision
security:sso-mobile [2026/06/16 05:28] – created phong2018security:sso-mobile [2026/06/16 07:08] (current) phong2018
Line 1: Line 1:
-====== SSO Flow Example: Mobile App, Server A, Server B, and SSO (OIDC + JWT + JWKS======+====== OIDC SSO System: Mobile App + PKCE + JWT + JWKS ======
  
-===== System Overview =====+**Version:** 1.0 \\ 
 +**Date:** 2026-06-16 \\ 
 +**Audience:** Backend Engineers, Mobile Developers, Security Architects
  
-We have 3 systems:+----
  
-Mobile App (iOS / Android) +===== Table of Contents =====
-Server A: crm.company.com +
-Server B: orders.company.com +
-SSO / Identity Provider: auth.company.com+
  
-Initial state:+  - [[#overview|Overview]] 
 +  - [[#architecture|Architecture & Components]] 
 +  - [[#key-concepts|Key Concepts]] 
 +  - [[#scenario1|Scenario 1 — Not Authenticated (First Login)]] 
 +  - [[#scenario2|Scenario 2 — Already Authenticated (SSO Token Reuse)]] 
 +  - [[#token-validation|Token Validation with JWKS]] 
 +  - [[#sequence-diagrams|Sequence Diagrams (Text)]] 
 +  - [[#security-notes|Security Notes]] 
 +  - [[#glossary|Glossary]]
  
-<code> Mobile App Token Storage = {} </code>+----
  
-(No cookies used in mobile flow)+===== Overview ===== {{anchor:overview}}
  
-===== Scenario 1: Login via Mobile App =====+This document describes a complete **OpenID Connect (OIDC) Single Sign-On (SSO)** system 
 +designed for a mobile application that communicates with two backend resource servers.
  
-===== Step 1User opens Mobile App =====+The system uses: 
 +  * **PKCE** (Proof Key for Code Exchange) — prevents authorization code interception on mobile 
 +  * **JWT** (JSON Web Token) — compact, self-contained token format 
 +  * **JWKS** (JSON Web Key Set) — public key endpoint for token signature verification 
 +  * **OIDC** — identity layer on top of OAuth 2.0
  
-<code> User → Mobile App No access_token yet </code>+----
  
-App detects user not logged in.+===== Architecture & Components ===== {{anchor:architecture}}
  
-===== Step 2: App opens SSO login (system browser) =====+==== Component List ====
  
-<code> App → opens browser → https://auth.company.com/authorize </code>+^ Component      ^ Role                                                                         ^ Typical Stack              ^ 
 +| **Mobile App** | End-user client. Initiates auth, holds tokens, calls resource servers.      | iOS Android React Native | 
 +| **ServerSSO**  | OIDC Authorization Server. Issues tokens, manages sessions, exposes JWKS.   | Keycloak / Auth0 / Custom 
 +| **ServerA**    | Resource Server A. Protected API. Validates JWT on each request.             | Node.js / Spring / Django 
 +| **ServerB**    | Resource Server B. Protected API. Validates JWT on each request            | Node.js Spring Django  |
  
-Includes:+==== Network Overview ====
  
-client_id +<code> 
-redirect_uri (deep link+  ┌─────────────┐         OIDC / PKCE          ┌─────────────────┐ 
-response_type=code +  │  Mobile App │ ◄──────────────────────────► │   ServerSSO     │ 
-PKCE challenge+  └──────┬──────┘                               │  (Auth Server │ 
 +         │                                      └────────┬────────┘ 
 +         │  JWT (Bearer Token)                           │  JWKS Public Keys 
 +         ├────────────────────────────────►  ┌──────────┴──────────┐ 
 +         │                                   │      ServerA        │ 
 +         │  JWT (Bearer Token)               │   (Resource API)    │ 
 +         └────────────────────────────────►  └─────────────────────┘ 
 +                                             ┌─────────────────────┐ 
 +                                             │      ServerB        │ 
 +                                             │   (Resource API)    │ 
 +                                             └─────────────────────┘ 
 +</code>
  
-===== Step 3User logs in =====+> **Note:** ServerA and ServerB never contact ServerSSO during normal request validation. 
 +> They verify JWT signatures locally using the public keys fetched from the JWKS endpoint.
  
-User enters:+----
  
-username +===== Key Concepts ===== {{anchor:key-concepts}}
-password +
-MFA (optional)+
  
-SSO creates session:+==== PKCE Flow (Mobile-Specific) ====
  
-<code> SSO_SESSION=123 </code>+PKCE prevents **authorization code interception attacks** that are common on mobile platforms 
 +(because redirect URIs on mobile can be hijacked by malicious apps).
  
-SSO sets cookie (browser only):+^ Step ^ PKCE Parameter        ^ Description                                              ^ 
 +| 1    | ``code_verifier``      | Random secret (43–128 charsgenerated by the Mobile App | 
 +| 2    | ``code_challenge``     | SHA-256 hash of code_verifier, base64url-encoded          | 
 +| 3    | Auth request           | App sends ``code_challenge`` + ``code_challenge_method=S256`` | 
 +| 4    | Token exchange         | App sends original ``code_verifier`` to prove identity    |
  
-<code> Set-Cookie: SSO_SESSION=123 Domain=auth.company.com </code>+==== JWT Structure ====
  
-===== Step 4Redirect back to Mobile App (Deep Link) =====+A JWT has three parts separated by dots``header.payload.signature``
  
-<code> myapp://callback?code=abc </code>+<code json> 
 +// Header 
 +
 +  "alg": "RS256", 
 +  "typ": "JWT", 
 +  "kid": "key-id-001"       // key ID — used to look up the right key in JWKS 
 +}
  
-Browser closes → control returns to app.+// Payload (claims) 
 +
 +  "iss": "https://sso.example.com",     // issuer 
 +  "sub": "user-uuid-123",               // subject (user ID) 
 +  "aud": ["serverA", "serverB"],        // intended audiences 
 +  "exp": 1718500000,                    // expiry (Unix timestamp) 
 +  "iat": 1718496400,                    // issued at 
 +  "email": "user@example.com", 
 +  "roles": ["read", "write"
 +}
  
-===== Step 5Mobile App exchanges code =====+// SignatureRS256(base64url(header) + "." + base64url(payload), privateKey) 
 +</code>
  
-<code> POST https://auth.company.com/token </code>+==== JWKS Endpoint ====
  
-Request includes:+ServerSSO exposes a public endpoint:
  
-code=abc +<code> 
-code_verifier (PKCE) +GET https://sso.example.com/.well-known/jwks.json 
-client_id+</code>
  
-SSO returns:+Response:
  
-access_token (JWT) +<code json> 
-id_token (JWT) +{ 
-refresh_token+  "keys":
 +    { 
 +      "kty": "RSA", 
 +      "use": "sig", 
 +      "kid": "key-id-001", 
 +      "alg": "RS256", 
 +      "n":   "0vx7agoebGcQ...", 
 +      "e":   "AQAB" 
 +    } 
 +  ] 
 +
 +</code>
  
-===== 🔐 JWT SIGNING (SSO - PRIVATE KEY) =====+ServerA and ServerB **cache** this response and use it to verify JWT signatures 
 +without calling ServerSSO on every request.
  
-Inside SSO:+==== OIDC Discovery Document ====
  
-<code> PRIVATE KEY → signs JWT tokens (RS256) </code>+ServerSSO also exposes:
  
-SSO:+<code> 
 +GET https://sso.example.com/.well-known/openid-configuration 
 +</code>
  
-creates ID token +This returns all endpoint URLs (authorization, token, userinfo, jwks_uri, etc.).
-creates Access token +
-signs both with PRIVATE KEY+
  
-✔ Only SSO has PRIVATE KEY +----
-✔ Never exposed to apps or servers+
  
-===== 🔐 JWT VERIFICATION (Server A / Server B - PUBLIC KEY) =====+===== Scenario 1 — Not Authenticated (First Login) ===== {{anchor:scenario1}}
  
-Server A / B validate token:+This scenario covers the **complete first-time login flow** from scratch.
  
-==== 1. Fetch JWKS ====+==== Step-by-Step ====
  
-<code> GET https://auth.company.com/.well-known/jwks.json </code>+=== Step 1Mobile App — Generate PKCE Parameters ===
  
-==== 2. Verify signature ====+The app generates cryptographic values **locally**, before any network call:
  
-<code> SSO PUBLIC KEY → verifies JWT </code>+<code> 
 +code_verifier  = base64url( random_bytes(32) ) 
 +                 e.g. "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
  
-==== 3Validate claims ====+code_challenge base64url( SHA256( code_verifier ) ) 
 +                 e.g. "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
  
-exp (expiration) +state          = base64urlrandom_bytes(16)   // CSRF protection 
-iss (issuer+nonce          = base64urlrandom_bytes(16)   // replay protection 
-aud (audience)+</code>
  
-If valid → accept request+The app stores ``code_verifier``, ``state``, and ``nonce`` **in memory** (never on disk).
  
-===== Step 6Store tokens in Mobile App =====+=== Step 2: Mobile App → ServerSSO — Authorization Request ===
  
-<code> access_token = eyJ... refresh_token = eyJ... </code>+The app opens the system browser (or in-app browser tab) and navigates to:
  
-Stored in:+<code> 
 +GET https://sso.example.com/authorize 
 +  ?response_type=code 
 +  &client_id=mobile-app-client 
 +  &redirect_uri=myapp://callback 
 +  &scope=openid%20profile%20email%20offline_access 
 +  &state=<state> 
 +  &nonce=<nonce> 
 +  &code_challenge=<code_challenge> 
 +  &code_challenge_method=S256 
 +</code>
  
-iOS Keychain +^ Parameter               ^ Value                     ^ Purpose                        ^ 
-Android Keystore+| ``response_type``        | ``code``                  | Request an authorization code  | 
 +| ``client_id``            | ``mobile-app-client``     | Identifies this application    | 
 +| ``redirect_uri``         | ``myapp://callback``      | Deep link back to the app      | 
 +| ``scope``                | ``openid profile email offline_access`` | Requested permissions | 
 +| ``state``                | Random value              | CSRF protection                | 
 +| ``nonce``                | Random value              | Replay attack protection       | 
 +| ``code_challenge``       | SHA256(code_verifier)     | PKCE challenge                 | 
 +| ``code_challenge_method``| ``S256``                  | Hash algorithm                 |
  
-===== Scenario 2Mobile App calls Server A =====+=== Step 3ServerSSO — Authenticate the User ===
  
-===== Step 7: Call Server A =====+ServerSSO presents the **login page** (username/password, MFA, social login, etc.).
  
-<code> Mobile App → crm.company.com/api AuthorizationBearer access_token </code>+The user authenticates successfullyServerSSO: 
 +  - Validates the PKCE challenge parameters 
 +  - Creates a server-side session 
 +  - Generates a short-lived **authorization code** (e.g., valid for 60 seconds, single-use)
  
-Server A:+=== Step 4ServerSSO → Mobile App — Authorization Code Redirect ===
  
-verifies JWT via JWKS +ServerSSO redirects the browser to the app's deep link:
-checks signature + claims +
-returns data+
  
-===== Scenario 3Mobile App calls Server B =====+<code> 
 +myapp://callback 
 +  ?code=SplxlOBeZQQYbYS6WxSbIA 
 +  &state=<original_state> 
 +</code>
  
-===== Step 8Call Server B =====+The app: 
 +  - Receives the redirect via the OS URL scheme handler 
 +  - **Validates** that ``state`` matches what was stored in Step 1 (CSRF check) 
 +  - Extracts the ``code``
  
-<code> Mobile App → orders.company.com/api Authorization: Bearer access_token </code>+=== Step 5: Mobile App → ServerSSO — Token Exchange (PKCE Verification) ===
  
-Server B:+The app makes a **back-channel** (direct HTTPS) POST request:
  
-verifies JWT via JWKS +<code> 
-trusts SSO identity +POST https://sso.example.com/token 
-returns data+Content-Type: application/x-www-form-urlencoded
  
-===== 🔁 Token Refresh Flow =====+grant_type=authorization_code 
 +&code=SplxlOBeZQQYbYS6WxSbIA 
 +&redirect_uri=myapp://callback 
 +&client_id=mobile-app-client 
 +&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk 
 +</code>
  
-When access_token expires:+> **Critical:** ``code_verifier`` is sent here for the first time. 
 +> ServerSSO recomputes SHA256(code_verifier) and compares it to the stored ``code_challenge``. 
 +> If they match, the exchange is authorized. This **proves** the requester is the same 
 +> entity that started the flow — defeating code interception attacks.
  
-<code> POST https://auth.company.com/token grant_type=refresh_token </code>+=== Step 6ServerSSO → Mobile App — Token Response ===
  
-SSO returns new access_token.+ServerSSO responds with:
  
-(No user login required again)+<code json> 
 +
 +  "access_token":  "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1pZC0wMDEifQ...", 
 +  "token_type":    "Bearer", 
 +  "expires_in":    3600, 
 +  "id_token":      "eyJhbGciOiJSUzI1NiJ9...", 
 +  "refresh_token": "8xLOxBtZp8", 
 +  "scope":         "openid profile email offline_access" 
 +
 +</code>
  
-===== 🔐 JWT SIGNING (SSO - PRIVATE KEY=====+^ Token           ^ Purpose                                           ^ Storage on Device       ^ 
 +| ``access_token`` | Sent to ServerA / ServerB as Bearer token        | Secure memory only      | 
 +| ``id_token``     | Contains user identity claims (verified by app  | Secure memory only      | 
 +| ``refresh_token``| Used to get new access_token when expired         | Encrypted secure storage |
  
-Same as login:+The app **validates the id_token**: 
 +  - Fetch JWKS from ``https://sso.example.com/.well-known/jwks.json`` 
 +  - Verify RS256 signature using the public key matching ``kid`` 
 +  - Check ``iss``, ``aud``, ``exp``, and ``nonce`` (nonce must match Step 1)
  
-<code> PRIVATE KEY → signs new access tokens </code>+=== Step 7: Mobile App → ServerA — API Request with JWT ===
  
-✔ Central signing authority (SSO)+<code> 
 +GET https://serverA.example.com/api/data 
 +Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1pZC0wMDEifQ... 
 +</code>
  
-===== 🔐 JWT VERIFICATION (SERVERS - PUBLIC KEY=====+=== Step 8: ServerA — JWT Validation (Local, No SSO Call) ===
  
-Server A / B:+ServerA validates the JWT **entirely locally**:
  
-fetch JWKS +<code> 
-verify JWT signature +1. Parse JWT header → extract "kid" = "key-id-001" 
-validate claims+2. Look up public key in local JWKS cache by kid 
 +   (If not cached: fetch https://sso.example.com/.well-known/jwks.json and cache) 
 +3. Verify RS256 signature using the RSA public key 
 +4. Validate claims
 +   - exp  > now()             → not expired 
 +   - iss == "https://sso.example.com"  → correct issuer 
 +   - aud contains "serverA"   → token is intended for this server 
 +5. Extract sub, roles, email from payload 
 +6. Apply authorization rules 
 +7. Return API response 
 +</code>
  
-No call to SSO per request+=== Step 9: Mobile App → ServerB — API Request with Same JWT ===
  
-===== Final State =====+The **same access_token** is reused for ServerB:
  
-Mobile App stores:+<code> 
 +GET https://serverB.example.com/api/resource 
 +Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1pZC0wMDEifQ... 
 +</code>
  
-<code> access_token (JWT) refresh_token </code>+ServerB performs identical JWT validation (Steps 8.1–8.6— this is the **SSO benefit**: one 
 +authentication, multiple resource servers, no re-login required.
  
-Servers do NOT store sessions.+==== Scenario 1 — Summary Flow ====
  
-===== Key Architecture Insight =====+<code> 
 +Mobile App          ServerSSO            ServerA           ServerB 
 +    │                   │                    │                 │ 
 +    │─ Generate PKCE ──►│                    │                 │ 
 +    │  code_verifier     │                    │                 │ 
 +    │  code_challenge    │                    │                 │ 
 +    │                   │                    │                 │ 
 +    │── GET /authorize ─►│                    │                 │ 
 +    │   (code_challenge) │                    │                 │ 
 +    │                   │                    │                 │ 
 +    │                   │◄── User Login ─────│                 │ 
 +    │                   │    (browser)        │                 │ 
 +    │                   │                    │                 │ 
 +    │◄── redirect ──────│                    │                 │ 
 +    │    ?code=XYZ       │                    │                 │ 
 +    │    &state=ABC      │                    │                 │ 
 +    │                   │                    │                 │ 
 +    │── POST /token ────►│                    │                 │ 
 +    │   code_verifier    │                    │                 │ 
 +    │                   │ verify: SHA256(     │                 │ 
 +    │                   │   code_verifier)    │                 │ 
 +    │                   │ == code_challenge   │                 │ 
 +    │                   │                    │                 │ 
 +    │◄── access_token ──│                    │                 │ 
 +    │    id_token        │                    │                 │ 
 +    │    refresh_token   │                    │                 │ 
 +    │                   │                    │                 │ 
 +    │── GET /api/data ──────────────────────►│                 │ 
 +    │   Bearer: JWT      │                    │ validate JWT    │ 
 +    │                   │                    │ (JWKS cache)    │ 
 +    │◄── 200 response ──────────────────────►│                 │ 
 +    │                   │                    │                 │ 
 +    │── GET /api/res ────────────────────────────────────────►│ 
 +    │   Bearer: JWT      │                    │                 │ validate JWT 
 +    │◄── 200 response ────────────────────────────────────────│ (JWKS cache) 
 +    │                   │                    │                 │ 
 +</code>
  
-SSO is the ONLY system that: +----
-uses PRIVATE KEY +
-signs JWT tokens+
  
-Applications: +===== Scenario 2 — Already Authenticated (SSO Token Reuse===== {{anchor:scenario2}}
-use PUBLIC KEY (JWKS) +
-verify JWT signatures +
-do NOT maintain server session per user+
  
-Mobile app+This scenario covers the case where the user **already has a valid session** or 
-stores tokens securely (Keychain/Keystore) +the app has a **cached refresh_token** and needs a new access_token.
-calls APIs directly+
  
-Cookies: +==== Sub-Scenario 2aValid Access Token Still in Memory ====
-NOT used in mobile flow+
  
-===== Final Interview One-liner =====+The simplest case: the app already holds a non-expired ``access_token``.
  
-In mobile SSO (OIDC with PKCE), the app authenticates via system browserreceives an authorization code, exchanges it for JWT tokens via backend call, and then uses those tokens to directly call Server A and Server BEach server independently verifies the JWT using JWKS without maintaining sessions or involving SSO per request.+=== Step 1: Mobile App — Check Token Expiry === 
 + 
 +Before making any API call, the app checks locally: 
 + 
 +<code> 
 +decoded = JWT.decode(access_token, verify_signature=false)  // decode without verification 
 +now     = current_unix_timestamp() 
 + 
 +if decoded.exp > now + 30:   // 30-second buffer 
 +    // Token is still valid — use it directly 
 +    proceed_to_step_2() 
 +else: 
 +    // Token expired or about to expire — refresh it (Sub-Scenario 2b) 
 +    refresh_access_token() 
 +</code> 
 + 
 +=== Step 2: Mobile App → ServerA / ServerB — API Request === 
 + 
 +Identical to Scenario 1 Steps 7–9. The app reuses the cached ``access_token``: 
 + 
 +<code> 
 +GET https://serverA.example.com/api/data 
 +Authorization: Bearer <cached_access_token> 
 +</code> 
 + 
 +No interaction with ServerSSO is needed. ✓ 
 + 
 +---- 
 + 
 +==== Sub-Scenario 2b: Access Token Expired — Refresh Flow ==== 
 + 
 +The ``access_token`` has expired, but the app holds a valid ``refresh_token``. 
 + 
 +=== Step 1: Mobile App → ServerSSO — Refresh Token Request === 
 + 
 +<code> 
 +POST https://sso.example.com/token 
 +Content-Type: application/x-www-form-urlencoded 
 + 
 +grant_type=refresh_token 
 +&refresh_token=8xLOxBtZp8 
 +&client_id=mobile-app-client 
 +</code> 
 + 
 +> **Note:** No PKCE is required for the refresh grant — PKCE was only needed 
 +> for the initial authorization code exchange. 
 + 
 +=== Step 2: ServerSSO — Validate Refresh Token === 
 + 
 +ServerSSO checks: 
 +  - Refresh token exists in its database and is not revoked 
 +  - Refresh token has not expired (typically 30 days for mobile apps) 
 +  - ``client_id`` matches 
 + 
 +If validServerSSO **rotates** the refresh token (issues a new one, invalidates the old one). 
 + 
 +=== Step 3: ServerSSO → Mobile App — New Tokens === 
 + 
 +<code json> 
 +
 +  "access_token":  "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1pZC0wMDEifQ...(new)", 
 +  "token_type":    "Bearer", 
 +  "expires_in":    3600, 
 +  "id_token":      "eyJhbGciOiJSUzI1NiJ9...(new)", 
 +  "refresh_token": "9yMpACuQr9", 
 +  "scope":         "openid profile email offline_access" 
 +
 +</code> 
 + 
 +The app stores the **new** ``refresh_token`` and **new** ``access_token``. 
 + 
 +=== Step 4: Mobile App → ServerA / ServerB — API Request === 
 + 
 +The app proceeds with the freshly issued ``access_token``. 
 + 
 +<code> 
 +GET https://serverA.example.com/api/data 
 +Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1pZC0wMDEifQ...(new) 
 +</code> 
 + 
 +---- 
 + 
 +==== Sub-Scenario 2c: Both Tokens Expired — Re-Authentication ==== 
 + 
 +If the ``refresh_token`` has also expired (e.g.user was inactive for 30+ days): 
 + 
 +=== Step 1: Mobile App — Detect Expired Refresh Token === 
 + 
 +The token endpoint returns: 
 + 
 +<code json> 
 +HTTP 400 Bad Request 
 +
 +  "error": "invalid_grant", 
 +  "error_description": "Refresh token expired or revoked" 
 +
 +</code> 
 + 
 +=== Step 2: Check for Existing SSO Session (Silent Auth) === 
 + 
 +Before forcing a full re-login, the app attempts **silent authentication**: 
 + 
 +<code> 
 +GET https://sso.example.com/authorize 
 +  ?response_type=code 
 +  &client_id=mobile-app-client 
 +  &redirect_uri=myapp://callback 
 +  &scope=openid profile email offline_access 
 +  &prompt=none                          ← key parameter: no UI shown 
 +  &code_challenge=<new_code_challenge> 
 +  &code_challenge_method=S256 
 +  &state=<new_state> 
 +</code> 
 + 
 +  * If ServerSSO has a **valid browser/SSO session** (cookie): it returns a new ``code`` immediately — no login prompt → proceed with token exchange (Scenario 1, Steps 5–6) 
 +  * If no session exists: ServerSSO returns ``error=login_required`` → app must show login UI → full Scenario 1 from Step 2 
 + 
 +==== Scenario 2b — Summary Flow ==== 
 + 
 +<code> 
 +Mobile App          ServerSSO            ServerA           ServerB 
 +    │                   │                    │                 │ 
 +    │  [access_token expired, refresh_token valid]             │ 
 +    │                   │                    │                 │ 
 +    │── POST /token ────►│                    │                 │ 
 +    │   grant_type=      │                    │                 │ 
 +    │   refresh_token    │                    │                 │ 
 +    │                   │ validate RT         │                 │ 
 +    │                   │ rotate RT           │                 │ 
 +    │◄── new tokens ────│                    │                 │ 
 +    │    access_token    │                    │                 │ 
 +    │    refresh_token   │                    │                 │ 
 +    │    (rotated)       │                    │                 │ 
 +    │                   │                    │                 │ 
 +    │── GET /api/data ──────────────────────►│                 │ 
 +    │   Bearer: new JWT  │                    │ validate JWT    │ 
 +    │◄── 200 OK ─────────────────────────────│                 │ 
 +    │                   │                    │                 │ 
 +    │── GET /api/res ────────────────────────────────────────►│ 
 +    │   Bearer: new JWT  │                    │                 │ validate JWT 
 +    │◄── 200 OK ──────────────────────────────────────────────│ 
 +</code> 
 + 
 +---- 
 + 
 +===== Token Validation with JWKS (ServerA & ServerB Detail) ===== {{anchor:token-validation}} 
 + 
 +==== JWKS Caching Strategy ==== 
 + 
 +Resource servers should **not** fetch JWKS on every request. Recommended strategy: 
 + 
 +^ Condition                          ^ Action                                               ^ 
 +| Startup                            | Fetch JWKS and cache in memory                       | 
 +| JWT ``kid`` found in cache         | Use cached key — no network call                     | 
 +| JWT ``kid`` **not** found in cache | Fetch JWKS again (key rotation may have occurred)    | 
 +| Cache age > 1 hour                 | Refresh JWKS in background                           | 
 +| JWKS fetch fails                   | Use stale cache; log warning; retry with backoff     | 
 + 
 +==== JWT Validation Algorithm (Pseudocode) ==== 
 + 
 +<code> 
 +function validateJWT(tokenaudience): 
 + 
 +  // 1. Split and decode (no verification yet) 
 +  [header_b64, payload_b64, signature_b64] = token.split("."
 +  header   = base64url_decode(header_b64) 
 +  payload  = base64url_decode(payload_b64) 
 +   
 +  // 2. Look up the signing key 
 +  kid = header.kid 
 +  key = jwksCache.get(kid) 
 +  if key is null: 
 +    jwksCache.refresh()           // re-fetch from ServerSSO 
 +    key = jwksCache.get(kid) 
 +  if key is null: 
 +    throw InvalidTokenError("Unknown kid"
 + 
 +  // 3. Verify signature 
 +  message  = header_b64 + "." + payload_b64 
 +  if NOT RSA_SHA256_verify(message, signature_b64, key.publicKey): 
 +    throw InvalidTokenError("Bad signature"
 + 
 +  // 4. Validate standard claims 
 +  if payload.exp < now(): 
 +    throw InvalidTokenError("Token expired"
 +  if payload.iss != "https://sso.example.com": 
 +    throw InvalidTokenError("Wrong issuer"
 +  if audience NOT IN payload.aud: 
 +    throw InvalidTokenError("Wrong audience"
 +  if payload.nbf is set AND payload.nbf > now(): 
 +    throw InvalidTokenError("Token not yet valid"
 + 
 +  // 5. Extract and return user context 
 +  return { userId: payload.sub, roles: payload.roles, email: payload.email } 
 +</code> 
 + 
 +==== Key Rotation ==== 
 + 
 +When ServerSSO rotates its signing key: 
 + 
 +  - New JWTs are signed with the **new** key (new ``kid``) 
 +  - Old JWTs remain valid until they expire (old key stays in JWKS temporarily) 
 +  - ServerA/ServerB fetch the new JWKS when they encounter an unknown ``kid`` 
 +  - No downtime or coordination required 
 + 
 +---- 
 + 
 +===== Security Notes ===== {{anchor:security-notes}} 
 + 
 +==== Token Storage on Mobile ==== 
 + 
 +^ Token           ^ Recommended Storage                                      ^ Never store in         ^ 
 +| access_token    | In-memory only (lost on app restart)                     | SharedPreferences (Android) / UserDefaults (iOS) without encryption | 
 +| refresh_token   | iOS Keychain / Android Keystore (hardware-backed)         | Plain files, AsyncStorage (unencrypted)                             | 
 +| id_token        | In-memory only (only needed at login)                     | Anywhere persistent                                                  | 
 + 
 +==== PKCE Requirements ==== 
 + 
 +  * ``code_verifier``: must be 43–128 characters, using ``[A-Z a-z 0-9 - . _ ~]`` 
 +  * Always use ``S256`` (SHA-256) — never ``plain`` (insecure) 
 +  * Generate a **new** ``code_verifier`` for every authorization request 
 +  * Never reuse or persist ``code_verifier`` across sessions 
 + 
 +==== Redirect URI Security ==== 
 + 
 +  * Register **exact** redirect URIs in ServerSSO (no wildcards) 
 +  * Use **private-use URI schemes** (e.g., ``com.example.myapp://callback``) or HTTPS with Universal Links (iOS) / App Links (Android) 
 +  * Always validate ``state`` parameter to prevent CSRF 
 + 
 +==== Access Token Lifetime Recommendations ==== 
 + 
 +^ Token          ^ Recommended Lifetime ^ Notes                                       ^ 
 +| access_token   | 15–60 minutes        | Short life limits exposure if intercepted   | 
 +| refresh_token  | 7–30 days            | Rotate on each use (refresh token rotation) | 
 +| id_token       | 15–60 minutes        | Same as access_token                        | 
 + 
 +==== HTTPS Enforcement ==== 
 + 
 +All endpoints (authorization, token, userinfo, JWKS, and resource APIs) **must** use HTTPS. 
 +Certificate pinning is recommended for the mobile app in high-security environments. 
 + 
 +---- 
 + 
 +===== Glossary ===== {{anchor:glossary}} 
 + 
 +^ Term             ^ Definition                                                                  ^ 
 +| OIDC             | OpenID Connect — identity layer on top of OAuth 2.0                         | 
 +| OAuth 2.0        | Authorization framework; OIDC extends it with identity (id_token)            | 
 +| PKCE             | Proof Key for Code Exchange — RFC 7636; prevents code interception on mobile 
 +| JWT              | JSON Web Token — RFC 7519; compact, URL-safe token format                    | 
 +| JWKS             | JSON Web Key Set — RFC 7517; public key set exposed by the auth server        | 
 +| access_token     | Short-lived credential presented to resource servers                          | 
 +| id_token         | JWT containing user identity claims; consumed by the client only              | 
 +| refresh_token    | Long-lived credential used to obtain new access tokens                        | 
 +| code_verifier    | Random secret generated by the client for PKCE                                | 
 +| code_challenge   | SHA-256 hash of code_verifier, sent in the authorization request              | 
 +| authorization code | Short-lived, single-use code exchanged for tokens                          | 
 +| kid              | Key ID — identifies which key in the JWKS was used to sign a JWT              | 
 +| SSO              | Single Sign-On — one authentication grants access to multiple services        | 
 +| Bearer token     | HTTP authentication scheme; token is presented as-is in the Authorization header | 
 +| RS256            | RSA Signature with SHA-256; asymmetric signing algorithm for JWTs             | 
 + 
 +---- 
 + 
 +//Document maintained by the Platform Security TeamLast updated: 2026-06-16//
security/sso-mobile.1781587707.txt.gz · Last modified: by phong2018