Skip to content

[Feat]: Improve client auth handling #445

Open

Description

@mikeas1

Is your feature request related to a problem? Please describe.

Agents declare their authentication requirements via the security_schemes and security fields in the AgentCard. Skills can also declare required security.

Clients are responsible for understanding how to fulfill these authentication requirements. As of now, this is effectively left to the developer to roll this code themselves. However, in some cases, the SecuritySchemes object contains enough information to programmatically determine how to fulfill the auth requirements. We should provide a base implementation for this that handles common use cases (such as OAuth).

The AuthInterceptor class class is a start, but we're missing pieces for easily using it. AuthInterceptor handles the work of placing the credential in the right HTTP header (if known). It's still up to the developer to set up a CredentialService and pass the AuthInterceptor in the right spots. We can do better.

Describe the solution you'd like

Many auth schemes require, at minimum, some configuration in order to fulfill. For OAuth, if you want to generate access tokens on demand, you need a Client ID and Secret. For API Key authentication, you need the key to be sent. For mTLS, you need a client certificate to use. Once you have this, however, it's generally possible to handle the authentication with common code.

My idea for the interface is essentially:

  1. Load up an object with a bunch of authentication configurations.
  2. Provide this object to the ClientFactory, perhaps via ClientConfig.
  3. When a client is constructed, analyze the SecuritySchemes + security in the AgentCard to determine whether we have sufficient information to meet the authentication requirements for the agent.
  4. If so, compose a credential handling object that wraps the logic needed to fulfill the authentication requirements and provide this to the client.
  5. If not, we could raise a warning that we don't know how to fulfill the authentication for the agent. You can ignore this warning and continue, or treat this as an error and be unable to use the client.
  6. When interacting with the client, authentication is handled as transparently as possible. There are cases where the code using the client needs to provide additional information, such as for OAuth Authorization Code access tokens.

I have not fully investigated how feasible this is or what the pieces I'm missing are. Authentication code is generally pretty complex, so for complicated cases I expect we'll need a lot of thought for creating an appropriate interface.

Here's a rough sketch of how the code could look:

client_credential_manager = ClientCredentialManager()
# Methods for configuring well-known authentication types.
client_credential_manager.add_oauth_client(
    id="google",
    token_url="https://oauth2.googleapis.com/token",
    authorization_url="https://accounts.google.com/o/oauth2/v2/auth",
    client_config=(MY_CLIENT_ID, MY_CLIENT_SECRET),
    use_authorization_code_flow=True,
)
client_credential_manager.add_mtls(
    id="example-com-mtls",
    certificate=MY_CLIENT_CERTIFICATE,
)
client_credential_manager.add_api_key(
    id="cool-api-key",
    host_domain="example.com",
    api_key=MY_API_KEY,
)
# But generalized underpinnings to be extensible.
# Something like AuthScheme.matches(SecurityScheme) and AuthScheme.get_handler(AgentCard, SecurityScheme)
custom_auth_scheme = MyAuthScheme()
client_credential_manager.add_handler(
    id="my-custom-auth",
    scheme=custom_auth_scheme
)

client_config = ClientConfig(
    # ... Normal config stuff,
    credential_manager=client_credential_manager
)
client_factory = ClientFactory(client_config)

And using the client should be as transparent as possible:

some_agent_card = AgentCard(
    url="https://agents.example.com/magic",
    # ...
    security_schemes = {
        "google_oauth": OAuth2SecurityScheme(
            flows=OAuthFlows(
                authorization_code=AuthorizationCodeOAuthFlow(
                    authorization_url="https://accounts.google.com/o/oauth2/v2/auth",
                    # ...
                )
            )
        ),
    },
    security=[{"google_oauth": []}, {}],
)

# Credential manager helps match and fulfill auth schemes.
client = client_factory.create(some_agent_card)
# Yay, it just works!
client.send_message(new_text_message("Hello!"))

Some additional thoughts:

  • We should handle both patterns of statically configured credentials and dynamically sourced credentials. Users may want to use request-level context to choose what credentials are provided on a request.
  • However, we should be able to know whether we have any chance at all of fulfilling auth requirements at client construction time.
  • OAuth is a really big use case, so we should build out as much support for this as we can. This should include being able to retrieve access tokens stored in a secret manager or performing an authorization flow on demand.
  • Our underlying abstractions should be pretty flexible -- be able to match an auth scheme against a SecurityScheme, and some kind of handler that alters outgoing requests to attach credentials. This gets a little complicated with our various transports -- JSON-RPC and REST use HTTP headers, gRPC uses Metadata. Something more esoteric might not have an equivalent side-channel, so there might be some interaction with ClientTransport implementations.

