Skip to content

Commit e7f1487

Browse files
committed
mcp/internal/oauthex: auth server metadata
Implement the Authorization Server Metadata spec.
1 parent df051ef commit e7f1487

File tree

4 files changed

+278
-0
lines changed

4 files changed

+278
-0
lines changed

internal/oauthex/auth_meta.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file implements Authorization Server Metadata.
6+
// See https://www.rfc-editor.org/rfc/rfc8414.html.
7+
8+
package oauthex
9+
10+
import (
11+
"context"
12+
"errors"
13+
"fmt"
14+
"net/http"
15+
)
16+
17+
// AuthServerMeta represents the metadata for an OAuth 2.0 authorization server,
18+
// as defined in RFC 8414 (https://tools.ietf.org/html/rfc8414).
19+
//
20+
// Not supported:
21+
// - signed metadata
22+
type AuthServerMeta struct {
23+
// GENERATED BY GEMINI 2.5.
24+
25+
// Issuer is the REQUIRED URL identifying the authorization server.
26+
Issuer string `json:"issuer"`
27+
28+
// AuthorizationEndpoint is the REQUIRED URL of the server's OAuth 2.0 authorization endpoint.
29+
AuthorizationEndpoint string `json:"authorization_endpoint"`
30+
31+
// TokenEndpoint is the REQUIRED URL of the server's OAuth 2.0 token endpoint.
32+
TokenEndpoint string `json:"token_endpoint"`
33+
34+
// JWKSURI is the REQUIRED URL of the server's JSON Web Key Set [JWK] document.
35+
JWKSURI string `json:"jwks_uri"`
36+
37+
// RegistrationEndpoint is the RECOMMENDED URL of the server's OAuth 2.0 Dynamic Client Registration endpoint.
38+
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
39+
40+
// ScopesSupported is a RECOMMENDED JSON array of strings containing a list of the OAuth 2.0
41+
// "scope" values that this server supports.
42+
ScopesSupported []string `json:"scopes_supported,omitempty"`
43+
44+
// ResponseTypesSupported is a REQUIRED JSON array of strings containing a list of the OAuth 2.0
45+
// "response_type" values that this server supports.
46+
ResponseTypesSupported []string `json:"response_types_supported"`
47+
48+
// ResponseModesSupported is a RECOMMENDED JSON array of strings containing a list of the OAuth 2.0
49+
// "response_mode" values that this server supports.
50+
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
51+
52+
// GrantTypesSupported is a RECOMMENDED JSON array of strings containing a list of the OAuth 2.0
53+
// grant type values that this server supports.
54+
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
55+
56+
// TokenEndpointAuthMethodsSupported is a RECOMMENDED JSON array of strings containing a list of
57+
// client authentication methods supported by this token endpoint.
58+
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
59+
60+
// TokenEndpointAuthSigningAlgValuesSupported is a RECOMMENDED JSON array of strings containing
61+
// a list of the JWS signing algorithms ("alg" values) supported by the token endpoint for
62+
// the signature on the JWT used to authenticate the client.
63+
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
64+
65+
// ServiceDocumentation is a RECOMMENDED URL of a page containing human-readable documentation
66+
// for the service.
67+
ServiceDocumentation string `json:"service_documentation,omitempty"`
68+
69+
// UILocalesSupported is a RECOMMENDED JSON array of strings representing supported
70+
// BCP47 [RFC5646] language tag values for display in the user interface.
71+
UILocalesSupported []string `json:"ui_locales_supported,omitempty"`
72+
73+
// OpPolicyURI is a RECOMMENDED URL that the server provides to the person registering
74+
// the client to read about the server's operator policies.
75+
OpPolicyURI string `json:"op_policy_uri,omitempty"`
76+
77+
// OpTOSURI is a RECOMMENDED URL that the server provides to the person registering the
78+
// client to read about the server's terms of service.
79+
OpTOSURI string `json:"op_tos_uri,omitempty"`
80+
81+
// RevocationEndpoint is a RECOMMENDED URL of the server's OAuth 2.0 revocation endpoint.
82+
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
83+
84+
// RevocationEndpointAuthMethodsSupported is a RECOMMENDED JSON array of strings containing
85+
// a list of client authentication methods supported by this revocation endpoint.
86+
RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"`
87+
88+
// RevocationEndpointAuthSigningAlgValuesSupported is a RECOMMENDED JSON array of strings
89+
// containing a list of the JWS signing algorithms ("alg" values) supported by the revocation
90+
// endpoint for the signature on the JWT used to authenticate the client.
91+
RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"`
92+
93+
// IntrospectionEndpoint is a RECOMMENDED URL of the server's OAuth 2.0 introspection endpoint.
94+
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
95+
96+
// IntrospectionEndpointAuthMethodsSupported is a RECOMMENDED JSON array of strings containing
97+
// a list of client authentication methods supported by this introspection endpoint.
98+
IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"`
99+
100+
// IntrospectionEndpointAuthSigningAlgValuesSupported is a RECOMMENDED JSON array of strings
101+
// containing a list of the JWS signing algorithms ("alg" values) supported by the introspection
102+
// endpoint for the signature on the JWT used to authenticate the client.
103+
IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"`
104+
105+
// CodeChallengeMethodsSupported is a RECOMMENDED JSON array of strings containing a list of
106+
// PKCE code challenge methods supported by this authorization server.
107+
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"`
108+
}
109+
110+
var wellKnownPaths = []string{
111+
"/.well-known/oauth-authorization-server",
112+
"/.well-known/openid-configuration",
113+
}
114+
115+
func GetAuthServerMeta(ctx context.Context, issuerURL string, c *http.Client) (*AuthServerMeta, error) {
116+
var errs []error
117+
for _, p := range wellKnownPaths {
118+
u, err := prependToPath(issuerURL, p)
119+
if err != nil {
120+
// issuerURL is bad; no point in continuing.
121+
return nil, err
122+
}
123+
asm, err := getJSON[AuthServerMeta](ctx, c, u)
124+
if err == nil {
125+
if asm.Issuer != issuerURL { // section 3.3
126+
// Security violation; don't keep trying.
127+
return nil, fmt.Errorf("metadata issuer %q does not match issuer URL %q", asm.Issuer, issuerURL)
128+
}
129+
return asm, nil
130+
}
131+
errs = append(errs, err)
132+
}
133+
return nil, fmt.Errorf("failed to get auth server metadata from %q: %w", issuerURL, errors.Join(errs...))
134+
}

