Skip to main content

Overview

The Hosted UI flow is the recommended approach for most applications. Users authenticate through our secure, pre-built login page to authorize your application to act on their behalf. This approach eliminates the need to handle user credentials in your application, providing the best balance of security, compliance, and developer experience.
User-Centered Design: The hosted UI ensures users maintain direct control over their authentication while granting your application permission to perform actions they authorize.
Complete in 4 steps: This is the simplest OAuth implementation, with authentication and authorization handled automatically by the hosted UI.

When to Use Hosted UI

Ideal For

  • Web applications
  • Third-party integrations
  • SaaS platforms
  • Quick implementations
  • Standard OAuth patterns

Not Ideal For

  • Mobile apps requiring native UI
  • Heavily branded experiences
  • Headless/API-only systems
  • Custom authentication flows

Prerequisites

Before starting, ensure you have:
  • ✅ API keys (x-client-key and x-secret-key)
  • ✅ Whitelisted redirect URI in your environment configuration
  • ✅ HTTPS endpoint for production (HTTP allowed in sandbox)
  • ✅ Understanding of PKCE implementation

Flow Diagram

Implementation Guide

Step 1: Initiate OAuth Flow

Generate PKCE parameters and request the hosted UI URL.
import crypto from 'crypto';

function generateCodeVerifier(): string {
  return crypto.randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier: string): string {
  return crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
}

async function initiateOAuth() {
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = generateCodeChallenge(codeVerifier);
  const state = crypto.randomBytes(16).toString('hex');

  // Store for later use
  sessionStorage.setItem('code_verifier', codeVerifier);
  sessionStorage.setItem('oauth_state', state);

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'your-client-id',
    redirect_uri: 'https://yourapp.com/oauth/callback',
    state: state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    scope: 'read write' // optional
  });

  const response = await fetch(
    `https://api.example.com/v1/auth/oauth/authorize/initiate?${params}`,
    {
      headers: {
        'x-client-key': 'your_public_key',
        'x-secret-key': 'your_secret_key'
      }
    }
  );

  const data = await response.json();

  // Redirect user to hosted UI
  window.location.href = data.url;
}
Request Parameters:
ParameterRequiredDescription
response_typeYesMust be code
client_idYesYour application’s client identifier
redirect_uriYesMust match a whitelisted URI exactly
stateYesRandom string for CSRF protection
code_challengeYesBASE64URL(SHA256(code_verifier))
code_challenge_methodYesMust be S256
scopeNoSpace-separated list of permissions
Response:
{
  "url": "https://auth.example.com/login?session=abc123...",
  "expires_in": 600
}
Session Expiry: The OAuth session expires after 10 minutes. Users must complete authentication before this time or restart the flow.

Step 2: User Authentication (Automatic)

After redirecting to the hosted UI URL:
  1. User sees the hosted login page - Our secure, pre-built interface
  2. User enters credentials - Email/username and password
  3. User completes 2FA if enabled - OTP or biometric verification
  4. Hosted UI handles authentication - No API call needed from your app
This step happens entirely on the hosted UI. Your application doesn’t need to make any API calls or handle credentials.

Step 3: Authorization (Automatic)

After successful login:
  1. User sees permission consent screen (if first time)
  2. User approves access - Grants your app permission
  3. Hosted UI generates authorization code - Automatic API call
  4. User redirected back to your app - With code and state parameters
Redirect Example:
https://yourapp.com/oauth/callback?code=auth_code_xyz123&state=random_csrf_protection_string_12345
This step is also handled automatically by the hosted UI. Your app simply receives the redirect with the authorization code.

Step 4: Exchange Code for Tokens

