Skip to content

Commit a39b8cf

Browse files
committed
mcp/internal/oauthex: auth server metadata
Implement the Authorization Server Metadata spec. Also, limit the size of the body we read for protected resource metadata (as well as auth server metadata).
1 parent 29c1650 commit a39b8cf

File tree

5 files changed

+282
-26
lines changed

5 files changed

+282
-26
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, 1<<20)
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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,64 @@
44

55
// Package oauthex implements extensions to OAuth2.
66
package oauthex
7+
8+
import (
9+
"context"
10+
"encoding/json"
11+
"fmt"
12+
"io"
13+
"net/http"
14+
"net/url"
15+
"strings"
16+
)
17+
18+
// prependToPath prepends pre to the path of urlStr.
19+
// When pre is the well-known path, this is the algorithm specified in both RFC 9728
20+
// section 3.1 and RFC 8414 section 3.1.
21+
func prependToPath(urlStr, pre string) (string, error) {
22+
u, err := url.Parse(urlStr)
23+
if err != nil {
24+
return "", err
25+
}
26+
p := "/" + strings.Trim(pre, "/")
27+
if u.Path != "" {
28+
p += "/"
29+
}
30+
31+
u.Path = p + strings.TrimLeft(u.Path, "/")
32+
return u.String(), nil
33+
}
34+
35+
// getJSON retrieves JSON and unmarshals JSON from the URL, as specified in both
36+
// RFC 9728 and RFC 8414.
37+
// It will not read more than limit bytes from the body.
38+
func getJSON[T any](ctx context.Context, c *http.Client, url string, limit int64) (*T, error) {
39+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
40+
if err != nil {
41+
return nil, err
42+
}
43+
if c == nil {
44+
c = http.DefaultClient
45+
}
46+
res, err := c.Do(req)
47+
if err != nil {
48+
return nil, err
49+
}
50+
defer res.Body.Close()
51+
52+
// Specs require a 200.
53+
if res.StatusCode != http.StatusOK {
54+
return nil, fmt.Errorf("bad status %s", res.Status)
55+
}
56+
// Specs require application/json.
57+
if ct := res.Header.Get("Content-Type"); ct != "application/json" {
58+
return nil, fmt.Errorf("bad content type %q", ct)
59+
}
60+
61+
var t T
62+
dec := json.NewDecoder(io.LimitReader(res.Body, limit))
63+
if err := dec.Decode(&t); err != nil {
64+
return nil, err
65+
}
66+
return &t, nil
67+
}

internal/oauthex/resource_meta.go

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package oauthex
99

1010
import (
1111
"context"
12-
"encoding/json"
1312
"errors"
1413
"fmt"
1514
"net/http"
@@ -164,38 +163,15 @@ func getPRM(ctx context.Context, url string, c *http.Client, wantResource string
164163
if !strings.HasPrefix(strings.ToUpper(url), "HTTPS://") {
165164
return nil, fmt.Errorf("resource URL %q does not use HTTPS", url)
166165
}
167-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
166+
prm, err := getJSON[ProtectedResourceMetadata](ctx, c, url, 1<<20)
168167
if err != nil {
169168
return nil, err
170169
}
171-
if c == nil {
172-
c = http.DefaultClient
173-
}
174-
res, err := c.Do(req)
175-
if err != nil {
176-
return nil, err
177-
}
178-
defer res.Body.Close()
179-
180-
// Spec §3.2 requires a 200.
181-
if res.StatusCode != http.StatusOK {
182-
return nil, fmt.Errorf("bad status %s", res.Status)
183-
}
184-
// Spec §3.2 requires application/json.
185-
if ct := res.Header.Get("Content-Type"); ct != "application/json" {
186-
return nil, fmt.Errorf("bad content type %q", ct)
187-
}
188-
189-
var prm ProtectedResourceMetadata
190-
dec := json.NewDecoder(res.Body)
191-
if err := dec.Decode(&prm); err != nil {
192-
return nil, err
193-
}
194170
// Validate the Resource field to thwart impersonation attacks (section 3.3).
195171
if prm.Resource != wantResource {
196172
return nil, fmt.Errorf("got metadata resource %q, want %q", prm.Resource, wantResource)
197173
}
198-
return &prm, nil
174+
return prm, nil
199175
}
200176

201177
// challenge represents a single authentication challenge from a WWW-Authenticate header.
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)