> ## Documentation Index
> Fetch the complete documentation index at: https://docs.baanx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Token Management

> Master token lifecycle, refresh strategies, and best practices

## Overview

Understanding token lifecycle management is critical for building reliable applications. This guide covers everything you need to know about handling JWT tokens, access tokens, and refresh tokens throughout your application's lifecycle.

## Token Types

The API uses three distinct token types, each serving a specific purpose:

<CardGroup cols={3}>
  <Card title="JWT Token" icon="clock">
    **OAuth Flow Session**

    * Lifetime: 10 minutes
    * Purpose: OAuth flow coordination
    * Usage: Only during OAuth authorization
    * Not for API calls
  </Card>

  <Card title="Access Token" icon="key">
    **API Authentication**

    * Lifetime: 6 hours
    * Purpose: Authenticate API requests
    * Usage: All authenticated endpoints
    * Include in Authorization header
  </Card>

  <Card title="Refresh Token" icon="rotate">
    **Token Renewal**

    * Lifetime: 7 days
    * Purpose: Obtain new access tokens
    * Usage: Token refresh endpoint
    * Store securely, never expose
  </Card>
</CardGroup>

## Token Lifecycle

```mermaid theme={null}
stateDiagram-v2
    [*] --> OAuth_Flow: Start OAuth
    OAuth_Flow --> JWT_Token: Initiate (10 min)
    JWT_Token --> Authorization: Generate Code
    Authorization --> Long_Lived_Tokens: Exchange Code
    Long_Lived_Tokens --> Access_Token: Active (6 hours)
    Long_Lived_Tokens --> Refresh_Token: Active (7 days)
    Access_Token --> Expired: After 6 hours
    Expired --> Access_Token: Refresh
    Refresh_Token --> Expired_Refresh: After 7 days
    Expired_Refresh --> OAuth_Flow: Re-authenticate
    Access_Token --> Revoked: User/App Revokes
    Refresh_Token --> Revoked: User/App Revokes
    Revoked --> [*]
```

## JWT Token (OAuth Session)

### Purpose

The JWT token is used **only during the OAuth authorization flow** to maintain session state between OAuth steps.

<Warning>
  **Not for API Calls:** This token is NOT used for authenticating API requests. It's exclusively for OAuth flow coordination.
</Warning>

### Characteristics

| Property      | Value                                           |
| ------------- | ----------------------------------------------- |
| Lifetime      | 10 minutes                                      |
| Obtained from | `GET /v1/auth/oauth/authorize/initiate`         |
| Used in       | `POST /v1/auth/oauth/authorize` (API mode only) |
| Format        | JWT (JSON Web Token)                            |
| Renewal       | Not renewable - restart OAuth flow              |

