@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
724 lines • 26.3 kB
JavaScript
/**
* BaseAuthProvider - Abstract base class for authentication providers
*
* Provides common functionality for all auth providers including:
* - Token extraction (header, cookie, query param, custom function)
* - Session management (create, validate, refresh, revoke)
* - RBAC authorization (roles, permissions, wildcards, hierarchy)
* - Token validation utilities (JWT parsing, expiry checks)
* - Event emission for auth lifecycle hooks
* - Error handling via unified AuthError factory
*/
import { randomUUID } from "crypto";
import { EventEmitter } from "events";
import { logger } from "../../utils/logger.js";
import { AuthError } from "../errors.js";
// =============================================================================
// BACKWARD-COMPAT RE-EXPORTS
// =============================================================================
/**
* @deprecated Use `AuthError` from `../errors.js` instead.
* Kept for backward compatibility with CognitoProvider / KeycloakProvider.
*/
export const AuthProviderError = AuthError;
// =============================================================================
// IN-MEMORY SESSION STORAGE
// =============================================================================
/**
* Default in-memory session storage
*/
export class InMemorySessionStorage {
sessions = new Map();
userSessions = new Map();
async get(sessionId) {
return this.sessions.get(sessionId) ?? null;
}
async save(session) {
this.sessions.set(session.id, session);
// Track sessions by user
const userSessionSet = this.userSessions.get(session.user.id) ?? new Set();
userSessionSet.add(session.id);
this.userSessions.set(session.user.id, userSessionSet);
}
async delete(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
this.sessions.delete(sessionId);
// Remove from user tracking
const userSessionSet = this.userSessions.get(session.user.id);
if (userSessionSet) {
userSessionSet.delete(sessionId);
if (userSessionSet.size === 0) {
this.userSessions.delete(session.user.id);
}
}
}
}
async deleteAllForUser(userId) {
const userSessionSet = this.userSessions.get(userId);
if (userSessionSet) {
for (const sessionId of userSessionSet) {
this.sessions.delete(sessionId);
}
this.userSessions.delete(userId);
}
}
async getForUser(userId) {
const userSessionSet = this.userSessions.get(userId);
if (!userSessionSet) {
return [];
}
const now = Date.now();
const sessions = [];
const expiredIds = [];
for (const sessionId of userSessionSet) {
const session = this.sessions.get(sessionId);
if (!session) {
continue;
}
// Filter out expired and revoked sessions so maxSessionsPerUser counts are accurate
if (session.expiresAt && session.expiresAt.getTime() < now) {
expiredIds.push(sessionId);
continue;
}
if (!session.isValid) {
expiredIds.push(sessionId);
continue;
}
sessions.push(session);
}
// Clean up expired sessions lazily
for (const id of expiredIds) {
this.sessions.delete(id);
userSessionSet.delete(id);
}
if (userSessionSet.size === 0) {
this.userSessions.delete(userId);
}
return sessions;
}
async exists(sessionId) {
return this.sessions.has(sessionId);
}
async touch(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.lastActivityAt = new Date();
this.sessions.set(sessionId, session);
}
}
async clear() {
this.sessions.clear();
this.userSessions.clear();
}
/**
* Get session count (for testing/monitoring)
*/
get size() {
return this.sessions.size;
}
}
// =============================================================================
// BASE PROVIDER IMPLEMENTATION
// =============================================================================
/**
* BaseAuthProvider - Abstract base class for all auth providers
*
* Subclasses must implement:
* - authenticateToken() - Validate and decode JWT/access tokens
*
* Optionally override:
* - getUser() - Fetch user by ID from provider
* - updateUserRoles() - Update user roles in provider
* - updateUserPermissions() - Update user permissions in provider
* - dispose() - Clean up resources
*/
export class BaseAuthProvider {
config;
sessionStorage;
sessionConfig;
rbacConfig;
emitter = new EventEmitter();
constructor(config) {
// Deep-merge tokenExtraction: preserve header defaults when partial config given
const defaultTokenExtraction = {
fromHeader: { name: "Authorization", scheme: "Bearer" },
};
this.config = {
required: true,
...config,
tokenExtraction: {
...defaultTokenExtraction,
...config.tokenExtraction,
},
};
// Initialize session configuration
this.sessionConfig = {
storage: "memory",
duration: 3600, // 1 hour default
autoRefresh: true,
refreshThreshold: 300, // 5 minutes
allowMultipleSessions: true,
maxSessionsPerUser: 10,
prefix: "neurolink:session:",
...config.session,
};
// Initialize RBAC configuration
this.rbacConfig = {
enabled: true,
defaultRoles: [],
roleHierarchy: {},
rolePermissions: {},
superAdminRoles: ["super_admin", "root"],
...config.rbac,
};
// Initialize session storage
this.sessionStorage =
config.session?.customStorage ?? new InMemorySessionStorage();
logger.debug(`[BaseAuthProvider] Initialized`);
}
// ===========================================================================
// TOKEN EXTRACTION
// ===========================================================================
/**
* Extract token using configured strategy
*
* Attempts extraction in order:
* 1. Header (Authorization: Bearer <token> by default)
* 2. Cookie
* 3. Query parameter
* 4. Custom function
*
* @param context - Request context containing headers, cookies, etc.
* @returns Extracted token or null if not found
*/
async extractToken(context) {
const strategy = this.config.tokenExtraction;
// Try header extraction (case-insensitive header lookup)
if (strategy?.fromHeader) {
const headerName = strategy.fromHeader.name.toLowerCase();
// Find header value with case-insensitive lookup
let headerValue;
for (const [key, value] of Object.entries(context.headers)) {
if (key.toLowerCase() === headerName && typeof value === "string") {
headerValue = value;
break;
}
}
if (typeof headerValue === "string") {
if (strategy.fromHeader.scheme) {
const prefix = `${strategy.fromHeader.scheme} `;
if (headerValue.startsWith(prefix)) {
return headerValue.slice(prefix.length);
}
}
else {
return headerValue;
}
}
}
// Try cookie extraction
if (strategy?.fromCookie && context.cookies) {
const cookieValue = context.cookies[strategy.fromCookie.name];
if (cookieValue) {
return cookieValue;
}
}
// Try query parameter extraction
if (strategy?.fromQuery && context.path) {
try {
const url = new URL(context.path, "http://localhost");
const queryValue = url.searchParams.get(strategy.fromQuery.name);
if (queryValue) {
return queryValue;
}
}
catch {
// Invalid URL, skip query extraction
}
}
// Try custom extraction (may be sync or async)
if (strategy?.custom) {
return await Promise.resolve(strategy.custom(context));
}
return null;
}
// ===========================================================================
// SESSION MANAGEMENT
// ===========================================================================
/**
* Create a new session for an authenticated user
*
* Session duration and metadata are derived from `this.sessionConfig` and
* the optional `context`. This matches the `AuthSessionManager` type
* signature: `createSession(user, context?)`.
*/
async createSession(user, context) {
const now = new Date();
const duration = this.sessionConfig.duration ?? 3600;
// Check session limits
if (!this.sessionConfig.allowMultipleSessions) {
await this.revokeAllSessions(user.id);
}
else if (this.sessionConfig.maxSessionsPerUser) {
const existingSessions = await this.sessionStorage.getForUser(user.id);
if (existingSessions.length >= this.sessionConfig.maxSessionsPerUser) {
// Remove oldest session
const oldestSession = existingSessions.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())[0];
if (oldestSession) {
await this.sessionStorage.delete(oldestSession.id);
}
}
}
const session = {
id: randomUUID(),
user,
accessToken: randomUUID(), // Internal session token
isValid: true,
expiresAt: new Date(now.getTime() + duration * 1000),
createdAt: now,
lastActivityAt: now,
ipAddress: context?.ip ?? context?.ipAddress,
userAgent: context?.userAgent,
};
await this.sessionStorage.save(session);
logger.debug(`[BaseAuthProvider] Created session ${session.id} for user ${user.id}`);
return session;
}
/**
* Validate an existing session
*/
async validateSession(sessionId) {
const session = await this.sessionStorage.get(sessionId);
if (!session) {
return {
valid: false,
error: "Session not found",
errorCode: "AUTH-010",
};
}
// Check expiration
if (session.expiresAt && session.expiresAt.getTime() < Date.now()) {
await this.sessionStorage.delete(sessionId);
return {
valid: false,
error: "Session expired",
errorCode: "AUTH-011",
};
}
// Check if revoked
if (!session.isValid) {
return {
valid: false,
error: "Session revoked",
errorCode: "AUTH-012",
};
}
// Auto-refresh if near expiration
let refreshed = false;
if (this.sessionConfig.autoRefresh &&
this.sessionConfig.refreshThreshold &&
session.expiresAt &&
session.expiresAt.getTime() - Date.now() <
this.sessionConfig.refreshThreshold * 1000) {
const refreshedSession = await this.refreshSession(sessionId);
refreshed = true;
return {
valid: true,
session: refreshedSession ?? undefined,
refreshed,
};
}
// Update last activity
await this.sessionStorage.touch(sessionId);
return {
valid: true,
session,
refreshed,
};
}
/**
* Refresh a session (extend expiration)
*/
async refreshSession(sessionId) {
const session = await this.sessionStorage.get(sessionId);
if (!session) {
throw AuthError.create("SESSION_NOT_FOUND", `Session not found: ${sessionId}`, { details: { sessionId } });
}
// Don't refresh revoked sessions
if (!session.isValid) {
throw AuthError.create("SESSION_REVOKED", `Cannot refresh revoked session: ${sessionId}`, { details: { sessionId } });
}
// Don't refresh expired sessions
if (session.expiresAt && session.expiresAt.getTime() < Date.now()) {
await this.sessionStorage.delete(sessionId);
throw AuthError.create("SESSION_EXPIRED", `Cannot refresh expired session: ${sessionId}`, { details: { sessionId } });
}
const duration = this.sessionConfig.duration ?? 3600;
session.expiresAt = new Date(Date.now() + duration * 1000);
session.lastActivityAt = new Date();
await this.sessionStorage.save(session);
logger.debug(`[BaseAuthProvider] Refreshed session ${sessionId}`);
return session;
}
/**
* Revoke a session
*
* Marks the session as invalid rather than deleting it immediately.
* This keeps a tombstone so that "revoked" is distinguishable from
* "not found" during subsequent validation attempts.
*/
async revokeSession(sessionId) {
const session = await this.sessionStorage.get(sessionId);
if (session) {
session.isValid = false;
await this.sessionStorage.save(session);
logger.debug(`[BaseAuthProvider] Revoked session ${sessionId}`);
}
}
/**
* Revoke all sessions for a user
*/
async revokeAllSessions(userId) {
await this.sessionStorage.deleteAllForUser(userId);
logger.debug(`[BaseAuthProvider] Revoked all sessions for user ${userId}`);
}
// ===========================================================================
// AUTHORIZATION (RBAC)
// ===========================================================================
/**
* Check if a user is authorized for specific roles/permissions
*/
async authorize(user, options) {
// Check if RBAC is enabled
if (!this.rbacConfig.enabled) {
return { authorized: true, user };
}
// Super admin bypass
if (this.isSuperAdmin(user)) {
return { authorized: true, user };
}
const result = {
authorized: true,
user,
requiredRoles: options.roles,
requiredPermissions: options.permissions,
missingRoles: [],
missingPermissions: [],
};
// Check roles
if (options.roles && options.roles.length > 0) {
const userRoles = this.getEffectiveRoles(user);
const missingRoles = options.roles.filter((r) => !userRoles.has(r));
if (options.requireAllRoles) {
// All roles required
if (missingRoles.length > 0) {
result.authorized = false;
result.missingRoles = missingRoles;
result.reason = `Missing required roles: ${missingRoles.join(", ")}`;
}
}
else {
// Any role is sufficient
const hasAnyRole = options.roles.some((r) => userRoles.has(r));
if (!hasAnyRole) {
result.authorized = false;
result.missingRoles = options.roles;
result.reason = `Missing any of required roles: ${options.roles.join(", ")}`;
}
}
}
// Check permissions (all required)
if (options.permissions && options.permissions.length > 0) {
const userPermissions = this.getEffectivePermissions(user);
const missingPermissions = options.permissions.filter((p) => !this.hasPermission(userPermissions, p));
if (missingPermissions.length > 0) {
result.authorized = false;
result.missingPermissions = missingPermissions;
result.reason = result.reason
? `${result.reason}; Missing permissions: ${missingPermissions.join(", ")}`
: `Missing required permissions: ${missingPermissions.join(", ")}`;
}
}
return result;
}
/**
* Check if user is a super admin
*/
isSuperAdmin(user) {
const superAdminRoles = this.rbacConfig.superAdminRoles ?? [];
return user.roles.some((r) => superAdminRoles.includes(r));
}
/**
* Get effective roles including inherited roles from hierarchy (transitive)
*/
getEffectiveRoles(user) {
const effectiveRoles = new Set(user.roles);
// Transitive closure: keep expanding until no new roles are added
const hierarchy = this.rbacConfig.roleHierarchy ?? {};
let added = true;
while (added) {
added = false;
for (const role of effectiveRoles) {
const inheritedRoles = hierarchy[role] ?? [];
for (const inherited of inheritedRoles) {
if (!effectiveRoles.has(inherited)) {
effectiveRoles.add(inherited);
added = true;
}
}
}
}
return effectiveRoles;
}
/**
* Get effective permissions including role-based permissions
*/
getEffectivePermissions(user) {
const effectivePermissions = new Set(user.permissions);
// Add permissions from roles
const rolePermissions = this.rbacConfig.rolePermissions ?? {};
const effectiveRoles = this.getEffectiveRoles(user);
for (const role of effectiveRoles) {
const permissions = rolePermissions[role] ?? [];
for (const permission of permissions) {
effectivePermissions.add(permission);
}
}
return effectivePermissions;
}
/**
* Check if a permission set grants a given permission.
* Supports exact match, global wildcard ("*"), and hierarchical wildcards
* (e.g. "tools:*" grants "tools:execute").
*/
hasPermission(permissions, required) {
if (permissions.has(required)) {
return true;
}
if (permissions.has("*")) {
return true;
}
const parts = required.split(":");
for (let i = parts.length - 1; i > 0; i--) {
const wildcard = [...parts.slice(0, i), "*"].join(":");
if (permissions.has(wildcard)) {
return true;
}
}
return false;
}
// ===========================================================================
// UTILITY METHODS
// ===========================================================================
/**
* Parse JWT token (without validation)
*/
parseJWT(token) {
try {
const parts = token.split(".");
if (parts.length !== 3) {
return null;
}
const payload = parts[1];
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
return JSON.parse(decoded);
}
catch {
return null;
}
}
/**
* Check if token is expired
*/
isTokenExpired(claims, clockTolerance = 0) {
if (!claims.exp) {
return false; // No expiration claim
}
const now = Math.floor(Date.now() / 1000);
return claims.exp + clockTolerance < now;
}
/**
* Check if token is not yet valid
*/
isTokenNotYetValid(claims, clockTolerance = 0) {
if (!claims.nbf) {
return false; // No nbf claim
}
const now = Math.floor(Date.now() / 1000);
return claims.nbf - clockTolerance > now;
}
/**
* Extract user from token claims
*/
extractUserFromClaims(claims, options) {
const rolesKey = options?.rolesClaimKey ?? "roles";
const permissionsKey = options?.permissionsClaimKey ?? "permissions";
const idKey = options?.idClaimKey ?? "sub";
const roles = Array.isArray(claims[rolesKey])
? claims[rolesKey]
: (this.rbacConfig.defaultRoles ?? []);
const permissions = Array.isArray(claims[permissionsKey])
? claims[permissionsKey]
: [];
return {
id: claims[idKey] ?? "",
email: claims.email,
name: claims.name,
picture: claims.picture,
roles,
permissions,
emailVerified: claims.email_verified,
providerData: claims,
};
}
// ===========================================================================
// OPTIONAL METHODS (can be overridden by subclasses)
// ===========================================================================
/**
* Get user by ID
* Override in subclass if provider supports user lookup
*/
async getUser(_userId) {
logger.debug(`[BaseAuthProvider] getUser not implemented for ${this.type}`);
return null;
}
/**
* Update user roles
* Override in subclass if provider supports role updates.
* Returns the user with updated roles.
*/
async updateUserRoles(_userId, _roles) {
throw AuthError.create("PROVIDER_ERROR", `updateUserRoles not supported by ${this.type} provider`);
}
/**
* Update user permissions
* Override in subclass if provider supports permission updates.
* Returns the user with updated permissions.
*/
async updateUserPermissions(_userId, _permissions) {
throw AuthError.create("PROVIDER_ERROR", `updateUserPermissions not supported by ${this.type} provider`);
}
/**
* Clean up resources
*/
async dispose() {
await this.sessionStorage.clear();
logger.debug(`[BaseAuthProvider] Disposed ${this.type} provider`);
}
// ===========================================================================
// METHODS FROM AuthProvider INTERFACE
// ===========================================================================
/**
* Check if a user is authorized to perform an action
*/
async authorizeUser(user, permission) {
return this.authorize(user, { permissions: [permission] });
}
/**
* Check if user has specific roles
*/
async authorizeRoles(user, roles) {
return this.authorize(user, { roles });
}
/**
* Check if user has all specified permissions
*/
async authorizePermissions(user, permissions) {
return this.authorize(user, { permissions });
}
/**
* Get an existing session by ID
*/
async getSession(sessionId) {
return this.sessionStorage.get(sessionId);
}
/**
* Invalidate/destroy a session
*/
async destroySession(sessionId) {
await this.revokeSession(sessionId);
}
/**
* Get all active sessions for a user
*/
async getUserSessions(userId) {
return this.sessionStorage.getForUser(userId);
}
/**
* Invalidate all sessions for a user (global logout)
*/
async destroyAllUserSessions(userId) {
await this.revokeAllSessions(userId);
}
/**
* Full request authentication flow
*
* Combines token extraction (with full strategy support), validation,
* and session creation/reuse.
*
* @param context - Request context
* @returns Authenticated context with user and session, or null
*/
async authenticateRequest(context) {
// Extract token (async to support custom extractors)
const token = await this.extractToken(context);
if (!token) {
if (!this.config.required) {
return null;
}
this.emitter.emit("auth:unauthorized", context, "No token provided");
return null;
}
// Validate token
const validation = await this.authenticateToken(token, context);
if (!validation.valid || !validation.user) {
this.emitter.emit("auth:unauthorized", context, validation.error ?? "Invalid token");
return null;
}
// Reuse existing session if one exists for this user
const existingSessions = await this.getUserSessions(validation.user.id);
const validSession = existingSessions.find((s) => s.isValid && (!s.expiresAt || s.expiresAt.getTime() > Date.now()));
const session = validSession ?? (await this.createSession(validation.user, context));
return {
...context,
user: validation.user,
session,
request: context,
authenticatedAt: new Date(),
provider: this.type,
};
}
/**
* Check provider health
*/
async healthCheck() {
return {
healthy: true,
providerConnected: true,
sessionStorageHealthy: true,
};
}
// ===========================================================================
// EVENT HELPERS
// ===========================================================================
/**
* Subscribe to auth events
*/
on(event, listener) {
this.emitter.on(event, listener);
}
/**
* Unsubscribe from auth events
*/
off(event, listener) {
this.emitter.off(event, listener);
}
/**
* Emit an auth event
*/
emit(event, ...args) {
this.emitter.emit(event, ...args);
}
}
//# sourceMappingURL=BaseAuthProvider.js.map