test(agents): add unit tests for auth provider infrastructure

This commit is contained in:
Adam Weidman
2026-01-29 14:21:21 -05:00
parent a58eff8c91
commit 54ab0c39b4
6 changed files with 646 additions and 380 deletions

View File

@@ -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<HttpHeaders> {
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();
});
});
});

View File

@@ -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<HttpHeaders>;
/**
* 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<HttpHeaders | undefined> {
// 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<void> {
// Default: no-op
}
/**
* Clean up resources. Override in subclasses that need cleanup.
*/
async dispose(): Promise<void> {
// Default: no-op
}
async initialize(): Promise<void> {}
}

View File

@@ -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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
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<string, SecurityScheme> = {
basicAuth: {
type: 'http',
scheme: 'Basic',
},
};
const result =
A2AAuthProviderFactory.describeRequiredAuth(securitySchemes);
expect(result).toBe('HTTP Basic (basicAuth)');
});
it('should describe oauth2 scheme', () => {
const securitySchemes: Record<string, SecurityScheme> = {
oauth2Auth: {
type: 'oauth2',
flows: {},
},
};
const result =
A2AAuthProviderFactory.describeRequiredAuth(securitySchemes);
expect(result).toBe('OAuth 2.0 (oauth2Auth)');
});
it('should describe openIdConnect scheme', () => {
const securitySchemes: Record<string, SecurityScheme> = {
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<string, SecurityScheme> = {
mtlsAuth: {
type: 'mutualTLS',
},
};
const result =
A2AAuthProviderFactory.describeRequiredAuth(securitySchemes);
expect(result).toBe('Mutual TLS (mtlsAuth)');
});
it('should join multiple schemes with OR', () => {
const securitySchemes: Record<string, SecurityScheme> = {
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();
});
});
});

View File

@@ -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<A2AAuthProvider | undefined> {
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<A2AAuthProvider> {
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<string, SecurityScheme> | 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<string, SecurityScheme>,
@@ -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, SecurityScheme>,
): 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}`,
);
}
}
}

View File

@@ -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.

View File

@@ -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<void>;
/**
* Clean up any resources held by the provider.
*/
dispose?(): Promise<void>;
}
// ============================================================================
// 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 };