// Copyright 2025 The Go MCP SDK Authors. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // This file implements Authorization Server Metadata. // See https://www.rfc-editor.org/rfc/rfc8414.html. package oauthex import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) // AuthServerMeta represents the metadata for an OAuth 2.0 authorization server, // as defined in [RFC 8414]. // // Not supported: // - signed metadata // // [RFC 8414]: https://tools.ietf.org/html/rfc8414) type AuthServerMeta struct { // GENERATED BY GEMINI 2.5. // Issuer is the REQUIRED URL identifying the authorization server. Issuer string `json:"issuer"` // AuthorizationEndpoint is the REQUIRED URL of the server's OAuth 2.0 authorization endpoint. AuthorizationEndpoint string `json:"authorization_endpoint"` // TokenEndpoint is the REQUIRED URL of the server's OAuth 2.0 token endpoint. TokenEndpoint string `json:"token_endpoint"` // JWKSURI is the REQUIRED URL of the server's JSON Web Key Set [JWK] document. JWKSURI string `json:"jwks_uri"` // RegistrationEndpoint is the RECOMMENDED URL of the server's OAuth 2.0 Dynamic Client Registration endpoint. RegistrationEndpoint string `json:"registration_endpoint,omitempty"` // ScopesSupported is a RECOMMENDED JSON array of strings containing a list of the OAuth 2.0 // "scope" values that this server supports. ScopesSupported []string `json:"scopes_supported,omitempty"` // ResponseTypesSupported is a REQUIRED JSON array of strings containing a list of the OAuth 2.0 // "response_type" values that this server supports. ResponseTypesSupported []string `json:"response_types_supported"` // ResponseModesSupported is a RECOMMENDED JSON array of strings containing a list of the OAuth 2.0 // "response_mode" values that this server supports. ResponseModesSupported []string `json:"response_modes_supported,omitempty"` // GrantTypesSupported is a RECOMMENDED JSON array of strings containing a list of the OAuth 2.0 // grant type values that this server supports. GrantTypesSupported []string `json:"grant_types_supported,omitempty"` // TokenEndpointAuthMethodsSupported is a RECOMMENDED JSON array of strings containing a list of // client authentication methods supported by this token endpoint. TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` // TokenEndpointAuthSigningAlgValuesSupported is a RECOMMENDED JSON array of strings containing // a list of the JWS signing algorithms ("alg" values) supported by the token endpoint for // the signature on the JWT used to authenticate the client. TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` // ServiceDocumentation is a RECOMMENDED URL of a page containing human-readable documentation // for the service. ServiceDocumentation string `json:"service_documentation,omitempty"` // UILocalesSupported is a RECOMMENDED JSON array of strings representing supported // BCP47 [RFC5646] language tag values for display in the user interface. UILocalesSupported []string `json:"ui_locales_supported,omitempty"` // OpPolicyURI is a RECOMMENDED URL that the server provides to the person registering // the client to read about the server's operator policies. OpPolicyURI string `json:"op_policy_uri,omitempty"` // OpTOSURI is a RECOMMENDED URL that the server provides to the person registering the // client to read about the server's terms of service. OpTOSURI string `json:"op_tos_uri,omitempty"` // RevocationEndpoint is a RECOMMENDED URL of the server's OAuth 2.0 revocation endpoint. RevocationEndpoint string `json:"revocation_endpoint,omitempty"` // RevocationEndpointAuthMethodsSupported is a RECOMMENDED JSON array of strings containing // a list of client authentication methods supported by this revocation endpoint. RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` // RevocationEndpointAuthSigningAlgValuesSupported is a RECOMMENDED JSON array of strings // containing a list of the JWS signing algorithms ("alg" values) supported by the revocation // endpoint for the signature on the JWT used to authenticate the client. RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"` // IntrospectionEndpoint is a RECOMMENDED URL of the server's OAuth 2.0 introspection endpoint. IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` // IntrospectionEndpointAuthMethodsSupported is a RECOMMENDED JSON array of strings containing // a list of client authentication methods supported by this introspection endpoint. IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` // IntrospectionEndpointAuthSigningAlgValuesSupported is a RECOMMENDED JSON array of strings // containing a list of the JWS signing algorithms ("alg" values) supported by the introspection // endpoint for the signature on the JWT used to authenticate the client. IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` // CodeChallengeMethodsSupported is a RECOMMENDED JSON array of strings containing a list of // PKCE code challenge methods supported by this authorization server. CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` } // ClientRegistrationMetadata represents the client metadata fields for the DCR POST request (RFC 7591). type ClientRegistrationMetadata struct { // RedirectURIs is a REQUIRED JSON array of redirection URI strings for use in // redirect-based flows (such as the authorization code grant). RedirectURIs []string `json:"redirect_uris"` // TokenEndpointAuthMethod is an OPTIONAL string indicator of the requested // authentication method for the token endpoint. // If omitted, the default is "client_secret_basic". TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` // GrantTypes is an OPTIONAL JSON array of OAuth 2.0 grant type strings // that the client will restrict itself to using. // If omitted, the default is ["authorization_code"]. GrantTypes []string `json:"grant_types,omitempty"` // ResponseTypes is an OPTIONAL JSON array of OAuth 2.0 response type strings // that the client will restrict itself to using. // If omitted, the default is ["code"]. ResponseTypes []string `json:"response_types,omitempty"` // ClientName is a RECOMMENDED human-readable name of the client to be presented // to the end-user. ClientName string `json:"client_name,omitempty"` // ClientURI is a RECOMMENDED URL of a web page providing information about the client. ClientURI string `json:"client_uri,omitempty"` // LogoURI is an OPTIONAL URL of a logo for the client, which may be displayed // to the end-user. LogoURI string `json:"logo_uri,omitempty"` // Scope is an OPTIONAL string containing a space-separated list of scope values // that the client will restrict itself to using. Scope string `json:"scope,omitempty"` // Contacts is an OPTIONAL JSON array of strings representing ways to contact // people responsible for this client (e.g., email addresses). Contacts []string `json:"contacts,omitempty"` // TOSURI is an OPTIONAL URL that the client provides to the end-user // to read about the client's terms of service. TOSURI string `json:"tos_uri,omitempty"` // PolicyURI is an OPTIONAL URL that the client provides to the end-user // to read about the client's privacy policy. PolicyURI string `json:"policy_uri,omitempty"` // JWKSURI is an OPTIONAL URL for the client's JSON Web Key Set [JWK] document. // This is preferred over the 'jwks' parameter. JWKSURI string `json:"jwks_uri,omitempty"` // JWKS is an OPTIONAL client's JSON Web Key Set [JWK] document, passed by value. // This is an alternative to providing a JWKSURI. JWKS string `json:"jwks,omitempty"` // SoftwareID is an OPTIONAL unique identifier string for the client software, // constant across all instances and versions. SoftwareID string `json:"software_id,omitempty"` // SoftwareVersion is an OPTIONAL version identifier string for the client software. SoftwareVersion string `json:"software_version,omitempty"` // SoftwareStatement is an OPTIONAL JWT that asserts client metadata values. // Values in the software statement take precedence over other metadata values. SoftwareStatement string `json:"software_statement,omitempty"` } // ClientRegistrationResponse represents the fields returned by the Authorization Server // (RFC 7591, Section 3.2.1 and 3.2.2). type ClientRegistrationResponse struct { // ClientRegistrationMetadata contains all registered client metadata, returned by the // server on success, potentially with modified or defaulted values. ClientRegistrationMetadata // ClientID is the REQUIRED newly issued OAuth 2.0 client identifier. ClientID string `json:"client_id"` // ClientSecret is an OPTIONAL client secret string. ClientSecret string `json:"client_secret,omitempty"` // ClientIDIssuedAt is an OPTIONAL Unix timestamp when the ClientID was issued. ClientIDIssuedAt time.Time `json:"client_id_issued_at,omitempty"` // ClientSecretExpiresAt is the REQUIRED (if client_secret is issued) Unix // timestamp when the secret expires, or 0 if it never expires. ClientSecretExpiresAt time.Time `json:"client_secret_expires_at,omitempty"` } func (r *ClientRegistrationResponse) MarshalJSON() ([]byte, error) { type alias ClientRegistrationResponse var clientIDIssuedAt int64 var clientSecretExpiresAt int64 if !r.ClientIDIssuedAt.IsZero() { clientIDIssuedAt = r.ClientIDIssuedAt.Unix() } if !r.ClientSecretExpiresAt.IsZero() { clientSecretExpiresAt = r.ClientSecretExpiresAt.Unix() } return json.Marshal(&struct { ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"` ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"` *alias }{ ClientIDIssuedAt: clientIDIssuedAt, ClientSecretExpiresAt: clientSecretExpiresAt, alias: (*alias)(r), }) } func (r *ClientRegistrationResponse) UnmarshalJSON(data []byte) error { type alias ClientRegistrationResponse aux := &struct { ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"` ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"` *alias }{ alias: (*alias)(r), } if err := json.Unmarshal(data, &aux); err != nil { return err } if aux.ClientIDIssuedAt != 0 { r.ClientIDIssuedAt = time.Unix(aux.ClientIDIssuedAt, 0) } if aux.ClientSecretExpiresAt != 0 { r.ClientSecretExpiresAt = time.Unix(aux.ClientSecretExpiresAt, 0) } return nil } // ClientRegistrationError is the error response from the Authorization Server // for a failed registration attempt (RFC 7591, Section 3.2.2). type ClientRegistrationError struct { // ErrorCode is the REQUIRED error code if registration failed (RFC 7591, 3.2.2). ErrorCode string `json:"error"` // ErrorDescription is an OPTIONAL human-readable error message. ErrorDescription string `json:"error_description,omitempty"` } func (e *ClientRegistrationError) Error() string { return fmt.Sprintf("registration failed: %s (%s)", e.ErrorCode, e.ErrorDescription) } var wellKnownPaths = []string{ "/.well-known/oauth-authorization-server", "/.well-known/openid-configuration", } // GetAuthServerMeta issues a GET request to retrieve authorization server metadata // from an OAuth authorization server with the given issuerURL. // // It follows [RFC 8414]: // - The well-known paths specified there are inserted into the URL's path, one at time. // The first to succeed is used. // - The Issuer field is checked against issuerURL. // // [RFC 8414]: https://tools.ietf.org/html/rfc8414 func GetAuthServerMeta(ctx context.Context, issuerURL string, c *http.Client) (*AuthServerMeta, error) { var errs []error for _, p := range wellKnownPaths { u, err := prependToPath(issuerURL, p) if err != nil { // issuerURL is bad; no point in continuing. return nil, err } asm, err := getJSON[AuthServerMeta](ctx, c, u, 1<<20) if err == nil { if asm.Issuer != issuerURL { // section 3.3 // Security violation; don't keep trying. return nil, fmt.Errorf("metadata issuer %q does not match issuer URL %q", asm.Issuer, issuerURL) } return asm, nil } errs = append(errs, err) } return nil, fmt.Errorf("failed to get auth server metadata from %q: %w", issuerURL, errors.Join(errs...)) } // RegisterClient performs Dynamic Client Registration according to RFC 7591. func RegisterClient(ctx context.Context, registrationEndpoint string, clientMeta *ClientRegistrationMetadata, c *http.Client) (*ClientRegistrationResponse, error) { if registrationEndpoint == "" { return nil, fmt.Errorf("registration_endpoint is required") } if c == nil { c = http.DefaultClient } payload, err := json.Marshal(clientMeta) if err != nil { return nil, fmt.Errorf("failed to marshal client metadata: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", registrationEndpoint, bytes.NewBuffer(payload)) if err != nil { return nil, fmt.Errorf("failed to create registration request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := c.Do(req) if err != nil { return nil, fmt.Errorf("registration request failed: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read registration response body: %w", err) } if resp.StatusCode == http.StatusCreated { var regResponse ClientRegistrationResponse if err := json.Unmarshal(body, ®Response); err != nil { return nil, fmt.Errorf("failed to decode successful registration response: %w (%s)", err, string(body)) } if regResponse.ClientID == "" { return nil, fmt.Errorf("registration response is missing required 'client_id' field") } return ®Response, nil } if resp.StatusCode == http.StatusBadRequest { var regError ClientRegistrationError if err := json.Unmarshal(body, ®Error); err != nil { return nil, fmt.Errorf("failed to decode registration error response: %w (%s)", err, string(body)) } return nil, ®Error } return nil, fmt.Errorf("registration failed with status %s: %s", resp.Status, string(body)) }