Skip to content

feat: add OAuth token flows, refresh and request authorization - #128

Open
mykytanetipa wants to merge 1 commit into
mykytanetipa/oauth-discoveryfrom
mykytanetipa/oauth-signin
Open

mykytanetipa wants to merge 1 commit into
mykytanetipa/oauth-discoveryfrom
mykytanetipa/oauth-signin

Conversation

@mykytanetipa

@mykytanetipa mykytanetipa commented Nov 4, 2026 •

Copy link
Copy Markdown

Adds the OAuth runtime manager to run interactive browser PKCE and device code sign-in, refresh and revoke tokens, and attach bearer grants to outgoing A2A client calls. Everything stays inside the internal OAuth package in this PR, keeping token lifecycle and prompt handling separate from CLI flag wiring in #130.

Example:

mgr := &oauth.Manager{
    Clients: []oauth.Registration{{Authority: as.URL, ClientID: "my-cli"}},
    Store:   &oauth.FileStore{Dir: storeDir},
}
httpClient := &http.Client{Transport: mgr.Transport(http.DefaultTransport)}
client, _ := a2aclient.NewFromCard(ctx, card,
    a2aclient.WithInterceptors(mgr.Interceptor()),
    a2aclient.WithJSONRPCTransport(httpClient),
)

resp, err := client.SendMessage(ctx, params)
var ire *oauth.InteractionRequiredError
if errors.As(err, &ire) {
    _, err = mgr.Acquire(signInCtx, ire.Requirement, oauth.AcquireOptions{
        Prompter: oauth.Prompter{Out: os.Stderr},
    })
    resp, err = client.SendMessage(retryCtx, params)
}

Hooking OAuth into the A2A client uses both a client interceptor and an HTTP transport wrapper because neither sees the full call on its own. The interceptor sees the resolved agent card before the first RPC and attaches a cached or freshly refreshed grant up front. On the wire, the HTTP transport catches raw 401 responses and retries once after refreshing or discovering the server. Only the transport can read the challenge header because the SDK drops HTTP headers on non-200 errors. Requests that already carry an Authorization header are left alone, and tokens are only sent over HTTPS or to loopback hosts.

Only a card that requires OAuth makes a missing grant an error. For any other agent, a stored grant is attached when one covers it (the same resource or a parent path, with at least the scopes it asks for), and the request goes out either way, so agents that accept API keys or no credentials are unaffected.

Neither hook ever blocks on a browser or device prompt inside an RPC, since waiting on a human would burn the per-request timeout. When silent auth cannot satisfy the agent, both return an interaction-required error carrying the discovered requirement. The caller can then run sign-in under its own deadline and retry cleanly. Every authorization and token call also passes the target agent URL as the resource indicator. Token requests therefore use standard net/http rather than x/oauth2, whose refresh path drops the resource parameter.

Refs #98

Grants are obtained with the authorization code flow with PKCE and a
loopback redirect to 127.0.0.1/oauth/callback (RFC 7636, RFC 8252), whose
URL is printed for the user, or with the device authorization grant
(RFC 8628) when requested or when the server has no authorization
endpoint. Every request to the authorization server carries the agent URL
as the resource indicator (RFC 8707).

An a2a client interceptor attaches stored grants before the first
request when the card declares OAuth, and an http.RoundTripper handles
401s: it refreshes a rejected grant, or discovers the server from the
challenge, and retries once. Only a card that requires OAuth makes a
missing grant an error; otherwise a stored grant is attached when one
covers the agent, and problems finding it are only logged. Tokens are
sent only over https or to loopback hosts, and requests that already
carry an Authorization header are left alone. When a grant has to be
obtained interactively, both report an InteractionRequiredError so the
caller can sign in outside the request timeout. Grants can also be
revoked (RFC 7009).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant