@dav3/mcp-dynamics365-server
Version:
Generic MCP server for Dynamics 365 CRM operations (AI-generated side project with limited maintenance)
88 lines (87 loc) • 3.63 kB
JavaScript
import axios from 'axios';
import jwt from 'jsonwebtoken';
import jwkToPem from 'jwk-to-pem';
// In-memory cache for JWKS and user API responses
const cache = new Map();
// Cache duration: configurable via environment variable (default: 24 hours)
const OAUTH_TOKEN_CACHE_DURATION_MS = parseInt(process.env.OAUTH_TOKEN_CACHE_DURATION_MS || '86400000');
function getCachedData(key) {
const entry = cache.get(key);
if (entry && Date.now() < entry.expiry) {
return entry.data;
}
// Remove expired entry
if (entry) {
cache.delete(key);
}
return null;
}
function setCachedData(key, data) {
cache.set(key, {
data,
expiry: Date.now() + OAUTH_TOKEN_CACHE_DURATION_MS
});
}
export async function validateAccessToken(token, flow, options = {}) {
if (flow === 'jwt') {
try {
// Decode header to get kid
const decodedHeader = jwt.decode(token, { complete: true });
if (!decodedHeader || typeof decodedHeader !== 'object')
return { valid: false, reason: 'Invalid token' };
const kid = decodedHeader.header?.kid;
// Fetch JWKS with caching
const jwksUri = options.jwksUri;
if (!jwksUri)
return { valid: false, reason: 'Missing JWKS URI' };
let jwksData = getCachedData(`jwks:${jwksUri}`);
if (!jwksData) {
const jwksResp = await axios.get(jwksUri);
jwksData = jwksResp.data;
setCachedData(`jwks:${jwksUri}`, jwksData);
}
const keys = jwksData.keys || [];
const jwk = keys.find((k) => k.kid === kid);
if (!jwk)
return { valid: false, reason: 'No matching JWK found for kid' };
const pem = jwkToPem(jwk);
// Verify JWT signature
const payload = jwt.verify(token, pem, { algorithms: ['RS256'] });
// Check expiry
if (payload.exp && Date.now() / 1000 > payload.exp)
return { valid: false, reason: 'Token expired' };
// Check audience
if (options.expectedAudience && payload.aud !== options.expectedAudience)
return { valid: false, reason: 'Token audience mismatch' };
return { valid: true, payload };
}
catch (err) {
return { valid: false, reason: err instanceof Error ? err.message : 'JWT validation error' };
}
}
else if (flow === 'opaque') {
// Generic opaque token validation via user info endpoint with caching
try {
const apiUrl = options.opaqueUserApi || 'https://api.github.com/user'; // Default to GitHub, but can be any provider
const cacheKey = `opaque:${apiUrl}:${token}`;
let userData = getCachedData(cacheKey);
if (!userData) {
const resp = await axios.get(apiUrl, {
headers: { Authorization: `Bearer ${token}` }
});
if (resp.status === 200) {
userData = resp.data;
setCachedData(cacheKey, userData);
}
else {
return { valid: false, reason: `Opaque token validation failed: ${resp.status}` };
}
}
return { valid: true, payload: userData };
}
catch (err) {
return { valid: false, reason: err instanceof Error ? err.message : 'Opaque token validation error' };
}
}
return { valid: false, reason: 'Unsupported flow' };
}