@oa2/core
Version:
A comprehensive, RFC-compliant OAuth 2.0 authorization server implementation in TypeScript
452 lines (447 loc) • 15.3 kB
JavaScript
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;