### Usage Example

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Step 1: Get JWT token
  const { token: jwtToken } = await initiateOAuth();

  // Step 3: Use JWT token (API mode only)
  await fetch('/v1/auth/oauth/authorize', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessTokenFromLogin}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      token: jwtToken  // JWT token goes in body, not header
    })
  });
  ```

  ```python Python theme={null}
  # Step 1: Get JWT token
  jwt_token = initiate_oauth()['token']

  # Step 3: Use JWT token (API mode only)
  requests.post(
      '/v1/auth/oauth/authorize',
      headers={
          'Authorization': f'Bearer {access_token_from_login}'
      },
      json={
          'token': jwt_token  # JWT token goes in body, not header
      }
  )
  ```
</CodeGroup>

### Expiration Handling

<CodeGroup>
  ```typescript TypeScript theme={null}
  const SESSION_TIMEOUT = 10 * 60 * 1000; // 10 minutes

  class OAuthSession {
    private startTime: number;
    private jwtToken: string;

    constructor(token: string) {
      this.jwtToken = token;
      this.startTime = Date.now();
    }

    isExpired(): boolean {
      return Date.now() - this.startTime >= SESSION_TIMEOUT;
    }

    getToken(): string {
      if (this.isExpired()) {
        throw new Error('OAuth session expired. Please restart authorization flow.');
      }
      return this.jwtToken;
    }

    getRemainingTime(): number {
      return Math.max(0, SESSION_TIMEOUT - (Date.now() - this.startTime));
    }
  }
  ```

  ```python Python theme={null}
  import time

  SESSION_TIMEOUT = 600  # 10 minutes in seconds

  class OAuthSession:
      def __init__(self, token: str):
          self.jwt_token = token
          self.start_time = time.time()

      def is_expired(self) -> bool:
          return time.time() - self.start_time >= SESSION_TIMEOUT

      def get_token(self) -> str:
          if self.is_expired():
              raise Exception('OAuth session expired. Please restart authorization flow.')
          return self.jwt_token

      def get_remaining_time(self) -> float:
          return max(0, SESSION_TIMEOUT - (time.time() - self.start_time))
  ```
</CodeGroup>

## Access Token (API Authentication)

### Purpose

Access tokens authorize your application to make API requests on behalf of the user. Include them in the `Authorization: Bearer` header to perform user-authorized operations.

<Note>
  **Acting on Behalf of Users:** Access tokens represent the user's grant of permission for your application to interact with their account. Use them only for actions the user has authorized.
</Note>

### Characteristics

| Property      | Value                                                |
| ------------- | ---------------------------------------------------- |
| Lifetime      | 6 hours (21,600 seconds)                             |
| Obtained from | `POST /v1/auth/oauth/token` or `POST /v1/auth/login` |
| Used in       | Authorization header for all authenticated endpoints |
| Format        | JWT (JSON Web Token)                                 |
| Renewal       | Via refresh token                                    |

### Usage

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function callAPI(endpoint: string) {
    const accessToken = await getAccessToken();

    const response = await fetch(`https://api.example.com${endpoint}`, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'x-client-key': process.env.CLIENT_KEY!
      }
    });

    return response.json();
  }

  // Examples
  const user = await callAPI('/v1/users/me');
  const wallets = await callAPI('/v1/wallets');
  const cards = await callAPI('/v1/cards');
  ```

  ```python Python theme={null}
  def call_api(endpoint: str):
      access_token = get_access_token()

      response = requests.get(
          f'https://api.example.com{endpoint}',
          headers={
              'Authorization': f'Bearer {access_token}',
              'x-client-key': os.getenv('CLIENT_KEY')
          }
      )

      return response.json()

  # Examples
  user = call_api('/v1/users/me')
  wallets = call_api('/v1/wallets')
  cards = call_api('/v1/cards')
  ```

  ```bash cURL theme={null}
  # All authenticated API calls use the same pattern
  curl -X GET "https://api.example.com/v1/users/me" \
    -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
    -H "x-client-key: your_public_key"
  ```
</CodeGroup>

### Storage

<Tabs>
  <Tab title="Web Applications">
    **Recommended: HTTP-only Cookies**

    ```typescript theme={null}
    // Server-side: Set HTTP-only cookie
    res.cookie('access_token', token, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 6 * 60 * 60 * 1000 // 6 hours
    });

    // Client-side: Cookie sent automatically
    fetch('/api/protected', {
      credentials: 'include'
    });
    ```

    **Alternative: Memory Storage (for SPAs)**

    ```typescript theme={null}
    // Store in application state, not localStorage
    class TokenManager {
      private accessToken: string | null = null;

      setToken(token: string) {
        this.accessToken = token;
      }

      getToken(): string | null {
        return this.accessToken;
      }

      clearToken() {
        this.accessToken = null;
      }
    }
    ```

    <Warning>
      **Avoid localStorage for sensitive applications:** XSS attacks can steal tokens from localStorage. Use HTTP-only cookies or memory storage instead.
    </Warning>
  </Tab>

  <Tab title="Mobile Applications">
    **Use Platform Secure Storage**

    ```typescript theme={null}
    // React Native: Use expo-secure-store or react-native-keychain
    import * as SecureStore from 'expo-secure-store';

    async function storeToken(token: string) {
      await SecureStore.setItemAsync('access_token', token);
    }

    async function getToken(): Promise<string | null> {
      return await SecureStore.getItemAsync('access_token');
    }

    async function deleteToken() {
      await SecureStore.deleteItemAsync('access_token');
    }
    ```

    ```swift theme={null}
    // iOS: Use Keychain
    import KeychainSwift

    let keychain = KeychainSwift()

    func storeToken(_ token: String) {
        keychain.set(token, forKey: "access_token")
    }

    func getToken() -> String? {
        return keychain.get("access_token")
    }

    func deleteToken() {
        keychain.delete("access_token")
    }
    ```

    ```kotlin theme={null}
    // Android: Use EncryptedSharedPreferences
    import androidx.security.crypto.EncryptedSharedPreferences
    import androidx.security.crypto.MasterKey

    val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    val sharedPreferences = EncryptedSharedPreferences.create(
        context,
        "secure_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    fun storeToken(token: String) {
        sharedPreferences.edit().putString("access_token", token).apply()
    }

    fun getToken(): String? {
        return sharedPreferences.getString("access_token", null)
    }
    ```
  </Tab>

  <Tab title="Server-Side">
    **Session or Database Storage**

    ```typescript theme={null}
    // Express.js with session
    app.post('/oauth/callback', async (req, res) => {
      const tokens = await exchangeCodeForTokens(req.query.code);

      req.session.accessToken = tokens.access_token;
      req.session.refreshToken = tokens.refresh_token;
      req.session.tokenExpiry = Date.now() + (tokens.expires_in * 1000);

      res.redirect('/dashboard');
    });

    // Middleware to check token
    async function requireAuth(req, res, next) {
      if (Date.now() >= req.session.tokenExpiry) {
        req.session.accessToken = await refreshToken(req.session.refreshToken);
        req.session.tokenExpiry = Date.now() + (6 * 60 * 60 * 1000);
      }
      next();
    }
    ```

    ```python theme={null}
    # Flask with database storage
    @app.route('/oauth/callback')
    def oauth_callback():
        code = request.args.get('code')
        tokens = exchange_code_for_tokens(code)

        # Store in database
        db.session.query(UserToken).filter_by(user_id=current_user.id).update({
            'access_token': tokens['access_token'],
            'refresh_token': tokens['refresh_token'],
            'expires_at': datetime.now() + timedelta(hours=6)
        })
        db.session.commit()

        return redirect('/dashboard')
    ```
  </Tab>
</Tabs>

### Expiration Detection

<CodeGroup>
  ```typescript TypeScript theme={null}
  interface TokenInfo {
    token: string;
    expiresAt: number;
  }

  class TokenManager {
    private tokenInfo: TokenInfo | null = null;

    setToken(token: string, expiresIn: number) {
      this.tokenInfo = {
        token,
        expiresAt: Date.now() + (expiresIn * 1000)
      };
    }

    isExpired(): boolean {
      if (!this.tokenInfo) return true;

      // Add 60 second buffer to refresh before actual expiry
      return Date.now() >= (this.tokenInfo.expiresAt - 60000);
    }

    async getValidToken(): Promise<string> {
      if (this.isExpired()) {
        await this.refreshToken();
      }
      return this.tokenInfo!.token;
    }

    private async refreshToken() {
      const refreshToken = await getRefreshToken();
      const tokens = await exchangeRefreshToken(refreshToken);
      this.setToken(tokens.access_token, tokens.expires_in);
    }
  }
  ```

  ```python Python theme={null}
  from datetime import datetime, timedelta
  from typing import Optional

  class TokenManager:
      def __init__(self):
          self.token: Optional[str] = None
          self.expires_at: Optional[datetime] = None

      def set_token(self, token: str, expires_in: int):
          self.token = token
          self.expires_at = datetime.now() + timedelta(seconds=expires_in)

      def is_expired(self) -> bool:
          if not self.expires_at:
              return True

          # Add 60 second buffer to refresh before actual expiry
          return datetime.now() >= (self.expires_at - timedelta(seconds=60))

      async def get_valid_token(self) -> str:
          if self.is_expired():
              await self.refresh_token()
          return self.token

      async def refresh_token(self):
          refresh_token = get_refresh_token()
          tokens = exchange_refresh_token(refresh_token)
          self.set_token(tokens['access_token'], tokens['expires_in'])
  ```
</CodeGroup>

<Note>
  **Best Practice:** Refresh tokens 60 seconds before expiry to account for network latency and clock differences.
</Note>

## Refresh Token (Token Renewal)

### Purpose

Refresh tokens allow you to obtain new access tokens without requiring the user to re-authenticate, providing a balance between security and user convenience.

<Warning>
  **Security for Financial Operations:** Given that this API handles sensitive financial operations (setting PINs, adding funds, connecting wallets, card management), the refresh token lifetime is intentionally short at 7 days. This aligns with modern banking security standards where US financial institutions now require re-authentication after 15 minutes of inactivity.
</Warning>

<Info>
  **With Biometric Authentication:** If your application implements FaceID, TouchID, or credential saving, the 7-day refresh token lifetime provides a good balance between security and user experience. Users authenticate frequently with biometrics while maintaining secure access.
</Info>

### Characteristics

| Property      | Value                                                   |
| ------------- | ------------------------------------------------------- |
| Lifetime      | 7 days (604,800 seconds)                                |
| Obtained from | `POST /v1/auth/oauth/token` (authorization\_code grant) |
| Used in       | `POST /v1/auth/oauth/token` (refresh\_token grant)      |
| Format        | Opaque string                                           |
| Single Use    | No - can be reused until expiry                         |

### Step-Up Authentication for Privileged Operations

<Warning>
  **Critical Security Requirement:** Certain privileged operations require step-up authentication regardless of token validity. Users must re-authenticate immediately before performing these actions.
</Warning>

**Privileged operations requiring step-up authentication:**

* Setting or changing PIN codes
* Adding or withdrawing funds
* Connecting new wallets (custodial or non-custodial)
* Card activation or sensitive card operations
* Updating security settings
* Large transactions above threshold

**Implementation:**

```typescript theme={null}
async function performPrivilegedOperation(operation: string) {
  // Check if step-up authentication is recent (within 15 minutes)
  const lastStepUp = getLastStepUpTimestamp();
  const fifteenMinutes = 15 * 60 * 1000;

  if (!lastStepUp || Date.now() - lastStepUp > fifteenMinutes) {
    // Require fresh authentication
    const authenticated = await promptBiometricOrPassword();
    if (!authenticated) {
      throw new Error('Step-up authentication required');
    }
    setLastStepUpTimestamp(Date.now());
  }

  // Proceed with privileged operation
  await executePrivilegedOperation(operation);
}
```

### Refresh Flow

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function refreshAccessToken(): Promise<TokenResponse> {
    const refreshToken = await secureStorage.get('refresh_token');

    const response = await fetch('https://api.example.com/v1/auth/oauth/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-client-key': process.env.CLIENT_KEY!,
        'x-secret-key': process.env.SECRET_KEY!
      },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        refresh_token: refreshToken
      })
    });

    if (!response.ok) {
      if (response.status === 401) {
        // Refresh token expired or invalid
        throw new Error('REFRESH_TOKEN_EXPIRED');
      }
      throw new Error('Token refresh failed');
    }

    const tokens = await response.json();

    // Update stored tokens
    await secureStorage.set('access_token', tokens.access_token);
    await secureStorage.set('refresh_token', tokens.refresh_token);
    await secureStorage.set('token_expiry', Date.now() + (tokens.expires_in * 1000));

    return tokens;
  }
  ```

  ```python Python theme={null}
  def refresh_access_token() -> dict:
      refresh_token = secure_storage.get('refresh_token')

      response = requests.post(
          'https://api.example.com/v1/auth/oauth/token',
          headers={
              'x-client-key': os.getenv('CLIENT_KEY'),
              'x-secret-key': os.getenv('SECRET_KEY')
          },
          json={
              'grant_type': 'refresh_token',
              'refresh_token': refresh_token
          }
      )

      if response.status_code == 401:
          # Refresh token expired or invalid
          raise Exception('REFRESH_TOKEN_EXPIRED')

      response.raise_for_status()
      tokens = response.json()

      # Update stored tokens
      secure_storage.set('access_token', tokens['access_token'])
      secure_storage.set('refresh_token', tokens['refresh_token'])
      secure_storage.set('token_expiry', time.time() + tokens['expires_in'])

      return tokens
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.example.com/v1/auth/oauth/token" \
    -H "Content-Type: application/json" \
    -H "x-client-key: your_public_key" \
    -H "x-secret-key: your_secret_key" \
    -d '{
      "grant_type": "refresh_token",
      "refresh_token": "def50200a1b2c3d4e5f6..."
    }'
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 21600,
  "refresh_token": "def50200a1b2c3d4e5f6...",
  "refresh_token_expires_in": 604800
}
```

