Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
server.ts66.5 kB
import { SignJWT, generateKeyPair, exportJWK, importPKCS8, importSPKI, exportPKCS8, exportSPKI, jwtVerify, JWTPayload, } from "jose"; import { randomBytes } from "crypto"; import { readFileSync, writeFileSync, existsSync } from "fs"; import type { User, AuthCodeStore, TokenClaims, OIDCDiscoveryDocument, } from "../types/index.js"; import { DatabaseClient } from "../database/client.js"; import { PKCEUtils } from "./pkce.js"; import { Logger } from "../utils/logger.js"; import { TokenFactory } from "../utils/token-factory.js"; import { Validators } from "../utils/validators.js"; export class OIDCServer { private keyPair!: { privateKey: any; publicKey: any }; private jwks!: any; private authCodes: AuthCodeStore = {}; private issuer: string; private clientId: string; private clientSecret: string; private dbClient: DatabaseClient; private publicBaseUrl: string; private tokenFactory!: TokenFactory; private clientAuthMethod: string; constructor( issuer: string, clientId: string, clientSecret: string, publicBaseUrl?: string, clientAuthMethod: string = "all" ) { this.issuer = issuer; this.clientId = clientId; this.clientSecret = clientSecret; this.publicBaseUrl = publicBaseUrl || issuer; this.clientAuthMethod = clientAuthMethod; this.dbClient = new DatabaseClient(); } async initialize(): Promise<void> { await this.loadOrGenerateKeyPair(); await this.dbClient.connect(); const publicJWK = await exportJWK(this.keyPair.publicKey); this.jwks = { keys: [ { ...publicJWK, kid: "phoenix-dev-key-1", alg: "RS256", use: "sig", }, ], }; // Initialize token factory this.tokenFactory = new TokenFactory( this.keyPair, this.issuer, this.clientId ); Logger.logEvent("oidc_server_initialized", { key_pair_status: "persistent_rsa_loaded", jwks_key_count: this.jwks.keys.length, client_auth_methods: this.clientAuthMethod, supported_flows: this.getSupportedFlows(), }); const users = this.getUsers(); Logger.logEvent("user_availability_status", { user_count: users.length, users: users.length > 0 ? users.map((u: User) => ({ id: u.id, email: u.email, name: u.name })) : [], status: users.length > 0 ? "users_available" : "awaiting_database", }); } private getSupportedFlows(): string[] { switch (this.clientAuthMethod) { case "oidc": return ["standard_oauth"]; case "pkce-public": return ["pkce_public"]; case "pkce-confidential": return ["pkce_confidential"]; case "all": return ["standard_oauth", "pkce_public", "pkce_confidential"]; default: return ["standard_oauth"]; } } private isFlowAllowed( flowType: "oidc" | "pkce-public" | "pkce-confidential" ): boolean { switch (this.clientAuthMethod) { case "oidc": return flowType === "oidc"; case "pkce-public": return flowType === "pkce-public"; case "pkce-confidential": return flowType === "pkce-confidential"; case "all": return true; default: return flowType === "oidc"; } } private async loadOrGenerateKeyPair(): Promise<void> { const keyDataPath = "/app/runtime/keypair.json"; try { if (existsSync(keyDataPath)) { Logger.logEvent("key_pair_loading_started", { source: "persistent_file", key_path: keyDataPath, }); const keyData = JSON.parse(readFileSync(keyDataPath, "utf8")); const privateKey = await importPKCS8(keyData.privateKey, "RS256"); const publicKey = await importSPKI(keyData.publicKey, "RS256"); this.keyPair = { privateKey, publicKey }; Logger.logEvent("key_pair_loaded_successfully", { source: "persistent_file", key_type: "RSA", algorithm: "RS256", }); } else { const keyGenStart = { timestamp: new Date().toISOString(), event: "key_pair_generation_started", algorithm: "RS256", reason: "no_existing_key_found", }; console.log(JSON.stringify(keyGenStart)); this.keyPair = await generateKeyPair("RS256"); const privateKeyPem = await exportPKCS8(this.keyPair.privateKey); const publicKeyPem = await exportSPKI(this.keyPair.publicKey); const keyData = { privateKey: privateKeyPem, publicKey: publicKeyPem, }; writeFileSync(keyDataPath, JSON.stringify(keyData)); const keyGenComplete = { timestamp: new Date().toISOString(), event: "key_pair_generated_and_saved", key_type: "RSA", algorithm: "RS256", saved_to: keyDataPath, status: "persistent_key_created", }; console.log(JSON.stringify(keyGenComplete)); } } catch (error) { const keyError = { timestamp: new Date().toISOString(), event: "key_pair_operation_failed", error: error instanceof Error ? error.message : String(error), fallback_action: "generating_in_memory_keys", }; console.log(JSON.stringify(keyError)); const fallbackStart = { timestamp: new Date().toISOString(), event: "key_pair_fallback_started", method: "in_memory_generation", algorithm: "RS256", }; console.log(JSON.stringify(fallbackStart)); this.keyPair = await generateKeyPair("RS256"); } } /** * OIDC Discovery Document * Spec: OpenID Connect Discovery 1.0 Section 3 * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata * * Returns metadata about the OpenID Provider's configuration. */ getDiscoveryDocument(): OIDCDiscoveryDocument { const authEndpoint = process.env.OIDC_AUTH_ENDPOINT || `${this.issuer}/auth`; const discoveryDoc = { // REQUIRED: Spec Section 3 - Issuer identifier URL issuer: this.issuer, // REQUIRED: Spec Section 3 - Authorization endpoint URL authorization_endpoint: authEndpoint, // REQUIRED: Spec Section 3 - Token endpoint URL token_endpoint: `${this.issuer}/token`, // RECOMMENDED: Spec Section 5.3 - UserInfo endpoint URL userinfo_endpoint: `${this.issuer}/userinfo`, // REQUIRED: Spec Section 3 - JWK Set document URL jwks_uri: `${this.issuer}/.well-known/jwks.json`, // REQUIRED: Spec Section 3 - OAuth 2.0 response types supported response_types_supported: ["code"], // RECOMMENDED: OAuth 2.0 grant types supported grant_types_supported: ["authorization_code"], // REQUIRED: Spec Section 8 - Subject identifier types supported subject_types_supported: ["public"], // REQUIRED: Spec Section 3 - JWS signing algorithms for ID Tokens id_token_signing_alg_values_supported: ["RS256"], // RECOMMENDED: OAuth 2.0 client authentication methods token_endpoint_auth_methods_supported: this.getSupportedAuthMethods(), // RECOMMENDED: Spec Section 5.4 - OAuth 2.0 scopes supported scopes_supported: ["openid", "email", "profile", "groups", "roles"], // RECOMMENDED: Spec Section 5.1 - Claims about the End-User supported claims_supported: ["sub", "email", "name", "groups", "role"], // OPTIONAL: RFC 7636 (PKCE) - Code challenge methods supported code_challenge_methods_supported: this.getSupportedPKCEMethods(), }; Logger.logEvent("discovery_document_generated", { client_auth_methods: this.clientAuthMethod, supported_flows: this.getSupportedFlows(), pkce_methods_supported: discoveryDoc.code_challenge_methods_supported, server_restrictions: { oidc_only: this.clientAuthMethod === "oidc", pkce_public_only: this.clientAuthMethod === "pkce-public", pkce_confidential_only: this.clientAuthMethod === "pkce-confidential", all_flows: this.clientAuthMethod === "all", }, }); return discoveryDoc; } private getSupportedPKCEMethods(): string[] { switch (this.clientAuthMethod) { case "oidc": return []; // No PKCE support for OIDC-only mode case "pkce-public": case "pkce-confidential": return ["S256", "plain"]; case "all": return ["S256", "plain"]; default: return []; } } private getSupportedAuthMethods(): string[] { const methods: string[] = []; // OAuth 2.0 spec defines these standard methods switch (this.clientAuthMethod) { case "oidc": case "pkce-confidential": // Confidential clients support client_secret_basic and client_secret_post methods.push("client_secret_basic", "client_secret_post"); break; case "pkce-public": // Public clients don't authenticate methods.push("none"); break; case "all": // Support all methods methods.push("client_secret_basic", "client_secret_post", "none"); break; } return methods; } getJWKS() { return this.jwks; } /** * Authorization Endpoint - Authorization Code Flow * Spec: OIDC Core Section 3.1.2 - Authentication using the Authorization Code Flow * https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth * * Handles authentication requests and returns authorization codes. * * Section 3.1.2.1 - Authentication Request * Section 3.1.2.2 - Authentication Request Validation * Section 3.1.2.5 - Successful Authentication Response * Section 3.1.2.6 - Authentication Error Response */ async handleAuth(query: any): Promise<{ redirectUrl: string; error?: string; error_description?: string; }> { // Auto-detect flow type (PKCE detection) const isPKCE = !!(query.code_challenge && query.code_challenge_method); // For authorization requests, we can't determine if it's public or confidential // because client secrets are NEVER sent in authorization requests (only in token requests) // So we accept the authorization request and validate the client type in the token request let flowType: "oidc" | "pkce-public" | "pkce-confidential"; if (isPKCE) { // Accept any PKCE flow in the authorization request // The actual validation happens in the token request flowType = "pkce-public"; // Use as a placeholder, will be validated in token request } else { flowType = "oidc"; } const authRequest = { timestamp: new Date().toISOString(), event: "oauth_auth_request_started", query_params: query, client_id: query.client_id, redirect_uri: query.redirect_uri, response_type: query.response_type, scope: query.scope, state: query.state, nonce: query.nonce, detected_flow_type: flowType, client_auth_methods_restriction: this.clientAuthMethod, flow_allowed: this.isFlowAllowed(flowType), }; console.log(JSON.stringify(authRequest)); // Check if the detected flow is allowed if (!this.isFlowAllowed(flowType)) { const flowRejection = { timestamp: new Date().toISOString(), event: "auth_flow_rejected", detected_flow: flowType, configured_client_auth_methods: this.clientAuthMethod, supported_flows: this.getSupportedFlows(), error: `Authentication method '${flowType}' not allowed with current configuration`, }; console.log(JSON.stringify(flowRejection)); return { redirectUrl: `${query.redirect_uri}?error=unsupported_response_type&error_description=${encodeURIComponent( `Authentication method '${flowType}' is not supported with current configuration` )}&state=${query.state}`, error: `Authentication method '${flowType}' not allowed`, error_description: `Authentication method '${flowType}' is not supported with current configuration`, }; } // Section 3.1.2.1 - Required parameters: response_type, client_id, redirect_uri, scope // state is RECOMMENDED for CSRF protection const { client_id, redirect_uri, response_type, scope, state, nonce } = query; // Section 3.1.2.2 - Authentication Request Validation // REQUIRED parameters must be present if (!client_id || !redirect_uri || !response_type) { const errorMsg = "Invalid request: missing required parameters"; const errorLog = { timestamp: new Date().toISOString(), event: "oauth_validation_failed", error: errorMsg, missing_params: { client_id: !client_id, redirect_uri: !redirect_uri, response_type: !response_type, }, }; console.log(JSON.stringify(errorLog)); return { redirectUrl: "", error: errorMsg, }; } // Section 3.1.2.1 - response_type MUST be "code" for Authorization Code Flow // Section 3.1.2.6 - Error response: unsupported_response_type if (response_type !== "code") { const errorMsg = "Only authorization code flow supported"; const errorLog = { timestamp: new Date().toISOString(), event: "unsupported_response_type", error: errorMsg, received_response_type: response_type, supported_types: ["code"], }; console.log(JSON.stringify(errorLog)); // Section 3.1.2.6 - Return error via redirect with state parameter return { redirectUrl: `${redirect_uri}?error=unsupported_response_type&state=${state}`, error: errorMsg, error_description: errorMsg, }; } const users = this.getUsers(); const userCheck = { timestamp: new Date().toISOString(), event: "user_availability_check", available_users: users.length, users: users.map((u) => ({ id: u.id, email: u.email, name: u.name })), }; console.log(JSON.stringify(userCheck)); if (users.length === 0) { const errorMsg = "No users available in database"; const errorLog = { timestamp: new Date().toISOString(), event: "no_users_available", error: errorMsg, }; console.log(JSON.stringify(errorLog)); return { redirectUrl: `${redirect_uri}?error=access_denied&error_description=No%20users%20available&state=${state}`, error: errorMsg, error_description: "No users available", }; } if (users.length === 1) { const user = users[0]; const authCode = this.generateAuthCode(); const singleUserLogin = { timestamp: new Date().toISOString(), event: "single_user_auto_login", user: { id: user.id, email: user.email, name: user.name }, client_id, auth_code: authCode.substring(0, 12) + "...", redirect_uri, }; console.log(JSON.stringify(singleUserLogin)); // Section 3.1.2.5 - Store authorization code with required information // Authorization codes MUST be short-lived and single-use (Section 4.1.2 of RFC 6749) this.authCodes[authCode] = { userId: user.id, clientId: client_id, redirectUri: redirect_uri, // REQUIRED: for validation in token request nonce, // OPTIONAL: returned in ID Token if provided (Section 3.1.2.1) createdAt: Date.now(), scope, }; this.cleanupExpiredCodes(); // Section 3.1.2.5 - Successful Authentication Response // Return authorization code via query parameter with state const redirectUrl = `${redirect_uri}?code=${authCode}&state=${state}`; const loginComplete = { timestamp: new Date().toISOString(), event: "single_user_login_completed", redirect_url: redirectUrl, user_email: user.email, }; console.log(JSON.stringify(loginComplete)); return { redirectUrl }; } const isPhoenixClient = client_id === "phoenix-oidc-client-id"; const multiUserDecision = { timestamp: new Date().toISOString(), event: "multiple_users_behavior_decision", client_id, is_phoenix_client: isPhoenixClient, available_users: users.length, behavior: isPhoenixClient ? "show_user_selector" : "auto_login_first_user", }; console.log(JSON.stringify(multiUserDecision)); if (isPhoenixClient) { const phoenixSelector = { timestamp: new Date().toISOString(), event: "phoenix_user_selector_initiated", available_users: users.length, users: users.map((user, i) => ({ index: i + 1, id: user.id, name: user.name, email: user.email, })), }; console.log(JSON.stringify(phoenixSelector)); const selectionUrl = `${this.publicBaseUrl}/select-user?${new URLSearchParams( { client_id, redirect_uri, response_type, scope: scope || "", state, nonce: nonce || "", } ).toString()}`; const selectorRedirect = { timestamp: new Date().toISOString(), event: "phoenix_selector_redirect", selection_url: selectionUrl, }; console.log(JSON.stringify(selectorRedirect)); return { redirectUrl: selectionUrl }; } else { const sortedUsers = [...users].sort((a, b) => a.id.localeCompare(b.id)); const user = sortedUsers[0]; const authCode = this.generateAuthCode(); const autoLogin = { timestamp: new Date().toISOString(), event: "multi_user_auto_login", client_id, total_users: users.length, selected_user: { id: user.id, email: user.email, name: user.name }, selection_method: "first_user_sorted_by_id", auth_code: authCode.substring(0, 12) + "...", }; console.log(JSON.stringify(autoLogin)); this.authCodes[authCode] = { userId: user.id, clientId: client_id, redirectUri: redirect_uri, nonce, createdAt: Date.now(), scope, }; this.cleanupExpiredCodes(); const redirectUrl = `${redirect_uri}?code=${authCode}&state=${state}`; const autoLoginComplete = { timestamp: new Date().toISOString(), event: "auto_login_completed", client_id, user_email: user.email, redirect_url: redirectUrl, }; console.log(JSON.stringify(autoLoginComplete)); return { redirectUrl }; } } async handleUserSelection( selectedUserId: string, query: any ): Promise<{ redirectUrl: string; error?: string; error_description?: string; }> { const { client_id, redirect_uri, response_type, state, nonce } = query; const users = this.getUsers(); const selectedUser = users.find((user) => user.id === selectedUserId); if (!selectedUser) { return { redirectUrl: `${redirect_uri}?error=access_denied&error_description=Selected%20user%20not%20found&state=${state}`, error: "Selected user not found", }; } const authCode = this.generateAuthCode(); this.authCodes[authCode] = { userId: selectedUser.id, clientId: client_id, redirectUri: redirect_uri, nonce, createdAt: Date.now(), scope: query.scope, }; this.cleanupExpiredCodes(); const redirectUrl = `${redirect_uri}?code=${authCode}&state=${state}`; const userSelectionComplete = { timestamp: new Date().toISOString(), event: "user_selection_completed", selected_user: { id: selectedUser.id, email: selectedUser.email, name: selectedUser.name, }, auth_code: authCode.substring(0, 12) + "...", redirect_url: redirectUrl, }; console.log(JSON.stringify(userSelectionComplete)); return { redirectUrl }; } /** * Token Endpoint - Authorization Code Flow * Spec: OIDC Core Section 3.1.3 - Token Endpoint * https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint * * Exchanges authorization codes for access tokens and ID tokens. * * Section 3.1.3.1 - Token Request * Section 3.1.3.2 - Token Request Validation * Section 3.1.3.3 - Successful Token Response * Section 3.1.3.4 - Token Error Response * Section 9 - Client Authentication */ async handleToken( body: any, headers: any = {} ): Promise<{ tokens?: any; error?: string; error_description?: string }> { // Auto-detect flow type const isPKCE = !!body.code_verifier; const hasClientSecret = !!( body.client_secret || headers.authorization?.startsWith("Basic ") ); // Determine the specific flow type for validation let flowType: "oidc" | "pkce-public" | "pkce-confidential"; if (isPKCE) { flowType = hasClientSecret ? "pkce-confidential" : "pkce-public"; } else { flowType = "oidc"; } // Check if the detected flow is allowed if (!this.isFlowAllowed(flowType)) { const flowRejection = { timestamp: new Date().toISOString(), event: "token_flow_rejected", detected_flow: flowType, configured_client_auth_methods: this.clientAuthMethod, supported_flows: this.getSupportedFlows(), error: `Authentication method '${flowType}' not allowed with current configuration`, }; console.log(JSON.stringify(flowRejection)); return { error: "unsupported_grant_type", error_description: `Authentication method '${flowType}' is not supported with current configuration`, }; } // COMPREHENSIVE DEBUG LOGGING - Security not a concern in debug server Logger.logEvent("token_request_debug_start", { headers_received: headers, body_received: body, authorization_header: headers.authorization || "none", user_agent: headers["user-agent"] || "none", content_type: headers["content-type"] || "none", detected_flow_type: flowType, client_auth_methods_restriction: this.clientAuthMethod, flow_allowed: this.isFlowAllowed(flowType), }); // Section 9 - Client Authentication // Supports client_secret_post (body) and client_secret_basic (HTTP Basic Auth) // Extract client credentials from either body or Authorization header let client_secret = body.client_secret; let authSource = "request_body"; let decodedBasicAuth = null; // Section 9 - HTTP Basic Authentication (client_secret_basic) // Format: Authorization: Basic BASE64(client_id:client_secret) if (headers.authorization?.startsWith("Basic ")) { try { const base64Part = headers.authorization.slice(6); const decoded = Buffer.from(base64Part, "base64").toString(); const [authClientId, authSecret] = decoded.split(":", 2); decodedBasicAuth = { original_header: headers.authorization, base64_part: base64Part, decoded_string: decoded, parsed_client_id: authClientId, parsed_client_secret: authSecret, }; Logger.logEvent("http_basic_auth_decoded", decodedBasicAuth); if (authSecret) { client_secret = authSecret; authSource = "http_basic_auth"; Logger.logEvent("client_secret_extracted_from_basic_auth", { extracted_secret: authSecret, client_id_from_auth: authClientId, client_id_from_body: body.client_id, client_ids_match: authClientId === body.client_id, }); } } catch (error) { Logger.logEvent("http_basic_auth_decode_failed", { error: error instanceof Error ? error.message : "unknown", auth_header_present: !!headers.authorization, auth_header_value: headers.authorization, stack_trace: error instanceof Error ? error.stack : "none", }); } } // Final client credential analysis Logger.logEvent("client_credential_analysis", { client_secret_from_body: body.client_secret || "none", client_secret_final: client_secret || "none", client_secret_source: authSource, client_type_determined: client_secret ? "confidential" : "public", has_basic_auth_header: !!headers.authorization?.startsWith("Basic "), basic_auth_decode_success: !!decodedBasicAuth, }); // CLIENT SECRET VALIDATION - Critical security check const clientId = body.client_id || decodedBasicAuth?.parsed_client_id; const expectedSecrets: { [clientId: string]: string } = { "phoenix-oidc-client-id": "phoenix-oidc-client-secret-abc-123", "grafana-oidc-client-id": "grafana-oidc-client-secret-abc-123", }; const expectedSecret = expectedSecrets[clientId]; const isValidSecret = client_secret && client_secret === expectedSecret; Logger.logEvent("client_secret_validation", { client_id: clientId, client_secret_provided: client_secret || "none", expected_secret_for_client: expectedSecret || "none", has_expected_secret: !!expectedSecret, secret_validation_result: client_secret ? isValidSecret : "not_applicable_public_client", client_recognized: !!expectedSecret, debug_validation_details: { provided_secret_length: client_secret?.length || 0, expected_secret_length: expectedSecret?.length || 0, secrets_match: isValidSecret, client_type: client_secret ? "confidential" : "public", }, }); // For confidential clients, validate the secret if (client_secret && !isValidSecret) { const validationError = { timestamp: new Date().toISOString(), event: "client_authentication_failed", error: "invalid_client", client_id: clientId, provided_secret: client_secret, expected_secret: expectedSecret, reason: !expectedSecret ? "unknown_client" : "invalid_secret", debug_hints: [ "Confidential clients must provide correct client secret", "Check client ID and secret configuration", expectedSecret ? `Expected: ${expectedSecret}` : "Client ID not recognized", ], }; console.log(JSON.stringify(validationError)); return { error: "invalid_client" }; } const tokenRequest = { timestamp: new Date().toISOString(), event: "token_exchange_request_started", body_type: typeof body, body_keys: Object.keys(body || {}), has_client_secret: !!client_secret, client_auth_source: authSource, client_type: client_secret ? "confidential" : "public", full_request_debug: { raw_body: body, raw_headers: headers, decoded_basic_auth: decodedBasicAuth, final_client_secret: client_secret, }, }; console.log(JSON.stringify(tokenRequest)); // Section 3.1.3.1 - Token Request Parameters // REQUIRED: grant_type, code, redirect_uri, client_id (for confidential clients) const { grant_type, code, redirect_uri, client_id } = body; const extractedParams = { timestamp: new Date().toISOString(), event: "token_request_params_extracted", grant_type, code: code ? code.substring(0, 12) + "..." : null, redirect_uri, client_id, client_secret_provided: !!client_secret, }; console.log(JSON.stringify(extractedParams)); // Section 3.1.3.2 - Token Request Validation // grant_type MUST be "authorization_code" for this flow if (grant_type !== "authorization_code") { const grantTypeError = { timestamp: new Date().toISOString(), event: "token_grant_type_validation_failed", error: "unsupported_grant_type", expected: "authorization_code", received: grant_type, }; console.log(JSON.stringify(grantTypeError)); return { error: "unsupported_grant_type" }; } if (!code) { const codeError = { timestamp: new Date().toISOString(), event: "token_missing_auth_code", error: "invalid_request", }; console.log(JSON.stringify(codeError)); return { error: "invalid_request" }; } if (!redirect_uri) { const uriError = { timestamp: new Date().toISOString(), event: "token_missing_redirect_uri", error: "invalid_request", }; console.log(JSON.stringify(uriError)); return { error: "invalid_request" }; } const validClientIds = [this.clientId, "grafana-oidc-client-id"]; const clientValidation = { timestamp: new Date().toISOString(), event: "token_client_validation", valid_client_ids: validClientIds, received_client_id: client_id, client_id_valid: validClientIds.includes(client_id), validation_mode: "debug_mode_lenient", }; console.log(JSON.stringify(clientValidation)); const authCodeLookup = { timestamp: new Date().toISOString(), event: "auth_code_lookup", looking_for_code: code.substring(0, 12) + "...", available_auth_codes: Object.keys(this.authCodes).length, stored_codes: Object.keys(this.authCodes).map((key, i) => ({ index: i + 1, code: key.substring(0, 12) + "...", user_id: this.authCodes[key].userId, client_id: this.authCodes[key].clientId, })), }; console.log(JSON.stringify(authCodeLookup)); // Section 3.1.3.2 - Validate authorization code // Code must exist and not be expired const authData = this.authCodes[code]; if (!authData) { const authCodeError = { timestamp: new Date().toISOString(), event: "auth_code_not_found", error: "invalid_grant", requested_code: code.substring(0, 12) + "...", reason: "auth_code_missing_or_expired", }; console.log(JSON.stringify(authCodeError)); // Section 3.1.3.4 - Token Error Response return { error: "invalid_grant" }; } const authCodeFound = { timestamp: new Date().toISOString(), event: "auth_code_found", user_id: authData.userId, client_id: authData.clientId, redirect_uri: authData.redirectUri, nonce: authData.nonce || null, created_at: new Date(authData.createdAt).toISOString(), age_ms: Date.now() - authData.createdAt, }; console.log(JSON.stringify(authCodeFound)); // Section 3.1.3.2 - Validate redirect_uri matches the one from auth request // This prevents authorization code injection attacks const redirectValidation = { timestamp: new Date().toISOString(), event: "redirect_uri_validation", expected: authData.redirectUri, received: redirect_uri, match: authData.redirectUri === redirect_uri, }; console.log(JSON.stringify(redirectValidation)); if (authData.redirectUri !== redirect_uri) { const redirectError = { timestamp: new Date().toISOString(), event: "redirect_uri_mismatch", error: "invalid_grant", }; console.log(JSON.stringify(redirectError)); // Section 16.10 - Security: redirect_uri mismatch is invalid_grant return { error: "invalid_grant" }; } const users = this.getUsers(); const userLookup = { timestamp: new Date().toISOString(), event: "user_lookup", available_users: users.length, users: users.map((u, i) => ({ index: i + 1, id: u.id, name: u.name, email: u.email, })), looking_for_user_id: authData.userId, }; console.log(JSON.stringify(userLookup)); const user = users.find((u: User) => u.id === authData.userId) || users[0]; if (!user) { const userNotFound = { timestamp: new Date().toISOString(), event: "user_not_found", error: "user_not_found", requested_user_id: authData.userId, available_user_ids: users.map((u) => u.id), }; console.log(JSON.stringify(userNotFound)); return { error: "user_not_found" }; } Logger.logEvent("user_found", { user: { id: user.id, name: user.name, email: user.email }, }); // Section 3.1.3.3 - Successful Token Response // Generate access token and ID token // Section 3.1.3.6 - ID Token (required for OpenID Connect) // Section 3.1.3.8 - Access Token (for UserInfo endpoint access) const { accessToken, idToken } = await this.tokenFactory.generateTokenPair( user, authData.nonce, authData.scope ); // Section 16.9 - Security: Authorization codes MUST be single-use const beforeCodeCleanup = Object.keys(this.authCodes).length; delete this.authCodes[code]; const afterCodeCleanup = Object.keys(this.authCodes).length; Logger.logEvent("auth_code_cleanup", { cleaned_code: code.substring(0, 12) + "...", auth_codes_before: beforeCodeCleanup, auth_codes_after: afterCodeCleanup, }); // Section 3.1.3.3 - Successful Token Response // REQUIRED: access_token, token_type, id_token // RECOMMENDED: expires_in, scope const tokenResponse = { access_token: accessToken, id_token: idToken, token_type: "Bearer", // REQUIRED: Must be "Bearer" per OAuth 2.0 expires_in: 3600, // RECOMMENDED: Token lifetime in seconds scope: "openid email profile", }; Logger.logEvent("token_exchange_completed", { user_email: user.email, client_type: client_secret ? "confidential" : "public", client_auth_source: authSource, tokens_issued: ["access_token", "id_token"], token_response: { access_token: accessToken.substring(0, 50) + "...", id_token: idToken.substring(0, 50) + "...", token_type: "Bearer", expires_in: 3600, scope: "openid email profile", }, }); return { tokens: tokenResponse }; } /** * UserInfo Endpoint * Spec: OIDC Core Section 5.3 - UserInfo Endpoint * https://openid.net/specs/openid-connect-core-1_0.html#UserInfo * * Returns Claims about the authenticated End-User. * * Section 5.3.1 - UserInfo Request (requires Bearer token) * Section 5.3.2 - Successful UserInfo Response * Section 5.3.3 - UserInfo Error Response */ async handleUserInfo( authHeader?: string ): Promise<{ user?: any; error?: string }> { Logger.logEvent("userinfo_request_received", { has_auth_header: !!authHeader, auth_header_format: authHeader?.startsWith("Bearer ") ? "bearer_token" : "invalid_format", }); // Section 5.3.1 - Authentication REQUIRED using Bearer token if (!authHeader || !authHeader.startsWith("Bearer ")) { Logger.logEvent("userinfo_auth_failed", { reason: "missing_or_invalid_bearer_token", auth_header: authHeader || "none", }); // Section 5.3.3 - UserInfo Error Response return { error: "invalid_token" }; } // Extract and verify the access token const token = authHeader.slice(7); // Remove "Bearer " prefix let tokenClaims: JWTPayload | null = null; let userId: string | null = null; try { const { payload } = await jwtVerify(token, this.keyPair.publicKey); tokenClaims = payload; userId = payload.sub as string; Logger.logEvent("userinfo_token_verified", { token_length: token.length, token_sub: payload.sub, token_aud: payload.aud, token_claims: Object.keys(payload), }); } catch (error) { Logger.logEvent("userinfo_token_verification_failed", { error: error instanceof Error ? error.message : "unknown_error", token_length: token.length, }); return { error: "invalid_token" }; } // Find the user from the token's sub claim const users = this.getUsers(); const user = users.find((u: User) => u.id === userId) || users[0]; if (!user) { Logger.logEvent("userinfo_failed", { reason: "no_users_available", requested_user_id: userId, }); return { error: "no_users_available" }; } // Section 5.3.2 - Successful UserInfo Response // Section 5.1 - Standard Claims: sub, email, name // Section 5.4 - Claims can be returned from UserInfo endpoint // Build userinfo response - ALWAYS include groups and role // This is the standard practice for Grafana and other OIDC clients // Groups should be retrieved from userinfo endpoint, not from ID token const userInfo: any = { sub: user.id, // REQUIRED: Subject identifier (Section 5.1) email: user.email, // Standard Claim (Section 5.1) name: user.name, // Standard Claim (Section 5.1) role: user.role, // Custom claim groups: user.groups, // Custom claim (best practice: in UserInfo, not ID token) }; Logger.logEvent("userinfo_response_prepared", { user_id: user.id, user_email: user.email, role_included: user.role, groups_count: user.groups.length, groups_included: user.groups, debug_userinfo_claims: userInfo, note: "Groups and role are ALWAYS included in userinfo endpoint for Grafana compatibility", }); return { user: userInfo }; } private generateAuthCode(): string { return randomBytes(32).toString("base64url"); } /** * Cleanup Expired Authorization Codes * Spec: Section 4.1.2 of RFC 6749 (OAuth 2.0) * * Authorization codes MUST be short-lived. This implementation uses * a 10-minute expiration as recommended by the spec. */ private cleanupExpiredCodes(): void { const now = Date.now(); // Section 4.1.2 - Authorization codes MUST expire (10 minutes) const expiredCodes = Object.keys(this.authCodes).filter( (code) => now - this.authCodes[code].createdAt > 10 * 60 * 1000 ); expiredCodes.forEach((code) => delete this.authCodes[code]); if (expiredCodes.length > 0) { const codeCleanup = { timestamp: new Date().toISOString(), event: "expired_auth_codes_cleaned", expired_code_count: expiredCodes.length, remaining_code_count: Object.keys(this.authCodes).length, expiration_threshold_minutes: 10, }; console.log(JSON.stringify(codeCleanup)); } } getUsers(): User[] { return this.dbClient.getUsers(); } async cleanup(): Promise<void> { await this.dbClient.close(); } /** * PKCE Authorization Endpoint * Spec: RFC 7636 - Proof Key for Code Exchange * https://tools.ietf.org/html/rfc7636 * * Authorization Code Flow with PKCE for public and confidential clients. * * Section 4.3 - Client Creates the Code Challenge * Section 4.4 - Client Sends the Code Challenge with Authorization Request */ async handlePKCEAuth(query: any): Promise<{ redirectUrl: string; error?: string; error_description?: string; }> { const requestId = `pkce-auth-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // For authorization requests, we can't determine if it's public or confidential // because client secrets are NEVER sent in authorization requests (only in token requests) // Accept the authorization request and validate the client type in the token request const flowType = "pkce-public"; // Placeholder, actual validation happens in token request // Check if ANY PKCE flow is allowed (public or confidential) const anyPKCEAllowed = this.isFlowAllowed("pkce-public") || this.isFlowAllowed("pkce-confidential"); if (!anyPKCEAllowed) { const flowRejection = { timestamp: new Date().toISOString(), event: "pkce_flow_rejected", request_id: requestId, configured_client_auth_methods: this.clientAuthMethod, supported_flows: this.getSupportedFlows(), error: `PKCE authentication method '${flowType}' not allowed with current configuration`, }; console.log(JSON.stringify(flowRejection)); return { redirectUrl: `${query.redirect_uri}?error=unsupported_response_type&error_description=${encodeURIComponent( `PKCE authentication method '${flowType}' is not supported with current configuration` )}&state=${query.state}`, error: `PKCE authentication method '${flowType}' not allowed`, }; } const authRequest = { timestamp: new Date().toISOString(), event: "pkce_auth_request_started", request_id: requestId, query_params: query, client_id: query.client_id, redirect_uri: query.redirect_uri, response_type: query.response_type, scope: query.scope, state: query.state, nonce: query.nonce, code_challenge: query.code_challenge ? "provided" : "missing", code_challenge_method: query.code_challenge_method, debug_pkce_analysis: { challenge_length: query.code_challenge?.length || 0, challenge_method_valid: ["S256", "plain"].includes( query.code_challenge_method ), challenge_format_check: { base64url_pattern: query.code_challenge ? /^[A-Za-z0-9._~-]+$/.test(query.code_challenge) : false, expected_s256_length: 43, // Base64URL of SHA256 is 43 chars actual_length: query.code_challenge?.length || 0, }, state_provided: !!query.state, nonce_provided: !!query.nonce, request_from_phoenix_client: query.client_id === "phoenix-oidc-client-id", request_from_grafana_client: query.client_id === "grafana-oidc-client-id", }, debug_request_metadata: { query_param_count: Object.keys(query || {}).length, required_oauth_params_present: { response_type: !!query.response_type, client_id: !!query.client_id, redirect_uri: !!query.redirect_uri, scope: !!query.scope, }, required_pkce_params_present: { code_challenge: !!query.code_challenge, code_challenge_method: !!query.code_challenge_method, }, }, }; console.log(JSON.stringify(authRequest)); const { client_id, redirect_uri, response_type, scope, state, nonce, code_challenge, code_challenge_method, } = query; // Basic OAuth validations (same as regular flow) if (!client_id || !redirect_uri || !response_type) { const errorMsg = "Invalid request: missing required parameters"; const errorLog = { timestamp: new Date().toISOString(), event: "pkce_validation_failed", request_id: requestId, error: errorMsg, missing_params: { client_id: !client_id, redirect_uri: !redirect_uri, response_type: !response_type, }, debug_received_params: { client_id: client_id || "MISSING", redirect_uri: redirect_uri || "MISSING", response_type: response_type || "MISSING", scope: scope || "not_provided", state: state ? "provided" : "not_provided", nonce: nonce ? "provided" : "not_provided", }, phoenix_debug_hints: [ !client_id ? "Phoenix should set client_id in OAuth2 request" : null, !redirect_uri ? "Phoenix should set valid redirect_uri" : null, !response_type ? "Phoenix should set response_type=code" : null, ].filter(Boolean), }; console.log(JSON.stringify(errorLog)); return { redirectUrl: "", error: errorMsg, }; } if (response_type !== "code") { const errorMsg = "Only authorization code flow supported"; const errorLog = { timestamp: new Date().toISOString(), event: "pkce_unsupported_response_type", error: errorMsg, received_response_type: response_type, supported_types: ["code"], }; console.log(JSON.stringify(errorLog)); return { redirectUrl: `${redirect_uri}?error=unsupported_response_type&state=${state}`, error: errorMsg, }; } // RFC 7636 Section 4.4 - PKCE Challenge Validation // code_challenge and code_challenge_method are REQUIRED for PKCE const pkceValidation = PKCEUtils.validatePKCEChallenge( code_challenge, code_challenge_method ); const pkceValidationLog = { timestamp: new Date().toISOString(), event: "pkce_challenge_validation", code_challenge_provided: !!code_challenge, code_challenge_method: code_challenge_method, validation_result: pkceValidation.valid, validation_error: pkceValidation.error || null, }; console.log(JSON.stringify(pkceValidationLog)); if (!pkceValidation.valid) { const errorMsg = pkceValidation.error || "Invalid PKCE parameters"; return { redirectUrl: `${redirect_uri}?error=invalid_request&error_description=${encodeURIComponent( errorMsg )}&state=${state}`, error: errorMsg, }; } const users = this.getUsers(); if (users.length === 0) { const errorMsg = "No users available in database"; const errorLog = { timestamp: new Date().toISOString(), event: "pkce_no_users_available", error: errorMsg, }; console.log(JSON.stringify(errorLog)); return { redirectUrl: `${redirect_uri}?error=access_denied&error_description=No%20users%20available&state=${state}`, error: errorMsg, }; } // Auto-login logic (same as regular flow) if (users.length === 1) { const user = users[0]; const authCode = this.generateAuthCode(); const singleUserLogin = { timestamp: new Date().toISOString(), event: "pkce_single_user_auto_login", request_id: requestId, user: { id: user.id, email: user.email, name: user.name }, client_id, auth_code: authCode.substring(0, 12) + "...", redirect_uri, pkce_enabled: true, debug_auth_completion: { auth_code_generated: true, challenge_stored: !!code_challenge, challenge_method_stored: code_challenge_method, nonce_stored: !!nonce, auto_login_reason: "single_user_available", total_users_found: users.length, user_selected: user.id, auth_code_length: authCode.length, redirect_target: redirect_uri, state_will_be_returned: !!state, }, debug_stored_auth_data: { user_id: user.id, client_id: client_id, redirect_uri: redirect_uri, nonce_present: !!nonce, code_challenge_present: !!code_challenge, code_challenge_method: code_challenge_method, created_at: new Date().toISOString(), expires_in_minutes: 10, }, }; console.log(JSON.stringify(singleUserLogin)); this.authCodes[authCode] = { userId: user.id, clientId: client_id, redirectUri: redirect_uri, nonce, createdAt: Date.now(), codeChallenge: code_challenge, codeChallengeMethod: code_challenge_method, scope, }; this.cleanupExpiredCodes(); const redirectUrl = `${redirect_uri}?code=${authCode}&state=${state}`; return { redirectUrl }; } const isPhoenixClient = client_id === "phoenix-oidc-client-id"; if (isPhoenixClient) { const selectionUrl = `${this.publicBaseUrl}/pkce/select-user?${new URLSearchParams( { client_id, redirect_uri, response_type, scope: scope || "", state, nonce: nonce || "", code_challenge, code_challenge_method, } ).toString()}`; const selectorRedirect = { timestamp: new Date().toISOString(), event: "pkce_selector_redirect", selection_url: selectionUrl, }; console.log(JSON.stringify(selectorRedirect)); return { redirectUrl: selectionUrl }; } else { const sortedUsers = [...users].sort((a, b) => a.id.localeCompare(b.id)); const user = sortedUsers[0]; const authCode = this.generateAuthCode(); const autoLogin = { timestamp: new Date().toISOString(), event: "pkce_multi_user_auto_login", client_id, total_users: users.length, selected_user: { id: user.id, email: user.email, name: user.name }, selection_method: "first_user_sorted_by_id", auth_code: authCode.substring(0, 12) + "...", pkce_enabled: true, }; console.log(JSON.stringify(autoLogin)); this.authCodes[authCode] = { userId: user.id, clientId: client_id, redirectUri: redirect_uri, nonce, createdAt: Date.now(), codeChallenge: code_challenge, codeChallengeMethod: code_challenge_method, scope, }; this.cleanupExpiredCodes(); const redirectUrl = `${redirect_uri}?code=${authCode}&state=${state}`; return { redirectUrl }; } } async handlePKCEUserSelection( selectedUserId: string, query: any ): Promise<{ redirectUrl: string; error?: string; error_description?: string; }> { const { client_id, redirect_uri, response_type, state, nonce, code_challenge, code_challenge_method, } = query; const users = this.getUsers(); const selectedUser = users.find((user) => user.id === selectedUserId); if (!selectedUser) { return { redirectUrl: `${redirect_uri}?error=access_denied&error_description=Selected%20user%20not%20found&state=${state}`, error: "Selected user not found", }; } const authCode = this.generateAuthCode(); this.authCodes[authCode] = { userId: selectedUser.id, clientId: client_id, redirectUri: redirect_uri, nonce, createdAt: Date.now(), codeChallenge: code_challenge, codeChallengeMethod: code_challenge_method, scope: query.scope, }; this.cleanupExpiredCodes(); const redirectUrl = `${redirect_uri}?code=${authCode}&state=${state}`; const userSelectionComplete = { timestamp: new Date().toISOString(), event: "pkce_user_selection_completed", selected_user: { id: selectedUser.id, email: selectedUser.email, name: selectedUser.name, }, auth_code: authCode.substring(0, 12) + "...", redirect_url: redirectUrl, pkce_enabled: true, }; console.log(JSON.stringify(userSelectionComplete)); return { redirectUrl }; } /** * PKCE Token Endpoint * Spec: RFC 7636 - Proof Key for Code Exchange * https://tools.ietf.org/html/rfc7636 * * Token exchange with PKCE verification. * * Section 4.5 - Client Sends the Authorization Code and Code Verifier * Section 4.6 - Server Verifies code_verifier against code_challenge */ async handlePKCEToken( body: any, headers: any = {} ): Promise<{ tokens?: any; error?: string; error_description?: string }> { const requestId = `pkce-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Determine if this is public or confidential PKCE const hasClientSecret = !!( (body.client_secret && body.client_secret.trim() !== "") || headers.authorization?.startsWith("Basic ") ); const flowType = hasClientSecret ? "pkce-confidential" : "pkce-public"; // Check if PKCE flow is allowed if (!this.isFlowAllowed(flowType)) { const flowRejection = { timestamp: new Date().toISOString(), event: "pkce_token_flow_rejected", request_id: requestId, configured_client_auth_methods: this.clientAuthMethod, supported_flows: this.getSupportedFlows(), error: `PKCE authentication method '${flowType}' not allowed with current configuration`, }; console.log(JSON.stringify(flowRejection)); return { error: "unsupported_grant_type", error_description: `PKCE authentication method '${flowType}' is not supported with current configuration`, }; } // COMPREHENSIVE PKCE DEBUG LOGGING - Security not a concern in debug server Logger.logEvent("pkce_token_request_debug_start", { request_id: requestId, headers_received: headers, body_received: body, authorization_header: headers.authorization || "none", user_agent: headers["user-agent"] || "none", content_type: headers["content-type"] || "none", code_verifier_present: !!body.code_verifier, code_verifier_length: body.code_verifier?.length || 0, client_auth_methods_restriction: this.clientAuthMethod, flow_allowed: this.isFlowAllowed(flowType), }); // Extract client credentials from either body or Authorization header let client_secret = body.client_secret; let authSource = "request_body"; let decodedBasicAuth = null; // Check for HTTP Basic Authentication if (headers.authorization?.startsWith("Basic ")) { try { const base64Part = headers.authorization.slice(6); const decoded = Buffer.from(base64Part, "base64").toString(); const [authClientId, authSecret] = decoded.split(":", 2); decodedBasicAuth = { original_header: headers.authorization, base64_part: base64Part, decoded_string: decoded, parsed_client_id: authClientId, parsed_client_secret: authSecret, }; Logger.logEvent("pkce_http_basic_auth_decoded", { request_id: requestId, ...decodedBasicAuth, }); if (authSecret) { client_secret = authSecret; authSource = "http_basic_auth"; Logger.logEvent("pkce_client_secret_extracted_from_basic_auth", { request_id: requestId, extracted_secret: authSecret, client_id_from_auth: authClientId, client_id_from_body: body.client_id, client_ids_match: authClientId === body.client_id, }); } } catch (error) { Logger.logEvent("pkce_http_basic_auth_decode_failed", { error: error instanceof Error ? error.message : "unknown", auth_header_present: !!headers.authorization, auth_header_value: headers.authorization, stack_trace: error instanceof Error ? error.stack : "none", request_id: requestId, }); } } // Final client credential analysis for PKCE Logger.logEvent("pkce_client_credential_analysis", { request_id: requestId, client_secret_from_body: body.client_secret || "none", client_secret_final: client_secret || "none", client_secret_source: authSource, client_type_determined: client_secret ? "confidential" : "public", has_basic_auth_header: !!headers.authorization?.startsWith("Basic "), basic_auth_decode_success: !!decodedBasicAuth, }); // PKCE CLIENT SECRET VALIDATION - Critical security check const clientId = body.client_id || decodedBasicAuth?.parsed_client_id; const expectedSecrets: { [clientId: string]: string } = { "phoenix-oidc-client-id": "phoenix-oidc-client-secret-abc-123", "grafana-oidc-client-id": "grafana-oidc-client-secret-abc-123", }; const expectedSecret = expectedSecrets[clientId]; const isValidSecret = client_secret && client_secret === expectedSecret; Logger.logEvent("pkce_client_secret_validation", { request_id: requestId, client_id: clientId, client_secret_provided: client_secret || "none", expected_secret_for_client: expectedSecret || "none", has_expected_secret: !!expectedSecret, secret_validation_result: client_secret ? isValidSecret : "not_applicable_public_client", client_recognized: !!expectedSecret, debug_validation_details: { provided_secret_length: client_secret?.length || 0, expected_secret_length: expectedSecret?.length || 0, secrets_match: isValidSecret, client_type: client_secret ? "confidential" : "public", }, }); // For confidential clients, validate the secret if (client_secret && !isValidSecret) { const validationError = { timestamp: new Date().toISOString(), event: "pkce_client_authentication_failed", request_id: requestId, error: "invalid_client", client_id: clientId, provided_secret: client_secret, expected_secret: expectedSecret, reason: !expectedSecret ? "unknown_client" : "invalid_secret", debug_hints: [ "PKCE confidential clients must provide correct client secret", "Check client ID and secret configuration", expectedSecret ? `Expected: ${expectedSecret}` : "Client ID not recognized", "This is PKCE + confidential client mode - both PKCE AND secret validation required", ], }; console.log(JSON.stringify(validationError)); return { error: "invalid_client" }; } const tokenRequest = { timestamp: new Date().toISOString(), event: "pkce_token_exchange_request_started", request_id: requestId, body_type: typeof body, body_keys: Object.keys(body || {}), has_code_verifier: !!body.code_verifier, code_verifier_length: body.code_verifier?.length || 0, request_size_bytes: JSON.stringify(body || {}).length, client_auth_source: authSource, client_type: client_secret ? "confidential" : "public", full_pkce_request_debug: { raw_body: body, raw_headers: headers, decoded_basic_auth: decodedBasicAuth, final_client_secret: client_secret, complete_code_verifier: body.code_verifier, // Full verifier for debugging }, debug_body_sample: { grant_type: body.grant_type, client_id: body.client_id, has_client_secret: !!client_secret, redirect_uri: body.redirect_uri, code_prefix: body.code?.substring(0, 8) + "..." || "missing", verifier_prefix: body.code_verifier?.substring(0, 8) + "..." || "missing", }, }; console.log(JSON.stringify(tokenRequest)); const { grant_type, code, redirect_uri, client_id, code_verifier } = body; // Basic validations (same as regular flow) if (grant_type !== "authorization_code") { const grantTypeError = { timestamp: new Date().toISOString(), event: "pkce_token_grant_type_validation_failed", error: "unsupported_grant_type", expected: "authorization_code", received: grant_type, }; console.log(JSON.stringify(grantTypeError)); return { error: "unsupported_grant_type" }; } if (!code) { const codeError = { timestamp: new Date().toISOString(), event: "pkce_token_missing_auth_code", error: "invalid_request", }; console.log(JSON.stringify(codeError)); return { error: "invalid_request" }; } if (!redirect_uri) { const uriError = { timestamp: new Date().toISOString(), event: "pkce_token_missing_redirect_uri", error: "invalid_request", }; console.log(JSON.stringify(uriError)); return { error: "invalid_request" }; } // RFC 7636 Section 4.5 - code_verifier is REQUIRED for PKCE if (!code_verifier) { const verifierError = { timestamp: new Date().toISOString(), event: "pkce_token_missing_code_verifier", error: "invalid_request", message: "code_verifier is required for PKCE flow", }; console.log(JSON.stringify(verifierError)); return { error: "invalid_request" }; } const authData = this.authCodes[code]; if (!authData) { const authCodeError = { timestamp: new Date().toISOString(), event: "pkce_auth_code_not_found", error: "invalid_grant", requested_code: code.substring(0, 12) + "...", }; console.log(JSON.stringify(authCodeError)); return { error: "invalid_grant" }; } // RFC 7636 Section 4.6 - Server Verifies code_verifier // For S256: BASE64URL(SHA256(code_verifier)) == code_challenge // For plain: code_verifier == code_challenge const verificationStartTime = Date.now(); const pkceVerification = PKCEUtils.verifyPKCECodeVerifier( code_verifier, authData.codeChallenge || "", authData.codeChallengeMethod || "" ); const verificationDurationMs = Date.now() - verificationStartTime; const pkceVerificationLog = { timestamp: new Date().toISOString(), event: "pkce_code_verifier_verification", request_id: requestId, verification_result: pkceVerification, verification_duration_ms: verificationDurationMs, code_challenge_method: authData.codeChallengeMethod, has_stored_challenge: !!authData.codeChallenge, stored_challenge_length: authData.codeChallenge?.length || 0, received_verifier_length: code_verifier.length, auth_code_age_ms: Date.now() - authData.createdAt, debug_verification_data: { stored_challenge_prefix: authData.codeChallenge?.substring(0, 10) + "..." || "none", received_verifier_prefix: code_verifier.substring(0, 10) + "...", challenge_method: authData.codeChallengeMethod, auth_code_created: new Date(authData.createdAt).toISOString(), }, }; console.log(JSON.stringify(pkceVerificationLog)); if (!pkceVerification) { const verificationError = { timestamp: new Date().toISOString(), event: "pkce_code_verifier_verification_failed", request_id: requestId, error: "invalid_grant", message: "code_verifier does not match code_challenge", // Comprehensive debug info for troubleshooting Phoenix integration debug_failure_analysis: { client_id: authData.clientId, user_id: authData.userId, challenge_method_expected: authData.codeChallengeMethod, challenge_method_supported: ["S256", "plain"], verifier_length_expected: authData.codeChallengeMethod === "S256" ? "43-128 chars" : "any", verifier_length_actual: code_verifier.length, verifier_format_valid: /^[A-Za-z0-9._~-]{43,128}$/.test( code_verifier ), challenge_stored: !!authData.codeChallenge, challenge_length: authData.codeChallenge?.length || 0, auth_code_age_seconds: Math.round( (Date.now() - authData.createdAt) / 1000 ), timing_suspicious: Date.now() - authData.createdAt < 100, // Less than 100ms }, troubleshooting_hints: [ code_verifier.length < 43 ? "Code verifier too short (min 43 chars for PKCE)" : null, code_verifier.length > 128 ? "Code verifier too long (max 128 chars)" : null, !authData.codeChallenge ? "No code challenge stored for this auth code" : null, authData.codeChallengeMethod !== "S256" && authData.codeChallengeMethod !== "plain" ? `Unknown challenge method: ${authData.codeChallengeMethod}` : null, Date.now() - authData.createdAt < 100 ? "Request too fast - possible replay attack" : null, ].filter(Boolean), }; console.log(JSON.stringify(verificationError)); return { error: "invalid_grant" }; } // Rest of token generation (same as regular flow) if (authData.redirectUri !== redirect_uri) { const redirectError = { timestamp: new Date().toISOString(), event: "pkce_redirect_uri_mismatch", error: "invalid_grant", }; console.log(JSON.stringify(redirectError)); return { error: "invalid_grant" }; } const users = this.getUsers(); const user = users.find((u: User) => u.id === authData.userId) || users[0]; if (!user) { const userNotFound = { timestamp: new Date().toISOString(), event: "pkce_user_not_found", error: "user_not_found", requested_user_id: authData.userId, }; console.log(JSON.stringify(userNotFound)); return { error: "user_not_found" }; } // Generate tokens using factory (maintains comprehensive logging) const { accessToken, idToken } = await this.tokenFactory.generateTokenPair( user, authData.nonce, authData.scope ); // Clean up auth code delete this.authCodes[code]; const tokenResponse = { access_token: accessToken, id_token: idToken, token_type: "Bearer", expires_in: 3600, scope: "openid email profile", }; const tokenExchangeComplete = { timestamp: new Date().toISOString(), event: "pkce_token_exchange_completed", request_id: requestId, user_email: user.email, user_id: user.id, client_id: authData.clientId, tokens_issued: ["access_token", "id_token"], pkce_verification: "successful", token_response_size_bytes: JSON.stringify(tokenResponse).length, debug_token_info: { access_token_length: accessToken.length, id_token_length: idToken.length, expires_in: tokenResponse.expires_in, scope: tokenResponse.scope, token_type: tokenResponse.token_type, }, debug_flow_metrics: { total_request_duration_ms: Date.now() - parseInt(requestId.split("-")[1]), verification_method_used: authData.codeChallengeMethod, auth_code_lifetime_ms: Date.now() - authData.createdAt, remaining_auth_codes: Object.keys(this.authCodes).length - 1, // -1 because we'll delete this one }, debug_phoenix_integration: { using_pkce_endpoints: true, client_type: client_secret ? "confidential" : "public", client_auth_source: authSource, redirect_uri_validated: true, nonce_included: !!authData.nonce, audience_client_id: client_id, }, }; console.log(JSON.stringify(tokenExchangeComplete)); return { tokens: tokenResponse }; } }

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/Arize-ai/phoenix'

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