Zero-Dependency JWT Authentication for High-Throughput MCP Servers
When building agent tool backends and Model Context Protocol servers intended for production deployment, dependency weight is an operational liability. Heavy cryptographic frameworks frequently introduce transitive vulnerabilities, inflate Docker cold-start times, and create native ABI compilation failures on serverless or edge runtimes.
By implementing zero-dependency HS256/RS256 JWT signing and verification using Node.js built-in node:crypto, MCP servers achieve sub-millisecond authentication overhead while remaining 100% compliant with RFC 7519 and RFC 6749.
1. Constant-Time Signature Verification
Timing attacks exploit variances in string comparison execution times to iteratively deduce valid HMAC signatures. Standard string equality (a === b) short-circuits on the first mismatched byte, creating measurable nanosecond leakage.
To prevent this vulnerability, all MCP token verification must employ constant-time hash comparisons:
import crypto from 'node:crypto';
export function safeTimingEqual(a: string, b: string): boolean {
const ha = crypto.createHash('sha256').update(a).digest();
const hb = crypto.createHash('sha256').update(b).digest();
return crypto.timingSafeEqual(ha, hb);
}
By computing SHA-256 digests prior to calling timingSafeEqual, buffers are guaranteed to possess identical lengths, eliminating short-circuit exceptions.
2. Base64URL Encoding without Third-Party Dependencies
RFC 7515 specifies Base64URL encoding (- and _ characters without = padding). This can be cleanly implemented in standard Node.js without extra npm packages:
function base64UrlEncode(input: Buffer | string): string {
const buf = typeof input === 'string' ? Buffer.from(input, 'utf8') : input;
return buf
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
function base64UrlDecodeToString(input: string): string {
const normalized = input.replace(/-/g, '+').replace(/_/g, '/');
const pad = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
return Buffer.from(normalized + pad, 'base64').toString('utf8');
}
3. Scoped RBAC in Autonomous Tool Calls
In MCP architectures, read operations (e.g. search_cms, list_recent_articles) should remain open or lightly rate-limited, while destructive or state-mutating tools (publish_quick_post, drop_database) must strictly enforce OAuth scopes.
export function verifyCredential(
rawToken: string | undefined | null,
requiredScope = 'write:articles'
): { authorized: boolean; tenantId: string; scopes: string[] } {
// Validate token signature, expiration (exp), and match scopes
const jwt = verifyOAuthJwt(rawToken);
if (!jwt.valid) return { authorized: false, tenantId: 'default', scopes: [] };
const hasScope = jwt.scopes.includes(requiredScope) || jwt.scopes.includes('*');
return {
authorized: hasScope,
tenantId: jwt.tenantId,
scopes: jwt.scopes
};
}
This security envelope guarantees multi-tenant protection even when agent harnesses interact with untrusted external tools.