Documentation
¶
Overview ¶
Package prm implements RFC 9728, OAuth 2.0 Protected Resource Metadata: the typed metadata document an OAuth 2.0 protected resource publishes about itself, its /.well-known/oauth-protected-resource endpoint, and the resource_metadata WWW-Authenticate challenge parameter that points clients at it.
A protected resource advertises which authorization servers it trusts, what scopes it exposes, and how it expects tokens, so a client can discover how to obtain a usable token without out-of-band configuration. This package provides the typed document with JSON round-tripping, both halves of the well-known endpoint — a server side that publishes the document and a client side that fetches and validates it — and a helper for the resource_metadata challenge parameter.
The library depends only on the standard library. Verifying the JWS signature of a signed_metadata document is out of scope: the package parses and exposes it, leaving signature verification to the caller and a JOSE library.
Index ¶
- Constants
- Variables
- func ChallengeParam(metadataURL string) (name, value string)
- func ServeMetadata(w http.ResponseWriter, m *Metadata) error
- func WellKnownPath(resource string) (string, error)
- func WellKnownRequestPath(resource string) (string, error)
- type HTTPError
- type HandlerOption
- type Metadata
- func (m *Metadata) GetExtra(name string, v any) (present bool, err error)
- func (m *Metadata) Handler(opts ...HandlerOption) http.Handler
- func (m *Metadata) Localized(member, tag string) (value string, ok bool)
- func (m Metadata) MarshalJSON() ([]byte, error)
- func (m *Metadata) ParseSignedMetadata() (*SignedMetadata, error)
- func (m *Metadata) SetExtra(name string, v any) error
- func (m *Metadata) UnmarshalJSON(data []byte) error
- func (m *Metadata) Validate() error
- type SignedMetadata
- type ValidationError
Examples ¶
Constants ¶
const ( BearerMethodHeader = "header" BearerMethodBody = "body" BearerMethodQuery = "query" )
Bearer token transmission methods for BearerMethodsSupported (RFC 9728 §2, RFC 6750 §2): in the Authorization header, the form-encoded body, or the URI query.
const ChallengeParamName = "resource_metadata"
ChallengeParamName is the WWW-Authenticate auth-param a protected resource uses to point a client at its metadata document (RFC 9728 §5.1).
const SpecVersion = "RFC 9728"
SpecVersion is the version of the specification this package targets.
const WellKnownPathSegment = "/.well-known/oauth-protected-resource"
WellKnownPathSegment is the well-known URI path under which a protected resource serves its metadata document (RFC 9728 §3.1). For a resource identifier with a path component, the resource's own path is appended after this segment; see WellKnownPath for the full construction rule.
Variables ¶
var ( // ErrResourceMismatch reports that the fetched document's resource value is // not identical to the requested resource identifier (RFC 9728 §3.3/§3.4). // The document MUST NOT be used; Fetch returns this and discards it. ErrResourceMismatch = errors.New("prm: fetched resource does not match requested resource identifier") // ErrUnexpectedStatus reports a non-200 HTTP status. The concrete error is an // *HTTPError carrying the status code. ErrUnexpectedStatus = errors.New("prm: unexpected HTTP status") // ErrInvalidResponse reports a 200 response whose body did not decode as a // metadata document. ErrInvalidResponse = errors.New("prm: invalid metadata response") )
Sentinel errors returned by Fetch and FetchMetadataURL. Match them with errors.Is; use errors.As with *HTTPError to read the status code.
var ErrInvalidSignedMetadata = errors.New("prm: invalid signed_metadata")
ErrInvalidSignedMetadata reports a signed_metadata value that is not a well-formed JWS Compact Serialization, is missing the required iss claim, or nests a signed_metadata claim of its own (RFC 9728 §2.2).
var ErrValidation = errors.New("prm: validation failed")
ErrValidation is the sentinel that every *ValidationError unwraps to, so a caller can match any validation failure with errors.Is(err, ErrValidation) or inspect the specifics with errors.As.
Functions ¶
func ChallengeParam ¶
ChallengeParam returns the WWW-Authenticate challenge parameter that points a client at the protected resource metadata document at metadataURL (RFC 9728 §5.1). It yields the bare (name, value) pair — ("resource_metadata", the URL) — for the caller to add to a challenge it is already building, for example a bearer-token library's challenge "extra" parameters:
name, value := prm.ChallengeParam(metadataURL) challenge.Extra[name] = value // serialized as resource_metadata="<url>"
The value is the raw URL. The challenge serializer is responsible for wrapping it in the double-quoted auth-param quoted-string the header grammar requires (RFC 9110 §11.2); when writing the header by hand, quote it:
WWW-Authenticate: Bearer resource_metadata="https://.../.well-known/oauth-protected-resource"
Keeping the seam to a plain string pair avoids a dependency on, and a duplicate of, any particular WWW-Authenticate challenge type. §5.1 permits the parameter with non-Bearer schemes (e.g. DPoP) and alongside other parameters.
Example ¶
package main
import (
"fmt"
prm "github.com/hstern/go-protected-resource-metadata"
)
func main() {
name, value := prm.ChallengeParam("https://resource.example.com/.well-known/oauth-protected-resource")
// A challenge serializer wraps the value in the quoted-string the header grammar wants.
fmt.Printf("%s=%q\n", name, value)
}
Output: resource_metadata="https://resource.example.com/.well-known/oauth-protected-resource"
func ServeMetadata ¶
func ServeMetadata(w http.ResponseWriter, m *Metadata) error
ServeMetadata writes m to w as a protected resource metadata response: it validates m (never serve an invalid document), sets Content-Type to application/json, and writes the JSON with status 200. It writes nothing and returns the error if m is invalid (a *ValidationError) or cannot be marshaled.
ServeMetadata is the à-la-carte primitive — it imposes no method handling or routing. Use it inside a handler you already run, or use Handler for a ready http.Handler. Mount either at the path from WellKnownRequestPath.
func WellKnownPath ¶
WellKnownPath returns the URL of the protected resource metadata document for a resource identifier, following the construction rule of RFC 9728 §3.1.
The well-known segment is inserted between the host and any path component of the resource identifier — it is not appended to the end:
https://resource.example.com -> https://resource.example.com/.well-known/oauth-protected-resource https://resource.example.com/resource1 -> https://resource.example.com/.well-known/oauth-protected-resource/resource1
A single terminating slash after the host is removed before insertion, so a bare host and a host with a trailing slash yield the same URL. A query component, if present, is preserved after the inserted path.
The resource identifier must be an https URL with no fragment (§2); otherwise WellKnownPath returns a *ValidationError.
Example ¶
package main
import (
"fmt"
prm "github.com/hstern/go-protected-resource-metadata"
)
func main() {
// The well-known segment is inserted before the resource's own path (§3.1).
u, _ := prm.WellKnownPath("https://resource.example.com/resource1")
fmt.Println(u)
}
Output: https://resource.example.com/.well-known/oauth-protected-resource/resource1
func WellKnownRequestPath ¶
WellKnownRequestPath returns the request path component at which a protected resource serves its metadata document — the server-side counterpart to WellKnownPath, suitable for registering a handler on an http.ServeMux. It is the path of the URL WellKnownPath produces (§3.1):
https://resource.example.com -> /.well-known/oauth-protected-resource https://resource.example.com/resource1 -> /.well-known/oauth-protected-resource/resource1
Any query component of the resource identifier is not part of the mount path and is omitted. The resource identifier must be an https URL with no fragment (§2); otherwise WellKnownRequestPath returns a *ValidationError.
Types ¶
type HTTPError ¶
type HTTPError struct {
StatusCode int
}
HTTPError is returned when the metadata endpoint responds with a non-200 status. It unwraps to ErrUnexpectedStatus.
type HandlerOption ¶
type HandlerOption func(*handlerConfig)
HandlerOption configures the http.Handler returned by Metadata.Handler.
func WithETag ¶
func WithETag(tag string) HandlerOption
WithETag sets the ETag header on the served document and enables 304 Not Modified responses to a matching If-None-Match request. The tag is wrapped in the double quotes an entity-tag requires if it is not already quoted.
func WithMaxAge ¶
func WithMaxAge(d time.Duration) HandlerOption
WithMaxAge sets a Cache-Control max-age on the served document (§7.10). A non-positive duration leaves Cache-Control unset.
type Metadata ¶
type Metadata struct {
// Resource is the protected resource's resource identifier (§2). REQUIRED.
// It is an https URL with no fragment.
Resource string `json:"resource"`
// AuthorizationServers lists the issuer identifiers of authorization
// servers that can be used with this protected resource (§2).
AuthorizationServers []string `json:"authorization_servers,omitempty"`
// JWKSURI is the URL of the protected resource's JWK Set document (§2).
JWKSURI string `json:"jwks_uri,omitempty"`
// ScopesSupported lists the scope values used in authorization requests to
// access this protected resource (§2). RECOMMENDED.
ScopesSupported []string `json:"scopes_supported,omitempty"`
// BearerMethodsSupported lists the supported methods of sending an OAuth 2.0
// bearer token to the resource (§2): some subset of BearerMethodHeader,
// BearerMethodBody, and BearerMethodQuery.
BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
// ResourceSigningAlgValuesSupported lists the JWS "alg" values supported by
// the resource for signing resource responses (§2).
ResourceSigningAlgValuesSupported []string `json:"resource_signing_alg_values_supported,omitempty"`
// ResourceName is a human-readable name of the protected resource intended
// for display to the end user (§2). RECOMMENDED. Internationalized variants
// are carried in Extra as "resource_name#<lang>"; read them with Localized.
ResourceName string `json:"resource_name,omitempty"`
// ResourceDocumentation is a URL of human-readable developer documentation
// for the protected resource (§2). Internationalizable; see Localized.
ResourceDocumentation string `json:"resource_documentation,omitempty"`
// ResourcePolicyURI is a URL of human-readable information about the
// resource's requirements on the client (§2). Internationalizable.
ResourcePolicyURI string `json:"resource_policy_uri,omitempty"`
// ResourceTOSURI is a URL of the resource's terms of service (§2).
// Internationalizable.
ResourceTOSURI string `json:"resource_tos_uri,omitempty"`
// TLSClientCertificateBoundAccessTokens indicates support for mutual-TLS
// client-certificate-bound access tokens (§2, RFC 8705). Absent means false.
TLSClientCertificateBoundAccessTokens bool `json:"tls_client_certificate_bound_access_tokens,omitempty"`
// AuthorizationDetailsTypesSupported lists the authorization_details type
// values supported by the resource server (§2, RFC 9396).
AuthorizationDetailsTypesSupported []string `json:"authorization_details_types_supported,omitempty"`
// DPoPSigningAlgValuesSupported lists the JWS "alg" values supported by the
// resource server for validating DPoP proof JWTs (§2, RFC 9449).
DPoPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"`
// DPoPBoundAccessTokensRequired indicates that the resource server always
// requires DPoP-bound access tokens (§2, RFC 9449). Absent means false.
DPoPBoundAccessTokensRequired bool `json:"dpop_bound_access_tokens_required,omitempty"`
// SignedMetadata is a JWT (JWS Compact Serialization) whose claims restate
// metadata parameters about the protected resource (§2.2). This library
// surfaces it verbatim; verifying its signature is the caller's job.
SignedMetadata string `json:"signed_metadata,omitempty"`
// Extra holds members not captured by the typed fields above —
// service-specific extensions, future registrations, and the §2.1
// "name#lang" internationalized variants. Values are kept as raw JSON for
// byte-stable round-trips and zero-cost pass-through.
Extra map[string]json.RawMessage `json:"-"`
}
Metadata is an RFC 9728 §2 OAuth 2.0 Protected Resource Metadata document.
Resource is the only REQUIRED member; every other field is optional and is omitted from the wire when unset. Service-specific and future-registered members — including the internationalized "name#lang" variants of §2.1 — are preserved verbatim in Extra for byte-stable round-trips.
Decoding is liberal (Postel's law): UnmarshalJSON accepts whatever the wire provides. Strict checks are opt-in via Validate. The resource-match check of §3.3/§3.4 is a fetch-time concern, not part of Validate.
Example (Marshal) ¶
package main
import (
"encoding/json"
"fmt"
prm "github.com/hstern/go-protected-resource-metadata"
)
func main() {
m := &prm.Metadata{
Resource: "https://resource.example.com",
ScopesSupported: []string{"profile", "email"},
}
b, _ := json.Marshal(m)
fmt.Println(string(b))
}
Output: {"resource":"https://resource.example.com","scopes_supported":["profile","email"]}
func Fetch ¶
Fetch retrieves and validates the protected resource metadata for a resource identifier. It builds the well-known URL with WellKnownPath (§3.1), performs a GET over c (defaulting to http.DefaultClient), decodes the JSON body, and enforces the §3.3/§3.4 anti-mix-up check: the returned resource value MUST be identical to resource. On mismatch it returns ErrResourceMismatch and does not return the document.
The comparison is code-point exact (§6); pass resource in the same canonical form the protected resource publishes. Transport security (TLS per BCP 195) is the responsibility of c.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
prm "github.com/hstern/go-protected-resource-metadata"
)
func main() {
// A protected resource publishing its metadata at the well-known path.
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = prm.ServeMetadata(w, &prm.Metadata{
Resource: "https://" + r.Host,
ScopesSupported: []string{"read"},
})
}))
defer srv.Close()
m, err := prm.Fetch(context.Background(), srv.Client(), srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(m.ScopesSupported[0])
}
Output: read
func FetchMetadataURL ¶
func FetchMetadataURL(ctx context.Context, c *http.Client, metadataURL, expectedResource string) (*Metadata, error)
FetchMetadataURL retrieves and validates a metadata document from a metadata URL obtained out of band — typically the resource_metadata parameter of a WWW-Authenticate challenge (RFC 9728 §5.1). Per §3.3, when the URL came from such a challenge the returned resource value MUST equal the URL the client used to call the resource server; pass that as expectedResource. On mismatch it returns ErrResourceMismatch.
The comparison is code-point exact (§6).
func (*Metadata) GetExtra ¶
GetExtra unmarshals the extension member named name into v, which must be a non-nil pointer. It reports whether the member was present; a missing member is not an error (present == false, err == nil).
Only members not captured by a typed field land in Extra, so GetExtra is the way to read extension members — including the §2.1 "name#lang" variants — byte-for-byte as the server sent them.
func (*Metadata) Handler ¶
func (m *Metadata) Handler(opts ...HandlerOption) http.Handler
Handler returns an http.Handler that serves the metadata document m. It answers GET and HEAD with the JSON document (200) and rejects other methods with 405 and an Allow header. The document is validated and marshaled once, when Handler is called; if m is invalid the handler responds 500 to every request — call m.Validate yourself first if you want to detect that up front.
Register the handler at WellKnownRequestPath(m.Resource):
path, err := prm.WellKnownRequestPath(m.Resource) // handle err mux.Handle(path, m.Handler(prm.WithMaxAge(time.Hour)))
Options configure HTTP caching (§7.10): WithMaxAge sets Cache-Control and WithETag sets an ETag and answers a matching If-None-Match with 304.
Example ¶
package main
import (
"fmt"
"net/http"
"time"
prm "github.com/hstern/go-protected-resource-metadata"
)
func main() {
m := &prm.Metadata{Resource: "https://resource.example.com"}
path, _ := prm.WellKnownRequestPath(m.Resource)
mux := http.NewServeMux()
mux.Handle(path, m.Handler(prm.WithMaxAge(time.Hour)))
// http.ListenAndServe(":8080", mux)
fmt.Println(path)
}
Output: /.well-known/oauth-protected-resource
func (*Metadata) Localized ¶
Localized returns the value of a human-readable member for a requested BCP 47 language tag (RFC 9728 §2.1). member is the member's JSON name, e.g. "resource_name". Localized looks for a tagged variant "member#tag" among the extension members, matching the language tag case-insensitively, and falls back to the untagged member value. ok reports whether any value — tagged or untagged — was found.
An empty tag requests the untagged value directly.
Example ¶
package main
import (
"encoding/json"
"fmt"
prm "github.com/hstern/go-protected-resource-metadata"
)
func main() {
m := &prm.Metadata{
ResourceName: "Example Protected Resource",
Extra: map[string]json.RawMessage{
"resource_name#fr": json.RawMessage(`"Ressource protégée"`),
},
}
fr, _ := m.Localized("resource_name", "fr")
de, _ := m.Localized("resource_name", "de") // no "de" variant: untagged fallback
fmt.Println(fr)
fmt.Println(de)
}
Output: Ressource protégée Example Protected Resource
func (Metadata) MarshalJSON ¶
MarshalJSON serializes the typed members and merges Extra back in. Typed members win on key collision. Output is byte-stable: with no extension members the typed members serialize in their declared order; with extensions the whole object serializes in encoding/json's sorted-key order. Either way a given Metadata value always marshals to the same bytes.
func (*Metadata) ParseSignedMetadata ¶
func (m *Metadata) ParseSignedMetadata() (*SignedMetadata, error)
ParseSignedMetadata parses m.SignedMetadata (RFC 9728 §2.2) without verifying its signature. It returns (nil, nil) when no signed_metadata is present.
func (*Metadata) SetExtra ¶
SetExtra marshals v and stores it as the extension member named name. It returns an error if name collides with a member that has its own typed field — set those through the field instead — or if v cannot be marshalled.
func (*Metadata) UnmarshalJSON ¶
UnmarshalJSON decodes the typed members and routes every other member of the JSON object into Extra.
func (*Metadata) Validate ¶
Validate reports whether the metadata document satisfies the structural requirements of RFC 9728 §2. It returns the first failure as a *ValidationError (matchable with errors.Is(err, ErrValidation)), or nil.
Validate is document-structural only and is independent of how the document was obtained. The §3.3/§3.4 requirement that a fetched document's resource equal the requested resource identifier is a fetch-time check, not part of Validate.
type SignedMetadata ¶
type SignedMetadata struct {
// Token is the original compact JWT string.
Token string
// Header is the decoded JWS protected header (alg, kid, typ, …).
Header map[string]json.RawMessage
// Issuer is the iss claim, REQUIRED by §2.2.
Issuer string
// Metadata is the protected resource metadata asserted by the JWT claims.
Metadata *Metadata
// SigningInput is the exact byte sequence the signature covers
// (base64url(header) + "." + base64url(payload)), for the caller's verifier.
SigningInput []byte
// Signature is the decoded JWS signature bytes.
Signature []byte
}
SignedMetadata is the parsed — but NOT signature-verified — content of a signed_metadata JWT (RFC 9728 §2.2). The library decodes the JWS Compact Serialization and exposes its parts so the caller can verify the signature with a JOSE library and a key belonging to the issuer; verifying the signature is deliberately out of scope (the boundary the rest of the suite draws for JOSE).
func ParseSignedMetadata ¶
func ParseSignedMetadata(token string) (*SignedMetadata, error)
ParseSignedMetadata parses a signed_metadata JWT (RFC 9728 §2.2) WITHOUT verifying its signature. It decodes the protected header, the metadata claims, and the iss claim, and exposes the signing input and signature for the caller to verify. It returns an error wrapping ErrInvalidSignedMetadata if token is not a three-part JWS Compact Serialization, if any part is not valid base64url/JSON, if the required iss claim is missing, or if the claims nest a signed_metadata of their own (§2.2 RECOMMENDED rejection).
Verifying the signature against a trusted issuer key is the caller's responsibility; only after that should the signed values be trusted (see SignedMetadata.Apply for the §2.2 precedence merge).
func (*SignedMetadata) Apply ¶
func (sm *SignedMetadata) Apply(base *Metadata) (*Metadata, error)
Apply returns a copy of base with the signed metadata's members overlaid, the signed values taking precedence on a per-member basis as RFC 9728 §2.2 requires. base is not modified. A member the signed metadata does not carry is left as base has it.
Call Apply only AFTER verifying the signature (SigningInput, Signature, Header) against a key belonging to a trusted Issuer. Applying unverified signed values would defeat the point of signing them.
type ValidationError ¶
type ValidationError struct {
// Field is the member at fault, e.g. "resource".
Field string
// Message explains the problem, lowercase and without trailing punctuation.
Message string
}
ValidationError reports a metadata member that does not satisfy an RFC 9728 requirement — a missing required parameter, a non-https resource identifier, an unsupported bearer method, and the like. It names the offending member so a caller can report a precise problem.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
func (*ValidationError) Unwrap ¶
func (e *ValidationError) Unwrap() error
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
specfixtures
Package specfixtures holds spec-derived conformance vectors for RFC 9728.
|
Package specfixtures holds spec-derived conformance vectors for RFC 9728. |