Token Lifecycle

Understanding OAuth token expiration, refresh flows, and session management.

Proper token lifecycle management is critical for a seamless user experience. This guide covers how tokens expire, how to refresh them, and how to handle edge cases.

Token Expiration Overview

Token TypeLifetimeRenewable
Access Token1 hourYes, via refresh token
Refresh Token30 daysNo, user must re-authorize

Access Tokens

Access tokens are short-lived (1 hour) and used for API authentication. When an access token expires, use the refresh token to obtain a new one without user interaction.

Refresh Tokens

Refresh tokens are longer-lived (30 days) and used exclusively to obtain new access tokens. When a refresh token expires, the user must complete the full authorization flow again.

Token Refresh Flow

┌──────────┐                              ┌─────────────┐
│  Your    │                              │ Euler Stream │
│   App    │                              │    API      │
└────┬─────┘                              └──────┬──────┘
     │                                           │
     │  1. API request with expired access token │
     │──────────────────────────────────────────>│
     │                                           │
     │  2. 401 Unauthorized                      │
     │<──────────────────────────────────────────│
     │                                           │
     │  3. POST /oauth/token (refresh_token)     │
     │──────────────────────────────────────────>│
     │                                           │
     │  4. New access_token + refresh_token      │
     │<──────────────────────────────────────────│
     │                                           │
     │  5. Retry original request                │
     │──────────────────────────────────────────>│
     │                                           │
     │  6. Success                               │
     │<──────────────────────────────────────────│

Refreshing Access Tokens

interface TokenResponse {
  access_token: string;
  refresh_token: string;
  token_type: 'Bearer';
  expires_in: number;
  scope: string;
}
 
async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
  const response = await fetch('https://tiktok.eulerstream.com/tiktok/oauth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
    }),
  });
 
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error_description || 'Token refresh failed');
  }
 
  return response.json();
}

Important: A successful refresh returns both a new access token AND a new refresh token. Always store the new refresh token—the old one may be invalidated.

Proactive Token Refresh

Don't wait for a 401 error. Refresh tokens proactively before they expire:

interface StoredTokens {
  accessToken: string;
  refreshToken: string;
  accessTokenExpiresAt: Date;
  refreshTokenExpiresAt: Date;
}
 
class TokenManager {
  private tokens: StoredTokens;
  private refreshPromise: Promise<void> | null = null;
 
  constructor(tokens: StoredTokens) {
    this.tokens = tokens;
  }
 
  async getValidAccessToken(): Promise<string> {
    // Check if access token expires within 5 minutes
    const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000);
 
    if (this.tokens.accessTokenExpiresAt < fiveMinutesFromNow) {
      await this.refreshTokens();
    }
 
    return this.tokens.accessToken;
  }
 
  private async refreshTokens(): Promise<void> {
    // Prevent concurrent refresh attempts
    if (this.refreshPromise) {
      return this.refreshPromise;
    }
 
    this.refreshPromise = this.doRefresh();
 
    try {
      await this.refreshPromise;
    } finally {
      this.refreshPromise = null;
    }
  }
 
  private async doRefresh(): Promise<void> {
    // Check if refresh token is expired
    if (this.tokens.refreshTokenExpiresAt < new Date()) {
      throw new TokenExpiredError('Refresh token expired. User must re-authorize.');
    }
 
    const response = await refreshAccessToken(this.tokens.refreshToken);
 
    this.tokens = {
      accessToken: response.access_token,
      refreshToken: response.refresh_token,
      accessTokenExpiresAt: new Date(Date.now() + response.expires_in * 1000),
      refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
    };
 
    // Persist updated tokens to your database
    await this.saveTokens(this.tokens);
  }
}

Handling the 30-Day Re-Authorization

When the refresh token expires after 30 days, users must complete the full OAuth flow again. Handle this gracefully:

class TokenExpiredError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'TokenExpiredError';
  }
}
 
async function makeAuthenticatedRequest(
  endpoint: string,
  tokenManager: TokenManager
): Promise<Response> {
  try {
    const accessToken = await tokenManager.getValidAccessToken();
 
    const response = await fetch(`https://tiktok.eulerstream.com${endpoint}`, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
      },
    });
 
    if (response.status === 401) {
      // Token was invalidated server-side (user revoked, etc.)
      throw new TokenExpiredError('Token invalid. User must re-authorize.');
    }
 
    return response;
  } catch (error) {
    if (error instanceof TokenExpiredError) {
      // Redirect user to re-authorize
      // Clear stored tokens
      // Show appropriate UI message
      await handleReauthorizationRequired();
    }
    throw error;
  }
}

User Experience for Re-Authorization

When tokens expire, provide a clear user experience:

function handleReauthorizationRequired() {
  // Option 1: Show a banner/modal
  showNotification({
    title: 'TikTok Connection Expired',
    message: 'Your TikTok connection has expired. Please reconnect to continue.',
    action: {
      label: 'Reconnect',
      onClick: () => startOAuthFlow(),
    },
  });
 
  // Option 2: For background services, queue for later
  await markUserAsNeedsReauth(userId);
 
  // Option 3: Send email notification
  await sendReauthEmail(userEmail);
}

Token Revocation

Users can revoke their tokens at any time. You can also programmatically revoke tokens:

async function revokeToken(token: string, tokenType: 'access_token' | 'refresh_token') {
  const response = await fetch('https://tiktok.eulerstream.com/tiktok/oauth/revoke', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      token: token,
      token_type_hint: tokenType,
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
    }),
  });
 
  return response.ok;
}

When to Revoke Tokens

  • User disconnects their TikTok account in your app
  • User deletes their account from your platform
  • Security incident or suspicious activity detected
  • User requests data deletion (GDPR, etc.)

Underlying TikTok Session Expiration

The OAuth tokens are backed by the user's TikTok session obtained during QR code authentication. If the underlying TikTok session expires or is invalidated (e.g., user changes password, logs out everywhere):

  1. API requests will start failing with specific error codes
  2. You must revoke the OAuth tokens and prompt the user to re-authorize
  3. The refresh token will not be able to recover from this state
async function handleApiResponse(response: Response) {
  if (response.status === 401) {
    const error = await response.json();
 
    if (error.error === 'session_expired' || error.error === 'invalid_session') {
      // Underlying TikTok session is gone
      // Must revoke and re-request authorization
      await revokeToken(refreshToken, 'refresh_token');
      throw new SessionExpiredError('TikTok session expired. Full re-authorization required.');
    }
  }
}

Complete Token Lifecycle Example

import { EventEmitter } from 'events';
 
interface UserTokens {
  userId: string;
  accessToken: string;
  refreshToken: string;
  accessTokenExpiresAt: Date;
  refreshTokenExpiresAt: Date;
  scopes: string[];
}
 
class OAuthTokenService extends EventEmitter {
  private db: Database;
 
  async getTokensForUser(userId: string): Promise<UserTokens | null> {
    return this.db.tokens.findOne({ userId });
  }
 
  async getValidAccessToken(userId: string): Promise<string> {
    const tokens = await this.getTokensForUser(userId);
 
    if (!tokens) {
      throw new Error('No tokens found for user');
    }
 
    // Check refresh token expiration (30 days)
    if (tokens.refreshTokenExpiresAt < new Date()) {
      this.emit('reauthorization_required', { userId });
      throw new TokenExpiredError('Refresh token expired after 30 days');
    }
 
    // Check access token expiration (1 hour) with 5-minute buffer
    const buffer = 5 * 60 * 1000;
    if (tokens.accessTokenExpiresAt < new Date(Date.now() + buffer)) {
      await this.refreshTokens(userId, tokens.refreshToken);
      const updatedTokens = await this.getTokensForUser(userId);
      return updatedTokens!.accessToken;
    }
 
    return tokens.accessToken;
  }
 
  private async refreshTokens(userId: string, refreshToken: string): Promise<void> {
    try {
      const response = await refreshAccessToken(refreshToken);
 
      await this.db.tokens.update(
        { userId },
        {
          accessToken: response.access_token,
          refreshToken: response.refresh_token,
          accessTokenExpiresAt: new Date(Date.now() + response.expires_in * 1000),
          // Note: refreshTokenExpiresAt stays the same unless you track it from login
        }
      );
 
      this.emit('tokens_refreshed', { userId });
    } catch (error) {
      this.emit('refresh_failed', { userId, error });
      throw error;
    }
  }
 
  async revokeUserTokens(userId: string): Promise<void> {
    const tokens = await this.getTokensForUser(userId);
 
    if (tokens) {
      await revokeToken(tokens.refreshToken, 'refresh_token');
      await this.db.tokens.delete({ userId });
      this.emit('tokens_revoked', { userId });
    }
  }
}

Summary

ScenarioAction
Access token expires (1 hour)Use refresh token to get new access token
Refresh token expires (30 days)User must re-authorize via full OAuth flow
User revokes accessHandle gracefully, prompt re-authorization
TikTok session expiresRevoke tokens, require full re-authorization
API returns 401Attempt refresh, then re-authorization if needed