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

396 lines 15.5 kB
/** * KeycloakProvider - Keycloak OpenID Connect provider implementation * * Provides JWT validation, session management, and RBAC for Keycloak. */ import { importJWK, jwtVerify } from "jose"; import { logger } from "../../utils/logger.js"; import { AuthError } from "../errors.js"; import { BaseAuthProvider } from "./BaseAuthProvider.js"; // ============================================================================= // JWKS CACHE // ============================================================================= const jwksCache = new Map(); // ============================================================================= // KEYCLOAK PROVIDER // ============================================================================= /** * KeycloakProvider - Keycloak OpenID Connect integration * * Features: * - Keycloak JWT token validation * - JWKS-based signature verification * - Realm roles and client roles support * - Resource access for fine-grained permissions * - Session management * * @example * ```typescript * const provider = new KeycloakProvider({ * type: 'keycloak', * serverUrl: 'https://keycloak.example.com', * realm: 'your-realm', * clientId: 'your-client-id', * }); * * const result = await provider.authenticateToken(accessToken); * if (result.valid) { * console.log('User:', result.user); * } * ``` */ export class KeycloakProvider extends BaseAuthProvider { type = "keycloak"; keycloakConfig; jwksUri; jwksCacheDuration; expectedIssuer; constructor(config) { super(config); if (config.type !== "keycloak") { throw AuthError.create("CONFIGURATION_ERROR", `Invalid provider type: ${config.type}. Expected: keycloak`); } this.keycloakConfig = config; if (!this.keycloakConfig.serverUrl) { throw AuthError.create("CONFIGURATION_ERROR", "Keycloak serverUrl is required"); } if (!this.keycloakConfig.realm) { throw AuthError.create("CONFIGURATION_ERROR", "Keycloak realm is required"); } if (!this.keycloakConfig.clientId) { throw AuthError.create("CONFIGURATION_ERROR", "Keycloak clientId is required"); } // Normalize server URL const serverUrl = this.keycloakConfig.serverUrl.replace(/\/$/, ""); // Set up issuer and JWKS URI this.expectedIssuer = `${serverUrl}/realms/${this.keycloakConfig.realm}`; this.jwksUri = `${this.expectedIssuer}/protocol/openid-connect/certs`; this.jwksCacheDuration = config.tokenValidation?.jwksCacheDuration ?? 600000; // 10 minutes logger.debug(`[KeycloakProvider] Initialized for realm: ${this.keycloakConfig.realm}`); } /** * Validate and authenticate a Keycloak JWT token */ async authenticateToken(token) { try { // Parse token without verification first const claims = this.parseJWT(token); if (!claims) { return { valid: false, error: "Failed to decode token", errorCode: "AUTH-006", }; } // Validate issuer if (claims.iss !== this.expectedIssuer) { return { valid: false, error: `Invalid issuer: ${claims.iss}. Expected: ${this.expectedIssuer}`, errorCode: "AUTH-001", }; } // Validate audience — always check aud contains clientId const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud]; if (!audiences.includes(this.keycloakConfig.clientId)) { return { valid: false, error: `Invalid audience: token aud does not contain clientId "${this.keycloakConfig.clientId}"`, errorCode: "AUTH-001", }; } // Additionally validate azp if present const azp = claims.azp; if (azp && azp !== this.keycloakConfig.clientId) { return { valid: false, error: `Invalid authorized party: ${azp}. Expected: ${this.keycloakConfig.clientId}`, errorCode: "AUTH-001", }; } // Check expiration const clockTolerance = this.config.tokenValidation?.clockTolerance ?? 0; if (this.isTokenExpired(claims, clockTolerance)) { return { valid: false, error: "Token has expired", errorCode: "AUTH-002", expiresAt: claims.exp ? new Date(claims.exp * 1000) : undefined, }; } // Check nbf (not before) if (this.isTokenNotYetValid(claims, clockTolerance)) { return { valid: false, error: "Token is not yet valid", errorCode: "AUTH-001", }; } // Verify signature if enabled if (this.keycloakConfig.verifyToken !== false && this.config.tokenValidation?.validateSignature !== false) { const signatureValid = await this.verifySignature(token); if (!signatureValid) { return { valid: false, error: "Invalid token signature", errorCode: "AUTH-004", }; } } // Extract user from claims const user = this.extractKeycloakUser(claims); // Convert claims to Record<string, JsonValue> by filtering out undefined const validClaims = {}; for (const [key, value] of Object.entries(claims)) { if (value !== undefined) { validClaims[key] = value; } } return { valid: true, user, claims: validClaims, expiresAt: claims.exp ? new Date(claims.exp * 1000) : undefined, issuer: claims.iss, audience: claims.aud, }; } catch (error) { logger.error(`[KeycloakProvider] Token validation error:`, error); return { valid: false, error: error instanceof Error ? error.message : "Token validation failed", errorCode: "AUTH-014", }; } } /** * Verify token signature using JWKS */ async verifySignature(token) { try { const parts = token.split("."); if (parts.length !== 3) { return false; } // Decode header to get kid const header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf-8")); const kid = header.kid; if (!kid) { logger.warn("[KeycloakProvider] Token missing kid in header"); return false; } // Get JWKS const jwks = await this.getJWKS(); const key = jwks.keys.find((k) => k.kid === kid); if (!key) { logger.warn(`[KeycloakProvider] Key not found for kid: ${kid}`); return false; } // Verify the JWT signature against the public key const publicKey = await importJWK(key, header.alg); const clockTolerance = this.config.tokenValidation?.clockTolerance ?? 30; await jwtVerify(token, publicKey, { clockTolerance }); return true; } catch (error) { logger.error(`[KeycloakProvider] Signature verification error:`, error); return false; } } /** * Fetch JWKS with caching */ async getJWKS() { const cached = jwksCache.get(this.jwksUri); if (cached && cached.expiresAt > Date.now()) { return cached.jwks; } try { const response = await fetch(this.jwksUri, { signal: AbortSignal.timeout(5000), }); if (!response.ok) { throw new Error(`JWKS fetch failed: ${response.status}`); } const jwks = (await response.json()); // Cache the JWKS jwksCache.set(this.jwksUri, { jwks, expiresAt: Date.now() + this.jwksCacheDuration, }); return jwks; } catch (error) { throw AuthError.create("JWKS_FETCH_FAILED", `Failed to fetch JWKS from ${this.jwksUri}: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined }); } } /** * Extract Keycloak-specific user data from claims */ extractKeycloakUser(claims) { const userId = claims.sub ?? ""; const email = claims.email; const name = claims.name ?? claims.preferred_username; const picture = claims.picture; // Get realm roles let roles = []; const realmAccess = claims.realm_access; if (realmAccess?.roles) { roles = [...realmAccess.roles]; } // Get client roles const resourceAccess = claims.resource_access; if (resourceAccess) { // Add roles from the specific client const clientRoles = resourceAccess[this.keycloakConfig.clientId]?.roles; if (clientRoles) { roles = [ ...roles, ...clientRoles.map((r) => `${this.keycloakConfig.clientId}:${r}`), ]; } // Optionally add roles from all clients (prefixed) for (const [client, access] of Object.entries(resourceAccess)) { if (client !== this.keycloakConfig.clientId && access.roles) { roles = [...roles, ...access.roles.map((r) => `${client}:${r}`)]; } } } // Apply default roles if (roles.length === 0 && this.rbacConfig.defaultRoles) { roles = this.rbacConfig.defaultRoles; } // Get scope as permissions let permissions = []; const scope = claims.scope; if (scope) { permissions = scope.split(" ").filter((s) => s.length > 0); } // Build provider data, filtering out undefined values const providerData = { provider: "keycloak", }; if (claims.preferred_username !== undefined) { providerData.preferred_username = claims.preferred_username; } if (claims.given_name !== undefined) { providerData.given_name = claims.given_name; } if (claims.family_name !== undefined) { providerData.family_name = claims.family_name; } if (realmAccess !== undefined) { providerData.realm_access = realmAccess; } if (resourceAccess !== undefined) { providerData.resource_access = resourceAccess; } if (claims.azp !== undefined) { providerData.azp = claims.azp; } if (claims.session_state !== undefined) { providerData.session_state = claims.session_state; } if (claims.acr !== undefined) { providerData.acr = claims.acr; } if (claims.typ !== undefined) { providerData.typ = claims.typ; } return { id: userId, email, name, picture, roles, permissions, emailVerified: claims.email_verified, providerData, }; } /** * Get user from Keycloak Admin API * Note: Requires client credentials with admin access */ async getUser(userId) { if (!this.keycloakConfig.clientSecret) { logger.debug("[KeycloakProvider] clientSecret required for admin API"); return null; } try { // Get admin token const tokenResponse = await fetch(`${this.expectedIssuer}/protocol/openid-connect/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: this.keycloakConfig.clientId, client_secret: this.keycloakConfig.clientSecret, }), signal: AbortSignal.timeout(5000), }); if (!tokenResponse.ok) { throw new Error(`Failed to get admin token: ${tokenResponse.status}`); } const tokenData = (await tokenResponse.json()); // Get user from admin API const serverUrl = this.keycloakConfig.serverUrl.replace(/\/$/, ""); const userResponse = await fetch(`${serverUrl}/admin/realms/${this.keycloakConfig.realm}/users/${encodeURIComponent(userId)}`, { headers: { Authorization: `Bearer ${tokenData.access_token}`, }, signal: AbortSignal.timeout(5000), }); if (!userResponse.ok) { if (userResponse.status === 404) { return null; } throw new Error(`Failed to get user: ${userResponse.status}`); } const userData = (await userResponse.json()); // Get user's realm roles const rolesResponse = await fetch(`${serverUrl}/admin/realms/${this.keycloakConfig.realm}/users/${encodeURIComponent(userId)}/role-mappings/realm`, { headers: { Authorization: `Bearer ${tokenData.access_token}`, }, signal: AbortSignal.timeout(5000), }); let roles = this.rbacConfig.defaultRoles ?? []; if (rolesResponse.ok) { const rolesData = (await rolesResponse.json()); roles = rolesData.map((r) => r.name); } // Convert userData to Record<string, JsonValue> by filtering out undefined const providerData = {}; for (const [key, value] of Object.entries(userData)) { if (value !== undefined) { providerData[key] = value; } } return { id: userData.id, email: userData.email, name: `${userData.firstName ?? ""} ${userData.lastName ?? ""}`.trim() || userData.username, picture: undefined, // Keycloak doesn't store picture by default roles, permissions: [], emailVerified: userData.emailVerified, providerData, createdAt: userData.createdTimestamp ? new Date(userData.createdTimestamp) : undefined, }; } catch (error) { logger.error(`[KeycloakProvider] Failed to get user ${userId}:`, error); return null; } } } //# sourceMappingURL=KeycloakProvider.js.map