or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

api

features

charts

charts.mdconditional-formatting.mdvisualizations.md
authorization.mdchangesets.mdcharts-as-code.mdcompiler.mddashboards.mddbt.mdee-features.mdformatting.mdparameters.mdpivot.mdprojects-spaces.mdsql-runner.mdtemplating.mdwarehouse.md
index.md
tile.json

oauth.mddocs/api/utilities/specialized/

OAuth Integration Utilities

Utilities for OAuth 2.0 authorization flows including page generation, scope parsing, and response handling. These functions create server-side HTML pages for OAuth consent screens, success/error responses, and redirect pages.

Capabilities

This module provides the following functionality:

Core Types

/**
 * Status type for OAuth responses
 */
type OAuthResponseStatus = 'success' | 'error';

/**
 * Icon types for OAuth pages
 */
type OAuthIconType = 'check' | 'warning' | 'lock' | 'shield' | 'key' | 'user';

/**
 * Hidden input field for OAuth forms
 */
interface OAuthHiddenInput {
  name: string;
  value: string;
}

/**
 * User information for OAuth authorization
 */
interface OAuthUser {
  firstName: string;
  lastName: string;
  organizationName: string;
}

/**
 * Scope description for OAuth authorization
 */
interface OAuthScopeDescription {
  icon: OAuthIconType;
  title: string;
  description: string;
}

/**
 * Parameters for OAuth authorization page
 */
interface OAuthAuthorizeParams {
  user: OAuthUser;
  clientName: string;
  scopes: OAuthScopeDescription[];
  hiddenInputs: OAuthHiddenInput[];
}

/**
 * Parameters for OAuth redirect page
 */
interface OAuthRedirectParams {
  redirectUri: string;
  hiddenInputs: OAuthHiddenInput[];
}

Core Functions

/**
 * CSS styles for OAuth pages (authorization, success, error pages).
 * Includes styles for container, logo, icons, buttons, and form elements.
 */
const oauthPageStyles: string;

/**
 * Parses a space-separated OAuth scope string into an array.
 * @param scopeString - Space-separated scope string (e.g., "read:user write:project")
 * @returns Array of individual scope strings
 */
function parseScopeString(scopeString: string): string[];

/**
 * Extracts a readable client name from a client ID.
 * @param clientId - The OAuth client identifier
 * @returns Human-readable client name
 */
function getClientName(clientId: string): string;

/**
 * Generates generic OAuth response HTML (success or error page).
 * @param status - Response status ('success' or 'error')
 * @param title - Page title
 * @param messages - Array of message strings to display
 * @param icon - SVG icon content
 * @returns Complete HTML page string
 */
function generateOAuthResponseHtml(params: {
  status: OAuthResponseStatus;
  title: string;
  messages: string[];
  icon: string;
}): string;

/**
 * Generates OAuth success response page.
 * Displays success icon and confirmation message to user.
 * @param title - Success message title
 * @param message - Success message content
 * @returns Complete HTML success page
 */
function generateOAuthSuccessResponse(title: string, message: string): string;

/**
 * Generates OAuth error response page.
 * Displays error icon and error details to user.
 * @param title - Error title
 * @param message - Error description
 * @returns Complete HTML error page
 */
function generateOAuthErrorResponse(title: string, message: string): string;

/**
 * Generates OAuth authorization/consent page.
 * Displays application details, requested scopes, and authorize/deny buttons.
 * @param params - Authorization page parameters
 * @returns Complete HTML authorization page with form
 */
function generateOAuthAuthorizePage(params: OAuthAuthorizeParams): string;

/**
 * Generates OAuth redirect page with auto-submit form.
 * Used to redirect users back to client application with authorization code.
 * @param params - Redirect parameters including callback URL and code
 * @returns Complete HTML page with auto-submitting form
 */
function generateOAuthRedirectPage(params: OAuthRedirectParams): string;

Examples

Parse OAuth Scopes

import { parseScopeString, getClientName } from '@lightdash/common';

// Parse OAuth scopes
const scopes = parseScopeString('read:user write:project admin:org');
// Returns: ['read:user', 'write:project', 'admin:org']

// Parse scopes from query parameter
const scopeParam = 'read:projects write:projects';
const parsedScopes = parseScopeString(scopeParam);
// Returns: ['read:projects', 'write:projects']

// Get readable client name
const clientName = getClientName('cli-abc123');
// Returns: 'CLI'

const clientName2 = getClientName('my-oauth-client-id');
// Returns: 'My OAuth Client Id' (formatted)

Generate Authorization Page

import {
  generateOAuthAuthorizePage,
  type OAuthAuthorizeParams,
} from '@lightdash/common';

