Token Refresh Flow
sequenceDiagram
participant User as User<br/>iOS Device
participant Keychain as iOS Keychain
participant Server as Signaling Server
Note over User,Server: TOKEN-BASED AUTHENTICATION FLOW<br/>✅ Standard HTTP 401 + Error Codes
Note over User,Server: 📝 Pre-requisite: User completed initial authentication<br/>See e2ee_01_authentication_flow.mermaid
Note over User,Server: KEY IMPROVEMENTS FROM VALIDATION
Note over User,Server: ✅ USE STANDARD HTTP 401<br/>• All auth failures return 401 (not 427)<br/>• Differentiate with error codes in body<br/>• Follows RFC 7235 (HTTP Authentication)
Note over User,Server: ✅ ERROR CODES IN RESPONSE BODY<br/>• "access_token_expired" → Try refresh<br/>• "refresh_token_expired" → Force logout<br/>• "token_revoked" → Force logout<br/>• "invalid_credentials" → Force logout
Note over User,Server: ✅ REQUIRESREAUTH FLAG<br/>• Tells client if re-authentication needed<br/>• Prevents infinite refresh loops
Note over User,Server: SCENARIO 1: INITIAL LOGIN WITH TOKEN GENERATION
User->>User: Launch app<br/>Complete authentication<br/>(Email/Phone OTP or Apple/Google)
Note over Server: After successful authentication
Server->>Server: Generate Access Token<br/>Claims: {<br/> userId: 12345,<br/> deviceId: 67890,<br/> type: "access",<br/> exp: now + 15 minutes<br/>}<br/>Signed with JWT_SECRET_KEY
Server->>Server: Generate Refresh Token<br/>Claims: {<br/> userId: 12345,<br/> deviceId: 67890,<br/> type: "refresh",<br/> exp: now + 30 days<br/>}<br/>Signed with JWT_SECRET_KEY
Server-->>User: HTTP 200 OK<br/>{<br/> "accessToken": "eyJhbGc...",<br/> "refreshToken": "eyJhbGc...",<br/> "expiresIn": 900,<br/> "refreshExpiresIn": 2592000,<br/> "userId": 12345,<br/> "deviceId": 67890<br/>}
Note right of User: Access Token: 15 minutes<br/>Refresh Token: 30 days
User->>Keychain: Save tokens to Keychain:<br/>• accessToken<br/>• refreshToken<br/>• tokenExpiry (timestamp)<br/>• refreshTokenExpiry (timestamp)<br/>• userId<br/>• deviceId
Note over User: ✅ Tokens stored securely<br/>kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
Note over User,Server: SCENARIO 2: API REQUESTS WITH VALID TOKEN
User->>User: User makes API call<br/>(e.g., start call, fetch contacts)
User->>Keychain: Retrieve accessToken
Keychain-->>User: Return accessToken
User->>User: Proactive check (optional):<br/>if (now + 300 > tokenExpiry) {<br/> // Refresh 5 min before expiry<br/>}
User->>Server: GET /api/endpoint<br/>Authorization: Bearer eyJhbGc...
Server->>Server: Verify Access Token:<br/>• Check signature (HMAC-SHA256)<br/>• Check expiration<br/>• Check token type = "access"<br/>• Check not in blacklist
alt Token Valid & Not Expired
Server->>Server: Process request
Server-->>User: HTTP 200 OK<br/>{<br/> "data": {...}<br/>}
Note over User: ✅ Request successful
else Token Expired
Server-->>User: HTTP 401 Unauthorized<br/>{<br/> "error": "access_token_expired",<br/> "message": "Access token has expired"<br/>}
Note over User: Trigger automatic refresh
else Token Revoked
Server-->>User: HTTP 401 Unauthorized<br/>{<br/> "error": "token_revoked",<br/> "message": "Token has been revoked",<br/> "requiresReauth": true<br/>}
Note over User: Force logout
else Invalid Token
Server-->>User: HTTP 401 Unauthorized<br/>{<br/> "error": "invalid_credentials",<br/> "message": "Invalid authentication credentials",<br/> "requiresReauth": true<br/>}
Note over User: Force logout
end
Note over User,Server: SCENARIO 3: AUTOMATIC TOKEN REFRESH (access_token_expired)
Note over User: Access Token expired (after 15 minutes)<br/>Received 401 with error: "access_token_expired"
User->>User: TokenManager.handleAPIError()<br/>Check error code
alt Error = "access_token_expired"
Note over User: Attempt automatic refresh
User->>User: Check if refresh in progress<br/>(Thread safety)
alt Refresh Not In Progress
User->>User: Set refreshInProgress = true<br/>Create refresh task
User->>Keychain: Retrieve refreshToken
Keychain-->>User: Return refreshToken
alt RefreshToken Exists
User->>User: Check local expiry:<br/>if (now > refreshTokenExpiry) {<br/> // Don't attempt refresh<br/>}
User->>Server: POST /auth/refresh<br/>{<br/> "refreshToken": "eyJhbGc..."<br/>}
Server->>Server: Verify Refresh Token:<br/>• Check signature<br/>• Check expiration<br/>• Check type = "refresh"<br/>• Check not in blacklist
alt Refresh Token Valid
Server->>Server: Generate NEW Access Token<br/>Claims: {<br/> userId: 12345,<br/> deviceId: 67890,<br/> type: "access",<br/> exp: now + 15 minutes<br/>}
Server->>Server: Generate NEW Refresh Token<br/>Claims: {<br/> userId: 12345,<br/> deviceId: 67890,<br/> type: "refresh",<br/> exp: now + 30 days<br/>}
Note over Server: Rolling refresh:<br/>Extend 30-day window
Server-->>User: HTTP 200 OK<br/>{<br/> "accessToken": "eyJnew...",<br/> "refreshToken": "eyJnew...",<br/> "expiresIn": 900,<br/> "refreshExpiresIn": 2592000<br/>}
User->>Keychain: Update tokens in Keychain:<br/>• NEW accessToken<br/>• NEW refreshToken<br/>• NEW tokenExpiry<br/>• NEW refreshTokenExpiry
User->>User: Set refreshInProgress = false<br/>Release waiting requests
Note over User: ✅ Tokens refreshed successfully
User->>User: Retry original API request<br/>with new accessToken
else Refresh Token Expired
Server-->>User: HTTP 401 Unauthorized<br/>{<br/> "error": "refresh_token_expired",<br/> "message": "Refresh token has expired. Please login again.",<br/> "requiresReauth": true<br/>}
User->>User: Set refreshInProgress = false
Note over User: ⚠️ Refresh token expired (30 days)<br/>Re-authentication required
User->>Keychain: Delete all tokens:<br/>• accessToken<br/>• refreshToken
User->>User: Post forceLogout notification<br/>Show login screen
Note over User: User must log in again
else Refresh Token Revoked
Server-->>User: HTTP 401 Unauthorized<br/>{<br/> "error": "token_revoked",<br/> "message": "Refresh token has been revoked",<br/> "requiresReauth": true<br/>}
User->>User: Set refreshInProgress = false
User->>Keychain: Delete all tokens
User->>User: Post forceLogout notification<br/>Show login screen
else Invalid Refresh Token
Server-->>User: HTTP 401 Unauthorized<br/>{<br/> "error": "invalid_refresh_token",<br/> "message": "Invalid refresh token",<br/> "requiresReauth": true<br/>}
User->>User: Set refreshInProgress = false
User->>Keychain: Delete all tokens
User->>User: Post forceLogout notification<br/>Show login screen
end
else RefreshToken Not Found
Note over User: No refresh token in Keychain<br/>Force logout
User->>Keychain: Delete all tokens
User->>User: Post forceLogout notification<br/>Show login screen
end
else Refresh Already In Progress
Note over User: Wait for existing refresh to complete
User->>User: Await refreshTask completion
alt Refresh Succeeded
User->>User: Retry original request<br/>with new token
else Refresh Failed
User->>User: Propagate error<br/>Force logout if requiresReauth
end
end
else Error = "refresh_token_expired"
Note over User: Cannot refresh, force logout
User->>Keychain: Delete all tokens
User->>User: Post forceLogout notification<br/>Show login screen
else Error = "token_revoked"
Note over User: Token revoked, force logout
User->>Keychain: Delete all tokens
User->>User: Post forceLogout notification<br/>Show login screen
else Error = "invalid_credentials"
Note over User: Invalid credentials, force logout
User->>Keychain: Delete all tokens
User->>User: Post forceLogout notification<br/>Show login screen
end
Note over User,Server: SCENARIO 4: PROACTIVE TOKEN REFRESH (Before Expiration)
Note over User: Best practice: Refresh before token expires
User->>User: App starts / comes to foreground<br/>or before making API call
User->>Keychain: Retrieve accessToken & expiry
Keychain-->>User: Return token + expiry
User->>User: TokenManager.getValidAccessToken()<br/>Check expiry:<br/>timeUntilExpiry = tokenExpiry - now
alt Token expires soon (< 5 minutes)
Note over User: Proactively refresh before expiration<br/>Better UX - no 401 errors
User->>Keychain: Retrieve refreshToken
Keychain-->>User: Return refreshToken
User->>Server: POST /auth/refresh<br/>{<br/> "refreshToken": "eyJhbGc..."<br/>}
Server->>Server: Verify & Generate new tokens
alt Refresh Success
Server-->>User: HTTP 200 OK<br/>{<br/> "accessToken": "eyJnew...",<br/> "refreshToken": "eyJnew...",<br/> "expiresIn": 900<br/>}
User->>Keychain: Update tokens in Keychain
Note over User: ✅ Tokens refreshed proactively<br/>Seamless user experience
else Refresh Failed
Server-->>User: HTTP 401 Unauthorized<br/>{<br/> "error": "refresh_token_expired",<br/> "requiresReauth": true<br/>}
User->>Keychain: Delete all tokens
User->>User: Force logout<br/>Show login screen
end
else Token still valid
Note over User: Continue using existing token<br/>No refresh needed
end
Note over User,Server: SCENARIO 5: THREAD SAFETY (Concurrent Requests)
participant Req1 as Request 1
participant Req2 as Request 2
participant Req3 as Request 3
Note over User,Server: Multiple concurrent API requests<br/>All receive 401 access_token_expired
Req1->>Server: API Request 1
Req2->>Server: API Request 2
Req3->>Server: API Request 3
Server-->>Req1: 401 access_token_expired
Server-->>Req2: 401 access_token_expired
Server-->>Req3: 401 access_token_expired
Note over Req1,Req3: All requests need refresh
Req1->>User: Check refreshTask
Req2->>User: Check refreshTask
Req3->>User: Check refreshTask
Note over User: refreshLock.lock()<br/>Check if refresh in progress
Req1->>User: refreshTask is nil<br/>Start new refresh
User->>User: Create new Task<String, Error><br/>refreshTask = task<br/>refreshLock.unlock()
Req2->>User: refreshTask exists<br/>Await existing task
Req3->>User: refreshTask exists<br/>Await existing task
User->>Server: POST /auth/refresh<br/>(Only ONE request)
Server-->>User: 200 OK + New Tokens
User->>Keychain: Update tokens
User->>User: refreshTask completes<br/>Return new accessToken
Note over Req1,Req3: All awaiting requests receive new token
Req1->>User: Received new token<br/>Retry request
Req2->>User: Received new token<br/>Retry request
Req3->>User: Received new token<br/>Retry request
Req1->>Server: Retry with new token
Req2->>Server: Retry with new token
Req3->>Server: Retry with new token
Server-->>Req1: Success
Server-->>Req2: Success
Server-->>Req3: Success
Note over User,Server: ✅ Only ONE refresh request<br/>All concurrent requests queued and retried
Note over User,Server: SCENARIO 6: LOGOUT & TOKEN REVOCATION
User->>User: User taps "Logout"
User->>Keychain: Retrieve accessToken
Keychain-->>User: Return accessToken
User->>Server: POST /auth/logout<br/>Authorization: Bearer {accessToken}
Server->>Server: Decode accessToken<br/>Extract userId and deviceId
Server->>Server: Revoke tokens:<br/>• Add accessToken to blacklist (15 min TTL)<br/>• Add refreshToken to blacklist (30 day TTL)<br/>• Mark device session as "logged_out"
Server-->>User: HTTP 200 OK<br/>{<br/> "message": "Logged out successfully"<br/>}
User->>Keychain: Delete all tokens:<br/>• accessToken<br/>• refreshToken<br/>• userId<br/>• deviceId
User->>User: Clear app state<br/>Navigate to login screen
Note over User: ✅ User logged out<br/>All tokens revoked