Skip to content

feat: add OAuth discovery and a grant store - #127

Closed
mykytanetipa wants to merge 1 commit into
mainfrom
mykytanetipa/oauth-discovery
Closed

mykytanetipa wants to merge 1 commit into
mainfrom
mykytanetipa/oauth-discovery

Conversation

@mykytanetipa

@mykytanetipa mykytanetipa commented Nov 4, 2026 •

Copy link
Copy Markdown

Adds the OAuth discovery, client selection, and grant storage package that the CLI authentication stack builds on. It also includes an in-process OAuth server and protected A2A agent so tests run over real localhost HTTP without mocks or external identity providers. CLI commands are unchanged until #130.

Example:

as := oauthtest.NewAuthServer(t, oauthtest.ServerOptions{CIMD: true})
agent := oauthtest.NewProtectedAgent(t, as, oauthtest.AgentOptions{
    Card:   oauthtest.CardMetadataURL,
    Scopes: []string{"a2a.invoke"},
})

resp, _ := http.Get(agent.URL + "/tasks") // 401 with WWW-Authenticate: Bearer ...
challenges := oauth.ParseChallenges(resp.Header.Values("WWW-Authenticate"))

store := &oauth.FileStore{Dir: t.TempDir()}
key := oauth.NewGrantKey(as.URL, oauth.DefaultClientID, agent.URL, []string{"a2a.invoke"})
_ = store.Put(oauth.Grant{Key: key, AccessToken: "at_123"})

Protected agents can advertise where to sign in either up front on the agent card or reactively in a 401 Bearer challenge header. Discovery normalizes both paths into the same requirement structure, following challenge headers through Protected Resource Metadata to the server's OAuth or OpenID Connect metadata. Along the way, it enforces HTTPS outside loopback, same-origin redirects, and matching issuer and resource identifiers. A card that also accepts other credentials, such as an API key, marks the requirement as optional.

Once an authorization server is known, client matching picks the configured registration for that authority. If none is configured and the server advertises support, it falls back to the default client metadata document URL, which can be changed or turned off. That document is proposed in A2A PR 2405, so https://a2a-protocol.org/oauth/a2a-cli.json returns 404 until it is merged and deployed. Saved grants live on disk in an owner-only JSON file keyed by issuer, client ID, normalized agent URL, and scopes. That keying keeps tokens issued for one agent from ever being sent to another, and an OS file lock (flock or LockFileEx) coordinates concurrent CLI runs without leaving a stale lock behind after a crash.

Refs #98

The CLI learns how to authorize from the agent card's securitySchemes
and securityRequirements (oauth2 with a metadata URL or inline flows, or
openIdConnect), or from a 401: every Bearer challenge is parsed and its
resource_metadata, or the well-known location, leads to the Protected
Resource Metadata (RFC 9728) and on to Authorization Server Metadata
(RFC 8414) or OpenID Connect Discovery. Metadata must be served over
https (http only on loopback), resource metadata redirects stay within
the origin, and the resource and issuer named in each document are
checked. A card that also accepts other credentials, such as an API key,
makes the requirement optional.

Client registrations come from configuration, with the a2a-cli Client ID
Metadata Document as the default client for servers that support CIMD
when none matches; the default can be changed or disabled. Grants are
kept in ~/.config/a2a-cli/oauth/grants.json (0600) keyed by issuer,
client ID, resource and sorted scopes, with an OS file lock (flock,
LockFileEx) so concurrent CLI processes don't lose rotated refresh
tokens.

Tests run against oauthtest, an in-process authorization server (RFC
8414 and OpenID Connect metadata, authorization code with PKCE, device
flow, refresh with optional rotation, revocation, optional TLS) and an
A2A echo agent that requires its tokens, declares OAuth in its card in
each supported form and answers 401 with a Bearer challenge, so the
flows are exercised end to end without a browser, network access or a
real identity provider.

Refs #98
Comment thread internal/oauth/discover.go Resolved
@mykytanetipa

mykytanetipa commented Nov 8, 2026 •

Copy link
Copy Markdown
Author

Closing in favor of the a2a-go OAuth client (a2aproject/a2a-go#568, a2aproject/a2a-go#569, a2aproject/a2a-go#570) and the CLI changes built on it (mykytanetipa#6, mykytanetipa#7).

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants