UNPKG

@oa2/core

Version:

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

338 lines (333 loc) 13.6 kB
Object.defineProperty(exports, '__esModule', { value: true }); var errors_cjs = require('./errors.cjs'); var utils_cjs = require('./utils.cjs'); /** * Creates a token object with the given parameters. * Helper function to ensure consistency across token creation. */ function createToken(params, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt) { return { accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, scope: params.scope, clientId: params.client.id, userId: params.userId }; } /** * Validates a token by checking if it exists and is not expired. * Returns null if the token is invalid or expired. */ async function validateTokenInStorage(tokenValue, storage, getTokenFn, expiresAtProperty) { const token = await getTokenFn(tokenValue); if (!token) { return null; } // Check if token is expired const expiresAt = token[expiresAtProperty]; if (expiresAt && expiresAt <= new Date()) { // Optionally revoke expired token await storage.revokeToken(tokenValue); return null; } return token; } /** * Generates an access token using the opaque strategy. * Creates a random token and stores it in the database. */ async function generateAccessToken(params, context, options, storage) { const accessToken = utils_cjs.generateRandomString(options.tokenLength || 32); const accessTokenExpiresAt = new Date(Date.now() + (options.accessTokenExpiresIn || 3600) * 1000); const token = createToken(params, accessToken, accessTokenExpiresAt); await storage.saveToken(token); return token; } /** * Generates a refresh token using the opaque strategy. * Creates a random token and stores it in the database. */ async function generateRefreshToken(params, context, options, storage) { const refreshToken = utils_cjs.generateRandomString(options.tokenLength || 32); const refreshTokenExpiresAt = new Date(Date.now() + (options.refreshTokenExpiresIn || 604800) * 1000); const token = createToken(params, '', new Date(), refreshToken, refreshTokenExpiresAt); await storage.saveToken(token); return token; } /** * Generates both access and refresh tokens in a single operation. * More efficient when both tokens are needed. */ async function generateTokenPair(params, context, options, storage) { const accessToken = utils_cjs.generateRandomString(options.tokenLength || 32); const refreshToken = utils_cjs.generateRandomString(options.tokenLength || 32); const accessTokenExpiresAt = new Date(Date.now() + (options.accessTokenExpiresIn || 3600) * 1000); const refreshTokenExpiresAt = new Date(Date.now() + (options.refreshTokenExpiresIn || 604800) * 1000); const token = createToken(params, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt); await storage.saveToken(token); return token; } /** * Validates an access token by looking it up in the database. */ async function validateAccessToken(accessToken, context) { return validateTokenInStorage(accessToken, context.storage, (token)=>context.storage.getAccessToken(token), 'accessTokenExpiresAt'); } /** * Validates a refresh token by looking it up in the database. */ async function validateRefreshToken(refreshToken, context) { return validateTokenInStorage(refreshToken, context.storage, (token)=>context.storage.getRefreshToken(token), 'refreshTokenExpiresAt'); } /** * Creates an Opaque Token Strategy. * * Opaque tokens are random strings stored in the database. They provide maximum * security as tokens can be easily revoked and contain no embedded information. * Validation requires database lookups but provides fine-grained control. * * @param storage The storage adapter for token persistence * @param options Opaque token configuration options * * @example * ```typescript * const storage = new YourStorageAdapter(); * const tokenStrategy = createOpaqueTokenStrategy(storage, { * accessTokenExpiresIn: 3600, // 1 hour * refreshTokenExpiresIn: 604800, // 7 days * tokenLength: 32 * }); * ``` */ function createOpaqueTokenStrategy(storage, options = {}) { return { generateAccessToken: (params, context)=>generateAccessToken(params, context, options, storage), generateRefreshToken: (params, context)=>generateRefreshToken(params, context, options, storage), generateTokenPair: (params, context)=>generateTokenPair(params, context, options, storage), validateAccessToken: (accessToken, context)=>validateAccessToken(accessToken, context), validateRefreshToken: (refreshToken, context)=>validateRefreshToken(refreshToken, context) }; } /** * Authenticates a client using the provided credentials. * Supports both Basic authentication and form-based credentials. */ async function authenticateClient(request, storage) { const body = utils_cjs.parseRequestBody(request); const { client_id, client_secret } = body; let authenticatedClientId; let authenticatedClientSecret; // Try Basic authentication first const basicAuth = utils_cjs.extractBasicAuthCredentials(request.headers.authorization || request.headers.Authorization); if (basicAuth) { authenticatedClientId = basicAuth.clientId; authenticatedClientSecret = basicAuth.clientSecret; } else if (client_id && client_secret) { // Fall back to form-based authentication authenticatedClientId = client_id; authenticatedClientSecret = client_secret; } else if (client_id) { // Public client (no secret required) authenticatedClientId = client_id; } else { throw new errors_cjs.InvalidRequestError('Client authentication required'); } if (!authenticatedClientId || authenticatedClientId.trim() === '') { throw new errors_cjs.UnauthorizedClientError('Client not found'); } const client = await storage.getClient(authenticatedClientId); if (!client) { throw new errors_cjs.UnauthorizedClientError('Client not found'); } // Verify client secret if provided if (authenticatedClientSecret && client.secret) { if (!utils_cjs.verifyClientSecret(authenticatedClientSecret, client.secret)) { throw new errors_cjs.UnauthorizedClientError('Invalid client credentials'); } } return client; } /** * Finds a grant that supports the specified response type. * Used for authorization endpoint requests. */ function findGrantByResponseType(grants, responseType) { const responseTypeGrants = grants.filter((grant)=>grant.handleAuthorization && grant.responseTypes?.includes(responseType)); const grant = responseTypeGrants[0]; if (!grant) { throw new errors_cjs.UnsupportedResponseTypeError(`Unsupported response_type: ${responseType}`); } return grant; } /** * Finds a grant that supports the specified grant type. * Used for token endpoint requests. */ function findGrantByType(grants, grantType) { const grant = grants.find((g)=>g.type === grantType); if (!grant) { throw new errors_cjs.UnsupportedGrantTypeError(`Unsupported grant_type: ${grantType}`); } return grant; } /** * Creates a complete server configuration with defaults. * Ensures all required fields are present and valid. */ function createCompleteConfig(config) { return { ...config, tokenStrategy: config.tokenStrategy || createOpaqueTokenStrategy(config.storage, { accessTokenExpiresIn: config.accessTokenLifetime || 3600, refreshTokenExpiresIn: config.refreshTokenLifetime || 604800 }) }; } /** * Creates a context object for grant handlers. * Includes all necessary information for processing OAuth 2.0 requests. */ function createContext(request, storage, client, config) { return { request, storage, client, config }; } /** * Processes an authorization request and delegates to the appropriate grant handler. * Validates the request parameters and client before proceeding. */ async function handleAuthorizeRequest(request, storage, config) { const { client_id, response_type } = request.query; if (!client_id) { throw new errors_cjs.InvalidRequestError('Missing client_id parameter'); } if (!response_type) { throw new errors_cjs.InvalidRequestError('Missing response_type parameter'); } // Find the appropriate grant for this response type const grant = findGrantByResponseType(config.grants, response_type); // Get the client const client = await storage.getClient(client_id); if (!client) { throw new errors_cjs.UnauthorizedClientError('Client not found'); } // Create context and delegate to grant handler const context = createContext(request, storage, client, config); if (!grant.handleAuthorization) { throw new errors_cjs.UnsupportedResponseTypeError(`Grant does not support authorization requests: ${response_type}`); } return grant.handleAuthorization(context); } /** * Token Endpoint * ============== * Handles OAuth 2.0 token requests. */ /** * Processes a token request and delegates to the appropriate grant handler. * Performs client authentication and grant type validation. */ async function handleTokenRequest(request, storage, config) { const body = utils_cjs.parseRequestBody(request); const { grant_type } = body; if (!grant_type) { throw new errors_cjs.InvalidRequestError('Missing grant_type parameter'); } // Authenticate the client const client = await authenticateClient(request, storage); // Find the appropriate grant for this grant type const grant = findGrantByType(config.grants, grant_type); // Create context and delegate to grant handler const context = createContext(request, storage, client, config); if (!grant.handleToken) { throw new errors_cjs.UnsupportedGrantTypeError(`Grant does not support token requests: ${grant_type}`); } return grant.handleToken(context); } /** * Revocation Endpoint * =================== * Handles OAuth 2.0 token revocation requests. */ /** * Processes a token revocation request. * Validates the token parameter and revokes the specified token. */ async function handleRevokeRequest(request, storage) { const body = utils_cjs.parseRequestBody(request); const { token } = body; if (!token) { throw new errors_cjs.InvalidRequestError('Missing token parameter'); } // In a real implementation, you would validate the client making the revocation request // and ensure they are authorized to revoke this token. await storage.revokeToken(token); return { statusCode: 200, headers: {}, body: {}, cookies: {} }; } /** * Processes a token introspection request. * Returns metadata about the specified token. */ async function handleIntrospectRequest(request, storage) { const body = utils_cjs.parseRequestBody(request); const { token } = body; if (!token) { throw new errors_cjs.InvalidRequestError('Missing token parameter'); } const accessToken = await storage.getAccessToken(token); const refreshToken = await storage.getRefreshToken(token); let active = false; let responseBody = { active: false }; if (accessToken) { active = accessToken.accessTokenExpiresAt > new Date(); if (active) { responseBody = { active: true, scope: accessToken.scope, client_id: accessToken.clientId, username: accessToken.userId, exp: Math.floor(accessToken.accessTokenExpiresAt.getTime() / 1000) }; } } else if (refreshToken) { active = refreshToken.refreshTokenExpiresAt ? refreshToken.refreshTokenExpiresAt > new Date() : false; if (active) { responseBody = { active: true, scope: refreshToken.scope, client_id: refreshToken.clientId, username: refreshToken.userId, exp: Math.floor(refreshToken.refreshTokenExpiresAt.getTime() / 1000) }; } } return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: responseBody, cookies: {} }; } /** * Creates and configures an OAuth 2.0 server instance. * Provides a clean, functional interface for handling OAuth 2.0 flows. * * @example * ```typescript * const storage = new MyStorageAdapter(); * const server = createOAuth2Server({ * storage, * grants: [createAuthorizationCodeGrant(), clientCredentialsGrant()], * predefinedScopes: ['read', 'write'], * tokenStrategy: createJwtTokenStrategy(storage, { secret: 'my-secret' }) * }); * ``` */ function createOAuth2Server(config) { const completeConfig = createCompleteConfig(config); const { storage } = completeConfig; return { authorize: (request)=>handleAuthorizeRequest(request, storage, completeConfig), token: (request)=>handleTokenRequest(request, storage, completeConfig), revoke: (request)=>handleRevokeRequest(request, storage), introspect: (request)=>handleIntrospectRequest(request, storage) }; } // For backward compatibility const createServer = createOAuth2Server; exports.createOAuth2Server = createOAuth2Server; exports.createServer = createServer;