Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1bcb85b
Phase 1: Add configuration for integrated/separate auth modes
bhosmer-ant Sep 4, 2025
98cec05
Phase 2: Extract shared auth logic
bhosmer-ant Sep 4, 2025
0135abd
Phase 3: Implement MCP server mode switching
bhosmer-ant Sep 4, 2025
03deb88
Fix build and lint issues
bhosmer-ant Sep 4, 2025
4771c6e
Add retry logic and token caching
bhosmer-ant Sep 4, 2025
5c408eb
Phase 4: Create standalone authorization server
bhosmer-ant Sep 5, 2025
3f930c5
Phase 7: Complete README documentation for dual auth modes
bhosmer-ant Sep 5, 2025
b796560
add docker-compose.yml
bhosmer-ant Sep 5, 2025
d368c8d
address comments:
bhosmer-ant Sep 11, 2025
9ffe376
Fix endpoint consistency and documentation accuracy
bhosmer-ant Sep 12, 2025
40773bd
Add end-to-end verification scripts and fix server issues
bhosmer-ant Sep 12, 2025
1e47b07
Update documentation for current implementation
bhosmer-ant Sep 12, 2025
890beed
Add rate limiting and improve e2e testing workflow
bhosmer-ant Sep 12, 2025
ecd82f4
Add rate limiting to auth server static file endpoint
bhosmer-ant Sep 12, 2025
4e3414f
Fix undefined staticFileRateLimit in auth server
bhosmer-ant Sep 12, 2025
fdbf40b
Simplify build scripts to catch all compilation errors
bhosmer-ant Sep 12, 2025
5f545ce
Fix CI build by using public npm registry in package-lock.json
bhosmer-ant Sep 12, 2025
67d9a94
improvements to README.md
bhosmer-ant Sep 12, 2025
56a43ac
Fix OAuth metadata endpoint in separate mode
bhosmer-ant Sep 15, 2025
b32d638
Consolidate TypeScript build configuration
bhosmer-ant Sep 15, 2025
4fe1374
Add debug logging for OAuth flow troubleshooting
bhosmer-ant Sep 15, 2025
4a081bc
Improve e2e tests with OAuth flow validation
bhosmer-ant Sep 15, 2025
34fe20e
Implement Redis namespace isolation for auth and MCP keys
bhosmer-ant Sep 15, 2025
e393cb2
Add OAuth protected resource metadata endpoint for separate mode
bhosmer-ant Sep 15, 2025
dbd3e9a
Revert OAuth metadata change for backwards compatibility
bhosmer-ant Sep 23, 2025
a735b98
Add comprehensive token validation to ExternalAuthVerifier
bhosmer-ant Sep 23, 2025
67def73
Fix token audience validation in separate auth mode
bhosmer-ant Sep 25, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add comprehensive token validation to ExternalAuthVerifier
Enhance token validation in separate mode to fully comply with MCP specification: - Validate audience (aud) claim to ensure tokens are issued for this specific MCP server - Validate temporal claims (nbf, iat) with appropriate clock skew tolerance - Add configurable canonical URI for audience validation - Improve logging for validation failures These changes prevent token passthrough attacks and ensure tokens are properly scoped to the intended resource server, as required by the MCP OAuth 2.0 specification. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
  • Loading branch information
bhosmer-ant and claude committed Sep 23, 2025
commit a735b9810c8bd5300597af6ffeceecea665ba906
44 changes: 40 additions & 4 deletions src/auth/external-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
import { TokenIntrospectionResponse } from '../../shared/types.js';
import { logger } from '../utils/logger.js';
import { BASE_URI } from '../config.js';

/**
* Token verifier that validates tokens with an external authorization server.
Expand All @@ -11,15 +12,20 @@ import { logger } from '../utils/logger.js';
export class ExternalAuthVerifier implements OAuthTokenVerifier {
// Token validation cache: token -> { authInfo, expiresAt }
private tokenCache = new Map<string, { authInfo: AuthInfo; expiresAt: number }>();

// Default cache TTL: 60 seconds (conservative for security)
private readonly defaultCacheTTL = 60 * 1000; // milliseconds


// The canonical URI of this MCP server for audience validation
private readonly canonicalUri: string;

/**
* Creates a new external auth verifier.
* @param authServerUrl Base URL of the external authorization server
* @param canonicalUri Optional canonical URI for audience validation (defaults to BASE_URI)
*/
constructor(private authServerUrl: string) {
constructor(private authServerUrl: string, canonicalUri?: string) {
this.canonicalUri = canonicalUri || BASE_URI;
// Periodically clean up expired cache entries
setInterval(() => this.cleanupCache(), 60 * 1000); // Every minute
}
Expand Down Expand Up @@ -85,7 +91,37 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
if (data.exp && data.exp < Date.now() / 1000) {
throw new InvalidTokenError('Token has expired');
}


// Validate audience (aud) claim to ensure token is for this MCP server
// According to MCP spec, servers MUST validate that tokens were issued specifically for them
if (data.aud) {
const audiences = Array.isArray(data.aud) ? data.aud : [data.aud];
if (!audiences.includes(this.canonicalUri)) {
logger.error('Token audience mismatch', undefined, {
expectedAudience: this.canonicalUri,
actualAudience: data.aud,
});
throw new InvalidTokenError('Token was not issued for this resource server');
}
} else {
// Log warning if no audience claim present (permissive for backwards compatibility)
logger.info('Token introspection response missing audience claim', {
warning: true,
tokenSub: data.sub,
clientId: data.client_id,
});
}

// Validate token is not used before its 'not before' time (nbf) if present
if (data.nbf && data.nbf > Date.now() / 1000) {
throw new InvalidTokenError('Token is not yet valid (nbf)');
}

// Validate token was issued in the past (iat) if present
if (data.iat && data.iat > Date.now() / 1000 + 60) { // Allow 60s clock skew
throw new InvalidTokenError('Token issued in the future (iat)');
}

// Extract user ID from standard 'sub' claim or custom 'userId' field
const userId = data.sub || data.userId;
if (!userId) {
Expand Down