/**
* postgres-mcp - OAuth Middleware
*
* Authentication and authorization middleware for HTTP transport.
*/
import type { TokenClaims } from './types.js';
import type { TokenValidator } from './TokenValidator.js';
import { TokenMissingError, InvalidTokenError, InsufficientScopeError } from './errors.js';
import { hasScope, hasAnyScope, SCOPES } from './scopes.js';
/**
* Authenticated request context
*/
export interface AuthenticatedContext {
/** Whether request is authenticated */
authenticated: boolean;
/** Token claims (if authenticated) */
claims?: TokenClaims;
/** Token scopes (convenience) */
scopes: string[];
}
/**
* Auth middleware configuration
*/
export interface AuthMiddlewareConfig {
/** Token validator instance */
tokenValidator: TokenValidator;
/** Whether to require authentication (default: true) */
required?: boolean;
/** Required scopes (any of these) */
requiredScopes?: string[];
}
/**
* Extract bearer token from Authorization header
*/
export function extractBearerToken(authHeader: string | undefined): string | null {
if (!authHeader) {
return null;
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0]?.toLowerCase() !== 'bearer') {
return null;
}
return parts[1] ?? null;
}
/**
* Create authentication context from request
*/
export async function createAuthContext(
authHeader: string | undefined,
tokenValidator: TokenValidator
): Promise<AuthenticatedContext> {
const token = extractBearerToken(authHeader);
if (!token) {
return { authenticated: false, scopes: [] };
}
const result = await tokenValidator.validate(token);
if (!result.valid || !result.claims) {
return { authenticated: false, scopes: [] };
}
return {
authenticated: true,
claims: result.claims,
scopes: result.claims.scopes
};
}
/**
* Validate authentication and authorization
*/
export async function validateAuth(
authHeader: string | undefined,
config: AuthMiddlewareConfig
): Promise<AuthenticatedContext> {
const token = extractBearerToken(authHeader);
// Check if token is required
if (!token) {
if (config.required !== false) {
throw new TokenMissingError();
}
return { authenticated: false, scopes: [] };
}
// Validate the token
const result = await config.tokenValidator.validate(token);
if (!result.valid || !result.claims) {
throw new InvalidTokenError(result.error ?? 'Invalid token');
}
const context: AuthenticatedContext = {
authenticated: true,
claims: result.claims,
scopes: result.claims.scopes
};
// Check required scopes
if (config.requiredScopes && config.requiredScopes.length > 0) {
if (!hasAnyScope(context.scopes, config.requiredScopes)) {
throw new InsufficientScopeError(config.requiredScopes);
}
}
return context;
}
/**
* Check if context has required scope
*/
export function requireScope(context: AuthenticatedContext, scope: string): void {
if (!context.authenticated) {
throw new TokenMissingError();
}
if (!hasScope(context.scopes, scope)) {
throw new InsufficientScopeError([scope]);
}
}
/**
* Check if context has any of the required scopes
*/
export function requireAnyScope(context: AuthenticatedContext, scopes: string[]): void {
if (!context.authenticated) {
throw new TokenMissingError();
}
if (!hasAnyScope(context.scopes, scopes)) {
throw new InsufficientScopeError(scopes);
}
}
/**
* Check if context has scope for a tool operation
*/
export function requireToolScope(context: AuthenticatedContext, requiredScopes: string[]): void {
if (!context.authenticated) {
throw new TokenMissingError();
}
// Map tool required scopes to actual OAuth scopes
const mappedScopes = requiredScopes.map(scope => {
switch (scope) {
case 'read': return SCOPES.READ;
case 'write': return SCOPES.WRITE;
case 'admin': return SCOPES.ADMIN;
default: return scope;
}
});
if (!hasAnyScope(context.scopes, mappedScopes)) {
throw new InsufficientScopeError(mappedScopes);
}
}
/**
* Format OAuth error for HTTP response
*/
export function formatOAuthError(error: unknown): { status: number; body: object } {
if (error instanceof TokenMissingError) {
return {
status: 401,
body: {
error: 'invalid_token',
error_description: error.message
}
};
}
if (error instanceof InvalidTokenError) {
return {
status: 401,
body: {
error: 'invalid_token',
error_description: error.message
}
};
}
if (error instanceof InsufficientScopeError) {
return {
status: 403,
body: {
error: 'insufficient_scope',
error_description: error.message,
scope: error.requiredScopes.join(' ')
}
};
}
// Generic error
return {
status: 500,
body: {
error: 'server_error',
error_description: 'Internal server error'
}
};
}