huskarl 0.9.1

A modern OAuth2 client library.
Documentation
# Making authenticated requests

[`HttpAuthorizer`](crate::authorizer::HttpAuthorizer) turns the token machinery
(grant, cache, `DPoP`) into request headers:
[`get_headers`](crate::authorizer::HttpAuthorizer::get_headers) builds the
authorization headers for a request — exchanging or refreshing tokens as
needed — and
[`process_response`](crate::authorizer::HttpAuthorizer::process_response)
records what each response reveals.

## The request loop

1. Build headers with
   [`get_headers`]crate::authorizer::HttpAuthorizer::get_headers and send the
   request with your HTTP client.
2. Pass every response's headers — success or failure — to
   [`process_response`]crate::authorizer::HttpAuthorizer::process_response.
3. On a `401 Unauthorized`, rebuild the headers and re-send **once** if your API
   semantics allow: step 2 already recorded any demanded `DPoP` nonce and
   dropped a token the server rejected, so the rebuilt headers carry the fix if
   there is one. A second `401` is definitive.

```rust
# use huskarl::authorizer::{HttpAuthorizer, parse_challenges};
# use http::{HeaderMap, Method, StatusCode, Uri};
# struct Response { status: StatusCode, headers: HeaderMap }
# async fn send(_headers: HeaderMap) -> Response {
#     Response { status: StatusCode::OK, headers: HeaderMap::new() }
# }
# async fn example(authorizer: &HttpAuthorizer) -> Result<(), Box<dyn std::error::Error>> {
let uri: Uri = "https://api.example.com/v1/widgets".parse()?;

let headers = authorizer.get_headers(&Method::GET, &uri).await?;
let mut response = send(headers).await;
authorizer.process_response(&uri, &response.headers);

if response.status == StatusCode::UNAUTHORIZED {
    // Optional: the WWW-Authenticate challenges say what the server
    // objected to. `insufficient_scope` needs broader authorization —
    // re-sending cannot fix it.
    let scope_problem = parse_challenges(&response.headers)
        .iter()
        .any(|challenge| challenge.error() == Some("insufficient_scope"));

    if !scope_problem {
        let headers = authorizer.get_headers(&Method::GET, &uri).await?;
        response = send(headers).await;
        authorizer.process_response(&uri, &response.headers);
    }
}
# drop(response);
# Ok(())
# }
```

Whether and when to re-send is the application's decision, not this library's —
[`parse_challenges`](crate::authorizer::parse_challenges) exposes the server's
stated objection for making it, as above. A `401` is issued before the request
is processed, so a single re-send is normally safe even for non-idempotent
requests.

## When the server doesn't emit a spec-correct challenge

Step 2's automatic token invalidation works only when the server emits a
spec-correct `invalid_token` challenge (RFC 6750 §3.1), and not all do. You know
your server better than this library can: when a bare `401`, a JSON error body,
or a custom convention tells you the token is bad, call
[`invalidate`](crate::authorizer::HttpAuthorizer::invalidate) yourself before
re-sending. Treating any `401` as a stale token is a common policy, at the cost
of an occasional unnecessary refresh.

[`process_response`](crate::authorizer::HttpAuthorizer::process_response) acts on
the headers alone and ignores the status code. That is normally fine, since a
spec-correct `invalid_token` challenge always accompanies a `401`. The one trap
is relaying: if your service copies an upstream server's `WWW-Authenticate` onto
its own response, that header reflects *the upstream's* view of *its* token, not
yours — passing it here would wrongly invalidate your token. Give
[`process_response`](crate::authorizer::HttpAuthorizer::process_response) only
the headers that describe your own request.