mcp: add client-side OAuth flow (#544)

This is a preliminary implementation of OAuth 2.1 for the client.

When a StreamableClientTransport encounters a 401 Unauthorized response
from the server, it initiates the OAuth flow described in thec
authorization section of the MCP spec

(https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization).
On success, the transport obtains an access token which it passes
to all subsequent requests.

Much remains to be done here:

* Resource Indicators, as described in section 2.5.1 of the MCP spec.

* All of this is unexported, so it is available only to our own
StreamingClientTransport. We should add API so people can use it
with their own transports.

* And, of course, tests. We should test against fake implementations
but also, if we can find any, real reference implementations.

For #19
Co-authored-by: Jonathan Amsterdam <jba@google.com>

---------

Co-authored-by: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
Sam Thanawalla 2025-09-30 16:16:30 -04:00 • committed by GitHub
commit 069aae16df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -146,11 +146,11 @@ func GetProtectedResourceMetadataFromHeader(ctx context.Context, header http.Hea
if len(headers) == 0 {
return nil, nil
}
cs, err := parseWWWAuthenticate(headers)
cs, err := ParseWWWAuthenticate(headers)
if err != nil {
return nil, err
}
url := resourceMetadataURL(cs)
url := ResourceMetadataURL(cs)
if url == "" {
return nil, nil
}
@ -193,9 +193,9 @@ type challenge struct {
Params map[string]string
}
// resourceMetadataURL returns a resource metadata URL from the given challenges,
// ResourceMetadataURL returns a resource metadata URL from the given challenges,
// or the empty string if there is none.
func resourceMetadataURL(cs []challenge) string {
func ResourceMetadataURL(cs []challenge) string {
for _, c := range cs {
if u := c.Params["resource_metadata"]; u != "" {
return u
@ -204,11 +204,11 @@ func resourceMetadataURL(cs []challenge) string {
return ""
}
// parseWWWAuthenticate parses a WWW-Authenticate header string.
// ParseWWWAuthenticate parses a WWW-Authenticate header string.
// The header format is defined in RFC 9110, Section 11.6.1, and can contain
// one or more challenges, separated by commas.
// It returns a slice of challenges or an error if one of the headers is malformed.
func parseWWWAuthenticate(headers []string) ([]challenge, error) {
func ParseWWWAuthenticate(headers []string) ([]challenge, error) {
// GENERATED BY GEMINI 2.5 (human-tweaked)
var challenges []challenge
for _, h := range headers {