// Generate authorization/consent page
const authPage = generateOAuthAuthorizePage({
  user: {
    firstName: 'John',
    lastName: 'Doe',
    organizationName: 'Acme Corp',
  },
  clientName: 'My Analytics App',
  scopes: [
    {
      icon: 'user',
      title: 'Access your profile',
      description: 'View your name, email, and organization',
    },
    {
      icon: 'key',
      title: 'Access your projects',
      description: 'Read and modify projects and dashboards',
    },
  ],
  hiddenInputs: [
    { name: 'client_id', value: 'abc123' },
    { name: 'redirect_uri', value: 'https://app.example.com/callback' },
    { name: 'state', value: 'random-state-value' },
  ],
});
// Returns HTML page with Lightdash branding, user info, scope list, and authorize/deny buttons

Generate Success/Error Responses

import {
  generateOAuthSuccessResponse,
  generateOAuthErrorResponse,
} from '@lightdash/common';

// Generate success response
const successPage = generateOAuthSuccessResponse(
  'Authorization Successful',
  'You can close this window and return to the application.'
);

// Generate error response
const errorPage = generateOAuthErrorResponse(
  'Authorization Failed',
  'The application was denied access to your account.'
);

// Generate custom error
const customError = generateOAuthErrorResponse(
  'Invalid Request',
  'The client_id parameter is missing or invalid.'
);

Generate Redirect Page

import { generateOAuthRedirectPage } from '@lightdash/common';

// Generate redirect page (auto-submits form to redirect user)
const redirectPage = generateOAuthRedirectPage({
  redirectUri: 'https://app.example.com/callback',
  hiddenInputs: [
    { name: 'code', value: 'authorization-code-xyz' },
    { name: 'state', value: 'random-state-value' },
  ],
});
// Returns HTML page that automatically submits form to redirect user back to app

OAuth Authorization Flow

import {
  parseScopeString,
  generateOAuthAuthorizePage,
  generateOAuthRedirectPage,
  generateOAuthErrorResponse,
} from '@lightdash/common';

// OAuth authorization endpoint
app.get('/oauth/authorize', async (req, res) => {
  const { client_id, redirect_uri, scope, state } = req.query;

  try {
    // Validate parameters
    if (!client_id || !redirect_uri || !scope) {
      const errorPage = generateOAuthErrorResponse(
        'Invalid Request',
        'Missing required parameters'
      );
      return res.send(errorPage);
    }

    // Get client and user info
    const client = await getOAuthClient(client_id as string);
    const user = req.user;

    // Parse scopes
    const requestedScopes = parseScopeString(scope as string);

    // Map scopes to descriptions
    const scopeDescriptions = requestedScopes.map(scope => ({
      icon: getScopeIcon(scope),
      title: getScopeTitle(scope),
      description: getScopeDescription(scope),
    }));

    // Generate authorization page
    const authPage = generateOAuthAuthorizePage({
      user: {
        firstName: user.firstName,
        lastName: user.lastName,
        organizationName: user.organization.name,
      },
      clientName: client.name,
      scopes: scopeDescriptions,
      hiddenInputs: [
        { name: 'client_id', value: client_id as string },
        { name: 'redirect_uri', value: redirect_uri as string },
        { name: 'scope', value: scope as string },
        { name: 'state', value: state as string || '' },
      ],
    });

    res.send(authPage);
  } catch (error) {
    const errorPage = generateOAuthErrorResponse(
      'Server Error',
      error.message
    );
    res.send(errorPage);
  }
});

// Authorization decision endpoint
app.post('/oauth/authorize', async (req, res) => {
  const { client_id, redirect_uri, scope, state, action } = req.body;

  if (action === 'deny') {
    const errorPage = generateOAuthErrorResponse(
      'Access Denied',
      'You denied access to the application.'
    );
    return res.send(errorPage);
  }

  try {
    // Generate authorization code
    const code = await generateAuthorizationCode({
      clientId: client_id,
      userId: req.user.id,
      scope,
    });

    // Generate redirect page
    const redirectPage = generateOAuthRedirectPage({
      redirectUri: redirect_uri,
      hiddenInputs: [
        { name: 'code', value: code },
        { name: 'state', value: state || '' },
      ],
    });

    res.send(redirectPage);
  } catch (error) {
    const errorPage = generateOAuthErrorResponse(
      'Authorization Failed',
      error.message
    );
    res.send(errorPage);
  }
});

Scope Mapping Helper

import type { OAuthIconType } from '@lightdash/common';

function getScopeIcon(scope: string): OAuthIconType {
  if (scope.includes('user')) return 'user';
  if (scope.includes('project')) return 'key';
  if (scope.includes('admin')) return 'shield';
  return 'lock';
}

function getScopeTitle(scope: string): string {
  const scopeMap: Record<string, string> = {
    'read:user': 'Read your profile',
    'write:user': 'Update your profile',
    'read:projects': 'View your projects',
    'write:projects': 'Create and modify projects',
    'admin:org': 'Manage organization',
  };
  return scopeMap[scope] || scope;
}

function getScopeDescription(scope: string): string {
  const descMap: Record<string, string> = {
    'read:user': 'View your name, email, and organization',
    'write:user': 'Update your profile information',
    'read:projects': 'View projects and dashboards',
    'write:projects': 'Create, edit, and delete projects and dashboards',
    'admin:org': 'Manage organization settings and members',
  };
  return descMap[scope] || 'Access to your account';
}

