UNPKG

@oa2/core

Version:

A comprehensive, RFC-compliant OAuth 2.0 authorization server implementation in TypeScript

452 lines (447 loc) 15.3 kB
Object.defineProperty(exports, '__esModule', { value: true }); var errors_cjs = require('../errors.cjs'); /** * Request/Response Transformation * ============================== */ /** * Converts Express request to OAuth2Request format. * Normalizes the request structure for consistent processing. */ function expressRequestToOAuth2Request(req) { return { path: req.path, method: req.method.toUpperCase(), headers: req.headers, query: req.query, body: req.body, cookies: req.cookies || {} }; } /** * Sends OAuth2Response via Express response. * Handles headers, cookies, redirects, and body formatting. */ function sendOAuth2Response(res, oauth2Response) { // Set headers Object.entries(oauth2Response.headers).forEach(([key, value])=>{ res.set(key, value); }); // Set cookies Object.entries(oauth2Response.cookies).forEach(([name, value])=>{ res.cookie(name, value); }); // Handle redirects if (oauth2Response.redirect) { res.redirect(oauth2Response.statusCode, oauth2Response.redirect); return; } // Send response res.status(oauth2Response.statusCode); if (typeof oauth2Response.body === 'string') { res.send(oauth2Response.body); } else { res.json(oauth2Response.body); } } /** * CORS Support * ============ */ /** * Handles CORS headers for OAuth2 endpoints. * Configures appropriate CORS policies for OAuth 2.0 flows. */ function handleCors(req, res, options) { if (!options.cors) return; const origin = req.headers.origin; const allowedOrigins = Array.isArray(options.corsOrigins) ? options.corsOrigins : options.corsOrigins ? [ options.corsOrigins ] : [ '*' ]; if (allowedOrigins.includes('*') || origin && allowedOrigins.includes(origin)) { res.set('Access-Control-Allow-Origin', origin || '*'); } res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); res.set('Access-Control-Allow-Credentials', 'true'); res.set('Access-Control-Max-Age', '86400'); // 24 hours } /** * Generic Handler Factory * ======================= */ /** * Creates a generic OAuth2 endpoint handler middleware. * Provides consistent error handling and response formatting. */ function createOAuth2Handler(handlerMethod, options) { return async (req, res, next)=>{ try { // Handle CORS handleCors(req, res, options); // Handle preflight requests if (req.method === 'OPTIONS' && options.handlePreflight !== false) { res.status(204).end(); return; } // Convert request format const oauth2Request = expressRequestToOAuth2Request(req); // Call the appropriate OAuth2 server method const oauth2Response = await options.server[handlerMethod](oauth2Request); // Send response sendOAuth2Response(res, oauth2Response); } catch (error) { if (error instanceof errors_cjs.OAuth2Error) { // Handle OAuth2-specific errors handleCors(req, res, options); res.status(error.statusCode).json({ error: error.code, error_description: error.description }); } else { // Pass other errors to Express error handler next(error); } } }; } /** * OAuth 2.0 Endpoint Handlers * =========================== */ /** * Express middleware for OAuth2 authorization endpoint. */ function expressAuthorizeHandler(options) { return createOAuth2Handler('authorize', options); } /** * Express middleware for OAuth2 token endpoint. */ function expressTokenHandler(options) { return createOAuth2Handler('token', options); } /** * Express middleware for OAuth2 token revocation endpoint. */ function expressRevokeHandler(options) { return createOAuth2Handler('revoke', options); } /** * Express middleware for OAuth2 token introspection endpoint. */ function expressIntrospectHandler(options) { return createOAuth2Handler('introspect', options); } /** * Express Router Factory * ===================== */ /** * Express router factory that sets up all OAuth2 endpoints. * Provides a complete OAuth 2.0 server in a single router. * * @example * ```typescript * const oauth2Router = createOAuth2Router({ * server: myOAuth2Server, * cors: true, * corsOrigins: ['https://myapp.com'] * }); * * app.use('/oauth', oauth2Router); * ``` */ function createOAuth2Router(options) { const { Router } = require('express'); const router = Router(); // Authorization endpoint (GET) router.get('/authorize', expressAuthorizeHandler(options)); // Token endpoint (POST) router.post('/token', expressTokenHandler(options)); // Revocation endpoint (POST) router.post('/revoke', expressRevokeHandler(options)); // Introspection endpoint (POST) router.post('/introspect', expressIntrospectHandler(options)); return router; } /** * Token Validation Middleware * =========================== */ /** * Express middleware to validate OAuth2 access tokens. * Protects routes by requiring valid OAuth 2.0 access tokens. * * @example * ```typescript * // Protect a route with required scopes * app.get('/api/protected', * validateOAuth2Token({ * server: myServer, * scopes: ['read', 'write'] * }), * (req, res) => { * // Access req.oauth2Token for token info * res.json({ message: 'Protected resource', user: req.oauth2Token.username }); * } * ); * ``` */ function validateOAuth2Token(options) { return async (req, res, next)=>{ try { const authHeader = req.headers.authorization; if (!authHeader) { if (options.optional) { next(); return; } res.status(401).json({ error: 'invalid_request', error_description: 'Missing Authorization header' }); return; } if (!authHeader.startsWith('Bearer ')) { res.status(401).json({ error: 'invalid_request', error_description: 'Invalid Authorization header format' }); return; } const token = authHeader.substring(7); const oauth2Request = expressRequestToOAuth2Request(req); // Use introspection to validate the token const introspectResponse = await options.server.introspect({ ...oauth2Request, method: 'POST', body: { token } }); if (introspectResponse.statusCode !== 200 || !introspectResponse.body.active) { res.status(401).json({ error: 'invalid_token', error_description: 'Token is not active' }); return; } // Check scopes if required if (options.scopes && options.scopes.length > 0) { const tokenScopes = (introspectResponse.body.scope || '').split(' '); const hasRequiredScope = options.scopes.some((scope)=>tokenScopes.includes(scope)); if (!hasRequiredScope) { res.status(403).json({ error: 'insufficient_scope', error_description: `Required scopes: ${options.scopes.join(', ')}` }); return; } } // Attach token information to request req.oauth2Token = introspectResponse.body; next(); } catch (error) { res.status(500).json({ error: 'server_error', error_description: 'Token validation failed' }); } }; } /** * Request Processing * ================== */ /** * Parses cookies from Cookie header. * Extracts cookie name-value pairs for OAuth 2.0 state management. */ function extractCookies(cookieHeader) { const cookies = {}; if (!cookieHeader) { return cookies; } cookieHeader.split(';').forEach((cookie)=>{ const [name, ...rest] = cookie.trim().split('='); if (name && rest.length > 0) { cookies[name] = rest.join('='); } }); return cookies; } /** * Extracts OAuth2Request from API Gateway event. * Handles both JSON and form-urlencoded request bodies. * * @example * ```typescript * export const handler: APIGatewayProxyHandler = async (event) => { * const oauth2Request = extractOAuth2Request(event); * const response = await server.token(oauth2Request); * return transformOAuth2Response(response); * }; * ``` */ function extractOAuth2Request(event) { let parsedBody; if (event.body) { const contentType = event.headers['Content-Type'] || event.headers['content-type'] || ''; // Handle form-urlencoded bodies (common for OAuth2 token requests) if (contentType.includes('application/x-www-form-urlencoded')) { parsedBody = event.body; // Keep as string for parseRequestBody utility to handle } else if (contentType.includes('application/json')) { try { parsedBody = JSON.parse(event.body); } catch (e) { parsedBody = event.body; // Fallback to raw string if JSON parsing fails } } else { parsedBody = event.body; // Raw string for other content types } } return { path: event.path, method: event.httpMethod.toUpperCase(), headers: event.headers, query: event.queryStringParameters ?? {}, body: parsedBody, cookies: extractCookies(event.headers.Cookie || event.headers.cookie || '') }; } /** * Response Processing * =================== */ /** * Transforms OAuth2Response into API Gateway result. * Handles redirects, headers, cookies, and body formatting. * * @example * ```typescript * const oauth2Response = await server.authorize(request); * return transformOAuth2Response(oauth2Response); * ``` */ function transformOAuth2Response(response) { const headers = { ...response.headers }; const result = { statusCode: response.statusCode, headers, body: typeof response.body === 'string' ? response.body : JSON.stringify(response.body), isBase64Encoded: false }; // Handle redirects for authorization endpoint if (response.redirect) { result.statusCode = 302; headers.Location = response.redirect; result.body = ''; } // Ensure Content-Type is set for JSON responses if (!headers['Content-Type'] && response.body && typeof response.body === 'object') { headers['Content-Type'] = 'application/json'; } // Handle cookies if present if (response.cookies && Object.keys(response.cookies).length > 0) { const cookieStrings = Object.entries(response.cookies).map(([name, value])=>`${name}=${value}`); headers['Set-Cookie'] = cookieStrings.join('; '); } return result; } /** * Error Handling * ============== */ /** * Transforms OAuth2Error into API Gateway result. * Formats errors according to OAuth 2.0 specifications. */ function transformOAuth2Error(error) { return { statusCode: error.statusCode || 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: error.code, error_description: error.description }), isBase64Encoded: false }; } /** * Generic Handler * =============== */ /** * Shared helper for handling OAuth2 responses with consistent formatting. * Provides unified error handling across all Lambda handlers. */ async function handleOAuth2Response(handler, event) { try { const request = extractOAuth2Request(event); const response = await handler(request); return transformOAuth2Response(response); } catch (error) { // Handle OAuth2Error instances if (error instanceof errors_cjs.OAuth2Error) { return transformOAuth2Error(error); } // Handle unexpected errors console.error('Unexpected error in OAuth2 handler:', error); return { statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'server_error', error_description: 'An unexpected error occurred' }), isBase64Encoded: false }; } } /** * OAuth 2.0 Endpoint Handlers * =========================== */ /** * AWS Lambda handler for OAuth2 authorization requests. * * @example * ```typescript * import { createOAuth2Server } from 'oauth'; * import { apiGatewayAuthorizeHandler } from 'oauth/adapters/aws'; * * const server = createOAuth2Server({ ... }); * export const authorize = apiGatewayAuthorizeHandler(server); * ``` */ function apiGatewayAuthorizeHandler(server) { return async (event)=>handleOAuth2Response(server.authorize.bind(server), event); } /** * AWS Lambda handler for OAuth2 token requests. * * @example * ```typescript * const server = createOAuth2Server({ ... }); * export const token = apiGatewayTokenHandler(server); * ``` */ function apiGatewayTokenHandler(server) { return async (event)=>handleOAuth2Response(server.token.bind(server), event); } /** * AWS Lambda handler for OAuth2 revoke requests. * * @example * ```typescript * const server = createOAuth2Server({ ... }); * export const revoke = apiGatewayRevokeHandler(server); * ``` */ function apiGatewayRevokeHandler(server) { return async (event)=>handleOAuth2Response(server.revoke.bind(server), event); } /** * AWS Lambda handler for OAuth2 introspect requests. * * @example * ```typescript * const server = createOAuth2Server({ ... }); * export const introspect = apiGatewayIntrospectHandler(server); * ``` */ function apiGatewayIntrospectHandler(server) { return async (event)=>handleOAuth2Response(server.introspect.bind(server), event); } exports.apiGatewayAuthorizeHandler = apiGatewayAuthorizeHandler; exports.apiGatewayIntrospectHandler = apiGatewayIntrospectHandler; exports.apiGatewayRevokeHandler = apiGatewayRevokeHandler; exports.apiGatewayTokenHandler = apiGatewayTokenHandler; exports.createOAuth2Router = createOAuth2Router; exports.expressAuthorizeHandler = expressAuthorizeHandler; exports.expressIntrospectHandler = expressIntrospectHandler; exports.expressRevokeHandler = expressRevokeHandler; exports.expressTokenHandler = expressTokenHandler; exports.extractOAuth2Request = extractOAuth2Request; exports.transformOAuth2Error = transformOAuth2Error; exports.transformOAuth2Response = transformOAuth2Response; exports.validateOAuth2Token = validateOAuth2Token;