OAuth
This page provides comprehensive documentation for the OAuth implementation in Talawa API, including the base provider class, registry system, and type definitions.
Introduction
The Talawa API OAuth system provides a robust and extensible framework for implementing OAuth 2.0 authentication with various providers (Google, GitHub, etc.). The system is built around a modular architecture that allows easy addition of new OAuth providers while maintaining consistent error handling and security practices.
Architecture Overview
The OAuth system consists of several key components:
- BaseOAuthProvider: Abstract base class that implements common HTTP logic and error handling
- OAuthProviderRegistry: Singleton registry for managing OAuth provider instances
- OAuth Accounts Table: Database table for storing OAuth account linkages and provider data
- Type Definitions: TypeScript interfaces for OAuth configurations and responses
- Error Classes: Specialized error classes for different OAuth failure scenarios
OAuth Accounts Database Table
The OAuth accounts table (oauth_accounts) stores provider-specific account information linked to users. This table serves as the bridge between Talawa users and their external OAuth provider accounts.
Table Structure
The table is defined using Drizzle ORM and includes the following fields:
export const oauthAccountsTable = pgTable("oauth_accounts", {
// Primary unique identifier
id: uuid("id").primaryKey().$default(uuidv7),
// Foreign key to users table
userId: uuid("user_id")
.notNull()
.references(() => usersTable.id, { onDelete: "cascade" }),
// OAuth provider information
provider: varchar("provider", { length: 50 }).notNull(),
providerId: varchar("provider_id", { length: 255 }).notNull(),
// Account details from provider
email: varchar("email", { length: 255 }),
profile: jsonb("profile").$type<OAuthAccountProfile>(),
// Timestamp tracking
linkedAt: timestamp("linked_at", {
withTimezone: true,
mode: "date",
precision: 3,
}).notNull().defaultNow(),
lastUsedAt: timestamp("last_used_at", {
withTimezone: true,
mode: "date",
precision: 3,
}).notNull().defaultNow(),
});
Field Descriptions
id: Primary unique identifier using UUIDv7 for better performance and orderinguserId: Foreign key reference to the user who owns this OAuth account (cascades on delete)provider: OAuth provider name (e.g., 'google', 'github', 'facebook')providerId: Provider-specific user identifier (unique per provider)email: Email address associated with the OAuth account from the providerprofile: Additional profile data from the OAuth provider stored as JSONlinkedAt: Timestamp when the OAuth account was first linked to the userlastUsedAt: Timestamp when the OAuth account was last used for authentication
Indexes and Constraints
The table includes several indexes and constraints for data integrity and performance:
// Ensures each external provider account is linked only once
providerUserUnique: unique("oauth_accounts_provider_provider_id_unique")
.on(table.provider, table.providerId),
// Index for efficient user lookups
userIdIdx: index("oauth_accounts_user_id_idx").on(table.userId),
// Index for provider-based queries
providerIdx: index("oauth_accounts_provider_idx").on(table.provider),
Relations
The table establishes a many-to-one relationship with the users table:
export const oauthAccountsTableRelations = relations(
oauthAccountsTable,
({ one }) => ({
user: one(usersTable, {
fields: [oauthAccountsTable.userId],
references: [usersTable.id],
relationName: "oauth_accounts.user_id:users.id",
}),
}),
);
Usage Examples
Querying OAuth Accounts
import { db } from '~/src/drizzle/db';
import { oauthAccountsTable } from '~/src/drizzle/tables/oauthAccount';
import { eq, and } from 'drizzle-orm';
// Find all OAuth accounts for a user
const userOAuthAccounts = await db
.select()
.from(oauthAccountsTable)
.where(eq(oauthAccountsTable.userId, userId));
// Find specific provider account
const googleAccount = await db
.select()
.from(oauthAccountsTable)
.where(
and(
eq(oauthAccountsTable.userId, userId),
eq(oauthAccountsTable.provider, 'google')
)
);
// Find account by provider ID
const providerAccount = await db
.select()
.from(oauthAccountsTable)
.where(
and(
eq(oauthAccountsTable.provider, 'google'),
eq(oauthAccountsTable.providerId, externalUserId)
)
);
Creating OAuth Account Linkage
import { oauthAccountsTableInsertSchema } from '~/src/drizzle/tables/oauthAccount';
// Validate and create new OAuth account linkage
const newOAuthAccount = oauthAccountsTableInsertSchema.parse({
userId: user.id,
provider: 'google',
providerId: userProfile.providerId,
email: userProfile.email,
profile: {
name: userProfile.name,
picture: userProfile.picture,
emailVerified: userProfile.emailVerified,
},
});
const [createdAccount] = await db
.insert(oauthAccountsTable)
.values(newOAuthAccount)
.returning();
Updating Last Used Timestamp
// Update lastUsedAt when account is used for authentication
await db
.update(oauthAccountsTable)
.set({ lastUsedAt: new Date() })
.where(eq(oauthAccountsTable.id, oauthAccountId));
OAuthAccountProfile Type
The profile field stores additional provider data using a typed JSONB column:
interface OAuthAccountProfile {
name?: string;
picture?: string;
emailVerified?: boolean;
[key: string]: any; // Additional provider-specific fields
}
This flexible structure allows storing provider-specific profile information while maintaining type safety for common fields.
Data Integrity and Security
- Cascade Deletion: When a user is deleted, all linked OAuth accounts are automatically removed
- Unique Constraints: Each external provider account can only be linked to one Talawa user
- Indexing: Optimized queries for user lookups and provider-based searches
- Timezone Support: All timestamps include timezone information for accurate tracking across regions
OAuth Configuration Functions
The OAuth configuration system provides utility functions for loading and validating OAuth provider configurations from environment variables.
loadOAuthConfig
The loadOAuthConfig function loads and validates OAuth configuration from environment variables, automatically enabling or disabling providers based on the availability of required credentials.
function loadOAuthConfig(env = process.env): OAuthProvidersConfig
Parameters
env(optional): Environment variables object. Defaults toprocess.env
Returns
OAuthProvidersConfig: Configuration object containing Google and GitHub provider settings
Behavior
- Provider Enablement: Providers are automatically enabled only when all required environment variables are present
- Timeout Handling: Uses
API_OAUTH_REQUEST_TIMEOUT_MSwith fallback to 10000ms (10 seconds) - Error Recovery: Invalid timeout values (NaN) automatically fall back to the default timeout
Environment Variables
- Google Provider:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_REDIRECT_URI - GitHub Provider:
GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,GITHUB_REDIRECT_URI - Request Timeout:
API_OAUTH_REQUEST_TIMEOUT_MS(optional, defaults to 10000)
Usage Example
import { loadOAuthConfig } from '~/src/config/oauth';
// Load configuration from process.env
const config = loadOAuthConfig();
if (config.google.enabled) {
console.log('Google OAuth is configured');
console.log('Timeout:', config.google.requestTimeoutMs);
}
// Load configuration from custom environment
const customEnv = {
GOOGLE_CLIENT_ID: 'your-google-client-id',
GOOGLE_CLIENT_SECRET: 'your-google-secret',
GOOGLE_REDIRECT_URI: 'http://localhost:4000/auth/google/callback',
API_OAUTH_REQUEST_TIMEOUT_MS: '15000',
};
const customConfig = loadOAuthConfig(customEnv);
Return Structure
interface OAuthProvidersConfig {
google: {
enabled: boolean;
clientId: string;
clientSecret: string;
redirectUri: string;
requestTimeoutMs: number;
};
github: {
enabled: boolean;
clientId: string;
clientSecret: string;
redirectUri: string;
requestTimeoutMs: number;
};
}
getProviderConfig
The getProviderConfig function retrieves a specific provider's configuration and validates that it's properly configured and enabled.
function getProviderConfig(
provider: ProviderKey,
env = process.env
): Required<OAuthProviderConfig>
Parameters
provider: Provider key ("google"or"github")env(optional): Environment variables object. Defaults toprocess.env
Returns
Required<OAuthProviderConfig>: Complete provider configuration with all required fields
Throws
Error: When the provider is not properly configured or disabled
Behavior
- Validation: Ensures the provider is enabled and has all required configuration
- Fallback Timeout: Provides 10000ms fallback if timeout is falsy (defensive programming)
- Type Safety: Returns a configuration object with all fields guaranteed to be present
Usage Example
import { getProviderConfig } from '~/src/config/oauth';
try {
// Get Google provider configuration
const googleConfig = getProviderConfig('google');
// Safe to use - all fields are guaranteed to be present
console.log('Client ID:', googleConfig.clientId);
console.log('Timeout:', googleConfig.requestTimeoutMs);
// Initialize OAuth provider
const provider = new GoogleOAuthProvider(googleConfig);
} catch (error) {
console.error('Google OAuth is not configured:', error.message);
}
// Custom environment example
try {
const githubConfig = getProviderConfig('github', customEnvironment);
// Use configuration...
} catch (error) {
console.error('GitHub OAuth configuration error:', error.message);
}
Error Handling
The function throws descriptive errors for various configuration issues:
// Missing environment variables
throw new Error('OAuth provider "google" is not properly configured');
// This covers scenarios where:
// - Provider is disabled (missing required credentials)
// - clientId is missing or empty
// - clientSecret is missing or empty
// - redirectUri is missing or empty
Provider Keys
type ProviderKey = "google" | "github";
Currently supported providers:
"google": Google OAuth 2.0"github": GitHub OAuth Apps
BaseOAuthProvider
The BaseOAuthProvider is an abstract base class that provides common functionality for all OAuth providers.
Key Features
- HTTP Request Handling: Built-in POST and GET methods with proper error handling
- Configuration Validation: Ensures required OAuth credentials are present
- URL Encoding: Automatic conversion of data to URLSearchParams for form submission
- Timeout Management: Configurable request timeouts with sensible defaults
Class Definition
export abstract class BaseOAuthProvider implements IOAuthProvider {
protected config: OAuthConfig;
protected providerName: string;
constructor(providerName: string, config: OAuthConfig);
abstract exchangeCodeForTokens(code: string, redirectUri: string): Promise<OAuthProviderTokenResponse>;
abstract getUserProfile(accessToken: string): Promise<OAuthUserProfile>;
}
Configuration
The provider requires an OAuthConfig object:
interface OAuthConfig {
clientId: string; // Required: OAuth client ID
clientSecret: string; // Required: OAuth client secret
redirectUri?: string; // Optional: Redirect URI (provider-specific)
requestTimeoutMs?: number; // Optional: Request timeout (default: 10000ms)
}
The clientSecret contains sensitive credentials and must:
- Be used server-side only
- Never be logged or exposed in error messages
- Be stored securely in environment variables
Usage Example
class GoogleOAuthProvider extends BaseOAuthProvider {
constructor(config: OAuthConfig) {
super('google', config);
}
async exchangeCodeForTokens(code: string, redirectUri: string): Promise<OAuthProviderTokenResponse> {
return await this.post<OAuthProviderTokenResponse>(
'https://oauth2.googleapis.com/token',
{
grant_type: 'authorization_code',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code,
redirect_uri: redirectUri,
}
);
}
async getUserProfile(accessToken: string): Promise<OAuthUserProfile> {
const response = await this.get<GoogleUserResponse>(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
Authorization: `Bearer ${accessToken}`,
}
);
return {
providerId: response.id,
email: response.email,
name: response.name,
picture: response.picture,
emailVerified: response.verified_email,
};
}
}
Protected Methods
post<T>(url, data, headers?)
Makes an HTTP POST request with automatic URL encoding:
protected async post<T>(
url: string,
data: Record<string, string> | URLSearchParams,
headers?: Record<string, string>
): Promise<T>
- Parameters:
url: Target URL for the requestdata: Request body data (automatically converted to URLSearchParams)headers: Optional additional headers
- Returns: Parsed response data
- Throws:
TokenExchangeErroron failure
get<T>(url, headers?)
Makes an HTTP GET request:
protected async get<T>(
url: string,
headers?: Record<string, string>
): Promise<T>
- Parameters:
url: Target URL for the requestheaders: Optional request headers
- Returns: Parsed response data
- Throws:
ProfileFetchErroron failure
validateConfig()
Validates that required configuration is present:
protected validateConfig(): void
- Validates:
clientIdandclientSecretare non-empty - Throws:
OAuthErrorwith codeINVALID_CONFIGif validation fails
OAuthProviderRegistry
The OAuthProviderRegistry is a singleton class that manages OAuth provider instances throughout the application lifecycle.
Key Features
- Singleton Pattern: Ensures one registry instance per application
- Provider Management: Register, retrieve, and manage OAuth providers
- Name Normalization: Automatic normalization of provider names (trim, lowercase)
- Error Handling: Comprehensive error handling with descriptive messages
- Testing Support: Methods for clearing and unregistering providers
Class Definition
export class OAuthProviderRegistry {
private providers: Map<string, IOAuthProvider>;
private static instance?: OAuthProviderRegistry;
static getInstance(): OAuthProviderRegistry;
register(provider: IOAuthProvider): void;
get(providerName: string): IOAuthProvider;
has(providerName: string): boolean;
listProviders(): string[];
unregister(providerName: string): void;
clear(): void;
}
Usage Example
import { OAuthProviderRegistry } from '~/src/utilities/auth/oauth/OAuthProviderRegistry';
import { GoogleOAuthProvider } from '~/src/utilities/auth/oauth/providers/GoogleOAuthProvider';
// Get singleton instance
const registry = OAuthProviderRegistry.getInstance();
// Register a provider
const googleProvider = new GoogleOAuthProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI,
});
registry.register(googleProvider);
// Retrieve and use a provider
const provider = registry.get('google');
const tokenResponse = await provider.exchangeCodeForTokens(code, redirectUri);
const userProfile = await provider.getUserProfile(tokenResponse.access_token);
Methods
getInstance()
Returns the singleton registry instance:
static getInstance(): OAuthProviderRegistry
register(provider)
Registers an OAuth provider:
register(provider: IOAuthProvider): void
- Parameters:
provider: Provider instance implementingIOAuthProvider
- Throws:
OAuthErrorwith codeINVALID_PROVIDER_NAMEif provider name is emptyOAuthErrorwith codeDUPLICATE_PROVIDERif provider already registered
get(providerName)
Retrieves a registered provider:
get(providerName: string): IOAuthProvider
- Parameters:
providerName: Name of the provider to retrieve
- Returns: Provider instance
- Throws:
OAuthErrorwith codePROVIDER_NOT_FOUNDif provider not found
has(providerName)
Checks if a provider is registered:
has(providerName: string): boolean
- Parameters:
providerName: Name of the provider to check
- Returns:
trueif provider exists,falseotherwise
listProviders()
Returns all registered provider names:
listProviders(): string[]
- Returns: Array of registered provider names
unregister(providerName) and clear()
Testing utilities for removing providers:
unregister(providerName: string): void // Remove specific provider
clear(): void // Remove all providers
Type Definitions
OAuthProviderTokenResponse
Response structure from OAuth provider token endpoints:
interface OAuthProviderTokenResponse {
access_token: string; // Required: OAuth access token
token_type: string; // Required: Token type (usually "Bearer")
expires_in?: number; // Optional: Token expiration time in seconds
refresh_token?: string; // Optional: Refresh token for renewing access
scope?: string; // Optional: Granted scopes
id_token?: string; // Optional: OpenID Connect ID token
}
OAuthUserProfile
Normalized user profile structure:
interface OAuthUserProfile {
providerId: string; // Required: Unique user ID from provider
email?: string; // Optional: User email address
name?: string; // Optional: User display name
picture?: string; // Optional: User profile picture URL
emailVerified?: boolean; // Optional: Email verification status
}
OAuthConfig
Configuration object for OAuth providers:
interface OAuthConfig {
clientId: string; // Required: OAuth client ID
clientSecret: string; // Required: OAuth client secret
redirectUri?: string; // Optional: Redirect URI
requestTimeoutMs?: number; // Optional: Request timeout (default: 10000ms)
}
Error Handling
The OAuth system uses specialized error classes for different failure scenarios:
TokenExchangeError
Thrown when token exchange fails:
class TokenExchangeError extends OAuthError {
constructor(message: string, details?: string);
}
ProfileFetchError
Thrown when user profile retrieval fails:
class ProfileFetchError extends OAuthError {
constructor(message: string);
}
OAuthError
Base error class for OAuth-related errors:
class OAuthError extends Error {
constructor(message: string, code: string, statusCode: number);
}
Common error codes:
INVALID_CONFIG: Configuration validation failedINVALID_PROVIDER_NAME: Provider name is empty or invalidDUPLICATE_PROVIDER: Attempting to register an already registered providerPROVIDER_NOT_FOUND: Requested provider is not registered
Best Practices
1. Environment Configuration
Store OAuth credentials securely in environment variables:
const config: OAuthConfig = {
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
redirectUri: process.env.OAUTH_REDIRECT_URI,
requestTimeoutMs: parseInt(process.env.OAUTH_TIMEOUT_MS || '10000'),
};
2. Error Handling
Always handle OAuth errors appropriately by using the centralized OAuthError hierarchy to represent well-defined failure cases with consistent error codes, status codes, and clear semantics:
try {
const provider = registry.get(providerName);
const tokenResponse = await provider.exchangeCodeForTokens(code, redirectUri);
const userProfile = await provider.getUserProfile(tokenResponse.access_token);
// Handle successful authentication
} catch (error) {
if (error instanceof TokenExchangeError) {
// Handle token exchange failure
logger.error('Token exchange failed:', error.message);
} else if (error instanceof ProfileFetchError) {
// Handle profile fetch failure
logger.error('Profile fetch failed:', error.message);
} else if (error instanceof OAuthError && error.code === 'PROVIDER_NOT_FOUND') {
// Handle unknown provider
throw new Error(`Unsupported OAuth provider: ${providerName}`);
} else {
// Handle unexpected errors
throw error;
}
}
3. Testing
When testing OAuth functionality:
import { OAuthProviderRegistry } from '~/src/utilities/auth/oauth/OAuthProviderRegistry';
beforeEach(() => {
// Clear registry before each test
const registry = OAuthProviderRegistry.getInstance();
registry.clear();
});
4. Security Considerations
- Never log or expose
clientSecretin error messages