diff --git a/packages/core/src/agents/auth-provider/base-provider.test.ts b/packages/core/src/agents/auth-provider/base-provider.test.ts new file mode 100644 index 0000000000..47e9339785 --- /dev/null +++ b/packages/core/src/agents/auth-provider/base-provider.test.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import type { HttpHeaders } from '@a2a-js/sdk/client'; +import { BaseA2AAuthProvider } from './base-provider.js'; +import type { A2AAuthProviderType } from './types.js'; + +/** + * Concrete implementation of BaseA2AAuthProvider for testing. + */ +class TestAuthProvider extends BaseA2AAuthProvider { + readonly type: A2AAuthProviderType = 'apiKey'; + private testHeaders: HttpHeaders; + + constructor(headers: HttpHeaders = { Authorization: 'test-token' }) { + super(); + this.testHeaders = headers; + } + + async headers(): Promise { + return this.testHeaders; + } + + setHeaders(headers: HttpHeaders): void { + this.testHeaders = headers; + } +} + +describe('BaseA2AAuthProvider', () => { + describe('shouldRetryWithHeaders', () => { + it('should return headers for 401 response', async () => { + const provider = new TestAuthProvider({ Authorization: 'Bearer token' }); + const response = new Response(null, { status: 401 }); + + const result = await provider.shouldRetryWithHeaders({}, response); + + expect(result).toEqual({ Authorization: 'Bearer token' }); + }); + + it('should return headers for 403 response', async () => { + const provider = new TestAuthProvider({ Authorization: 'Bearer token' }); + const response = new Response(null, { status: 403 }); + + const result = await provider.shouldRetryWithHeaders({}, response); + + expect(result).toEqual({ Authorization: 'Bearer token' }); + }); + + it('should return undefined for 200 response', async () => { + const provider = new TestAuthProvider(); + const response = new Response(null, { status: 200 }); + + const result = await provider.shouldRetryWithHeaders({}, response); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for 500 response', async () => { + const provider = new TestAuthProvider(); + const response = new Response(null, { status: 500 }); + + const result = await provider.shouldRetryWithHeaders({}, response); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for 404 response', async () => { + const provider = new TestAuthProvider(); + const response = new Response(null, { status: 404 }); + + const result = await provider.shouldRetryWithHeaders({}, response); + + expect(result).toBeUndefined(); + }); + + it('should call headers() to get fresh headers on retry', async () => { + const provider = new TestAuthProvider({ Authorization: 'old-token' }); + const response = new Response(null, { status: 401 }); + + // Change headers before retry + provider.setHeaders({ Authorization: 'new-token' }); + + const result = await provider.shouldRetryWithHeaders({}, response); + + expect(result).toEqual({ Authorization: 'new-token' }); + }); + }); + + describe('initialize', () => { + it('should be a no-op by default', async () => { + const provider = new TestAuthProvider(); + + // Should not throw + await expect(provider.initialize()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/packages/core/src/agents/auth-provider/base-provider.ts b/packages/core/src/agents/auth-provider/base-provider.ts index 36b5e96192..dfbda4878c 100644 --- a/packages/core/src/agents/auth-provider/base-provider.ts +++ b/packages/core/src/agents/auth-provider/base-provider.ts @@ -4,61 +4,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { - A2AAuthProvider, - A2AAuthProviderType, - HttpHeaders, -} from './types.js'; +import type { HttpHeaders } from '@a2a-js/sdk/client'; +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; /** - * 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 + * Default: retry on 401/403 with fresh headers. + * Subclasses with cached tokens must override to force-refresh to avoid infinite retries. */ async shouldRetryWithHeaders( _req: RequestInit, res: Response, ): Promise { - // Retry on authentication errors if (res.status === 401 || res.status === 403) { return this.headers(); } return undefined; } - /** - * 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 - } + async initialize(): Promise {} } diff --git a/packages/core/src/agents/auth-provider/factory.test.ts b/packages/core/src/agents/auth-provider/factory.test.ts new file mode 100644 index 0000000000..1588e4de89 --- /dev/null +++ b/packages/core/src/agents/auth-provider/factory.test.ts @@ -0,0 +1,482 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { A2AAuthProviderFactory } from './factory.js'; +import type { AgentCard, SecurityScheme } from '@a2a-js/sdk'; +import type { A2AAuthConfig } from './types.js'; + +describe('A2AAuthProviderFactory', () => { + describe('validateAuthConfig', () => { + describe('when no security schemes required', () => { + it('should return valid when securitySchemes is undefined', () => { + const result = A2AAuthProviderFactory.validateAuthConfig( + undefined, + undefined, + ); + expect(result).toEqual({ valid: true }); + }); + + it('should return valid when securitySchemes is empty', () => { + const result = A2AAuthProviderFactory.validateAuthConfig(undefined, {}); + expect(result).toEqual({ valid: true }); + }); + + it('should return valid when auth config provided but not required', () => { + const authConfig: A2AAuthConfig = { + type: 'apiKey', + key: 'test-key', + }; + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + {}, + ); + expect(result).toEqual({ valid: true }); + }); + }); + + describe('when auth is required but not configured', () => { + it('should return invalid with diff', () => { + const securitySchemes: Record = { + apiKeyAuth: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + undefined, + securitySchemes, + ); + + expect(result.valid).toBe(false); + expect(result.diff).toBeDefined(); + expect(result.diff?.requiredSchemes).toContain('apiKeyAuth'); + expect(result.diff?.configuredType).toBeUndefined(); + expect(result.diff?.missingConfig).toContain( + 'Authentication is required but not configured', + ); + }); + }); + + describe('apiKey scheme matching', () => { + it('should match apiKey config with apiKey scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'apiKey', + key: 'my-key', + }; + const securitySchemes: Record = { + apiKeyAuth: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result).toEqual({ valid: true }); + }); + + it('should not match http config with apiKey scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'http', + scheme: 'Bearer', + token: 'my-token', + }; + const securitySchemes: Record = { + apiKeyAuth: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result.valid).toBe(false); + expect(result.diff?.missingConfig).toContain( + "Scheme 'apiKeyAuth' requires apiKey authentication", + ); + }); + }); + + describe('http scheme matching', () => { + it('should match http Bearer config with http Bearer scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'http', + scheme: 'Bearer', + token: 'my-token', + }; + const securitySchemes: Record = { + bearerAuth: { + type: 'http', + scheme: 'Bearer', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result).toEqual({ valid: true }); + }); + + it('should match http Basic config with http Basic scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'http', + scheme: 'Basic', + username: 'user', + password: 'pass', + }; + const securitySchemes: Record = { + basicAuth: { + type: 'http', + scheme: 'Basic', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result).toEqual({ valid: true }); + }); + + it('should not match http Basic config with http Bearer scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'http', + scheme: 'Basic', + username: 'user', + password: 'pass', + }; + const securitySchemes: Record = { + bearerAuth: { + type: 'http', + scheme: 'Bearer', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result.valid).toBe(false); + expect(result.diff?.missingConfig).toContain( + "Scheme 'bearerAuth' requires HTTP Bearer authentication, but Basic was configured", + ); + }); + + it('should match google-credentials with http Bearer scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'google-credentials', + }; + const securitySchemes: Record = { + bearerAuth: { + type: 'http', + scheme: 'Bearer', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result).toEqual({ valid: true }); + }); + }); + + describe('oauth2 scheme matching', () => { + it('should match oauth2 config with oauth2 scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'oauth2', + }; + const securitySchemes: Record = { + oauth2Auth: { + type: 'oauth2', + flows: {}, + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result).toEqual({ valid: true }); + }); + + it('should not match apiKey config with oauth2 scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'apiKey', + key: 'my-key', + }; + const securitySchemes: Record = { + oauth2Auth: { + type: 'oauth2', + flows: {}, + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result.valid).toBe(false); + expect(result.diff?.missingConfig).toContain( + "Scheme 'oauth2Auth' requires OAuth 2.0 authentication", + ); + }); + }); + + describe('openIdConnect scheme matching', () => { + it('should match openIdConnect config with openIdConnect scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'openIdConnect', + issuer_url: 'https://auth.example.com', + client_id: 'client-id', + }; + const securitySchemes: Record = { + oidcAuth: { + type: 'openIdConnect', + openIdConnectUrl: + 'https://auth.example.com/.well-known/openid-configuration', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result).toEqual({ valid: true }); + }); + + it('should not match google-credentials for openIdConnect scheme', () => { + const authConfig: A2AAuthConfig = { + type: 'google-credentials', + }; + const securitySchemes: Record = { + oidcAuth: { + type: 'openIdConnect', + openIdConnectUrl: + 'https://auth.example.com/.well-known/openid-configuration', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result.valid).toBe(false); + expect(result.diff?.missingConfig).toContain( + "Scheme 'oidcAuth' requires OpenID Connect authentication", + ); + }); + }); + + describe('mutualTLS scheme', () => { + it('should always fail for mutualTLS (not supported)', () => { + const authConfig: A2AAuthConfig = { + type: 'apiKey', + key: 'test', + }; + const securitySchemes: Record = { + mtlsAuth: { + type: 'mutualTLS', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result.valid).toBe(false); + expect(result.diff?.missingConfig).toContain( + "Scheme 'mtlsAuth' requires mTLS authentication (not yet supported)", + ); + }); + }); + + describe('multiple security schemes', () => { + it('should match if any scheme matches', () => { + const authConfig: A2AAuthConfig = { + type: 'http', + scheme: 'Bearer', + token: 'my-token', + }; + const securitySchemes: Record = { + apiKeyAuth: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + }, + bearerAuth: { + type: 'http', + scheme: 'Bearer', + }, + }; + + const result = A2AAuthProviderFactory.validateAuthConfig( + authConfig, + securitySchemes, + ); + + expect(result).toEqual({ valid: true }); + }); + }); + }); + + describe('describeRequiredAuth', () => { + it('should describe apiKey scheme', () => { + const securitySchemes: Record = { + apiKeyAuth: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + }, + }; + + const result = + A2AAuthProviderFactory.describeRequiredAuth(securitySchemes); + + expect(result).toBe('API Key (apiKeyAuth): Send X-API-Key in header'); + }); + + it('should describe http Bearer scheme', () => { + const securitySchemes: Record = { + bearerAuth: { + type: 'http', + scheme: 'Bearer', + }, + }; + + const result = + A2AAuthProviderFactory.describeRequiredAuth(securitySchemes); + + expect(result).toBe('HTTP Bearer (bearerAuth)'); + }); + + it('should describe http Basic scheme', () => { + const securitySchemes: Record = { + basicAuth: { + type: 'http', + scheme: 'Basic', + }, + }; + + const result = + A2AAuthProviderFactory.describeRequiredAuth(securitySchemes); + + expect(result).toBe('HTTP Basic (basicAuth)'); + }); + + it('should describe oauth2 scheme', () => { + const securitySchemes: Record = { + oauth2Auth: { + type: 'oauth2', + flows: {}, + }, + }; + + const result = + A2AAuthProviderFactory.describeRequiredAuth(securitySchemes); + + expect(result).toBe('OAuth 2.0 (oauth2Auth)'); + }); + + it('should describe openIdConnect scheme', () => { + const securitySchemes: Record = { + oidcAuth: { + type: 'openIdConnect', + openIdConnectUrl: + 'https://auth.example.com/.well-known/openid-configuration', + }, + }; + + const result = + A2AAuthProviderFactory.describeRequiredAuth(securitySchemes); + + expect(result).toBe('OpenID Connect (oidcAuth)'); + }); + + it('should describe mutualTLS scheme', () => { + const securitySchemes: Record = { + mtlsAuth: { + type: 'mutualTLS', + }, + }; + + const result = + A2AAuthProviderFactory.describeRequiredAuth(securitySchemes); + + expect(result).toBe('Mutual TLS (mtlsAuth)'); + }); + + it('should join multiple schemes with OR', () => { + const securitySchemes: Record = { + apiKeyAuth: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + }, + bearerAuth: { + type: 'http', + scheme: 'Bearer', + }, + }; + + const result = + A2AAuthProviderFactory.describeRequiredAuth(securitySchemes); + + expect(result).toBe( + 'API Key (apiKeyAuth): Send X-API-Key in header OR HTTP Bearer (bearerAuth)', + ); + }); + }); + + describe('create', () => { + it('should return undefined when no auth config and no security schemes', async () => { + const result = await A2AAuthProviderFactory.create({ + agentName: 'test-agent', + }); + + expect(result).toBeUndefined(); + }); + + it('should return undefined when no auth config but AgentCard has security schemes', async () => { + const result = await A2AAuthProviderFactory.create({ + agentName: 'test-agent', + agentCard: { + securitySchemes: { + apiKeyAuth: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + }, + }, + } as unknown as AgentCard, + }); + + // Returns undefined - caller should prompt user to configure auth + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/packages/core/src/agents/auth-provider/factory.ts b/packages/core/src/agents/auth-provider/factory.ts index 26f257705e..39fa0ebd22 100644 --- a/packages/core/src/agents/auth-provider/factory.ts +++ b/packages/core/src/agents/auth-provider/factory.ts @@ -11,96 +11,55 @@ import type { AuthValidationResult, } from './types.js'; -/** - * Options for creating an auth provider. - */ export interface CreateAuthProviderOptions { - /** - * Name of the agent (for error messages and token storage). - */ - agentName: string; - - /** - * Auth configuration from the agent definition frontmatter. - */ + /** Required for OAuth/OIDC token storage. */ + agentName?: string; authConfig?: A2AAuthConfig; - - /** - * The fetched AgentCard with securitySchemes. - */ agentCard?: AgentCard; } /** * Factory for creating A2A authentication providers. + * @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, authConfig, agentCard } = options; + 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 ) { - // AgentCard requires auth but none configured - // The caller should handle this case by prompting the user - return undefined; + return undefined; // Caller should prompt user to configure auth } return undefined; } - // Create provider based on config type - // Providers are lazy-loaded to support incremental implementation switch (authConfig.type) { - case 'google-credentials': { - const { GoogleAdcAuthProvider } = await import( - './google-adc-provider.js' - ); - const provider = new GoogleAdcAuthProvider(agentName, authConfig); - await provider.initialize(); - return provider; - } + case 'google-credentials': + // TODO: Implement + throw new Error('google-credentials 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 'apiKey': + // TODO: Implement + throw new Error('apiKey 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 'http': + // TODO: Implement + throw new Error('http auth provider not yet implemented'); - case 'oauth2': { - const { A2AOAuthProvider } = await import('./oauth-provider.js'); - const provider = new A2AOAuthProvider(agentName, authConfig, agentCard); - await provider.initialize(); - return provider; - } + case 'oauth2': + // TODO: Implement + throw new Error('oauth2 auth provider not yet implemented'); - case 'openIdConnect': { - const { OidcAuthProvider } = await import('./oidc-provider.js'); - const provider = new OidcAuthProvider(agentName, authConfig); - await provider.initialize(); - return provider; - } + case 'openIdConnect': + // TODO: Implement + 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}`, @@ -109,51 +68,33 @@ export class A2AAuthProviderFactory { } } - /** - * 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 - */ + /** Create provider directly from config, bypassing AgentCard validation. */ static async createFromConfig( - agentName: string, authConfig: A2AAuthConfig, + agentName?: string, ): Promise { const provider = await A2AAuthProviderFactory.create({ - agentName, authConfig, + agentName, }); - if (!provider) { - throw new Error( - `Failed to create auth provider for config type: ${authConfig.type}`, - ); - } - - return provider; + // 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!; } - /** - * 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 - */ + /** Validate auth config against AgentCard's security requirements. */ 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, @@ -165,7 +106,6 @@ export class A2AAuthProviderFactory { }; } - // Check if the configured type matches any of the required schemes const matchResult = A2AAuthProviderFactory.findMatchingScheme( authConfig, securitySchemes, @@ -185,9 +125,6 @@ export class A2AAuthProviderFactory { }; } - /** - * Find a matching security scheme for the given auth config. - */ private static findMatchingScheme( authConfig: A2AAuthConfig, securitySchemes: Record, @@ -207,7 +144,6 @@ export class A2AAuthProviderFactory { case 'http': if (authConfig.type === 'http') { - // Check if the scheme matches (Bearer, Basic, etc.) if ( authConfig.scheme.toLowerCase() === scheme.scheme.toLowerCase() ) { @@ -220,7 +156,6 @@ export class A2AAuthProviderFactory { authConfig.type === 'google-credentials' && scheme.scheme.toLowerCase() === 'bearer' ) { - // Google credentials can provide Bearer tokens return { matched: true, missingConfig: [] }; } else { missingConfig.push( @@ -242,13 +177,6 @@ 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`, ); @@ -260,19 +188,19 @@ export class A2AAuthProviderFactory { ); break; - default: + default: { + const _exhaustive: never = scheme; missingConfig.push( - `Unknown security scheme type: ${(scheme as SecurityScheme).type}`, + `Unknown security scheme type: ${(_exhaustive as SecurityScheme).type}`, ); + } } } return { matched: false, missingConfig }; } - /** - * Get a human-readable description of required auth for an AgentCard. - */ + /** Get human-readable description of required auth for error messages. */ static describeRequiredAuth( securitySchemes: Record, ): string { @@ -297,9 +225,13 @@ export class A2AAuthProviderFactory { case 'mutualTLS': descriptions.push(`Mutual TLS (${name})`); break; - default: - descriptions.push(`Unknown (${name}: ${scheme.type})`); - 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 deleted file mode 100644 index c6249b65ec..0000000000 --- a/packages/core/src/agents/auth-provider/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @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 2c03a4a4bc..f6bdb66e8d 100644 --- a/packages/core/src/agents/auth-provider/types.ts +++ b/packages/core/src/agents/auth-provider/types.ts @@ -4,12 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { AuthenticationHandler, HttpHeaders } from '@a2a-js/sdk/client'; - /** - * Authentication provider types supported for A2A remote agents. - * These align with the SecurityScheme types from the A2A specification. + * 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 */ + +import type { AuthenticationHandler } from '@a2a-js/sdk/client'; + export type A2AAuthProviderType = | 'google-credentials' | 'apiKey' @@ -17,213 +19,62 @@ 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; } -// ============================================================================ -// Google Credentials configuration -// ============================================================================ - -/** - * Configuration for Google Application Default Credentials (ADC). - */ +/** Client config for google-credentials (not in A2A spec, Gemini-specific). */ 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; } -// ============================================================================ -// API Key configuration -// ============================================================================ - -/** - * Configuration for API Key authentication. - * The API key can be sent in a header, query parameter, or cookie. - */ +/** Client config corresponding to APIKeySecurityScheme. */ export interface ApiKeyAuthConfig extends BaseAuthConfig { type: 'apiKey'; - - /** - * The API key value. Supports: - * - `$ENV_VAR`: Read from environment variable - * - `!command`: Execute shell command and use output - * - Literal string value - */ + /** The secret. Supports $ENV_VAR, !command, or literal. */ key: string; - - /** - * 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 - */ + /** Defaults to server's SecurityScheme.in value. */ + location?: 'header' | 'query' | 'cookie'; + /** Defaults to server's SecurityScheme.name value. */ name?: string; } -// ============================================================================ -// HTTP Auth configuration -// ============================================================================ - -/** - * Configuration for HTTP authentication (Bearer or Basic). - */ +/** Client config corresponding to HTTPAuthSecurityScheme. */ 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 - */ + /** For Bearer. Supports $ENV_VAR, !command, or literal. */ token?: string; - - /** - * Username for Basic authentication. Supports $ENV_VAR and !command. - */ + /** For Basic. Supports $ENV_VAR, !command, or literal. */ username?: string; - - /** - * Password for Basic authentication. Supports $ENV_VAR and !command. - */ + /** For Basic. Supports $ENV_VAR, !command, or literal. */ password?: string; } -// ============================================================================ -// OAuth 2.0 configuration -// ============================================================================ - -/** - * Configuration for OAuth 2.0 authentication. - * Endpoints can be discovered from the AgentCard's securitySchemes. - */ +/** Client config corresponding to OAuth2SecurityScheme. */ 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[]; } -// ============================================================================ -// 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.). - */ +/** Client config corresponding to OpenIdConnectSecurityScheme. */ 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 @@ -231,47 +82,13 @@ 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 };