@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
JavaScript
// 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