Skip to main content
Glama
enterpriseAuth.js7.75 kB
// src/server/auth/enterpriseAuth.js // MCP 2025-11-25 Enterprise-Managed Authorization (SEP-990) // Cross App Access (XAA) for enterprise SSO integration const config = require('../../../config'); // Lazy load jose to keep it optional let jose = null; async function getJose() { if (!jose) { jose = await import('jose'); } return jose; } class EnterpriseAuthHandler { constructor() { this.enabled = config.mcp?.auth?.enterpriseAuthEnabled || false; this.idpUrl = config.mcp?.auth?.enterpriseIdpUrl || process.env.ENTERPRISE_IDP_URL; this.expectedIssuer = process.env.ENTERPRISE_IDP_ISSUER || this.idpUrl; this.expectedAudience = process.env.ENTERPRISE_EXPECTED_AUD || 'mcp-server'; this.jwksCache = null; this.jwksCacheTime = 0; this.jwksCacheTtl = 3600000; // 1 hour } /** * Check if enterprise auth is configured and enabled * @returns {boolean} */ isEnabled() { return this.enabled && !!this.idpUrl; } /** * Get JWKS (JSON Web Key Set) from enterprise IdP * @returns {Function} JWKS getter function for jose */ async getJWKS() { const { createRemoteJWKSet } = await getJose(); // Cache JWKS for performance const now = Date.now(); if (!this.jwksCache || (now - this.jwksCacheTime) > this.jwksCacheTtl) { const jwksUrl = new URL('/.well-known/jwks.json', this.idpUrl); this.jwksCache = createRemoteJWKSet(jwksUrl); this.jwksCacheTime = now; process.stderr.write(`[${new Date().toISOString()}] EnterpriseAuth: Refreshed JWKS from ${jwksUrl}\n`); } return this.jwksCache; } /** * Validate an Identity Assertion Grant (ID-JAG) token * RFC 7523 JWT Profile for OAuth 2.0 Authorization Grants * * @param {string} idJag - The ID-JAG JWT token * @returns {Object} Validated token claims * @throws {Error} If validation fails */ async validateIdentityAssertionGrant(idJag) { if (!this.isEnabled()) { throw new Error('Enterprise IdP not configured'); } const { jwtVerify } = await getJose(); const JWKS = await this.getJWKS(); let payload; try { const result = await jwtVerify(idJag, JWKS, { issuer: this.expectedIssuer, audience: this.expectedAudience }); payload = result.payload; } catch (verifyError) { process.stderr.write(`[${new Date().toISOString()}] EnterpriseAuth: JWT verification failed: ${verifyError.message}\n`); throw new Error(`Invalid ID-JAG: ${verifyError.message}`); } // Validate required claims if (!payload.sub) { throw new Error('Invalid ID-JAG: missing subject (sub) claim'); } if (!payload.aud) { throw new Error('Invalid ID-JAG: missing audience (aud) claim'); } // Check expiration (jose already validates this, but double-check) if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) { throw new Error('Invalid ID-JAG: token expired'); } process.stderr.write(`[${new Date().toISOString()}] EnterpriseAuth: Validated ID-JAG for user ${payload.sub}\n`); return { userId: payload.sub, clientId: payload.azp || payload.client_id, scope: payload.scope || '', expiresAt: payload.exp ? new Date(payload.exp * 1000) : null, issuedAt: payload.iat ? new Date(payload.iat * 1000) : null, issuer: payload.iss, claims: payload }; } /** * Exchange an ID token for an access token via RFC 8693 Token Exchange * Used for Cross App Access (XAA) flow * * @param {string} idToken - User's ID token from SSO * @param {string} targetResource - Target MCP server resource identifier * @param {string} scope - Requested scopes (optional) * @returns {Object} Token exchange response with access_token * @throws {Error} If exchange fails */ async exchangeToken(idToken, targetResource, scope = '') { if (!this.isEnabled()) { throw new Error('Enterprise IdP not configured'); } const tokenEndpoint = new URL('/oauth/token', this.idpUrl).toString(); process.stderr.write(`[${new Date().toISOString()}] EnterpriseAuth: Initiating token exchange for resource ${targetResource}\n`); const response = await fetch(tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', subject_token: idToken, subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', requested_token_type: 'urn:ietf:params:oauth:token-type:access_token', resource: targetResource, scope: scope }) }); if (!response.ok) { const errorBody = await response.text(); process.stderr.write(`[${new Date().toISOString()}] EnterpriseAuth: Token exchange failed: ${response.status} ${errorBody}\n`); throw new Error(`Token exchange failed: ${response.status}`); } const tokenResponse = await response.json(); // Validate response has required fields if (!tokenResponse.access_token) { throw new Error('Token exchange response missing access_token'); } process.stderr.write(`[${new Date().toISOString()}] EnterpriseAuth: Token exchange successful\n`); return { accessToken: tokenResponse.access_token, tokenType: tokenResponse.token_type || 'Bearer', expiresIn: tokenResponse.expires_in, scope: tokenResponse.scope, issuedTokenType: tokenResponse.issued_token_type }; } /** * Validate an access token from the authorization server * @param {string} accessToken - Access token to validate * @returns {Object} Token claims */ async validateAccessToken(accessToken) { if (!this.isEnabled()) { throw new Error('Enterprise IdP not configured'); } // If JWT, validate signature if (accessToken.includes('.')) { const { jwtVerify } = await getJose(); const JWKS = await this.getJWKS(); try { const result = await jwtVerify(accessToken, JWKS, { issuer: this.expectedIssuer }); return { valid: true, claims: result.payload }; } catch (error) { return { valid: false, error: error.message }; } } // For opaque tokens, use introspection endpoint const introspectionEndpoint = new URL('/oauth/introspect', this.idpUrl).toString(); const response = await fetch(introspectionEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ token: accessToken, token_type_hint: 'access_token' }) }); if (!response.ok) { return { valid: false, error: `Introspection failed: ${response.status}` }; } const introspection = await response.json(); return { valid: introspection.active === true, claims: introspection.active ? introspection : null }; } /** * Get enterprise IdP metadata from .well-known endpoint * @returns {Object} IdP metadata */ async getIdPMetadata() { if (!this.idpUrl) { throw new Error('Enterprise IdP URL not configured'); } const metadataUrl = new URL('/.well-known/openid-configuration', this.idpUrl).toString(); const response = await fetch(metadataUrl, { headers: { 'Accept': 'application/json' } }); if (!response.ok) { throw new Error(`Failed to fetch IdP metadata: ${response.status}`); } return response.json(); } } module.exports = new EnterpriseAuthHandler();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wheattoast11/openrouter-deep-research-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server