internal/oauthex/auth_meta_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package oauthex
6+
7+
import (
8+
"encoding/json"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
)
13+
14+
func TestAuthMetaParse(t *testing.T) {
15+
// Verify that we parse Google's auth server metadata.
16+
data, err := os.ReadFile(filepath.FromSlash("testdata/google-auth-meta.json"))
17+
if err != nil {
18+
t.Fatal(err)
19+
}
20+
var a AuthServerMeta
21+
if err := json.Unmarshal(data, &a); err != nil {
22+
t.Fatal(err)
23+
}
24+
// Spot check.
25+
if g, w := a.Issuer, "https://accounts.google.com"; g != w {
26+
t.Errorf("got %q, want %q", g, w)
27+
}
28+
}

internal/oauthex/oauth2.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,62 @@
44

55
// Package oauthex implements extensions to OAuth2.
66
package oauthex
7+
8+
import (
9+
"context"
10+
"encoding/json"
11+
"fmt"
12+
"net/http"
13+
"net/url"
14+
"strings"
15+
)
16+
17+
// prependToPath prepends pre to the path of urlStr.
18+
// When pre is the well-known path, this is the algorithm specified in both RFC 9728
19+
// section 3.1 and RFC 8414 section 3.1.
20+
func prependToPath(urlStr, pre string) (string, error) {
21+
u, err := url.Parse(urlStr)
22+
if err != nil {
23+
return "", err
24+
}
25+
p := "/" + strings.Trim(pre, "/")
26+
if u.Path != "" {
27+
p += "/"
28+
}
29+
30+
u.Path = p + strings.TrimLeft(u.Path, "/")
31+
return u.String(), nil
32+
}
33+
34+
// getJSON retrieves JSON and unmarshals JSON from the URL, as specified in both
35+
// RFC 9728 and RFC 8414.
36+
func getJSON[T any](ctx context.Context, c *http.Client, url string) (*T, error) {
37+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
38+
if err != nil {
39+
return nil, err
40+
}
41+
if c == nil {
42+
c = http.DefaultClient
43+
}
44+
res, err := c.Do(req)
45+
if err != nil {
46+
return nil, err
47+
}
48+
defer res.Body.Close()
49+
50+
// Specs require a 200.
51+
if res.StatusCode != http.StatusOK {
52+
return nil, fmt.Errorf("bad status %s", res.Status)
53+
}
54+
// Specs require application/json.
55+
if ct := res.Header.Get("Content-Type"); ct != "application/json" {
56+
return nil, fmt.Errorf("bad content type %q", ct)
57+
}
58+
59+
var t T
60+
dec := json.NewDecoder(res.Body)
61+
if err := dec.Decode(&t); err != nil {
62+
return nil, err
63+
}
64+
return &t, nil
65+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"issuer": "https://accounts.google.com",
3+
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
4+
"device_authorization_endpoint": "https://oauth2.googleapis.com/device/code",
5+
"token_endpoint": "https://oauth2.googleapis.com/token",
6+
"userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
7+
"revocation_endpoint": "https://oauth2.googleapis.com/revoke",
8+
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
9+
"response_types_supported": [
10+
"code",
11+
"token",
12+
"id_token",
13+
"code token",
14+
"code id_token",
15+
"token id_token",
16+
"code token id_token",
17+
"none"
18+
],
19+
"subject_types_supported": [
20+
"public"
21+
],
22+
"id_token_signing_alg_values_supported": [
23+
"RS256"
24+
],
25+
"scopes_supported": [
26+
"openid",
27+
"email",
28+
"profile"
29+
],
30+
"token_endpoint_auth_methods_supported": [
31+
"client_secret_post",
32+
"client_secret_basic"
33+
],
34+
"claims_supported": [
35+
"aud",
36+
"email",
37+
"email_verified",
38+
"exp",
39+
"family_name",
40+
"given_name",
41+
"iat",
42+
"iss",
43+
"name",
44+
"picture",
45+
"sub"
46+
],
47+
"code_challenge_methods_supported": [
48+
"plain",
49+
"S256"
50+
],
51+
"grant_types_supported": [
52+
"authorization_code",
53+
"refresh_token",
54+
"urn:ietf:params:oauth:grant-type:device_code",
55+
"urn:ietf:params:oauth:grant-type:jwt-bearer"
56+
]
57+
}

0 commit comments

Comments
 (0)