/**
* postgres-mcp - Authorization Server Discovery
*
* RFC 8414 Authorization Server Metadata discovery.
*/
import type { AuthServerDiscoveryConfig, AuthorizationServerMetadata } from './types.js';
import { AuthServerDiscoveryError } from './errors.js';
import { logger } from '../utils/logger.js';
/**
* Authorization Server Discovery (RFC 8414)
*/
export class AuthorizationServerDiscovery {
private readonly config: AuthServerDiscoveryConfig;
private metadataCache: AuthorizationServerMetadata | null = null;
private cacheTime = 0;
constructor(config: AuthServerDiscoveryConfig) {
this.config = {
...config,
cacheTtl: config.cacheTtl ?? 3600,
timeout: config.timeout ?? 5000
};
}
/**
* Discover authorization server metadata
*/
async discover(): Promise<AuthorizationServerMetadata> {
const now = Date.now();
const cacheTtlMs = (this.config.cacheTtl ?? 3600) * 1000;
// Check cache
if (this.metadataCache && (now - this.cacheTime) < cacheTtlMs) {
return this.metadataCache;
}
try {
// RFC 8414: well-known endpoint - append to base URL path
const baseUrl = this.config.authServerUrl.endsWith('/')
? this.config.authServerUrl.slice(0, -1)
: this.config.authServerUrl;
const wellKnownUrl = `${baseUrl}/.well-known/oauth-authorization-server`;
const controller = new AbortController();
const timeoutId = setTimeout(() => { controller.abort(); }, this.config.timeout);
const response = await fetch(wellKnownUrl, {
method: 'GET',
headers: { 'Accept': 'application/json' },
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status.toString()}: ${response.statusText}`);
}
const metadata = await response.json() as AuthorizationServerMetadata;
// Validate required fields
if (!metadata.issuer || !metadata.token_endpoint) {
throw new Error('Invalid metadata: missing required fields');
}
// Cache the metadata
this.metadataCache = metadata;
this.cacheTime = now;
logger.debug('Auth server metadata discovered', { issuer: metadata.issuer });
return metadata;
} catch (error) {
logger.error('Auth server discovery failed', {
url: this.config.authServerUrl,
error: String(error)
});
throw new AuthServerDiscoveryError(
`Failed to discover auth server at ${this.config.authServerUrl}: ${String(error)}`
);
}
}
/**
* Get JWKS URI from discovered metadata
*/
async getJwksUri(): Promise<string> {
const metadata = await this.discover();
if (!metadata.jwks_uri) {
throw new AuthServerDiscoveryError('Auth server metadata does not include jwks_uri');
}
return metadata.jwks_uri;
}
/**
* Get token endpoint from discovered metadata
*/
async getTokenEndpoint(): Promise<string> {
const metadata = await this.discover();
return metadata.token_endpoint;
}
/**
* Get registration endpoint (if available)
*/
async getRegistrationEndpoint(): Promise<string | undefined> {
const metadata = await this.discover();
return metadata.registration_endpoint;
}
/**
* Check if auth server supports a specific grant type
*/
async supportsGrantType(grantType: string): Promise<boolean> {
const metadata = await this.discover();
return metadata.grant_types_supported?.includes(grantType) ?? false;
}
/**
* Invalidate cache
*/
invalidateCache(): void {
this.metadataCache = null;
this.cacheTime = 0;
}
}
/**
* Create an authorization server discovery instance
*/
export function createAuthServerDiscovery(config: AuthServerDiscoveryConfig): AuthorizationServerDiscovery {
return new AuthorizationServerDiscovery(config);
}