example-remote-server/src/auth/external-verifier.ts
Basil Hosmer 03deb88ea0
Fix build and lint issues
- Corrected imports in external-verifier.ts (OAuthTokenVerifier from provider.js)
- Fixed InvalidTokenError import from errors.js
- Changed logger.warn to logger.info (warn method doesn't exist)
- Removed unused OAuthTokens import from services/auth.ts
2025-09-04 19:15:45 -04:00

90 lines
No EOL
3.2 KiB
TypeScript

import { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
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';
/**
* Token verifier that validates tokens with an external authorization server.
* Used when the MCP server is running in 'separate' mode.
*/
export class ExternalAuthVerifier implements OAuthTokenVerifier {
/**
* Creates a new external auth verifier.
* @param authServerUrl Base URL of the external authorization server
*/
constructor(private authServerUrl: string) {}
/**
* Verifies an access token by calling the external auth server's introspection endpoint.
* @param token The access token to verify
* @returns Authentication information if the token is valid
* @throws InvalidTokenError if the token is invalid or expired
*/
async verifyAccessToken(token: string): Promise<AuthInfo> {
try {
// Token introspection is OAuth 2.0 standard (RFC 7662) for validating tokens
// The auth server checks if the token is valid and returns metadata about it
const response = await fetch(`${this.authServerUrl}/oauth/introspect`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `token=${encodeURIComponent(token)}`,
});
if (!response.ok) {
logger.error('Token introspection request failed', undefined, {
status: response.status,
statusText: response.statusText,
});
throw new InvalidTokenError('Token validation failed');
}
const data: TokenIntrospectionResponse = await response.json();
// Check if token is active
if (!data.active) {
throw new InvalidTokenError('Token is not active');
}
// Check if token is expired
if (data.exp && data.exp < Date.now() / 1000) {
throw new InvalidTokenError('Token has expired');
}
// Extract user ID from standard 'sub' claim or custom 'userId' field
const userId = data.sub || data.userId;
if (!userId) {
logger.info('Token introspection response missing user ID', {
hasSub: !!data.sub,
hasUserId: !!data.userId,
});
}
return {
token,
clientId: data.client_id || 'unknown',
scopes: data.scope?.split(' ') || [], // Empty array if no scopes specified (permissive)
expiresAt: data.exp,
extra: {
userId: userId || 'unknown',
// Include other potentially useful fields
username: data.username,
iss: data.iss,
aud: data.aud,
},
};
} catch (error) {
if (error instanceof InvalidTokenError) {
throw error;
}
logger.error('Failed to verify token with external auth server', error as Error, {
authServerUrl: this.authServerUrl,
});
// Network or other errors should be treated as invalid token
// to prevent access with unverifiable tokens
throw new InvalidTokenError('Unable to verify token');
}
}
}