Expand description
OAuth 2.0 for the harness’s modules and adapters (issue #531).
Three-legged OAuth is the same shape everywhere and is still easy to get
wrong in ways that matter: a scope list that is not encoded, a refresh
that loops, a token pasted into a log. This crate speaks the protocol
once, over the HttpClient port, so the
code runs unchanged on Workers and native, and the provider is
configuration (ProviderConfig) rather than a client type per brand.
Sealing what comes back is a trait, TokenSealer, so moving storage to
the secrets store later is a swap of implementations, not a rewrite of
every module: fz-module-linkedin seals today with XChaChaSealer
(ADR 0102), and the blob format it writes is the one that has to keep
opening.
wasm-safe. No std::net, no wall clock, no threads: HTTP goes
through the HttpClient port and randomness through getrandom, whose
wasm backend the manifest enables on wasm32.
§cratefield-oauth-client
OAuth 2.0 for the harness (issue #531): the authorize URL, the code
exchange, the refresh and the revocation, spoken once over the harness’s
HttpClient port — so a module runs it
unchanged on Workers and native. No provider SDK, no discovery document:
the provider’s three URLs are configuration.
ProviderConfignames the endpoints, the client credentials, the scopes, and where the credentials go on token calls (form body, or HTTP Basic per RFC 6749 §2.3.1).OAuthClientspeaks the token endpoint: authorization-code exchange (with PKCE, RFC 7636, when the provider supports it), refresh, and RFC 7009 revocation. Failures come back asOAuthError, which carries the RFC 6749 §5.2error/error_descriptionpair and the raw body, because providers put provider-specific meaning in that prose.TokenSealeris how a caller stores what came back. The trait is the future seam for the secrets store;XChaChaSealeris the implementation the harness already chose (ADR 0102): XChaCha20-Poly1305, a fresh nonce per encryption, the ciphertext bound to its row and column.send_with_refreshis the whole 401 policy: one refresh, one replay, never a loop.devicespeaks RFC 8628 device authorization:request_codegets auser_codethe human types at the verification URI, andpollwaits out the interval (honouringslow_down) until the venture issues its credential. The credential is the venture’s own contract, sopollreturns the raw JSON —poll_as::<T>when the caller has a type.
let client = OAuthClient::new(http, &config);
let tokens = client.exchange_code(&code, &redirect_uri, None).await?;
let kept = tokens.rotated_refresh_token(&previous_refresh_token);Device authorization (RFC 8628), for the client that has no browser — a CLI
or a screen. The human approves on another device; the poll runs on clock,
so it obeys the server’s interval without a timer of its own:
let auth = request_code(http, "https://api.example.com/v1/device-auth/code",
"client-1", Some("read"), Some("My CLI")).await?;
println!("Go to {} and enter {}", auth.verification_uri, auth.user_code);
let credential: Credential = poll_as(http, clock, "https://api.example.com/v1/device-auth/token",
"client-1", &auth).await?;wasm-safe: randomness comes from getrandom (wasm backend enabled on
wasm32), time stays in the caller, HTTP goes through the port.
Modules§
- device
- Device authorization (RFC 8628): a client with no browser asks for a
user_code, the human types it atverification_uri, and the client polls the token endpoint until the venture issues the credential.
Structs§
- OAuth
Client - Speaks one provider’s token endpoint over an
HttpClient. Cheap to build, holds nothing but the two references it is given. - Pkce
- An S256 proof-key pair (RFC 7636 §4): a verifier kept for the token exchange and the challenge derived from it for the authorize URL.
- Provider
Config - One OAuth 2.0 provider, as far as this crate needs to know it. Built once per call site (the credentials usually come from config), never global.
- Provider
Error - The provider’s answer when a token call fails.
codeanddescriptionare the RFC 6749 §5.2error/error_descriptionpair when the body was JSON;bodyis the raw response either way, because providers put provider-specific meaning in that prose — LinkedIn names a dead refresh token only in the description, never in the code. - Random
Error - The OS refused randomness. A verifier or a state that is not fresh is worth exactly nothing, so this is an error and never a fallback.
- Seal
Context - Where a sealed value belongs: one column of one row of one namespace.
Rendered to the additional authenticated data as
"{namespace}/{table}/{row_id}/{column}". - Token
Response - The token endpoint’s answer (RFC 6749 §5.1), with the fields providers
actually add. Everything but the access token is optional or defaulted:
providers omit what does not apply, and a missing
expires_inmust not become an error the caller has to defend against. - XCha
ChaSealer - The default
TokenSealer: XChaCha20-Poly1305 under one key, writingbase64url(version || key id || nonce || ciphertext).
Enums§
- Client
Auth - How client credentials are presented at the token endpoint (RFC 6749 §2.3).
- OAuth
Error - What went wrong with an OAuth call, in the shape a caller branches on.
- Seal
Error - Send
With Refresh Error - What a
send_with_refreshcall can end as. The replay’s own outcome — whatever status the provider answered it with — is a success here: a 401 on the replay is data for the caller’s token layer, not this helper’s error to name.
Constants§
- KEY_LEN
- The length in bytes of the key
XChaChaSealertakes. Exported so a caller’s config validation can name it.
Traits§
- Token
Sealer - Stores OAuth tokens as opaque strings. Whatever the implementation, the
ciphertext is bound to the
SealContextit was sealed for.
Functions§
- authorize_
url - The authorization URL a human’s browser opens: the provider’s authorize
endpoint with
response_type=code, the client, the redirect, the state and the scopes (RFC 6749 §4.1.1), plus the PKCE pair when the caller is handed one. No PKCE where the provider does not document it — LinkedIn does not, and acode_challengeit ignores is fine, but one it rejects is a connect that never worked, so the caller decides. - random_
state - A fresh opaque
state(RFC 6749 §10.12): base64url of 16 random bytes. Whatever else a caller binds to the state — a signed token, a database row — freshness starts here. - send_
with_ refresh - Sends a bearer-authenticated request; if the provider answers 401, refreshes the token once and replays the request with it.