Skip to main content

Crate cratefield_oauth_client

Crate cratefield_oauth_client 

Source
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.

  • ProviderConfig names 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).
  • OAuthClient speaks the token endpoint: authorization-code exchange (with PKCE, RFC 7636, when the provider supports it), refresh, and RFC 7009 revocation. Failures come back as OAuthError, which carries the RFC 6749 §5.2 error / error_description pair and the raw body, because providers put provider-specific meaning in that prose.
  • TokenSealer is how a caller stores what came back. The trait is the future seam for the secrets store; XChaChaSealer is 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_refresh is the whole 401 policy: one refresh, one replay, never a loop.
  • device speaks RFC 8628 device authorization: request_code gets a user_code the human types at the verification URI, and poll waits out the interval (honouring slow_down) until the venture issues its credential. The credential is the venture’s own contract, so poll returns 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 at verification_uri, and the client polls the token endpoint until the venture issues the credential.

Structs§

OAuthClient
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.
ProviderConfig
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.
ProviderError
The provider’s answer when a token call fails. code and description are the RFC 6749 §5.2 error / error_description pair when the body was JSON; body is 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.
RandomError
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.
SealContext
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}".
TokenResponse
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_in must not become an error the caller has to defend against.
XChaChaSealer
The default TokenSealer: XChaCha20-Poly1305 under one key, writing base64url(version || key id || nonce || ciphertext).

Enums§

ClientAuth
How client credentials are presented at the token endpoint (RFC 6749 §2.3).
OAuthError
What went wrong with an OAuth call, in the shape a caller branches on.
SealError
SendWithRefreshError
What a send_with_refresh call 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 XChaChaSealer takes. Exported so a caller’s config validation can name it.

Traits§

TokenSealer
Stores OAuth tokens as opaque strings. Whatever the implementation, the ciphertext is bound to the SealContext it 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 a code_challenge it 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.