Skip to content

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