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 ¶
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.
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.
var ErrResponseTooLarge = errors.New("jwksclient: key set exceeds the size limit")
ErrResponseTooLarge is returned when a JWKS response exceeds MaxResponseBytes.
Functions ¶
func NewHTTPClient ¶
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 ¶
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 (*Client) Due ¶
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 ¶
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 ¶
RefreshFailure names one URI whose background refresh did not succeed.