huskarl 0.9.0

A modern OAuth2 client library.
# How a grant token source resolves a token

A [`GrantTokenSource`](crate::cache::GrantTokenSource) turns three possible
inputs — a primed token, a stored refresh token, and a grant-parameter source —
into a single token, and decides what to keep or abandon when one of them fails.
The [reference docs](crate::cache::GrantTokenSource) state the rules; this page
explains why they are the way they are.

## Why refresh comes before the parameter source

Each [`token`](crate::cache::TokenSource::token) call tries, in order: a primed
token, a refresh of the stored refresh token, then a fresh exchange using the
configured [`GrantParametersSource`](crate::cache::GrantParametersSource).

The parameter source is consulted **last**, only after the refresh attempt,
because producing parameters can be expensive or lossy — a
[`from_fn`](crate::cache::from_fn) source re-signs a JWT-bearer assertion, a
[`single_use`](crate::cache::single_use) source is spent by reading it. A usable
refresh token should avoid that work entirely, so it is tried first.

## Why credentials are abandoned

Two inputs are dropped once they provably cannot succeed again, so the source
stops replaying a dead credential on every call:

- The **refresh token** is discarded on `invalid_grant` — the server has
  rejected it, and a rejected refresh token never becomes valid again.
- A **fixed parameter source** is spent on `invalid_grant`, *provided it opts in*
  via
  [`discard_after_rejection`]crate::cache::GrantParametersSource::discard_after_rejection.
  A fixed value that was rejected is dead (true for concrete values); a
  [`from_fn`]crate::cache::from_fn source is *not* spent, because its next value
  may differ (a freshly minted assertion). A
  [`single_use`]crate::cache::single_use source is consumed on first use and
  never replayed regardless.

Because a spent fixed source's rejected value cannot become valid again, neither
[`prime`](crate::cache::GrantTokenSource::prime) nor
[`clear`](crate::cache::TokenSource::clear) revives it. To supply a fresh
credential, build a new source or use a [`from_fn`](crate::cache::from_fn) source,
which mints a new value per exchange and is never spent.

A **request-shape rejection**
([`RequestRejected`](crate::core::ErrorKind::RequestRejected): `invalid_scope`,
`invalid_target`, `invalid_resource`) is deliberately *not* treated as a
credential failure. The credential is fine; the *request* asked for something the
server would not grant, so the source is kept and the error surfaced unchanged,
letting a caller retry with a narrower request using the same credential.

The source returns
[`ReauthRequired`](crate::core::ErrorKind::ReauthRequired) **only when no
automatic recovery path remains** — a retryable transport failure, a retained
refresh token, a request-shape rejection, or a still-live dynamic source each
keeps its own classification instead, so an application only forces a user back
through login when nothing else can recover.

## Why there is a backoff breaker

Some sources keep failing non-recoverably from scratch — most often a
[`from_fn`](crate::cache::from_fn) re-signing against a revoked key: every fresh
value is rejected with `invalid_grant`, the source is never spent (its next value
*might* differ), and so each call hits the signer and the token endpoint again.
Without a bound this is a hot loop against your own signer and the authorization
server.

A breaker caps it: after `breaker_threshold` consecutive non-recoverable failures
(transient and request-shape failures don't count) the source backs off for
`breaker_cooldown`, then allows one trial per cooldown until it succeeds. Any
success, or a fresh [`prime`](crate::cache::GrantTokenSource::prime), resets it.

The breaker gates only the **from-scratch exchange** — a refresh is still
attempted first on every call, so a usable refresh token always recovers
independently. While the breaker is open the from-scratch path short-circuits
with [`GetTokenError::Backoff`](crate::cache::GetTokenError) under
[`Backoff`](crate::core::ErrorKind::Backoff), without re-running the signer or the
exchange.

That [`Backoff`](crate::core::ErrorKind::Backoff) is deliberately not
[`ReauthRequired`](crate::core::ErrorKind::ReauthRequired): the failure is a dead
credential the *application* configured, not something a user can fix by logging
in again, so the caller should retry on a delay rather than bouncing the user
through an interactive login.