Skip to main content
Glama
multi-auth-manager.ts6.12 kB
import { createRemoteJWKSet, jwtVerify } from 'jose' import type { AuthHeaders, OAuthDelegation, OAuthToken } from '../types/auth.js' import type { MasterAuthConfig, ServerAuthConfig } from '../types/config.js' import { AuthStrategy } from '../types/config.js' import { Logger } from '../utils/logger.js' import { getOAuthProvider } from './oauth-providers.js' import { TokenManager } from './token-manager.js' export class MultiAuthManager { private serverAuth: Map<string, { strategy: AuthStrategy; config?: ServerAuthConfig }> = new Map() private jwks?: ReturnType<typeof createRemoteJWKSet> private tokenManager = new TokenManager() constructor(private readonly config: MasterAuthConfig) { if (config.jwks_uri) { try { this.jwks = createRemoteJWKSet(new URL(config.jwks_uri)) } catch (err) { Logger.warn('Failed to initialize JWKS for client token validation', err) } } } registerServerAuth(serverId: string, strategy: AuthStrategy, authConfig?: ServerAuthConfig): void { this.serverAuth.set(serverId, { strategy, config: authConfig }) } private keyFor(clientToken: string, serverId: string): string { return `${serverId}::${clientToken.slice(0, 16)}` } async validateClientToken(token: string): Promise<boolean> { if (!token || typeof token !== 'string') return false if (!this.jwks) { // Best-effort: check structural validity and expiration if it is a JWT; otherwise accept as opaque bearer try { const { payload } = await jwtVerify(token, async () => { // No key ⇒ force failure to reach catch where we treat opaque tokens as valid throw new Error('no-jwks') }) const now = Math.floor(Date.now() / 1000) return typeof payload.exp !== 'number' || payload.exp > now } catch { return true // Accept opaque tokens when no JWKS is configured } } try { await jwtVerify(token, this.jwks, { issuer: this.config.issuer, audience: this.config.audience ?? this.config.client_id, }) return true } catch (err) { Logger.warn('Client token verification failed', String(err)) return false } } async prepareAuthForBackend(serverId: string, clientToken: string): Promise<AuthHeaders | OAuthDelegation> { const isValid = await this.validateClientToken(clientToken) if (!isValid) throw new Error('Invalid client token') const entry = this.serverAuth.get(serverId) if (!entry) { // Default: pass-through return { Authorization: `Bearer ${clientToken}` } } const { strategy, config } = entry switch (strategy) { case AuthStrategy.MASTER_OAUTH: return this.handleMasterOAuth(serverId, clientToken) case AuthStrategy.DELEGATE_OAUTH: if (!config) throw new Error(`Missing auth config for server ${serverId}`) return this.handleDelegatedOAuth(serverId, clientToken, config) case AuthStrategy.BYPASS_AUTH: return {} case AuthStrategy.PROXY_OAUTH: if (!config) throw new Error(`Missing auth config for server ${serverId}`) return this.handleProxyOAuth(serverId, clientToken, config) default: return { Authorization: `Bearer ${clientToken}` } } } public async handleMasterOAuth(_serverId: string, clientToken: string): Promise<AuthHeaders> { // Pass-through the client's master token return { Authorization: `Bearer ${clientToken}` } } public async handleDelegatedOAuth( serverId: string, clientToken: string, serverAuthConfig: ServerAuthConfig ): Promise<OAuthDelegation> { // Return instructions for the client to complete OAuth against the provider const scopes = Array.isArray(serverAuthConfig.scopes) ? serverAuthConfig.scopes : ['openid'] // Create state binding server + client const state = this.tokenManager.generateState({ serverId }) // Store a minimal pending marker for later exchange if needed await this.tokenManager.storeToken(this.keyFor(clientToken, serverId), { access_token: '', expires_at: 0, scope: [], }) return { type: 'oauth_delegation', auth_endpoint: serverAuthConfig.authorization_endpoint, token_endpoint: serverAuthConfig.token_endpoint, client_info: { client_id: serverAuthConfig.client_id, metadata: { state } }, required_scopes: scopes, redirect_after_auth: true, } } public async handleProxyOAuth( serverId: string, clientToken: string, serverAuthConfig: ServerAuthConfig ): Promise<AuthHeaders> { const key = this.keyFor(clientToken, serverId) const existing = await this.tokenManager.getToken(key) const now = Date.now() if (existing && existing.access_token && existing.expires_at > now + 30_000) { return { Authorization: `Bearer ${existing.access_token}` } } if (existing?.refresh_token) { try { const provider = getOAuthProvider(serverAuthConfig as any) const refreshed = await provider.refreshToken(existing.refresh_token) await this.tokenManager.storeToken(key, refreshed) return { Authorization: `Bearer ${refreshed.access_token}` } } catch (err) { Logger.warn('Refresh token failed; falling back to pass-through', err) } } // Fallback: pass through the client token (may be accepted by backend if configured) return { Authorization: `Bearer ${clientToken}` } } async storeDelegatedToken(clientToken: string, serverId: string, serverToken: string | OAuthToken): Promise<void> { const key = this.keyFor(clientToken, serverId) const tokenObj: OAuthToken = typeof serverToken === 'string' ? { access_token: serverToken, expires_at: Date.now() + 3600_000, scope: [] } : serverToken await this.tokenManager.storeToken(key, tokenObj) } async getStoredServerToken(serverId: string, clientToken: string): Promise<string | undefined> { const tok = await this.tokenManager.getToken(this.keyFor(clientToken, serverId)) return tok?.access_token } }

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/Jakedismo/master-mcp-server'

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