diff --git a/packages/core/src/agents/auth-provider/base-provider.ts b/packages/core/src/agents/auth-provider/base-provider.ts index 7b21853a09..f4489a084a 100644 --- a/packages/core/src/agents/auth-provider/base-provider.ts +++ b/packages/core/src/agents/auth-provider/base-provider.ts @@ -9,17 +9,33 @@ import type { A2AAuthProvider, A2AAuthProviderType } from './types.js'; /** * Abstract base class for A2A authentication providers. + * Provides default implementations for optional methods. */ export abstract class BaseA2AAuthProvider implements A2AAuthProvider { + /** + * The type of authentication provider. + */ abstract readonly type: A2AAuthProviderType; + + /** + * Get the HTTP headers to include in requests. + * Subclasses must implement this method. + */ abstract headers(): Promise; private static readonly MAX_AUTH_RETRIES = 2; private authRetryCount = 0; /** - * Default: retry on 401/403 with fresh headers. - * Subclasses with cached tokens must override to force-refresh to avoid infinite retries. + * Check if a request should be retried with new headers. + * + * The default implementation checks for 401/403 status codes and + * returns fresh headers for retry. Subclasses can override for + * custom retry logic. + * + * @param _req The original request init + * @param res The response from the server + * @returns New headers for retry, or undefined if no retry should be made */ async shouldRetryWithHeaders( _req: RequestInit, @@ -37,5 +53,17 @@ export abstract class BaseA2AAuthProvider implements A2AAuthProvider { return undefined; } - async initialize(): Promise {} + /** + * Initialize the provider. Override in subclasses that need async setup. + */ + async initialize(): Promise { + // Default: no-op + } + + /** + * Clean up resources. Override in subclasses that need cleanup. + */ + async dispose(): Promise { + // Default: no-op + } } diff --git a/packages/core/src/agents/auth-provider/factory.ts b/packages/core/src/agents/auth-provider/factory.ts index b79c8b4f77..f1d773d171 100644 --- a/packages/core/src/agents/auth-provider/factory.ts +++ b/packages/core/src/agents/auth-provider/factory.ts @@ -11,10 +11,23 @@ import type { AuthValidationResult, } from './types.js'; +/** + * Options for creating an auth provider. + */ export interface CreateAuthProviderOptions { - /** Required for OAuth/OIDC token storage. */ - agentName?: string; + /** + * Name of the agent (for error messages and token storage). + */ + agentName: string; + + /** + * Auth configuration from the agent definition frontmatter. + */ authConfig?: A2AAuthConfig; + + /** + * The fetched AgentCard with securitySchemes. + */ agentCard?: AgentCard; } @@ -23,33 +36,50 @@ export interface CreateAuthProviderOptions { * @see https://a2a-protocol.org/latest/specification/#451-securityscheme */ export class A2AAuthProviderFactory { + /** + * Create an auth provider from configuration. + * + * @param options Creation options including agent name and config + * @returns The created auth provider, or undefined if no auth is needed + */ static async create( options: CreateAuthProviderOptions, ): Promise { const { agentName: _agentName, authConfig, agentCard } = options; + // If no auth config, check if the AgentCard requires auth if (!authConfig) { if ( agentCard?.securitySchemes && Object.keys(agentCard.securitySchemes).length > 0 ) { - return undefined; // Caller should prompt user to configure auth + // AgentCard requires auth but none configured + // The caller should handle this case by prompting the user + return undefined; } return undefined; } + // Create provider based on config type + // Providers are lazy-loaded to support incremental implementation switch (authConfig.type) { case 'google-credentials': // TODO: Implement throw new Error('google-credentials auth provider not yet implemented'); - case 'apiKey': - // TODO: Implement - throw new Error('apiKey auth provider not yet implemented'); + case 'apiKey': { + const { ApiKeyAuthProvider } = await import('./api-key-provider.js'); + const provider = new ApiKeyAuthProvider(authConfig); + await provider.initialize(); + return provider; + } - case 'http': - // TODO: Implement - throw new Error('http auth provider not yet implemented'); + case 'http': { + const { HttpAuthProvider } = await import('./http-auth-provider.js'); + const provider = new HttpAuthProvider(authConfig); + await provider.initialize(); + return provider; + } case 'oauth2': // TODO: Implement @@ -60,6 +90,7 @@ export class A2AAuthProviderFactory { throw new Error('openIdConnect auth provider not yet implemented'); default: { + // TypeScript exhaustiveness check const _exhaustive: never = authConfig; throw new Error( `Unknown auth type: ${(_exhaustive as A2AAuthConfig).type}`, @@ -68,33 +99,51 @@ export class A2AAuthProviderFactory { } } - /** Create provider directly from config, bypassing AgentCard validation. */ + /** + * Create an auth provider directly from a config (for AgentCard fetching). + * This bypasses AgentCard-based validation since we need auth to fetch the card. + * + * @param agentName Name of the agent + * @param authConfig Auth configuration + * @returns The created auth provider + */ static async createFromConfig( + agentName: string, authConfig: A2AAuthConfig, - agentName?: string, ): Promise { const provider = await A2AAuthProviderFactory.create({ - authConfig, agentName, + authConfig, }); - // create() returns undefined only when authConfig is missing. - // Since authConfig is required here, provider will always be defined - // (or create() throws for unimplemented types). - return provider!; + if (!provider) { + throw new Error( + `Failed to create auth provider for config type: ${authConfig.type}`, + ); + } + + return provider; } - /** Validate auth config against AgentCard's security requirements. */ + /** + * Validate that the auth configuration satisfies the AgentCard's security requirements. + * + * @param authConfig The configured auth from agent-definition + * @param securitySchemes The security schemes declared in the AgentCard + * @returns Validation result with diff if invalid + */ static validateAuthConfig( authConfig: A2AAuthConfig | undefined, securitySchemes: Record | undefined, ): AuthValidationResult { + // If no security schemes required, any config is valid if (!securitySchemes || Object.keys(securitySchemes).length === 0) { return { valid: true }; } const requiredSchemes = Object.keys(securitySchemes); + // If auth is required but none configured if (!authConfig) { return { valid: false, @@ -106,6 +155,7 @@ export class A2AAuthProviderFactory { }; } + // Check if the configured type matches any of the required schemes const matchResult = A2AAuthProviderFactory.findMatchingScheme( authConfig, securitySchemes, @@ -145,6 +195,7 @@ export class A2AAuthProviderFactory { case 'http': if (authConfig.type === 'http') { + // Check if the scheme matches (Bearer, Basic, etc.) if ( authConfig.scheme.toLowerCase() === scheme.scheme.toLowerCase() ) { @@ -157,6 +208,7 @@ export class A2AAuthProviderFactory { authConfig.type === 'google-credentials' && scheme.scheme.toLowerCase() === 'bearer' ) { + // Google credentials can provide Bearer tokens return { matched: true, missingConfig: [] }; } else { missingConfig.push( @@ -178,6 +230,13 @@ export class A2AAuthProviderFactory { if (authConfig.type === 'openIdConnect') { return { matched: true, missingConfig: [] }; } + // Google credentials with target_audience can work as OIDC + if ( + authConfig.type === 'google-credentials' && + authConfig.target_audience + ) { + return { matched: true, missingConfig: [] }; + } missingConfig.push( `Scheme '${schemeName}' requires OpenID Connect authentication`, ); @@ -201,7 +260,9 @@ export class A2AAuthProviderFactory { return { matched: false, missingConfig }; } - /** Get human-readable description of required auth for error messages. */ + /** + * Get a human-readable description of required auth for an AgentCard. + */ static describeRequiredAuth( securitySchemes: Record, ): string { @@ -228,7 +289,6 @@ export class A2AAuthProviderFactory { break; default: { const _exhaustive: never = scheme; - // This ensures TypeScript errors if a new SecurityScheme type is added descriptions.push( `Unknown (${name}): ${(_exhaustive as SecurityScheme).type}`, ); diff --git a/packages/core/src/agents/auth-provider/index.ts b/packages/core/src/agents/auth-provider/index.ts new file mode 100644 index 0000000000..c6249b65ec --- /dev/null +++ b/packages/core/src/agents/auth-provider/index.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Types +export type { + A2AAuthProvider, + A2AAuthProviderType, + A2AAuthConfig, + GoogleCredentialsAuthConfig, + ApiKeyAuthConfig, + HttpAuthConfig, + OAuth2AuthConfig, + OpenIdConnectAuthConfig, + BaseAuthConfig, + AuthConfigDiff, + AuthValidationResult, + AuthenticationHandler, + HttpHeaders, +} from './types.js'; + +// Base class +export { BaseA2AAuthProvider } from './base-provider.js'; + +// Factory +export { + A2AAuthProviderFactory, + type CreateAuthProviderOptions, +} from './factory.js'; + +// Note: Individual providers are lazy-loaded by the factory. +// They will be exported as they are implemented in subsequent PRs. diff --git a/packages/core/src/agents/auth-provider/types.ts b/packages/core/src/agents/auth-provider/types.ts index 67fce94ca8..10e84be652 100644 --- a/packages/core/src/agents/auth-provider/types.ts +++ b/packages/core/src/agents/auth-provider/types.ts @@ -4,14 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { AuthenticationHandler, HttpHeaders } from '@a2a-js/sdk/client'; + /** - * Client-side auth configuration for A2A remote agents. - * Corresponds to server-side SecurityScheme types from @a2a-js/sdk. - * @see https://a2a-protocol.org/latest/specification/#451-securityscheme + * Authentication provider types supported for A2A remote agents. + * These align with the SecurityScheme types from the A2A specification. */ - -import type { AuthenticationHandler } from '@a2a-js/sdk/client'; - export type A2AAuthProviderType = | 'google-credentials' | 'apiKey' @@ -19,68 +17,213 @@ export type A2AAuthProviderType = | 'oauth2' | 'openIdConnect'; +/** + * Extended authentication handler interface for A2A remote agents. + * Extends the base AuthenticationHandler from the A2A SDK with + * lifecycle management methods. + */ export interface A2AAuthProvider extends AuthenticationHandler { + /** + * The type of authentication provider. + */ readonly type: A2AAuthProviderType; + + /** + * Initialize the provider. Called before first use. + * For OAuth/OIDC, this may trigger discovery or browser-based auth. + */ initialize?(): Promise; + + /** + * Clean up any resources held by the provider. + */ + dispose?(): Promise; } +// ============================================================================ +// Base configuration interface +// ============================================================================ + +/** + * Base configuration shared by all auth types. + */ export interface BaseAuthConfig { + /** + * If true, use this auth configuration to fetch the AgentCard. + * Required when the AgentCard endpoint itself requires authentication. + */ agent_card_requires_auth?: boolean; } -/** Client config for google-credentials (not in A2A spec, Gemini-specific). */ +// ============================================================================ +// Google Credentials configuration +// ============================================================================ + +/** + * Configuration for Google Application Default Credentials (ADC). + */ export interface GoogleCredentialsAuthConfig extends BaseAuthConfig { type: 'google-credentials'; + + /** + * OAuth scopes to request. Required for access tokens. + * @example ['https://www.googleapis.com/auth/cloud-platform'] + */ scopes?: string[]; + + /** + * Target audience for ID token requests. + * When specified, an ID token is requested instead of an access token. + * Typically the URL of the Cloud Run service or other GCP resource. + * @example 'https://my-agent.run.app' + */ + target_audience?: string; } -/** Client config corresponding to APIKeySecurityScheme. */ +// ============================================================================ +// API Key configuration +// ============================================================================ + +/** + * Configuration for API Key authentication. + * The API key can be sent in a header, query parameter, or cookie. + */ export interface ApiKeyAuthConfig extends BaseAuthConfig { type: 'apiKey'; - /** The secret. Supports $ENV_VAR, !command, or literal. */ + + /** + * The API key value. Supports: + * - `$ENV_VAR`: Read from environment variable + * - `!command`: Execute shell command and use output + * - Literal string value + */ key: string; - /** Defaults to server's SecurityScheme.in value. */ - location?: 'header' | 'query' | 'cookie'; - /** Defaults to server's SecurityScheme.name value. */ + + /** + * Where to include the API key in requests. + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + + /** + * The name of the header, query parameter, or cookie. + * @default 'X-API-Key' for header, 'api_key' for query/cookie + */ name?: string; } -/** Client config corresponding to HTTPAuthSecurityScheme. */ -export type HttpAuthConfig = BaseAuthConfig & { - type: 'http'; -} & ( - | { - scheme: 'Bearer'; - /** For Bearer. Supports $ENV_VAR, !command, or literal. */ - token: string; - } - | { - scheme: 'Basic'; - /** For Basic. Supports $ENV_VAR, !command, or literal. */ - username: string; - /** For Basic. Supports $ENV_VAR, !command, or literal. */ - password: string; - } - ); +// ============================================================================ +// HTTP Auth configuration +// ============================================================================ -/** Client config corresponding to OAuth2SecurityScheme. */ +/** + * Configuration for HTTP authentication (Bearer or Basic). + */ +export interface HttpAuthConfig extends BaseAuthConfig { + type: 'http'; + + /** + * The HTTP authentication scheme. + */ + scheme: 'Bearer' | 'Basic'; + + /** + * The token for Bearer authentication. Supports: + * - `$ENV_VAR`: Read from environment variable + * - `!command`: Execute shell command and use output + * - Literal string value + */ + token?: string; + + /** + * Username for Basic authentication. Supports $ENV_VAR and !command. + */ + username?: string; + + /** + * Password for Basic authentication. Supports $ENV_VAR and !command. + */ + password?: string; +} + +// ============================================================================ +// OAuth 2.0 configuration +// ============================================================================ + +/** + * Configuration for OAuth 2.0 authentication. + * Endpoints can be discovered from the AgentCard's securitySchemes. + */ export interface OAuth2AuthConfig extends BaseAuthConfig { type: 'oauth2'; + + /** + * Client ID for OAuth. Supports $ENV_VAR and !command. + */ client_id?: string; + + /** + * Client secret for OAuth. Supports $ENV_VAR and !command. + * May be omitted for public clients using PKCE. + */ client_secret?: string; + + /** + * OAuth scopes to request. + */ scopes?: string[]; } -/** Client config corresponding to OpenIdConnectSecurityScheme. */ +// ============================================================================ +// OpenID Connect configuration +// ============================================================================ + +/** + * Configuration for OpenID Connect authentication. + * This is a generic OIDC provider that works with any compliant issuer + * (Auth0, Okta, Keycloak, Google, etc.). + */ export interface OpenIdConnectAuthConfig extends BaseAuthConfig { type: 'openIdConnect'; + + /** + * The OIDC issuer URL for discovery. + * Used to fetch the .well-known/openid-configuration. + * @example 'https://auth.example.com' + */ issuer_url: string; + + /** + * Client ID for OIDC. Supports $ENV_VAR and !command. + */ client_id: string; + + /** + * Client secret for OIDC. Supports $ENV_VAR and !command. + * May be omitted for public clients. + */ client_secret?: string; + + /** + * Target audience for ID token requests. + * @example 'https://protected-agent.example.com' + */ target_audience?: string; + + /** + * OAuth scopes to request. + * @default ['openid'] + */ scopes?: string[]; } +// ============================================================================ +// Union type for all auth configs +// ============================================================================ + +/** + * Union type of all supported A2A authentication configurations. + */ export type A2AAuthConfig = | GoogleCredentialsAuthConfig | ApiKeyAuthConfig @@ -88,13 +231,47 @@ export type A2AAuthConfig = | OAuth2AuthConfig | OpenIdConnectAuthConfig; +// ============================================================================ +// Auth validation types +// ============================================================================ + +/** + * Describes a mismatch between configured auth and AgentCard requirements. + */ export interface AuthConfigDiff { + /** + * Security scheme names required by the AgentCard. + */ requiredSchemes: string[]; + + /** + * The auth type configured in the agent definition, if any. + */ configuredType?: A2AAuthProviderType; + + /** + * Description of what's missing to satisfy the requirements. + */ missingConfig: string[]; } +/** + * Result of validating auth configuration against AgentCard requirements. + */ export interface AuthValidationResult { + /** + * Whether the configuration is valid for the AgentCard's requirements. + */ valid: boolean; + + /** + * Details about the mismatch, if any. + */ diff?: AuthConfigDiff; } + +// ============================================================================ +// Re-export useful types from the SDK +// ============================================================================ + +export type { AuthenticationHandler, HttpHeaders }; diff --git a/packages/core/src/agents/types.ts b/packages/core/src/agents/types.ts index 337a837ea7..1865d444ad 100644 --- a/packages/core/src/agents/types.ts +++ b/packages/core/src/agents/types.ts @@ -109,6 +109,7 @@ export interface RemoteAgentDefinition< > extends BaseAgentDefinition { kind: 'remote'; agentCardUrl: string; + /** * Optional authentication configuration for the remote agent. * If not specified, the agent will try to use defaults based on the AgentCard's