Describe alternatives you've considered

We currently have some initial support for ClientCallInterceptors. This was built with auth in mind, but hasn't been integrated as smoothly as we'd like and it isn't clear how it works with all transports. We may use ClientCallInterceptors and ClientCallContext under the hood, but it should be largely transparent to users.

There's some initial auth support in src/a2a/client/auth, but it's not as extensive as what I'm proposing. It's the right start, but needs to be cleaned up and better structured.

Additional context

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct

Activity

  1. added theissue type on Aug 29, 2025
  2. dmandar commented on Sep 2, 2025

    @dmandar
    Contributor
  3. assigned and unassigned on Oct 10, 2025
  4. mtsmyassin commented on Apr 17, 2026

    @mtsmyassin

    Adding a concrete sketch of the CredentialManager shape the author described, factored so both the static-config and request-scoped patterns work without forcing one over the other.

    # src/a2a/client/auth/credential_manager.py
    from __future__ import annotations
    from abc import ABC, abstractmethod
    from dataclasses import dataclass
    from typing import Awaitable, Callable, Mapping, Sequence
    from a2a.types import AgentCard, SecurityScheme, OAuth2SecurityScheme
    
    @dataclass(frozen=True)
    class Credential:
        """A resolved credential ready to be applied to a request."""
        scheme_name: str        # the SecurityScheme key from the AgentCard
        header: str             # e.g. "Authorization"
        value: str              # e.g. "Bearer eyJ..."
    
    class CredentialProvider(ABC):
        """Knows how to fulfill ONE kind of SecurityScheme."""
    
        @abstractmethod
        def supports(self, scheme: SecurityScheme) -> bool: ...
    
        @abstractmethod
        async def fetch(
            self,
            scheme_name: str,
            scheme: SecurityScheme,
            scopes: Sequence[str],
            context: Mapping[str, object] | None = None,
        ) -> Credential | None: ...
    
    
    class CredentialManager:
        """Matches AgentCard security requirements to registered providers."""
    
        def __init__(self, providers: Sequence[CredentialProvider]):
            self._providers = list(providers)
    
        def can_satisfy(self, card: AgentCard) -> tuple[bool, list[str]]:
            """At construction time: do we have a provider for at least one
            complete `security` requirement set? Returns (ok, missing_scheme_names)."""
            if not card.security:
                return True, []
            missing: list[str] = []
            for requirement in card.security:  # list[dict[str, list[str]]]
                if all(self._has_provider(card.security_schemes[name]) for name in requirement):
                    return True, []
                for name in requirement:
                    if not self._has_provider(card.security_schemes[name]):
                        missing.append(name)
            return False, sorted(set(missing))
    
        def _has_provider(self, scheme: SecurityScheme) -> bool:
            return any(p.supports(scheme) for p in self._providers)
    
        async def credentials_for(
            self,
            card: AgentCard,
            *,
            context: Mapping[str, object] | None = None,
        ) -> list[Credential]:
            """Per-request: fetch credentials for the first satisfiable requirement set."""
            if not card.security:
                return []
            for requirement in card.security:
                try:
                    creds: list[Credential] = []
                    for name, scopes in requirement.items():
                        scheme = card.security_schemes[name]
                        provider = next(p for p in self._providers if p.supports(scheme))
                        cred = await provider.fetch(name, scheme, scopes, context)
                        if cred is None:
                            raise _Skip()
                        creds.append(cred)
                    return creds
                except _Skip:
                    continue
            raise A2AClientAuthError(f"No credential provider could satisfy {card.security!r}")
    
    class _Skip(Exception): ...

    …with a couple of stock providers shipping in the SDK:

    # src/a2a/client/auth/providers.py
    class StaticBearerProvider(CredentialProvider):
        def __init__(self, scheme_names: set[str], token: str):
            self._names = scheme_names
            self._token = token
        def supports(self, scheme): return scheme.type == "http" and scheme.scheme == "bearer"
        async def fetch(self, name, scheme, scopes, context):
            return Credential(name, "Authorization", f"Bearer {self._token}") if name in self._names else None
    
    class ApiKeyHeaderProvider(CredentialProvider):
        def __init__(self, scheme_names: set[str], key: str):
            self._names, self._key = scheme_names, key
        def supports(self, scheme): return scheme.type == "apiKey" and scheme.in_ == "header"
        async def fetch(self, name, scheme, scopes, context):
            return Credential(name, scheme.name, self._key) if name in self._names else None
    
    class OAuth2ClientCredentialsProvider(CredentialProvider):
        """Performs the client_credentials grant on demand and caches by (scheme_name, scopes)."""
        def __init__(self, client_id: str, client_secret: str, *, http: httpx.AsyncClient):
            self._id, self._sec, self._http = client_id, client_secret, http
            self._cache: dict[tuple[str, tuple[str, ...]], tuple[str, float]] = {}
        def supports(self, scheme): return isinstance(scheme, OAuth2SecurityScheme)
        async def fetch(self, name, scheme, scopes, context):
            key = (name, tuple(sorted(scopes)))
            cached = self._cache.get(key)
            if cached and cached[1] > time.time() + 30: return Credential(name, "Authorization", f"Bearer {cached[0]}")
            token_url = scheme.flows.client_credentials.token_url  # only this flow here
            resp = await self._http.post(token_url, data={
                "grant_type": "client_credentials",
                "client_id": self._id, "client_secret": self._sec,
                "scope": " ".join(scopes),
            })
            resp.raise_for_status()
            body = resp.json()
            self._cache[key] = (body["access_token"], time.time() + body.get("expires_in", 3600))
            return Credential(name, "Authorization", f"Bearer {body['access_token']}")

    Wiring into ClientFactory + transports stays small:

    1. ClientConfig gets an optional credential_manager: CredentialManager | None = None.
      1. ClientFactory.create(card) calls credential_manager.can_satisfy(card) and emits a one-line warning on missing providers (per the author's "warning, can ignore and continue" semantics).
      1. A new CredentialInterceptor(credential_manager) (a ClientCallInterceptor) calls credentials_for(card, context=...) per outgoing request and applies each Credential to the transport's request — JSON-RPC/REST as headers, gRPC as metadata. This subsumes today's AuthInterceptor and means the transport stays auth-agnostic.
        Two places this design intentionally diverges from "inspect AgentCard once at construction":
    • Per-request context. Passing context from ClientCallContext to credentials_for is what lets users implement per-tenant/per-end-user OAuth without subclassing. This dovetails directly with [Feat]: passing call context to unauthenticated card modifier #657 (passing call context to the unauthenticated card modifier) — the same context object should flow through both paths.
      • Multiple satisfiable requirement sets. The A2A spec models security as list[dict[name -> scopes]] (OR of ANDs). The first-satisfiable-set logic above lets users drop in just one provider and have it work even if the card lists several alternatives.
        @lkawka happy to put this together as a draft PR against main (or 1.0-dev?) including (a) the manager + three stock providers, (b) the CredentialInterceptor, (c) ClientConfig plumbing, and (d) tests covering the OR-of-ANDs requirement matching. Let me know which branch and whether you'd prefer the OAuth providers as an optional extra (a2a[oauth]) to keep the base install lean.
  5. mikeas1 commented on Apr 17, 2026

    @mikeas1
    ContributorAuthor

    @mtsmyassin overall this looks pretty good. It matches what I had in mind fairly closely, though there are a few parts that I have some thoughts on:

    1. A CredentialProvider may want to consider additional information beyond what's in the SecurityScheme to determine whether supports(...) is true. For example, a credential provider for API keys might want to consider the domain of the agent's URL ("this is an API key for agents hosted on example.com only"). I think the simplest option is to pass both the overall AgentCard and the specific SecurityScheme to consider. This also allows security schemes that link to extensions, which is a nice benefit.
    2. There's an intrinsic link between the SecurityScheme definition and how a Credential is conveyed. Technically, the knowledge of where a credential goes (header, query param, something more exotic) comes from the SecurityScheme itself. This is pretty minor in the design you proposed, but currently the CredentialProvider is responsible for producing a Credential, which contains both the information on how the credential is conveyed and the credential material itself. Each CredentialProvider would then be responsible for understanding this relationship between SecurityScheme and how a credential is used in a request. The CredentialProvider should probably just be responsible for providing the type of credential it provides. Of course, it can still use information about the SecurityScheme when it retrieves the credential. A separate piece of code can then determine how to use that credential in the request, which requires knowing the SecurityScheme.
    3. The most common type of credential is a string token value that's independent of the request, but there are some odd cases. One is mTLS, where the credential is an x.509 certificate used to initiate the connection to the client. Another case is signature/digest-based auth, where the token provided is derived from the request content itself. AWS' SigV4, HTTP Message Signatures, and OAuth DPoP work this way. None of these are clearly represented in the SecuritySchemes we support today (other than mTLS, which is new in v1.0), so it's not pressing to solve right way. I think the main takeaway is that a CredentialProvider may need to be able to see request details to construct a credential.
    4. I'd love to find a way to not completely reinvent the wheel for these auth structures. httpx, which we use for HTTP based transports, has an Auth concept. gRPC has client credentials (both channel credentials and per-request credentials). If we could find a path to integrating with these, that would be great. httpx doesn't have the same concept of providing channel credentials, but we probably have a need for this if we want to support mTLS. We can start without this, of course, but it's something to keep in mind. Ideally we wouldn't architecturally lock ourselves out of providing credentials at client construction time, even if we don't implement it right now. Anyway, I think what you proposed should have a relatively clear path to integrating with httpx and gRPC.

    Drafting a PR for this would be great. You can target 1.0-dev, though I don't think the difference will matter too much since this will mostly be entirely new code. Fair warning, the team is quite busily implementing 1.0 features. A PR will be super helpful for grounding discussions even if we don't merge it immediately.

    @ishymko @herczyn FYI.

  6. mtsmyassin commented on Apr 18, 2026

    @mtsmyassin

    Thanks for the detailed review — I really appreciate you taking the time to walk through each point. This is exactly the kind of feedback that helps avoid churn later.

    Agreed on all four. Point 2 especially is a good catch — I was combining credential retrieval and credential conveyance, but they should be separate concerns. Splitting them also makes the extension story a lot cleaner.

    Here's what I'm planning to change before I open the draft, stated as concrete interface commitments so they're easier to push back on:

    1. Provider matching/resolution takes the full AgentCard alongside the SecurityScheme. Concretely: CredentialProvider.supports(card: AgentCard, scheme: SecurityScheme) -> bool and fetch(...) gets the same card argument. That covers the domain-scoped API key case and leaves room for schemes that reference extensions.

    2. Split retrieval from conveyance. Providers return opaque credential material (e.g. a bearer token, an API key value, later an mTLS cert). A separate CredentialApplier, keyed off the SecurityScheme, is responsible for placing it on the outgoing request. The stock appliers cover http bearer, apiKey in: header|query|cookie, and oauth2 → Authorization: Bearer; custom schemes register their own applier.

    3. The applier interface takes a mutable view of the outgoing request (headers + body/bytes accessor), not just a header dict. That's the seam SigV4, HTTP Message Signatures, and OAuth DPoP need later — they're not in the first PR, but the signature won't have to change to add them.

    4. Providers expose adapters rather than reimplementing transport auth. For HTTP transports, a provider can yield an httpx.Auth; for gRPC, grpc.CallCredentials. Channel-level concerns (mTLS, gRPC channel credentials) go through a separate ChannelCredentialProvider hook surfaced on ClientConfig, even though the first PR won't wire it end-to-end.

    I'll target 1.0-dev. First PR stays focused: core manager/interfaces, the retrieval/conveyance split, common providers (static bearer, API key, OAuth2 client credentials), ClientFactory / ClientConfig plumbing, and tests around the OR-of-ANDs requirement matching. I'll mark it as a draft and treat it mainly as a way to ground the discussion in concrete code while the team is busy with 1.0 work.

    If that direction sounds right, I'll start putting the draft together.

  7. mikeas1 commented on Apr 20, 2026

    @mikeas1
    ContributorAuthor

    That sounds good. Very curious to see what you come up with. Thanks for being willing to contribute!

  8. arthurpanhku commented on Jul 31, 2026

    @arthurpanhku

    Hi! I’m interested in contributing to this. I noticed the earlier design discussion, but I couldn’t find a linked draft PR. Is this still actively being worked on?

    If help is still needed, I’d be happy to take a narrowly scoped part—such as an API-key credential provider or tests for security requirement matching—to avoid overlapping with existing work. Please let me know which slice would be most useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions