Authentication API

Secure authentication with JWT tokens, session management, and Clerk integration for user management

JWT Security

Industry-standard JWT tokens with RS256 algorithm and Clerk integration

Session Management

Long-lived session tokens with automatic refresh and machine binding

Machine Authorization

Machine-specific authentication for service-to-service communication

Authentication Flow

Playcast uses a two-tier authentication system with JWT tokens and session management.

Client Request
JWT/Session Check
Token Validation
Clerk Sign-in Token
Session Token
Two-Token System:
  • JWT Token - Initial authentication with Clerk
  • Session Token - Long-lived machine-specific sessions

Quick Start

Get started with Playcast authentication:

// For first-time authentication with JWT
const authResponse = await fetch('/realtime/auth/login-token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    machineId: 'machine-123',
    jwt: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...'
  })
});

const { loginToken, sessionToken, expiresIn } = await authResponse.json();

// For subsequent requests with session token
const quickAuth = await fetch('/realtime/auth/login-token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    machineId: 'machine-123',
    sessionToken: sessionToken
  })
});

// Store session token for future use
localStorage.setItem('playcast_session', sessionToken);

Login Token

Request a Clerk sign-in token for authentication.

POST /realtime/auth/login-token

Request Body

Parameter Type Required Description
machineId string Required Unique identifier for the requesting machine
sessionToken string Optional Existing session token for re-authentication
jwt string Optional Clerk JWT token for initial authentication

Response

Success Response (200)

{
  "success": true,
  "loginToken": "situ_2b1234567890abcdef",
  "sessionToken": "sess_1a234567890bcdef",
  "expiresIn": 1800,
  "sessionExpiresIn": 86400
}

Error Response (401)

{
  "success": false,
  "error": "Authentication failed - valid session token or JWT required"
}

Usage Examples

First-time Authentication with JWT

async function authenticateWithJWT(jwtToken, machineId) {
  try {
    const response = await fetch('/realtime/auth/login-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        machineId: machineId,
        jwt: jwtToken
      })
    });

    const result = await response.json();

    if (result.success) {
      // Store session token for future use
      localStorage.setItem('playcast_session', result.sessionToken);
      localStorage.setItem('playcast_session_expires',
        Date.now() + (result.sessionExpiresIn * 1000));

      return result.loginToken;
    } else {
      throw new Error(result.error);
    }
  } catch (error) {
    console.error('Authentication failed:', error);
    throw error;
  }
}

Re-authentication with Session Token

async function authenticateWithSession(machineId) {
  const sessionToken = localStorage.getItem('playcast_session');
  const sessionExpires = localStorage.getItem('playcast_session_expires');

  if (!sessionToken || Date.now() > parseInt(sessionExpires)) {
    throw new Error('Session token expired or missing');
  }

  try {
    const response = await fetch('/realtime/auth/login-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        machineId: machineId,
        sessionToken: sessionToken
      })
    });

    const result = await response.json();

    if (result.success) {
      return result.loginToken;
    } else {
      // Session might be expired, clear it
      localStorage.removeItem('playcast_session');
      localStorage.removeItem('playcast_session_expires');
      throw new Error(result.error);
    }
  } catch (error) {
    console.error('Session authentication failed:', error);
    throw error;
  }
}

Token Validation

Validate a session token without requesting a new login token.

POST /realtime/auth/validate-token

Request Body

Parameter Type Required Description
sessionToken string Required Session token to validate
machineId string Required Machine ID that session token is bound to

Response

Valid Token Response (200)

{
  "success": true,
  "valid": true,
  "authId": "user_2b1234567890abcdef"
}

Invalid Token Response (200)

{
  "success": true,
  "valid": false
}

Usage Example

async function validateSession(sessionToken, machineId) {
  try {
    const response = await fetch('/realtime/auth/validate-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        sessionToken: sessionToken,
        machineId: machineId
      })
    });

    const result = await response.json();

    if (result.success && result.valid) {
      console.log('Session is valid for user:', result.authId);
      return true;
    } else {
      console.log('Session is invalid or expired');
      return false;
    }
  } catch (error) {
    console.error('Token validation failed:', error);
    return false;
  }
}

// Use in your application
const isValid = await validateSession(
  localStorage.getItem('playcast_session'),
  'machine-123'
);

if (!isValid) {
  // Redirect to login or request new authentication
  redirectToLogin();
}

Session Refresh

Refresh an existing session token to extend its validity period.

POST /realtime/auth/refresh-session

Request Body

Parameter Type Required Description
sessionToken string Required Current session token to refresh
machineId string Required Machine ID bound to the session

Response

Success Response (200)

{
  "success": true,
  "sessionToken": "sess_2c345678901cdefg",
  "expiresIn": 86400
}

Error Response (401)

{
  "success": false,
  "error": "Invalid or expired session token"
}

Usage Example

async function refreshSessionToken(sessionToken, machineId) {
  try {
    const response = await fetch('/realtime/auth/refresh-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        sessionToken: sessionToken,
        machineId: machineId
      })
    });

    const result = await response.json();

    if (result.success) {
      // Update stored session token
      localStorage.setItem('playcast_session', result.sessionToken);
      localStorage.setItem('playcast_session_expires',
        Date.now() + (result.expiresIn * 1000));

      console.log('Session refreshed successfully');
      return result.sessionToken;
    } else {
      throw new Error(result.error);
    }
  } catch (error) {
    console.error('Session refresh failed:', error);
    // Clear invalid session
    localStorage.removeItem('playcast_session');
    localStorage.removeItem('playcast_session_expires');
    throw error;
  }
}

