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.
This module provides the following functionality:
/**
* 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[];
}/**
* 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;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)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 buttonsimport {
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.'
);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 appimport {
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);
}
});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';
}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)
);
}
}
}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');
});
});