Handle the callback in your application and exchange the authorization code for access and refresh tokens.
async function handleOAuthCallback() {
  // Parse URL parameters
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code');
  const state = params.get('state');

  // Verify state parameter (CSRF protection)
  const savedState = sessionStorage.getItem('oauth_state');
  if (state !== savedState) {
    throw new Error('Invalid state parameter - possible CSRF attack');
  }

  // Retrieve stored code_verifier
  const codeVerifier = sessionStorage.getItem('code_verifier');
  if (!codeVerifier) {
    throw new Error('Code verifier not found');
  }

  // Exchange authorization code for tokens
  const response = await fetch('https://api.example.com/v1/auth/oauth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-client-key': 'your_public_key',
      'x-secret-key': 'your_secret_key'
    },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: 'https://yourapp.com/oauth/callback',
      code_verifier: codeVerifier
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Token exchange failed: ${error.error_description}`);
  }

  const tokens = await response.json();

  // Store tokens securely
  localStorage.setItem('access_token', tokens.access_token);
  localStorage.setItem('refresh_token', tokens.refresh_token);
  localStorage.setItem('token_expiry', Date.now() + (tokens.expires_in * 1000));

  // Clean up
  sessionStorage.removeItem('code_verifier');
  sessionStorage.removeItem('oauth_state');

  // Redirect to dashboard or home
  window.location.href = '/dashboard';
}
Request Body:
FieldRequiredDescription
grant_typeYesMust be authorization_code
codeYesAuthorization code from Step 3
redirect_uriYesMust exactly match Step 1
code_verifierYesOriginal PKCE verifier (43-128 chars)
Response:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 21600,
  "refresh_token": "def50200a1b2c3d4e5f6...",
  "refresh_token_expires_in": 604800,
  "scope": "read write"
}
Token Expiry Times:
  • Access token: 6 hours (21,600 seconds)
  • Refresh token: 7 days (604,800 seconds)

Using Access Tokens

Once you have an access token, use it to make API requests on behalf of the user. Include the token in the Authorization header for all authenticated requests:
Respect User Authorization: Access tokens enable your application to act on behalf of users. Only perform actions that users have explicitly authorized and expect your application to perform.
async function callAPI() {
  const accessToken = localStorage.getItem('access_token');

  const response = await fetch('https://api.example.com/v1/users/me', {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'x-client-key': 'your_public_key'
    }
  });

  return await response.json();
}

Token Refresh

When the access token expires (after 6 hours), use the refresh token to obtain a new one:
async function refreshAccessToken() {
  const refreshToken = localStorage.getItem('refresh_token');

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

  const tokens = await response.json();

  // Update stored tokens
  localStorage.setItem('access_token', tokens.access_token);
  localStorage.setItem('refresh_token', tokens.refresh_token);
  localStorage.setItem('token_expiry', Date.now() + (tokens.expires_in * 1000));

  return tokens.access_token;
}
Learn more about token management in the Token Management Guide.

Error Handling

Cause: Required parameters are missing or malformed.Solution:
  • Verify all required parameters are present
  • Check parameter formatting (e.g., base64url encoding for PKCE)
  • Ensure response_type is exactly code
{
  "error": "invalid_request",
  "error_description": "Missing required parameter: code_challenge"
}
Cause: The authorization code is invalid, already used, or expired.Solution:
  • Authorization codes are single-use only
  • Codes expire after 10 minutes
  • Restart the OAuth flow from Step 1
{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired"
}
Cause: Client key or secret is incorrect or not authorized.Solution:
  • Verify your x-client-key and x-secret-key
  • Ensure keys are for the correct environment (sandbox/production)
  • Check that OAuth is enabled for your client
{
  "error": "invalid_client",
  "error_description": "Client authentication failed"
}
Cause: User clicked “Deny” on the consent screen.Solution:
  • This is expected user behavior
  • Redirect user to appropriate page
  • Allow user to retry authorization if needed
{
  "error": "access_denied",
  "error_description": "User denied the authorization request"
}
Cause: State parameter doesn’t match what was sent in Step 1.Solution:
  • This indicates a possible CSRF attack
  • Do not proceed with token exchange
  • Log the incident and restart the flow
Client-side validation:
if (state !== sessionStorage.getItem('oauth_state')) {
  throw new Error('State mismatch - possible CSRF attack');
}

Testing Checklist

Before going to production, verify:
  • PKCE code_verifier is 43-128 characters from [A-Za-z0-9-._~]
  • State parameter is random and validated on callback
  • Redirect URI exactly matches whitelisted configuration
  • Secret key is never exposed in client-side code
  • Tokens are stored securely (not in localStorage for sensitive apps)
  • Token expiry is checked before API calls
  • Refresh token flow is implemented
  • Error handling covers all OAuth error codes
  • HTTPS is used in production (HTTP only for sandbox)
  • Session timeout (10 minutes) is handled gracefully

Complete Example

Here’s a complete working example for a Next.js application:
import { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');
  const state = crypto.randomBytes(16).toString('hex');

  // Store in secure session
  req.session.set('code_verifier', codeVerifier);
  req.session.set('oauth_state', state);
  await req.session.save();

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: process.env.OAUTH_CLIENT_ID!,
    redirect_uri: `${process.env.APP_URL}/api/oauth/callback`,
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  });

  const response = await fetch(
    `${process.env.API_URL}/v1/auth/oauth/authorize/initiate?${params}`,
    {
      headers: {
        'x-client-key': process.env.CLIENT_KEY!,
        'x-secret-key': process.env.SECRET_KEY!
      }
    }
  );

  const data = await response.json();
  res.redirect(data.url);
}

Next Steps