Help us shape the future of Go! Take the 2026 Go Developer Survey

jwksclient

package
v1.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package jwksclient fetches and caches JSON Web Key Sets for inbound token verification. Every limit in here exists because the fetch is triggered by traffic we do not control.

Index

Constants

This section is empty.

Variables

View Source
var ErrFetchFailed = errors.New("jwksclient: key set fetch failed")

ErrFetchFailed is returned when we could not load the key set at all: the endpoint was unreachable, answered non-200, sent something we could not parse, or was refused by the URI/dial guards. It says nothing about the token, only about our own ability to check it right now, so a caller must answer with something that invites a retry rather than a permanent rejection. Every error surfaced from a failed fetch wraps this.

View Source
var ErrKeyNotFound = errors.New("jwksclient: no key for kid")

ErrKeyNotFound is returned when the key set loaded cleanly and simply does not carry the wanted kid. It is a statement about the IdP's published keys, so a caller may safely treat it as a permanent rejection of the token.

View Source
var ErrResponseTooLarge = errors.New("jwksclient: key set exceeds the size limit")

ErrResponseTooLarge is returned when a JWKS response exceeds MaxResponseBytes.

Functions

func NewHTTPClient

func NewHTTPClient(timeout time.Duration) *http.Client

NewHTTPClient returns a client for fetching documents an inbound request named: JWKS sets, OIDC discovery, provider userinfo. It refuses to dial loopback, private, link-local and unspecified addresses at connect time, which catches both a hostname that resolves to an internal address and a DNS answer that changed since the URL was validated; it follows at most two same-host https redirects; and it gives up after timeout. Pair it with ValidateURI, which rejects the obvious cases before any network work.

func ValidateURI

func ValidateURI(rawURL string) error

ValidateURI is a cheap pre-filter, run when a JWKS URI is accepted and stored on a stream. It refuses plain HTTP and a host that is a literal private, loopback or link-local IP address (the obvious cases: someone pastes "https://169.254.169.254/..." directly). It does NOT resolve hostnames, so it cannot catch "https://localhost/keys" or "https://metadata.google.internal/keys" -- a bare hostname always fails net.ParseIP and skips the whole check. It also cannot catch DNS rebinding, where a hostname resolves to a public address now and an internal one later. Neither gap matters for what actually reaches the network: the default HTTP client's dialer (see guardDialAddress) inspects the resolved address immediately before every connection, after DNS resolution, and is what actually enforces the address policy for real traffic.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client caches one key set per JWKS URI.

func New

func New(opts Options) *Client

New builds a Client, filling in the hardened defaults for any zero option.

func (*Client) Due

func (c *Client) Due() int

Due reports how many cached key sets RefreshAll would fetch right now: those whose last fetch is older than MinRefetchInterval and that no other goroutine is already fetching. A background round that finds none has no work to do.

func (*Client) Key

func (c *Client) Key(ctx context.Context, jwksURI, kid string) (crypto.PublicKey, error)

Key returns the public key for kid from the set at jwksURI. A cache miss, or a hit older than MaxKeyAge, triggers at most one fetch per MinRefetchInterval per URI, so neither an unknown kid nor an aged entry can be used to drive outbound traffic.

A key that is still cached is returned even when the refresh that was supposed to confirm it failed. That is deliberate and it is the one place this file trades a little freshness for availability: a successful refresh replaces the entry wholesale, so a retired kid does disappear, while a transient 503 on the IdP's side must not blind the receiver to a compromise event signed with a key we already hold and already trust.

That trade has a ceiling. Past MaxKeyUsableAge the cached key is refused even though the refresh failed, and the error stays wrapped in ErrFetchFailed so the caller answers "we cannot verify right now" rather than a permanent rejection. A successful refresh past that age is not affected: it replaces the entry, and a kid missing from the set the IdP just served is still ErrKeyNotFound, which is the token's problem.

func (*Client) RefreshAll

func (c *Client) RefreshAll(ctx context.Context) []RefreshFailure

RefreshAll re-fetches every JWKS URI this client has cached and returns the ones it could not reach. It is the background half of key freshness: without it, an IdP's key rotation is only ever noticed by an inbound token unlucky enough to arrive after MaxKeyAge, and an entry drifts towards MaxKeyUsableAge on quiet streams that receive nothing for hours.

It refreshes what is already cached rather than what is registered, which is the same set for this purpose: an entry can only age out if it exists, and it only exists because a token used it. A cold cache after a restart therefore has nothing to keep warm, and the first inbound token pays for the fetch exactly as it always did.

Fetches go through refresh, so the MinRefetchInterval rate guard still applies and a ticker configured too tightly cannot turn into a hammer on somebody else's endpoint. Being inside that window is not a failure -- only an error that reached the network and came back wrong is reported.

type Options

type Options struct {
	HTTPClient         *http.Client
	MinRefetchInterval time.Duration
	MaxResponseBytes   int64
	MaxKeys            int
	// MaxKeyAge bounds how long a cached key set is served without going
	// back to the IdP. Without it, a kid that is present in the cache is
	// trusted forever and an IdP that pulls a compromised signing key from
	// its JWKS never causes us to stop honouring it, because only an
	// UNKNOWN kid triggers a refetch. Checking the age inside Key is
	// simpler than a background goroutine and needs no lifecycle of its
	// own: the next inbound token pays for the refresh.
	MaxKeyAge time.Duration
	// MaxKeyUsableAge is the hard ceiling above MaxKeyAge's soft staleness,
	// and it is the only limit in here that can refuse a key.
	//
	// MaxKeyAge only ever ASKS for a refresh; freshness is restored solely by
	// a fetch that succeeds. So an IdP that retires a compromised signing key
	// and then becomes unreachable -- an outage, or an attacker holding that
	// endpoint down precisely to preserve the retired key -- leaves us
	// honouring it for as long as the process lives. A SET is an instruction
	// to revoke somebody's sessions, so that is forged revocations honoured
	// indefinitely.
	//
	// Past this age the entry stops being usable even though we cannot
	// replace it, which turns the failure mode from "we trust a key the IdP
	// withdrew" into "we cannot verify right now". The caller sees
	// ErrFetchFailed and answers 503, so the transmitter keeps retrying.
	MaxKeyUsableAge time.Duration
	Now             func() time.Time
	// ValidateURI gates every fetch. Defaults to the package-level
	// ValidateURI. Tests substitute a permissive validator so they can serve
	// a key set from an httptest loopback server; production must not.
	ValidateURI func(rawURL string) error
}

Options configures a Client.

type RefreshFailure

type RefreshFailure struct {
	URI string
	Err error
}

RefreshFailure names one URI whose background refresh did not succeed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL