import {createRemoteJWKSet,jwtVerify} from 'jose'; export function createKeycloakVerifier({issuer,allowedClient,jwks}){ if(!issuer?.startsWith('https://')||!allowedClient)throw new Error('Keycloak MCP issuer and allowed client are required'); const normalized=issuer.replace(/\/$/,''); const jwkset=jwks||createRemoteJWKSet(new URL(normalized+'/protocol/openid-connect/certs')); return async token=>{ const {payload}=await jwtVerify(token,jwkset,{issuer:normalized}); if(payload.azp!==allowedClient)throw new Error('Client not allowed'); if(typeof payload.sub!=='string'||!payload.sub)throw new Error('Missing Keycloak subject'); // A service account is not a human user. Isolate it from all browser sheets. return {sub:'mcp-machine:'+payload.azp+':'+payload.sub,scopes:['characters:read','characters:write'],clientId:payload.azp,expiresAt:payload.exp}; }; } export function installKeycloakMcpAuth(app,{origin,issuer,allowedClient,verify}){ if(!issuer?.startsWith('https://')||!allowedClient)throw new Error('Keycloak MCP configuration required'); const verifyToken=verify||createKeycloakVerifier({issuer,allowedClient}); const metadata={resource:origin+'/mcp',authorization_servers:[issuer.replace(/\/$/,'')],bearer_methods_supported:['header']}; app.get('/.well-known/oauth-protected-resource',(_,res)=>res.json(metadata)); app.get('/.well-known/oauth-protected-resource/mcp',(_,res)=>res.json(metadata)); return async(req,res,next)=>{ const header=String(req.get('authorization')||''); if(!header.startsWith('Bearer ')){res.set('WWW-Authenticate','Bearer resource_metadata="'+origin+'/.well-known/oauth-protected-resource/mcp"');return res.status(401).json({error:'mcp_bearer_token_required'})} try{ const identity=await verifyToken(header.slice(7).trim()); req.auth={token:header.slice(7).trim(),clientId:identity.clientId,scopes:identity.scopes,expiresAt:identity.expiresAt,extra:{sub:identity.sub}}; next(); }catch{res.set('WWW-Authenticate','Bearer error="invalid_token"');res.status(401).json({error:'invalid_mcp_token'})} }; }