UNPKG

@oa2/core

Version:

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

1 lines 9.92 kB
{"version":3,"file":"server.d.ts","sources":["../src/server.ts"],"sourcesContent":["import {\n OAuth2Request,\n OAuth2Response,\n OAuth2Server,\n Context,\n Grant,\n Client,\n StorageAdapter,\n ServerConfig,\n TokenStrategy,\n} from './types';\nimport {\n InvalidRequestError,\n UnsupportedResponseTypeError,\n UnauthorizedClientError,\n UnsupportedGrantTypeError,\n} from './errors';\nimport { extractBasicAuthCredentials, parseRequestBody, verifyClientSecret } from './utils';\nimport { createOpaqueTokenStrategy } from './tokens/opaque';\n\n/**\n * Authenticates a client using the provided credentials.\n * Supports both Basic authentication and form-based credentials.\n */\nasync function authenticateClient(request: OAuth2Request, storage: StorageAdapter): Promise<Client> {\n const body = parseRequestBody(request);\n const { client_id, client_secret } = body;\n\n let authenticatedClientId: string | undefined;\n let authenticatedClientSecret: string | undefined;\n\n // Try Basic authentication first\n const basicAuth = extractBasicAuthCredentials(request.headers.authorization || request.headers.Authorization);\n if (basicAuth) {\n authenticatedClientId = basicAuth.clientId;\n authenticatedClientSecret = basicAuth.clientSecret;\n } else if (client_id && client_secret) {\n // Fall back to form-based authentication\n authenticatedClientId = client_id;\n authenticatedClientSecret = client_secret;\n } else if (client_id) {\n // Public client (no secret required)\n authenticatedClientId = client_id;\n } else {\n throw new InvalidRequestError('Client authentication required');\n }\n\n if (!authenticatedClientId || authenticatedClientId.trim() === '') {\n throw new UnauthorizedClientError('Client not found');\n }\n\n const client = await storage.getClient(authenticatedClientId);\n if (!client) {\n throw new UnauthorizedClientError('Client not found');\n }\n\n // Verify client secret if provided\n if (authenticatedClientSecret && client.secret) {\n if (!verifyClientSecret(authenticatedClientSecret, client.secret)) {\n throw new UnauthorizedClientError('Invalid client credentials');\n }\n }\n\n return client;\n}\n\n/**\n * Finds a grant that supports the specified response type.\n * Used for authorization endpoint requests.\n */\nfunction findGrantByResponseType(grants: Grant[], responseType: string): Grant {\n const responseTypeGrants = grants.filter(\n (grant) => grant.handleAuthorization && grant.responseTypes?.includes(responseType),\n );\n\n const grant = responseTypeGrants[0];\n if (!grant) {\n throw new UnsupportedResponseTypeError(`Unsupported response_type: ${responseType}`);\n }\n\n return grant;\n}\n\n/**\n * Finds a grant that supports the specified grant type.\n * Used for token endpoint requests.\n */\nfunction findGrantByType(grants: Grant[], grantType: string): Grant {\n const grant = grants.find((g) => g.type === grantType);\n if (!grant) {\n throw new UnsupportedGrantTypeError(`Unsupported grant_type: ${grantType}`);\n }\n\n return grant;\n}\n\n/**\n * Creates a complete server configuration with defaults.\n * Ensures all required fields are present and valid.\n */\nfunction createCompleteConfig(config: ServerConfig): ServerConfig & { tokenStrategy: TokenStrategy } {\n return {\n ...config,\n tokenStrategy:\n config.tokenStrategy ||\n createOpaqueTokenStrategy(config.storage, {\n accessTokenExpiresIn: config.accessTokenLifetime || 3600,\n refreshTokenExpiresIn: config.refreshTokenLifetime || 604800,\n }),\n };\n}\n\n/**\n * Creates a context object for grant handlers.\n * Includes all necessary information for processing OAuth 2.0 requests.\n */\nfunction createContext(\n request: OAuth2Request,\n storage: StorageAdapter,\n client: Client | undefined,\n config: ServerConfig & { tokenStrategy: TokenStrategy },\n): Context {\n return {\n request,\n storage,\n client,\n config,\n };\n}\n\n/**\n * Processes an authorization request and delegates to the appropriate grant handler.\n * Validates the request parameters and client before proceeding.\n */\nasync function handleAuthorizeRequest(\n request: OAuth2Request,\n storage: StorageAdapter,\n config: ServerConfig & { tokenStrategy: TokenStrategy },\n): Promise<OAuth2Response> {\n const { client_id, response_type } = request.query;\n\n if (!client_id) {\n throw new InvalidRequestError('Missing client_id parameter');\n }\n\n if (!response_type) {\n throw new InvalidRequestError('Missing response_type parameter');\n }\n\n // Find the appropriate grant for this response type\n const grant = findGrantByResponseType(config.grants, response_type);\n\n // Get the client\n const client = await storage.getClient(client_id);\n if (!client) {\n throw new UnauthorizedClientError('Client not found');\n }\n\n // Create context and delegate to grant handler\n const context = createContext(request, storage, client, config);\n if (!grant.handleAuthorization) {\n throw new UnsupportedResponseTypeError(`Grant does not support authorization requests: ${response_type}`);\n }\n\n return grant.handleAuthorization(context);\n}\n\n/**\n * Token Endpoint\n * ==============\n * Handles OAuth 2.0 token requests.\n */\n\n/**\n * Processes a token request and delegates to the appropriate grant handler.\n * Performs client authentication and grant type validation.\n */\nasync function handleTokenRequest(\n request: OAuth2Request,\n storage: StorageAdapter,\n config: ServerConfig & { tokenStrategy: TokenStrategy },\n): Promise<OAuth2Response> {\n const body = parseRequestBody(request);\n const { grant_type } = body;\n\n if (!grant_type) {\n throw new InvalidRequestError('Missing grant_type parameter');\n }\n\n // Authenticate the client\n const client = await authenticateClient(request, storage);\n\n // Find the appropriate grant for this grant type\n const grant = findGrantByType(config.grants, grant_type);\n\n // Create context and delegate to grant handler\n const context = createContext(request, storage, client, config);\n if (!grant.handleToken) {\n throw new UnsupportedGrantTypeError(`Grant does not support token requests: ${grant_type}`);\n }\n\n return grant.handleToken(context);\n}\n\n/**\n * Revocation Endpoint\n * ===================\n * Handles OAuth 2.0 token revocation requests.\n */\n\n/**\n * Processes a token revocation request.\n * Validates the token parameter and revokes the specified token.\n */\nasync function handleRevokeRequest(request: OAuth2Request, storage: StorageAdapter): Promise<OAuth2Response> {\n const body = parseRequestBody(request);\n const { token } = body;\n\n if (!token) {\n throw new InvalidRequestError('Missing token parameter');\n }\n\n // In a real implementation, you would validate the client making the revocation request\n // and ensure they are authorized to revoke this token.\n await storage.revokeToken(token);\n\n return {\n statusCode: 200,\n headers: {},\n body: {},\n cookies: {},\n };\n}\n\n/**\n * Processes a token introspection request.\n * Returns metadata about the specified token.\n */\nasync function handleIntrospectRequest(request: OAuth2Request, storage: StorageAdapter): Promise<OAuth2Response> {\n const body = parseRequestBody(request);\n const { token } = body;\n\n if (!token) {\n throw new InvalidRequestError('Missing token parameter');\n }\n\n const accessToken = await storage.getAccessToken(token);\n const refreshToken = await storage.getRefreshToken(token);\n\n let active = false;\n let responseBody: Record<string, any> = { active: false };\n\n if (accessToken) {\n active = accessToken.accessTokenExpiresAt > new Date();\n if (active) {\n responseBody = {\n active: true,\n scope: accessToken.scope,\n client_id: accessToken.clientId,\n username: accessToken.userId,\n exp: Math.floor(accessToken.accessTokenExpiresAt.getTime() / 1000),\n };\n }\n } else if (refreshToken) {\n active = refreshToken.refreshTokenExpiresAt ? refreshToken.refreshTokenExpiresAt > new Date() : false;\n if (active) {\n responseBody = {\n active: true,\n scope: refreshToken.scope,\n client_id: refreshToken.clientId,\n username: refreshToken.userId,\n exp: Math.floor(refreshToken.refreshTokenExpiresAt!.getTime() / 1000),\n };\n }\n }\n\n return {\n statusCode: 200,\n headers: { 'Content-Type': 'application/json' },\n body: responseBody,\n cookies: {},\n };\n}\n\n/**\n * Creates and configures an OAuth 2.0 server instance.\n * Provides a clean, functional interface for handling OAuth 2.0 flows.\n *\n * @example\n * ```typescript\n * const storage = new MyStorageAdapter();\n * const server = createOAuth2Server({\n * storage,\n * grants: [createAuthorizationCodeGrant(), clientCredentialsGrant()],\n * predefinedScopes: ['read', 'write'],\n * tokenStrategy: createJwtTokenStrategy(storage, { secret: 'my-secret' })\n * });\n * ```\n */\nexport function createOAuth2Server(config: ServerConfig): OAuth2Server {\n const completeConfig = createCompleteConfig(config);\n const { storage } = completeConfig;\n\n return {\n authorize: (request: OAuth2Request) => handleAuthorizeRequest(request, storage, completeConfig),\n token: (request: OAuth2Request) => handleTokenRequest(request, storage, completeConfig),\n revoke: (request: OAuth2Request) => handleRevokeRequest(request, storage),\n introspect: (request: OAuth2Request) => handleIntrospectRequest(request, storage),\n };\n}\n\n// For backward compatibility\nexport const createServer = createOAuth2Server;\n"],"names":[],"mappings":";;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,kBAAA,SAAA,YAAA,GAAA,YAAA;AACA,cAAA,YAAA,SAAA,kBAAA;;;;"}