30 lines
2.0 KiB
JavaScript
30 lines
2.0 KiB
JavaScript
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'})}
|
|
};
|
|
}
|