Complete OAuth Server Example

import {
  parseScopeString,
  generateOAuthAuthorizePage,
  generateOAuthRedirectPage,
  generateOAuthSuccessResponse,
  generateOAuthErrorResponse,
} from '@lightdash/common';

class OAuthServer {
  // Authorization endpoint
  async authorize(req: Request, res: Response) {
    const { client_id, redirect_uri, scope, state } = req.query;

    // Validate request
    const validation = this.validateAuthRequest({
      client_id,
      redirect_uri,
      scope,
    });

    if (!validation.valid) {
      return res.send(
        generateOAuthErrorResponse('Invalid Request', validation.error)
      );
    }

    // Get client and user
    const client = await this.getClient(client_id as string);
    const user = await this.getCurrentUser(req);

    // Parse and map scopes
    const scopes = parseScopeString(scope as string).map(s => ({
      icon: this.getScopeIcon(s),
      title: this.getScopeTitle(s),
      description: this.getScopeDescription(s),
    }));

    // Show authorization page
    const page = generateOAuthAuthorizePage({
      user: {
        firstName: user.firstName,
        lastName: user.lastName,
        organizationName: user.organization.name,
      },
      clientName: client.name,
      scopes,
      hiddenInputs: [
        { name: 'client_id', value: client_id as string },
        { name: 'redirect_uri', value: redirect_uri as string },
        { name: 'scope', value: scope as string },
        { name: 'state', value: state as string || '' },
      ],
    });

    res.send(page);
  }

  // Handle authorization decision
  async decision(req: Request, res: Response) {
    const { client_id, redirect_uri, scope, state, action } = req.body;

    if (action === 'deny') {
      return res.send(
        generateOAuthErrorResponse(
          'Access Denied',
          'You denied access to this application.'
        )
      );
    }

    try {
      // Create authorization code
      const code = await this.createAuthorizationCode({
        clientId: client_id,
        userId: req.user.id,
        scope,
      });

      // Redirect to client
      const page = generateOAuthRedirectPage({
        redirectUri: redirect_uri,
        hiddenInputs: [
          { name: 'code', value: code },
          { name: 'state', value: state || '' },
        ],
      });

      res.send(page);
    } catch (error) {
      res.send(
        generateOAuthErrorResponse('Authorization Failed', error.message)
      );
    }
  }
}

Testing

import {
  parseScopeString,
  generateOAuthAuthorizePage,
  generateOAuthSuccessResponse,
} from '@lightdash/common';

describe('OAuth utilities', () => {
  it('should parse scope string', () => {
    const scopes = parseScopeString('read:user write:projects');
    expect(scopes).toEqual(['read:user', 'write:projects']);
  });

  it('should generate authorization page', () => {
    const html = generateOAuthAuthorizePage({
      user: {
        firstName: 'Test',
        lastName: 'User',
        organizationName: 'Test Org',
      },
      clientName: 'Test App',
      scopes: [
        {
          icon: 'user',
          title: 'Profile Access',
          description: 'View your profile',
        },
      ],
      hiddenInputs: [
        { name: 'client_id', value: 'test123' },
      ],
    });

    expect(html).toContain('Test User');
    expect(html).toContain('Test App');
    expect(html).toContain('Profile Access');
  });

  it('should generate success response', () => {
    const html = generateOAuthSuccessResponse(
      'Success',
      'Authorization complete'
    );

    expect(html).toContain('Success');
    expect(html).toContain('Authorization complete');
  });
});

Security Notes

  • All pages include consistent Lightdash branding (logo, styles)
  • Authorization page includes CSRF protection via hidden form inputs
  • Redirect page uses POST method for security (doesn't expose code in URL)
  • Success/error pages are final pages (no auto-redirect)
  • Always validate client_id and redirect_uri before generating pages
  • Use state parameter for CSRF protection in authorization flow

Page Types

Authorization Page

  • Shows user information
  • Lists requested scopes with icons and descriptions
  • Provides Authorize and Deny buttons
  • Includes hidden form inputs for OAuth parameters

Success Page

  • Displays success icon
  • Shows confirmation message
  • Final page (user closes window)

Error Page

  • Displays error icon
  • Shows error title and description
  • Final page (user closes window)

Redirect Page

  • Auto-submits form to redirect URI
  • Uses POST method for security
  • Includes authorization code and state

Use Cases

  • OAuth 2.0 Server: Implement authorization endpoint with consent screen
  • CLI Authentication: OAuth flow for command-line tools
  • Third-Party Integrations: Allow external apps to access Lightdash
  • API Authentication: Generate access tokens via OAuth flow
  • Embedded Analytics: OAuth for embedded Lightdash instances

Related Utilities

  • Session Management: Handle OAuth sessions and tokens
  • Client Management: Register and validate OAuth clients
  • Scope Validation: Validate requested scopes against allowed scopes