Skip to main content
Glama
ComposedAuthenticationManager.ts11.9 kB
/** * Composed Authentication Manager * Uses composition instead of inheritance for better flexibility and testability */ import type { WordPressClientConfig, AuthStatus, AppPasswordCredentials, JwtCredentials, BasicCredentials, } from "@/types/client.js"; import { AuthenticationError } from "@/types/client.js"; import { AUTH_METHODS, type AuthMethod } from "@/types/wordpress.js"; import { debug } from "@/utils/debug.js"; import type { ConfigurationProvider, ErrorHandler, ParameterValidator, AuthenticationProvider, } from "./interfaces/ManagerInterfaces.js"; import { ConfigurationProviderImpl } from "./implementations/ConfigurationProviderImpl.js"; import { ErrorHandlerImpl } from "./implementations/ErrorHandlerImpl.js"; import { ParameterValidatorImpl } from "./implementations/ParameterValidatorImpl.js"; interface AuthenticationDependencies { configProvider: ConfigurationProvider; errorHandler: ErrorHandler; validator: ParameterValidator; } export class ComposedAuthenticationManager implements AuthenticationProvider { private isAuth: boolean = false; private authToken: string | null = null; private tokenExpiry: Date | null = null; private lastAuthAttempt: Date | null = null; private authMethod: AuthMethod; constructor(private dependencies: AuthenticationDependencies) { this.authMethod = this.getAuthMethodFromConfig(); this.validateAuthConfiguration(); } /** * Factory method to create ComposedAuthenticationManager with default implementations */ static create(clientConfig: WordPressClientConfig): ComposedAuthenticationManager { const configProvider = new ConfigurationProviderImpl(clientConfig); const errorHandler = new ErrorHandlerImpl(configProvider); const validator = new ParameterValidatorImpl(); return new ComposedAuthenticationManager({ configProvider, errorHandler, validator, }); } /** * Authenticate with the configured method */ async authenticate(): Promise<boolean> { try { this.lastAuthAttempt = new Date(); switch (this.authMethod) { case "app-password": return await this.authenticateAppPassword(); case "jwt": return await this.authenticateJWT(); case "basic": return await this.authenticateBasic(); case "api-key": return await this.authenticateApiKey(); default: throw new AuthenticationError(`Unsupported authentication method: ${this.authMethod}`, this.authMethod); } } catch (error) { this.isAuth = false; this.authToken = null; this.dependencies.errorHandler.handleError(error, "authentication"); } } /** * Check if currently authenticated */ isAuthenticated(): boolean { if (!this.isAuth) { return false; } // Check token expiry for JWT if (this.authMethod === "jwt" && this.tokenExpiry) { if (new Date() > this.tokenExpiry) { this.isAuth = false; this.authToken = null; return false; } } return true; } /** * Get authentication headers for requests */ getAuthHeaders(): Record<string, string> { const headers: Record<string, string> = {}; switch (this.authMethod) { case "app-password": case "basic": headers.Authorization = this.getBasicAuthHeader(); break; case "jwt": if (this.authToken) { headers.Authorization = `Bearer ${this.authToken}`; } break; case "api-key": const apiKey = this.getApiKeyFromConfig(); if (apiKey) { headers["X-API-Key"] = apiKey; } break; } return headers; } /** * Handle authentication failure */ async handleAuthFailure(error: unknown): Promise<boolean> { debug.log("Handling authentication failure", { error: String(error), method: this.authMethod }); this.isAuth = false; this.authToken = null; // For JWT, try to refresh token if (this.authMethod === "jwt") { try { return await this.refreshJWTToken(); } catch (_refreshError) { // If refresh fails, try full re-authentication return await this.authenticate(); } } // For other methods, try full re-authentication return await this.authenticate(); } /** * Get authentication status */ getAuthStatus(): AuthStatus { return { isAuthenticated: this.isAuthenticated(), method: this.authMethod, lastAuthAttempt: this.lastAuthAttempt, tokenExpiry: this.tokenExpiry, }; } /** * App Password authentication */ private async authenticateAppPassword(): Promise<boolean> { const credentials = this.getAppPasswordCredentials(); // App passwords are stateless - just validate credentials format this.dependencies.validator.validateString(credentials.username, "username", { required: true }); this.dependencies.validator.validateString(credentials.appPassword, "appPassword", { required: true }); this.isAuth = true; debug.log("App password authentication configured"); return true; } /** * JWT authentication */ private async authenticateJWT(): Promise<boolean> { const credentials = this.getJWTCredentials(); const tokenEndpoint = "/jwt-auth/v1/token"; try { const response = await fetch(`${this.dependencies.configProvider.config.baseUrl}/wp-json${tokenEndpoint}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: credentials.username, password: credentials.password, }), }); if (!response.ok) { throw new AuthenticationError(`JWT authentication failed: ${response.statusText}`, "jwt"); } const data = (await response.json()) as { token?: string; expires_in?: number }; if (!data.token) { throw new AuthenticationError("No token received from JWT authentication", "jwt"); } this.authToken = data.token; this.isAuth = true; // Set token expiry (default to 24 hours if not provided) const expiresIn = data.expires_in || 86400; // 24 hours in seconds this.tokenExpiry = new Date(Date.now() + expiresIn * 1000); debug.log("JWT authentication successful", { expiresIn, expiry: this.tokenExpiry.toISOString(), }); return true; } catch (error) { throw new AuthenticationError(`JWT authentication failed: ${String(error)}`, "jwt"); } } /** * Basic authentication */ private async authenticateBasic(): Promise<boolean> { const credentials = this.getBasicCredentials(); this.dependencies.validator.validateString(credentials.username, "username", { required: true }); this.dependencies.validator.validateString(credentials.password, "password", { required: true }); this.isAuth = true; debug.log("Basic authentication configured"); return true; } /** * API Key authentication */ private async authenticateApiKey(): Promise<boolean> { const apiKey = this.getApiKeyFromConfig(); this.dependencies.validator.validateString(apiKey, "apiKey", { required: true }); this.isAuth = true; debug.log("API key authentication configured"); return true; } /** * Refresh JWT token */ private async refreshJWTToken(): Promise<boolean> { if (!this.authToken) { throw new AuthenticationError("No token to refresh", "jwt"); } const refreshEndpoint = "/jwt-auth/v1/token/validate"; try { const response = await fetch(`${this.dependencies.configProvider.config.baseUrl}/wp-json${refreshEndpoint}`, { method: "POST", headers: { Authorization: `Bearer ${this.authToken}`, "Content-Type": "application/json", }, }); if (response.ok) { // Token is still valid debug.log("JWT token validated successfully"); return true; } // Token invalid, need to re-authenticate this.authToken = null; return await this.authenticateJWT(); } catch (error) { throw new AuthenticationError(`JWT token refresh failed: ${String(error)}`, "jwt"); } } /** * Get Basic auth header */ private getBasicAuthHeader(): string { let credentials: { username: string; password?: string; appPassword?: string }; if (this.authMethod === "app-password") { credentials = this.getAppPasswordCredentials(); const auth = `${credentials.username}:${credentials.appPassword}`; return `Basic ${Buffer.from(auth).toString("base64")}`; } else { credentials = this.getBasicCredentials(); const auth = `${credentials.username}:${credentials.password}`; return `Basic ${Buffer.from(auth).toString("base64")}`; } } /** * Extract auth method from configuration */ private getAuthMethodFromConfig(): AuthMethod { const authConfig = this.dependencies.configProvider.config.auth; if (authConfig.method && Object.values(AUTH_METHODS).includes(authConfig.method as AuthMethod)) { return authConfig.method as AuthMethod; } // Auto-detect based on available credentials if (authConfig.appPassword) return "app-password"; if (authConfig.secret || authConfig.password) return "jwt"; if (authConfig.apiKey) return "api-key"; return "basic"; // fallback } /** * Validate authentication configuration */ private validateAuthConfiguration(): void { const authConfig = this.dependencies.configProvider.config.auth; if (!authConfig) { throw new AuthenticationError("No authentication configuration provided", this.authMethod); } switch (this.authMethod) { case "app-password": if (!authConfig.username || !authConfig.appPassword) { throw new AuthenticationError( "App password authentication requires username and appPassword", this.authMethod, ); } break; case "jwt": if (!authConfig.username || !authConfig.password) { throw new AuthenticationError("JWT authentication requires username and password", this.authMethod); } break; case "basic": if (!authConfig.username || !authConfig.password) { throw new AuthenticationError("Basic authentication requires username and password", this.authMethod); } break; case "api-key": if (!authConfig.apiKey) { throw new AuthenticationError("API key authentication requires apiKey", this.authMethod); } break; } } /** * Get app password credentials from config */ private getAppPasswordCredentials(): AppPasswordCredentials { const authConfig = this.dependencies.configProvider.config.auth; return { username: authConfig.username!, appPassword: authConfig.appPassword!, }; } /** * Get JWT credentials from config */ private getJWTCredentials(): JwtCredentials { const authConfig = this.dependencies.configProvider.config.auth; const credentials: JwtCredentials = { username: authConfig.username!, password: authConfig.password!, }; if (authConfig.secret) { credentials.jwtSecret = authConfig.secret; } return credentials; } /** * Get basic credentials from config */ private getBasicCredentials(): BasicCredentials { const authConfig = this.dependencies.configProvider.config.auth; return { username: authConfig.username!, password: authConfig.password!, }; } /** * Get API key from config */ private getApiKeyFromConfig(): string { const authConfig = this.dependencies.configProvider.config.auth; return authConfig.apiKey || ""; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server