// Automatic refresh before expiration
function scheduleSessionRefresh() {
  const sessionExpires = localStorage.getItem('playcast_session_expires');
  if (!sessionExpires) return;

  const expirationTime = parseInt(sessionExpires);
  const refreshTime = expirationTime - (30 * 60 * 1000); // 30 minutes before expiry
  const timeUntilRefresh = refreshTime - Date.now();

  if (timeUntilRefresh > 0) {
    setTimeout(async () => {
      try {
        const sessionToken = localStorage.getItem('playcast_session');
        await refreshSessionToken(sessionToken, 'machine-123');
        scheduleSessionRefresh(); // Schedule next refresh
      } catch (error) {
        console.error('Automatic refresh failed:', error);
      }
    }, timeUntilRefresh);
  }
}

Error Handling

Handle authentication errors gracefully in your application.

Common Error Scenarios

class PlaycastAuth {
  constructor(machineId) {
    this.machineId = machineId;
    this.baseUrl = '/realtime/auth';
  }

  async authenticate() {
    try {
      // Try session token first
      const sessionToken = localStorage.getItem('playcast_session');
      if (sessionToken && !this.isSessionExpired()) {
        return await this.authenticateWithSession(sessionToken);
      }

      // Fall back to JWT if available
      const jwtToken = this.getStoredJWT();
      if (jwtToken) {
        return await this.authenticateWithJWT(jwtToken);
      }

      // No authentication available
      throw new Error('No authentication credentials available');
    } catch (error) {
      return this.handleAuthError(error);
    }
  }

  async authenticateWithSession(sessionToken) {
    const response = await fetch(`${this.baseUrl}/login-token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        machineId: this.machineId,
        sessionToken: sessionToken
      })
    });

    const result = await response.json();
    if (!result.success) {
      throw new AuthError(result.error, 'SESSION_INVALID');
    }

    return result.loginToken;
  }

  async authenticateWithJWT(jwtToken) {
    const response = await fetch(`${this.baseUrl}/login-token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        machineId: this.machineId,
        jwt: jwtToken
      })
    });

    const result = await response.json();
    if (!result.success) {
      throw new AuthError(result.error, 'JWT_INVALID');
    }

    // Store new session token
    if (result.sessionToken) {
      localStorage.setItem('playcast_session', result.sessionToken);
      localStorage.setItem('playcast_session_expires',
        Date.now() + (result.sessionExpiresIn * 1000));
    }

    return result.loginToken;
  }

  handleAuthError(error) {
    if (error instanceof AuthError) {
      switch (error.code) {
        case 'SESSION_INVALID':
          // Clear invalid session and retry with JWT
          localStorage.removeItem('playcast_session');
          localStorage.removeItem('playcast_session_expires');
          return this.authenticateWithJWT(this.getStoredJWT());

        case 'JWT_INVALID':
          // JWT expired or invalid, need user re-authentication
          this.redirectToLogin();
          throw new Error('Please log in again');

        default:
          console.error('Authentication error:', error.message);
          throw error;
      }
    }

    // Network or other errors
    console.error('Authentication request failed:', error);
    throw new Error('Authentication service unavailable');
  }

  isSessionExpired() {
    const expires = localStorage.getItem('playcast_session_expires');
    return !expires || Date.now() > parseInt(expires);
  }

  getStoredJWT() {
    // Implement based on your JWT storage strategy
    return localStorage.getItem('clerk_jwt');
  }

  redirectToLogin() {
    // Implement based on your application's login flow
    window.location.href = '/login';
  }
}

class AuthError extends Error {
  constructor(message, code) {
    super(message);
    this.code = code;
    this.name = 'AuthError';
  }
}

Retry Logic with Exponential Backoff

async function authenticateWithRetry(auth, maxRetries = 3) {
  let retryCount = 0;

  while (retryCount < maxRetries) {
    try {
      return await auth.authenticate();
    } catch (error) {
      retryCount++;

      if (retryCount >= maxRetries) {
        throw new Error(`Authentication failed after ${maxRetries} attempts: ${error.message}`);
      }

      // Exponential backoff
      const delay = Math.pow(2, retryCount) * 1000;
      console.log(`Authentication attempt ${retryCount} failed, retrying in ${delay}ms...`);

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
Security Best Practices:
  • Always validate tokens server-side before trusting client claims
  • Use HTTPS in production to protect tokens in transit
  • Implement proper token storage (HttpOnly cookies when possible)
  • Set reasonable session expiration times
  • Log authentication failures for security monitoring

Integration with Clerk

Playcast authentication integrates with Clerk for user management and JWT token verification.

Environment Setup

# Production environment
CLERK_PROD_SECRET_KEY=sk_prod_xxxxxxxxxxxxxxxxxxxx

# Development environment
CLERK_DEVELOPMENT_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxx

# Clerk public keys are embedded in the API for JWT verification

JWT Token Format

// Example JWT payload from Clerk
{
  "sub": "user_2b1234567890abcdef",  // User ID
  "iss": "https://clerk.domain.com", // Issuer
  "aud": "your-app-id",              // Audience
  "exp": 1640995200,                 // Expiration timestamp
  "iat": 1640991600,                 // Issued at timestamp
  "nbf": 1640991600                  // Not before timestamp
}
Benefits of Clerk Integration:
  • Industry-standard security practices
  • Multiple authentication providers
  • User management dashboard
  • Automatic security updates
  • Compliance with privacy regulations