<Info>
  **Token Rotation:** The API returns a NEW refresh token with each refresh. Always update your stored refresh token.
</Info>

### Automatic Refresh Strategy

Implement automatic token refresh in your API client:

<CodeGroup>
  ```typescript TypeScript theme={null}
  class APIClient {
    private isRefreshing = false;
    private refreshPromise: Promise<string> | null = null;

    async request(endpoint: string, options: RequestInit = {}) {
      let token = await this.getValidToken();

      const response = await fetch(`${this.baseURL}${endpoint}`, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${token}`,
          'x-client-key': this.clientKey
        }
      });

      // If 401, token might be expired - try refreshing once
      if (response.status === 401) {
        token = await this.refreshAccessToken();

        // Retry with new token
        return fetch(`${this.baseURL}${endpoint}`, {
          ...options,
          headers: {
            ...options.headers,
            'Authorization': `Bearer ${token}`,
            'x-client-key': this.clientKey
          }
        });
      }

      return response;
    }

    private async getValidToken(): Promise<string> {
      const expiry = await secureStorage.get('token_expiry');

      // Refresh if within 60 seconds of expiry
      if (Date.now() >= (Number(expiry) - 60000)) {
        return this.refreshAccessToken();
      }

      return secureStorage.get('access_token');
    }

    private async refreshAccessToken(): Promise<string> {
      // Prevent multiple simultaneous refresh calls
      if (this.isRefreshing) {
        return this.refreshPromise!;
      }

      this.isRefreshing = true;
      this.refreshPromise = this.doRefresh();

      try {
        const token = await this.refreshPromise;
        return token;
      } finally {
        this.isRefreshing = false;
        this.refreshPromise = null;
      }
    }

    private async doRefresh(): Promise<string> {
      const refreshToken = await secureStorage.get('refresh_token');

      const response = await fetch(`${this.baseURL}/v1/auth/oauth/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-client-key': this.clientKey,
          'x-secret-key': this.secretKey
        },
        body: JSON.stringify({
          grant_type: 'refresh_token',
          refresh_token: refreshToken
        })
      });

      if (!response.ok) {
        // Refresh token expired - need to re-authenticate
        await this.handleAuthenticationRequired();
        throw new Error('REFRESH_FAILED');
      }

      const tokens = await response.json();

      await secureStorage.set('access_token', tokens.access_token);
      await secureStorage.set('refresh_token', tokens.refresh_token);
      await secureStorage.set('token_expiry', Date.now() + (tokens.expires_in * 1000));

      return tokens.access_token;
    }

    private async handleAuthenticationRequired() {
      // Clear tokens
      await secureStorage.remove('access_token');
      await secureStorage.remove('refresh_token');
      await secureStorage.remove('token_expiry');

      // Redirect to login or show login modal
      window.location.href = '/login';
    }
  }
  ```

  ```python Python theme={null}
  import asyncio
  from typing import Optional

  class APIClient:
      def __init__(self):
          self.is_refreshing = False
          self.refresh_lock = asyncio.Lock()

      async def request(self, endpoint: str, **kwargs):
          token = await self.get_valid_token()

          response = requests.request(
              url=f'{self.base_url}{endpoint}',
              headers={
                  'Authorization': f'Bearer {token}',
                  'x-client-key': self.client_key,
                  **kwargs.get('headers', {})
              },
              **{k: v for k, v in kwargs.items() if k != 'headers'}
          )

          # If 401, token might be expired - try refreshing once
          if response.status_code == 401:
              token = await self.refresh_access_token()

              # Retry with new token
              response = requests.request(
                  url=f'{self.base_url}{endpoint}',
                  headers={
                      'Authorization': f'Bearer {token}',
                      'x-client-key': self.client_key,
                      **kwargs.get('headers', {})
                  },
                  **{k: v for k, v in kwargs.items() if k != 'headers'}
              )

          return response

      async def get_valid_token(self) -> str:
          expiry = float(secure_storage.get('token_expiry'))

          # Refresh if within 60 seconds of expiry
          if time.time() >= (expiry - 60):
              return await self.refresh_access_token()

          return secure_storage.get('access_token')

      async def refresh_access_token(self) -> str:
          # Prevent multiple simultaneous refresh calls
          async with self.refresh_lock:
              return await self.do_refresh()

      async def do_refresh(self) -> str:
          refresh_token = secure_storage.get('refresh_token')

          response = requests.post(
              f'{self.base_url}/v1/auth/oauth/token',
              headers={
                  'x-client-key': self.client_key,
                  'x-secret-key': self.secret_key
              },
              json={
                  'grant_type': 'refresh_token',
                  'refresh_token': refresh_token
              }
          )

          if response.status_code == 401:
              # Refresh token expired - need to re-authenticate
              await self.handle_authentication_required()
              raise Exception('REFRESH_FAILED')

          tokens = response.json()

          secure_storage.set('access_token', tokens['access_token'])
          secure_storage.set('refresh_token', tokens['refresh_token'])
          secure_storage.set('token_expiry', time.time() + tokens['expires_in'])

          return tokens['access_token']

      async def handle_authentication_required(self):
          # Clear tokens
          secure_storage.remove('access_token')
          secure_storage.remove('refresh_token')
          secure_storage.remove('token_expiry')

          # Trigger re-authentication
          raise Exception('AUTHENTICATION_REQUIRED')
  ```
</CodeGroup>

## Token Revocation

Understanding when and how to revoke tokens is crucial for security and user experience:

### Token Revocation Policies

<Tabs>
  <Tab title="Login Flow Tokens">
    **Standard Login Access Tokens**

    Login access tokens from `POST /v1/auth/login` are **short-lived and irrevocable**:

    <Info>
      **Key Points:**

      * Access tokens expire after 6 hours automatically
      * No explicit revocation needed on login failure
      * Tokens naturally expire if not used
      * Cannot be refreshed - user must re-authenticate
    </Info>

    **When OTP Fails:**

    ```javascript theme={null}
    // OTP failure scenario
    const loginResponse = await login(email, password);

    if (loginResponse.isOtpRequired) {
      await sendOtp(loginResponse.userId);

      // User enters wrong OTP
      try {
        await loginWithOtp(email, password, wrongOtp);
      } catch (error) {
        // ✅ You can retry with the same credentials
        // No need to revoke anything
        const newOtp = await promptUserForNewOtp();
        await loginWithOtp(email, password, newOtp);
      }
    }
    ```

    **Best Practice:**

    * Allow multiple OTP retry attempts
    * No revocation necessary - tokens expire naturally
    * Clear tokens from local storage on user logout only
  </Tab>

  <Tab title="OAuth Flow Tokens">
    **OAuth Access and Refresh Tokens**

    OAuth tokens should be explicitly revoked in specific scenarios:

    <Warning>
      **When to Revoke OAuth Tokens:**

      * User explicitly logs out
      * User revokes app access via settings
      * Security incident detected
      * App uninstall or data clear
    </Warning>

    **When NOT to Revoke:**

    ```javascript theme={null}
    // ❌ DON'T revoke on OTP failure during OAuth flow
    const oauthResponse = await completeOAuthFlow();

    if (oauthResponse.isOtpRequired) {
      // OTP fails - just retry, don't revoke
      try {
        await verifyOtp(otp);
      } catch (error) {
        // ✅ Retry without revoking
        await verifyOtp(newOtp);
      }
    }
    ```

    **When TO Revoke:**

    ```javascript theme={null}
    // ✅ DO revoke on explicit logout
    async function logout() {
      await revokeOAuthTokens();  // Explicit revocation
      await clearLocalTokens();
      redirectToLogin();
    }

    // ✅ DO revoke on security events
    async function handleSecurityEvent() {
      await revokeOAuthTokens();
      await notifyUser('Your session has been terminated for security reasons');
    }
    ```
  </Tab>

  <Tab title="Revocation Decision Tree">
    Use this decision tree to determine if revocation is needed:

    ```
    ┌─ Login Flow?
    │
    ├─ YES → No revocation needed
    │         - Tokens expire in 6 hours
    │         - OTP failures: just retry
    │         - Login failures: tokens never valid
    │
    └─ NO (OAuth Flow) → Check scenario:
        │
        ├─ User clicking "Logout"? → ✅ REVOKE
        ├─ User revoking app access? → ✅ REVOKE
        ├─ Security incident? → ✅ REVOKE
        ├─ OTP failure? → ❌ DON'T REVOKE (retry)
         ├─ Successful token issued? → ❌ DON'T REVOKE (use it)
         └─ Token refresh? → ❌ DON'T REVOKE (normal flow)
    ```
  </Tab>
</Tabs>

### OAuth Token Revocation Implementation

Revoke OAuth tokens when users log out or revoke app access:

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function revokeAccess() {
    const accessToken = await secureStorage.get('access_token');

    try {
      await fetch('https://api.example.com/v1/auth/oauth/revoke', {
        method: 'DELETE',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'x-client-key': process.env.CLIENT_KEY!
        }
      });
    } finally {
      // Always clear local tokens, even if API call fails
      await secureStorage.remove('access_token');
      await secureStorage.remove('refresh_token');
      await secureStorage.remove('token_expiry');
    }
  }

  // Example: Logout with proper revocation
  async function logout() {
    try {
      await revokeAccess();
    } catch (error) {
      console.error('Revocation failed, but continuing logout:', error);
    } finally {
      // Always clear local state, even if revocation fails
      await clearAllTokens();
      redirectToLogin();
    }
  }
  ```

  ```python Python theme={null}
  def revoke_access():
      access_token = secure_storage.get('access_token')

      try:
          requests.delete(
              'https://api.example.com/v1/auth/oauth/revoke',
              headers={
                  'Authorization': f'Bearer {access_token}',
                  'x-client-key': os.getenv('CLIENT_KEY')
              }
          )
      finally:
          # Always clear local tokens, even if API call fails
          secure_storage.remove('access_token')
          secure_storage.remove('refresh_token')
          secure_storage.remove('token_expiry')

  # Example: Logout with proper revocation
  def logout():
      try:
          revoke_access()
      except Exception as error:
          print(f'Revocation failed, but continuing logout: {error}')
      finally:
          # Always clear local state, even if revocation fails
          clear_all_tokens()
          redirect_to_login()
  ```

  ```bash cURL theme={null}
  curl -X DELETE "https://api.example.com/v1/auth/oauth/revoke" \
    -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
    -H "x-client-key: your_public_key"
  ```
</CodeGroup>

### Revocation Response

**Success:**

```json theme={null}
{
  "success": true,
  "message": "Tokens revoked successfully"
}
```

**Failure Scenarios:**

```json theme={null}
// Token already expired or invalid
{
  "error": "invalid_token",
  "message": "Token is already invalid"
}

// Token not found
{
  "error": "not_found",
  "message": "No active tokens found for this user"
}
```

<Note>
  **Effect of OAuth Revocation:** Both access and refresh tokens become invalid immediately. Users must complete the OAuth flow again to regain access.
</Note>

### Best Practices for Token Revocation

<CardGroup cols={2}>
  <Card title="Always Clear Local Storage" icon="broom">
    Even if the API revocation call fails, always clear tokens from local storage to prevent unauthorized access.

    ```javascript theme={null}
    try {
      await revokeAccess();
    } finally {
      await clearAllTokens();  // Always execute
    }
    ```
  </Card>

  <Card title="Handle Revocation Failures Gracefully" icon="shield-check">
    Network issues shouldn't block logout. Log the error and continue.

    ```javascript theme={null}
    try {
      await revokeAccess();
    } catch (error) {
      logError('Revocation failed', error);
      // Continue logout anyway
    }
    ```
  </Card>

  <Card title="Don't Revoke on Retry Scenarios" icon="rotate">
    Failed OTP or temporary errors should allow retry without revocation.

    ```javascript theme={null}
    // ❌ Wrong
    if (otpFailed) {
      await revokeTokens();  // Don't do this
    }

    // ✅ Correct
    if (otpFailed) {
      await promptForNewOtp();  // Just retry
    }
    ```
  </Card>

  <Card title="Revoke on Security Events" icon="triangle-exclamation">
    Immediately revoke tokens when security issues are detected.

    ```javascript theme={null}
    if (suspiciousActivity) {
      await revokeAccess();
      await notifySecurityTeam();
      await alertUser();
    }
    ```
  </Card>
</CardGroup>

## Best Practices

### 1. Proactive Refresh

<Card title="Refresh Before Expiry" icon="clock-rotate-left">
  Don't wait for a 401 error. Refresh tokens 60 seconds before they expire to ensure uninterrupted service.

  ```typescript theme={null}
  if (Date.now() >= (tokenExpiry - 60000)) {
    await refreshAccessToken();
  }
  ```
</Card>

### 2. Race Condition Prevention

<Card title="Prevent Concurrent Refreshes" icon="lock">
  Use locks or flags to prevent multiple simultaneous refresh requests.

  ```typescript theme={null}
  private isRefreshing = false;
  private refreshPromise: Promise<string> | null = null;

  async refreshAccessToken() {
    if (this.isRefreshing) {
      return this.refreshPromise!;
    }
    this.isRefreshing = true;
    // ... refresh logic
  }
  ```
</Card>

### 3. Secure Storage

<Card title="Use Platform Secure Storage" icon="vault">
  Never store tokens in:

  * localStorage (web)
  * AsyncStorage (React Native)
  * Plain SharedPreferences (Android)
  * UserDefaults (iOS)

  Always use:

  * HTTP-only cookies (web)
  * SecureStore (React Native)
  * Keychain (iOS)
  * EncryptedSharedPreferences (Android)

  **OWASP Guidance:** Review [OWASP Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) and [OWASP HTML5 Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html) for comprehensive security guidance.
</Card>

## OWASP Security Best Practices

The following recommendations align with [OWASP (Open Web Application Security Project)](https://owasp.org) security standards for token storage and session management.

### Web Applications

<Warning>
  **XSS Vulnerability Risk:** A single Cross-Site Scripting (XSS) attack can steal ALL tokens stored in localStorage or sessionStorage. These storage mechanisms are always accessible to JavaScript running on your page.
</Warning>

#### Cookie-Based Storage (Recommended)

HTTP-only cookies provide the strongest protection against XSS attacks:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Server-side: Express.js example
  app.post('/auth/callback', async (req, res) => {
    const tokens = await exchangeCodeForTokens(req.query.code);

    res.cookie('access_token', tokens.access_token, {
      httpOnly: true,        // Prevents JavaScript access
      secure: true,          // HTTPS only
      sameSite: 'strict',    // CSRF protection
      maxAge: 6 * 60 * 60 * 1000,  // 6 hours
      domain: '.yourdomain.com',   // Narrow domain scope
      path: '/api'           // Narrow path scope
    });

    res.cookie('refresh_token', tokens.refresh_token, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000,  // 7 days
      domain: '.yourdomain.com',
      path: '/api/auth/refresh'  // Even narrower path
    });

    res.redirect('/dashboard');
  });

  // Client-side: Cookies sent automatically
  fetch('/api/users/me', {
    credentials: 'include'  // Include cookies in request
  });
  ```

  ```python Python theme={null}
  from flask import Flask, request, make_response

  @app.route('/auth/callback')
  def auth_callback():
      tokens = exchange_code_for_tokens(request.args.get('code'))

      response = make_response(redirect('/dashboard'))

      # Access token cookie
      response.set_cookie(
          'access_token',
          tokens['access_token'],
          httponly=True,      # Prevents JavaScript access
          secure=True,        # HTTPS only
          samesite='Strict',  # CSRF protection
          max_age=6 * 60 * 60,  # 6 hours
          domain='.yourdomain.com',
          path='/api'
      )

      # Refresh token cookie
      response.set_cookie(
          'refresh_token',
          tokens['refresh_token'],
          httponly=True,
          secure=True,
          samesite='Strict',
          max_age=7 * 24 * 60 * 60,  # 7 days
          domain='.yourdomain.com',
          path='/api/auth/refresh'
      )

      return response
  ```
</CodeGroup>

**OWASP Cookie Security Attributes:**

| Attribute         | Purpose                                                          | Required |
| ----------------- | ---------------------------------------------------------------- | -------- |
| `HttpOnly`        | Prevents JavaScript from reading the cookie (XSS protection)     | ✅ Yes    |
| `Secure`          | Cookie only sent over HTTPS connections                          | ✅ Yes    |
| `SameSite=Strict` | Prevents CSRF attacks by blocking cross-site cookie transmission | ✅ Yes    |
| `Domain`          | Limit cookie to specific domain (narrow scope)                   | ✅ Yes    |
| `Path`            | Limit cookie to specific path (narrow scope)                     | ✅ Yes    |
| `Max-Age`         | Cookie expiration time in seconds                                | ✅ Yes    |

<Note>
  **SameSite Options:**

  * `Strict`: Cookie never sent on cross-site requests (most secure)
  * `Lax`: Cookie sent on top-level navigation (GET requests)
  * `None`: Cookie sent on all requests (requires `Secure` flag)
</Note>

#### Memory Storage (Alternative for SPAs)

For Single-Page Applications where cookies aren't feasible, use in-memory storage:

```typescript theme={null}
class SecureTokenManager {
  private accessToken: string | null = null;
  private refreshToken: string | null = null;
  private tokenExpiry: number | null = null;

  setTokens(access: string, refresh: string, expiresIn: number) {
    this.accessToken = access;
    this.refreshToken = refresh;
    this.tokenExpiry = Date.now() + (expiresIn * 1000);
  }

  getAccessToken(): string | null {
    if (this.isExpired()) {
      return null;
    }
    return this.accessToken;
  }

  getRefreshToken(): string | null {
    return this.refreshToken;
  }

  clearTokens() {
    this.accessToken = null;
    this.refreshToken = null;
    this.tokenExpiry = null;
  }

  private isExpired(): boolean {
    if (!this.tokenExpiry) return true;
    return Date.now() >= (this.tokenExpiry - 60000);
  }
}

const tokenManager = new SecureTokenManager();
```

<Warning>
  **Memory Storage Limitation:** Tokens are lost on page refresh. You'll need to implement a refresh mechanism or use short-lived sessions. This trade-off prioritizes security over convenience.
</Warning>

#### localStorage: Last Resort Only

<Warning>
  **⚠️ Use Only If Absolutely Necessary:** localStorage should be avoided for token storage. If you must use it, implement these additional protections:
</Warning>

```typescript theme={null}
class LocalStorageTokenManager {
  private readonly ACCESS_TOKEN_KEY = 'app_access_token';
  private readonly REFRESH_TOKEN_KEY = 'app_refresh_token';

  async setTokens(access: string, refresh: string) {
    // Encrypt before storing
    const encryptedAccess = await this.encrypt(access);
    const encryptedRefresh = await this.encrypt(refresh);

    localStorage.setItem(this.ACCESS_TOKEN_KEY, encryptedAccess);
    localStorage.setItem(this.REFRESH_TOKEN_KEY, encryptedRefresh);
  }

  async getAccessToken(): Promise<string | null> {
    const encrypted = localStorage.getItem(this.ACCESS_TOKEN_KEY);
    if (!encrypted) return null;

    return await this.decrypt(encrypted);
  }

  private async encrypt(data: string): Promise<string> {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);

    const key = await this.getEncryptionKey();
    const iv = crypto.getRandomValues(new Uint8Array(12));

    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      key,
      dataBuffer
    );

    const combined = new Uint8Array(iv.length + encrypted.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(encrypted), iv.length);

    return btoa(String.fromCharCode(...combined));
  }

  private async decrypt(data: string): Promise<string> {
    const combined = Uint8Array.from(atob(data), c => c.charCodeAt(0));
    const iv = combined.slice(0, 12);
    const encrypted = combined.slice(12);

    const key = await this.getEncryptionKey();

    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv },
      key,
      encrypted
    );

    const decoder = new TextDecoder();
    return decoder.decode(decrypted);
  }

  private async getEncryptionKey(): Promise<CryptoKey> {
    // In production, derive from secure source
    // This is a simplified example
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode('your-256-bit-secret-key-here!!'),
      'PBKDF2',
      false,
      ['deriveBits', 'deriveKey']
    );

    return crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: new TextEncoder().encode('your-salt'),
        iterations: 100000,
        hash: 'SHA-256'
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt', 'decrypt']
    );
  }

  clearTokens() {
    localStorage.removeItem(this.ACCESS_TOKEN_KEY);
    localStorage.removeItem(this.REFRESH_TOKEN_KEY);
  }
}
```

<Note>
  **OWASP Encryption Standards:**

  * Use AES-256-GCM for symmetric encryption (minimum 128-bit, prefer 256-bit)
  * Use secure key derivation (PBKDF2, scrypt, or Argon2)
  * Never hardcode encryption keys
  * Rotate keys regularly
</Note>

### Mobile Applications

Mobile apps must use platform-native secure storage mechanisms to leverage hardware-backed encryption.

<Warning>
  **OWASP Mobile Top 10 - M9:** Insecure data storage is one of the top mobile security risks. Never store tokens in:

  * AsyncStorage (React Native)
  * Plain SharedPreferences (Android)
  * UserDefaults (iOS)
  * Application sandboxed directories without encryption
</Warning>

#### iOS: Keychain Services

<CodeGroup>
  ```swift Swift theme={null}
  import Security
  import Foundation

  class KeychainTokenManager {
      private let service = "com.yourapp.tokens"

      func saveToken(_ token: String, forKey key: String) throws {
          let data = token.data(using: .utf8)!

          let query: [String: Any] = [
              kSecClass as String: kSecClassGenericPassword,
              kSecAttrService as String: service,
              kSecAttrAccount as String: key,
              kSecValueData as String: data,
              kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
          ]

          // Delete any existing item
          SecItemDelete(query as CFDictionary)

          let status = SecItemAdd(query as CFDictionary, nil)
          guard status == errSecSuccess else {
              throw KeychainError.saveFailed
          }
      }

      func getToken(forKey key: String) throws -> String? {
          let query: [String: Any] = [
              kSecClass as String: kSecClassGenericPassword,
              kSecAttrService as String: service,
              kSecAttrAccount as String: key,
              kSecReturnData as String: true,
              kSecMatchLimit as String: kSecMatchLimitOne
          ]

          var result: AnyObject?
          let status = SecItemCopyMatching(query as CFDictionary, &result)

          guard status == errSecSuccess,
                let data = result as? Data,
                let token = String(data: data, encoding: .utf8) else {
              return nil
          }

          return token
      }

      func deleteToken(forKey key: String) {
          let query: [String: Any] = [
              kSecClass as String: kSecClassGenericPassword,
              kSecAttrService as String: service,
              kSecAttrAccount as String: key
          ]

          SecItemDelete(query as CFDictionary)
      }

      func deleteAllTokens() {
          let query: [String: Any] = [
              kSecClass as String: kSecClassGenericPassword,
              kSecAttrService as String: service
          ]

          SecItemDelete(query as CFDictionary)
      }
  }

  // Usage
  let keychainManager = KeychainTokenManager()
  try keychainManager.saveToken(accessToken, forKey: "access_token")
  try keychainManager.saveToken(refreshToken, forKey: "refresh_token")
  ```

  ```typescript React Native (iOS) theme={null}
  import * as SecureStore from 'expo-secure-store';

  class SecureTokenStorage {
    private readonly ACCESS_TOKEN_KEY = 'access_token';
    private readonly REFRESH_TOKEN_KEY = 'refresh_token';

    async saveTokens(accessToken: string, refreshToken: string) {
      await Promise.all([
        SecureStore.setItemAsync(this.ACCESS_TOKEN_KEY, accessToken, {
          keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
        }),
        SecureStore.setItemAsync(this.REFRESH_TOKEN_KEY, refreshToken, {
          keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
        })
      ]);
    }

    async getAccessToken(): Promise<string | null> {
      return await SecureStore.getItemAsync(this.ACCESS_TOKEN_KEY);
    }

    async getRefreshToken(): Promise<string | null> {
      return await SecureStore.getItemAsync(this.REFRESH_TOKEN_KEY);
    }

    async deleteAllTokens() {
      await Promise.all([
        SecureStore.deleteItemAsync(this.ACCESS_TOKEN_KEY),
        SecureStore.deleteItemAsync(this.REFRESH_TOKEN_KEY)
      ]);
    }
  }
  ```
</CodeGroup>

**iOS Keychain Accessibility Options:**

| Option                                             | Description                                         | Use Case                     |
| -------------------------------------------------- | --------------------------------------------------- | ---------------------------- |
| `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`     | Accessible only when device unlocked, not backed up | ✅ Recommended for tokens     |
| `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | Accessible after first unlock, not backed up        | Background processing        |
| `kSecAttrAccessibleWhenUnlocked`                   | Accessible when unlocked, backed up to iCloud       | ❌ Avoid (iCloud backup risk) |

#### Android: Keystore & EncryptedSharedPreferences

<CodeGroup>
  ```kotlin Kotlin theme={null}
  import androidx.security.crypto.EncryptedSharedPreferences
  import androidx.security.crypto.MasterKey
  import android.content.Context

  class SecureTokenStorage(context: Context) {
      private val masterKey = MasterKey.Builder(context)
          .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
          .build()

      private val sharedPreferences = EncryptedSharedPreferences.create(
          context,
          "secure_token_prefs",
          masterKey,
          EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
          EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
      )

      fun saveTokens(accessToken: String, refreshToken: String) {
          sharedPreferences.edit().apply {
              putString("access_token", accessToken)
              putString("refresh_token", refreshToken)
              putLong("token_saved_at", System.currentTimeMillis())
              apply()
          }
      }

      fun getAccessToken(): String? {
          return sharedPreferences.getString("access_token", null)
      }

      fun getRefreshToken(): String? {
          return sharedPreferences.getString("refresh_token", null)
      }

      fun deleteAllTokens() {
          sharedPreferences.edit().clear().apply()
      }

      fun isTokenExpired(expirySeconds: Long): Boolean {
          val savedAt = sharedPreferences.getLong("token_saved_at", 0)
          val currentTime = System.currentTimeMillis()
          val elapsed = (currentTime - savedAt) / 1000
          return elapsed >= (expirySeconds - 60)
      }
  }

  // Usage
  val storage = SecureTokenStorage(context)
  storage.saveTokens(accessToken, refreshToken)
  ```

  ```typescript React Native (Android) theme={null}
  import * as SecureStore from 'expo-secure-store';

  class SecureTokenStorage {
    private readonly ACCESS_TOKEN_KEY = 'access_token';
    private readonly REFRESH_TOKEN_KEY = 'refresh_token';

    async saveTokens(accessToken: string, refreshToken: string) {
      await Promise.all([
        SecureStore.setItemAsync(this.ACCESS_TOKEN_KEY, accessToken, {
          keychainService: 'com.yourapp.tokens'
        }),
        SecureStore.setItemAsync(this.REFRESH_TOKEN_KEY, refreshToken, {
          keychainService: 'com.yourapp.tokens'
        })
      ]);
    }

    async getAccessToken(): Promise<string | null> {
      return await SecureStore.getItemAsync(this.ACCESS_TOKEN_KEY);
    }

    async getRefreshToken(): Promise<string | null> {
      return await SecureStore.getItemAsync(this.REFRESH_TOKEN_KEY);
    }

    async deleteAllTokens() {
      await Promise.all([
        SecureStore.deleteItemAsync(this.ACCESS_TOKEN_KEY),
        SecureStore.deleteItemAsync(this.REFRESH_TOKEN_KEY)
      ]);
    }
  }
  ```
</CodeGroup>

**Android Security Features:**

* **Hardware-Backed Keystore:** Keys stored in Trusted Execution Environment (TEE) or Secure Element
* **AES-256-GCM Encryption:** Industry-standard authenticated encryption
* **Key Derivation:** EncryptedSharedPreferences handles key generation and rotation
* **Biometric Binding:** Optional binding to device biometrics

### Additional Security Measures

<CardGroup cols={2}>
  <Card title="Transport Security" icon="lock">
    **HTTPS Everywhere**

    * Always use HTTPS for token transmission
    * Implement certificate pinning for mobile apps
    * Validate SSL/TLS certificates
    * Use TLS 1.2 or higher

    ```typescript theme={null}
    const API_BASE = 'https://api.example.com'; // Never HTTP

    fetch(API_BASE + '/endpoint', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    ```
  </Card>

  <Card title="Token Binding" icon="fingerprint">
    **Additional Context Validation**

    Bind tokens to additional user context:

    * Device fingerprint
    * IP address (with caution)
    * User-Agent string
    * Geolocation (for high-risk operations)

    ```typescript theme={null}
    const deviceFingerprint = await generateFingerprint();

    await fetch('/api/sensitive-operation', {
      headers: {
        'Authorization': `Bearer ${token}`,
        'X-Device-Fingerprint': deviceFingerprint
      }
    });
    ```
  </Card>

  <Card title="Token Rotation" icon="arrows-rotate">
    **Regular Token Rotation**

    * Refresh tokens before expiry
    * Rotate refresh tokens on each use
    * Implement token versioning
    * Revoke old tokens immediately

    ```typescript theme={null}
    const tokens = await refreshToken(oldRefreshToken);

    // New refresh token returned
    await storage.save('refresh_token', tokens.refresh_token);

    // Old token now invalid
    ```
  </Card>

  <Card title="Logging & Monitoring" icon="chart-line">
    **Security Event Logging**

    Log security-relevant events:

    * Token issuance
    * Token refresh attempts
    * Failed authentication
    * Token revocation
    * Unusual access patterns

    ```typescript theme={null}
    logger.info('Token refreshed', {
      userId: user.id,
      timestamp: new Date().toISOString(),
      ipAddress: req.ip,
      userAgent: req.headers['user-agent']
    });
    ```
  </Card>
</CardGroup>

### OWASP Security Checklist

Use this checklist to ensure your token storage implementation follows OWASP best practices:

**Web Applications:**

* [ ] Tokens stored in HTTP-only cookies (not localStorage)
* [ ] `Secure` flag set on all cookies (HTTPS only)
* [ ] `SameSite=Strict` or `Lax` set on cookies
* [ ] Narrow `Domain` and `Path` cookie attributes
* [ ] Cookies expire with token lifetime
* [ ] HTTPS used for entire application
* [ ] XSS protection headers implemented (`X-XSS-Protection`, `Content-Security-Policy`)
* [ ] Tokens never logged or exposed in URLs

**Mobile Applications:**

* [ ] iOS: Tokens stored in Keychain with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`
* [ ] Android: Tokens stored using EncryptedSharedPreferences with AES-256-GCM
* [ ] No tokens stored in AsyncStorage, SharedPreferences, or UserDefaults
* [ ] Certificate pinning implemented
* [ ] Root/jailbreak detection implemented
* [ ] No tokens in application logs or crash reports
* [ ] Tokens cleared on app uninstall or logout

**General:**

* [ ] Access tokens short-lived (6 hours or less)
* [ ] Refresh tokens limited lifetime (7 days for financial apps)
* [ ] Token refresh implemented with 60-second buffer
* [ ] Automatic logout on token expiry
* [ ] Token revocation on logout
* [ ] No tokens in version control or environment files
* [ ] Encryption keys rotated regularly
* [ ] Security event logging implemented

<Note>
  **Compliance Note:** For applications handling financial data or PHI (Protected Health Information), additional requirements may apply under PCI DSS, HIPAA, or regional regulations (GDPR, CCPA).
</Note>

### 4. Error Handling

<Card title="Handle Refresh Failures Gracefully" icon="triangle-exclamation">
  When refresh fails, clear tokens and redirect to login:

  ```typescript theme={null}
  if (response.status === 401) {
    await clearAllTokens();
    redirectToLogin();
  }
  ```
</Card>

### 5. Token Cleanup

<Card title="Clear Tokens on Logout" icon="broom">
  Always clear tokens from storage when users log out:

  ```typescript theme={null}
  async function logout() {
    await revokeAccess();  // API call
    await clearAllTokens();  // Local cleanup
    redirectToLogin();
  }
  ```
</Card>

## Troubleshooting

<AccordionGroup>
  <Accordion title="401 Unauthorized after refresh" icon="ban">
    **Cause:** Refresh token expired (7 days) or was revoked.

    **Solution:**

    * Clear all tokens from storage
    * Redirect user to login
    * Restart OAuth flow from Step 1

    ```typescript theme={null}
    if (refreshResponse.status === 401) {
      await clearAllTokens();
      window.location.href = '/login?session_expired=true';
    }
    ```
  </Accordion>

  <Accordion title="Token confusion (using wrong token)" icon="arrows-rotate">
    **Symptom:** 401 errors despite having valid tokens.

    **Solution:**

    * JWT Token: Only for OAuth flow (Step 3 body in API mode)
    * Access Token (login): Only for Step 3 Authorization header (API mode)
    * Access Token (final): For all subsequent API calls
    * Refresh Token: Only for token refresh endpoint

    ```typescript theme={null}
    // CORRECT usage
    // Step 3 (API mode)
    headers: { 'Authorization': `Bearer ${loginAccessToken}` },
    body: { token: jwtToken }

    // All other API calls
    headers: { 'Authorization': `Bearer ${finalAccessToken}` }
    ```
  </Accordion>

  <Accordion title="Multiple refresh requests" icon="clone">
    **Cause:** Race condition when multiple API calls detect expiry simultaneously.

    **Solution:**

    * Implement refresh lock/flag
    * Return same promise for concurrent refresh calls
    * Queue API calls during refresh

    ```typescript theme={null}
    if (this.isRefreshing) {
      // Wait for existing refresh to complete
      await this.refreshPromise;
      return this.getToken();
    }
    ```
  </Accordion>

  <Accordion title="Clock skew issues" icon="clock">
    **Cause:** Server and client clocks are out of sync.

    **Solution:**

    * Add 60-second buffer before expiry
    * Use server time in responses when possible
    * Handle 401s gracefully with automatic retry

    ```typescript theme={null}
    const EXPIRY_BUFFER = 60000; // 60 seconds
    if (Date.now() >= (tokenExpiry - EXPIRY_BUFFER)) {
      await refreshToken();
    }
    ```
  </Accordion>
</AccordionGroup>

## Testing Checklist

**Functional Testing:**

* [ ] Tokens stored in secure storage
* [ ] Access token automatically refreshes before expiry
* [ ] Refresh token failure triggers re-authentication
* [ ] Concurrent API calls don't cause multiple refreshes
* [ ] 401 errors trigger token refresh and retry
* [ ] Logout clears all tokens from storage
* [ ] Token expiry has 60-second buffer
* [ ] Revocation invalidates tokens immediately
* [ ] Token confusion doesn't occur (right token in right place)
* [ ] Clock skew doesn't cause premature expiry

**Security Testing:**

See the comprehensive [OWASP Security Checklist](#owasp-security-checklist) above for platform-specific security requirements including:

* Web application cookie security
* Mobile platform secure storage verification
* Transport security and encryption
* Compliance requirements

## Next Steps

<CardGroup cols={2}>
  <Card title="Security Best Practices" icon="shield" href="/guides/oauth/security">
    Essential security guidelines for production
  </Card>

  <Card title="Troubleshooting Guide" icon="screwdriver-wrench" href="/guides/oauth/troubleshooting">
    Common issues and solutions
  </Card>

  <Card title="Hosted UI Flow" icon="browser" href="/guides/oauth/hosted-ui">
    Implement OAuth with hosted authentication
  </Card>

  <Card title="API Mode Flow" icon="code" href="/guides/oauth/api-mode">
    Implement OAuth with custom UI
  </Card>
</CardGroup>
