UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

304 lines 10.8 kB
// src/lib/auth/providers/oauth2.ts import * as jose from "jose"; import { createProxyFetch } from "../../proxy/proxyFetch.js"; import { logger } from "../../utils/logger.js"; import { AuthError } from "../errors.js"; import { BaseAuthProvider } from "./BaseAuthProvider.js"; /** * Generic OAuth2/OIDC Provider * * Supports any OAuth2-compliant identity provider with configurable endpoints. * Works with both JWKS-based JWT validation and token introspection. * * Features: * - JWT validation with JWKS (if jwksUrl provided) * - Token introspection endpoint support * - User info endpoint integration * - PKCE support * * @example * ```typescript * const oauth2 = new OAuth2Provider({ * type: "oauth2", * authorizationUrl: "https://idp.example.com/oauth/authorize", * tokenUrl: "https://idp.example.com/oauth/token", * userInfoUrl: "https://idp.example.com/userinfo", * jwksUrl: "https://idp.example.com/.well-known/jwks.json", * clientId: "your-client-id", * clientSecret: "your-client-secret", * }); * * const result = await oauth2.authenticateToken(accessToken); * ``` */ export class OAuth2Provider extends BaseAuthProvider { type = "oauth2"; authorizationUrl; tokenUrl; userInfoUrl; jwksUrl; clientId; clientSecret; scopes; redirectUrl; usePKCE; jwks = null; constructor(config) { super(config); if (!config.authorizationUrl) { throw AuthError.create("CONFIGURATION_ERROR", "OAuth2 authorizationUrl is required"); } if (!config.tokenUrl) { throw AuthError.create("CONFIGURATION_ERROR", "OAuth2 tokenUrl is required"); } if (!config.clientId) { throw AuthError.create("CONFIGURATION_ERROR", "OAuth2 clientId is required"); } this.authorizationUrl = config.authorizationUrl; this.tokenUrl = config.tokenUrl; this.userInfoUrl = config.userInfoUrl; this.jwksUrl = config.jwksUrl; this.clientId = config.clientId; this.clientSecret = config.clientSecret; this.scopes = config.scopes ?? ["openid", "profile", "email"]; this.redirectUrl = config.redirectUrl; this.usePKCE = config.usePKCE ?? false; } /** * Initialize JWKS for JWT verification (if jwksUrl is provided) */ async initialize() { if (this.jwksUrl) { try { const jwksUrl = new URL(this.jwksUrl); this.jwks = jose.createRemoteJWKSet(jwksUrl); logger.debug(`OAuth2 provider initialized with JWKS: ${this.jwksUrl}`); } catch (error) { throw AuthError.create("PROVIDER_INIT_FAILED", "Failed to initialize OAuth2 JWKS", { cause: error instanceof Error ? error : new Error(String(error)), }); } } } /** * Validate OAuth2 access token * * Uses JWKS validation if available, otherwise falls back to userinfo endpoint */ async authenticateToken(token, _context) { // Try JWKS validation first if available if (this.jwksUrl) { // Lazy-init JWKS on first use if initialize() was not called if (!this.jwks) { await this.initialize(); } if (!this.jwks) { return { valid: false, error: "JWKS not available after initialization", }; } try { const { payload } = await jose.jwtVerify(token, this.jwks); // Validate issuer against the authorization server origin if (payload.iss) { const expectedIssuerOrigin = new URL(this.authorizationUrl).origin; if (!payload.iss.startsWith(expectedIssuerOrigin)) { return { valid: false, error: `Invalid issuer: ${payload.iss}. Expected origin: ${expectedIssuerOrigin}`, }; } } // Validate audience against the configured clientId if (payload.aud) { const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; if (!audiences.includes(this.clientId)) { return { valid: false, error: `Invalid audience: ${audiences.join(", ")}. Expected: ${this.clientId}`, }; } } if (!payload.sub) { return { valid: false, error: "JWT is missing required 'sub' claim: cannot identify user", }; } const user = { id: payload.sub, email: payload.email, name: payload.name, picture: payload.picture, roles: payload.roles ?? [], permissions: payload.permissions ?? [], metadata: payload, }; return { valid: true, payload: payload, user, expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined, tokenType: "jwt", }; } catch { logger.debug("JWKS validation failed, trying userinfo endpoint"); } } // Fall back to userinfo endpoint if available if (this.userInfoUrl) { return this.validateViaUserInfo(token); } return { valid: false, error: "No validation method available (provide jwksUrl or userInfoUrl)", }; } /** * Validate token via userinfo endpoint */ async validateViaUserInfo(token) { try { const proxyFetch = createProxyFetch(); if (!this.userInfoUrl) { return { valid: false, error: "UserInfo URL not configured", }; } const response = await proxyFetch(this.userInfoUrl, { headers: { Authorization: `Bearer ${token}`, }, signal: AbortSignal.timeout(5000), }); if (!response.ok) { return { valid: false, error: `UserInfo endpoint returned ${response.status}`, }; } const data = (await response.json()); const userId = data.sub ?? data.id; if (!userId) { return { valid: false, error: "UserInfo response is missing 'sub' and 'id': cannot identify user", }; } const user = { id: userId, email: data.email, name: data.name, picture: data.picture, emailVerified: data.email_verified, roles: data.roles ?? [], permissions: data.permissions ?? [], metadata: data, }; return { valid: true, payload: data, user, tokenType: "oauth", }; } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.warn("OAuth2 userinfo validation failed:", message); return { valid: false, error: message, }; } } /** * Get authorization URL for OAuth2 flow */ getAuthorizationUrl(state, codeChallenge) { const params = new URLSearchParams({ response_type: "code", client_id: this.clientId, scope: this.scopes.join(" "), state, }); if (this.redirectUrl) { params.set("redirect_uri", this.redirectUrl); } if (this.usePKCE && codeChallenge) { params.set("code_challenge", codeChallenge); params.set("code_challenge_method", "S256"); } return `${this.authorizationUrl}?${params.toString()}`; } /** * Exchange authorization code for tokens */ async exchangeCode(code, codeVerifier) { const proxyFetch = createProxyFetch(); const body = new URLSearchParams({ grant_type: "authorization_code", client_id: this.clientId, code, }); if (this.clientSecret) { body.set("client_secret", this.clientSecret); } if (this.redirectUrl) { body.set("redirect_uri", this.redirectUrl); } if (this.usePKCE && codeVerifier) { body.set("code_verifier", codeVerifier); } const response = await proxyFetch(this.tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), signal: AbortSignal.timeout(5000), }); if (!response.ok) { throw AuthError.create("PROVIDER_ERROR", `Token exchange failed: ${response.status}`); } const data = (await response.json()); return { accessToken: data.access_token, refreshToken: data.refresh_token, idToken: data.id_token, }; } /** * Health check */ async healthCheck() { try { // Try to fetch JWKS or authorization endpoint to check connectivity const proxyFetch = createProxyFetch(); const checkUrl = this.jwksUrl ?? this.authorizationUrl; const response = await proxyFetch(checkUrl, { method: "HEAD" }); return { healthy: response.ok || response.status === 405, // 405 is ok for HEAD providerConnected: true, sessionStorageHealthy: true, error: response.ok || response.status === 405 ? undefined : `HTTP ${response.status}`, }; } catch (error) { return { healthy: false, providerConnected: false, sessionStorageHealthy: true, error: error instanceof Error ? error.message : String(error), }; } } } //# sourceMappingURL=oauth2.js.map