Authorization integration pattern

External provider hand-offs use one shared tenant-safe pattern. Google Search Console is the reference implementation; future OAuth, installation, and signed authorization flows should reuse the same state primitive.

Required invariants

  1. Authenticated initiation: only a principal with the provider's connect permission can start.
  2. Subject ownership: verify the target site or resource with both account_id and subject ID.
  3. Opaque state: send 32 random bytes as base64url; do not encode account or subject IDs.
  4. Hash-only persistence: store only SHA-256(raw_state).
  5. Provider binding: bind state to provider, tenant, subject, redirect URI, metadata, and expiry.
  6. One use: consume with an atomic update and reject expiry, replay, malformed input, and wrong-provider state identically.
  7. Public callback, private authority: callbacks need no bearer header; valid state is their only authority.
  8. Exact redirect: exchange the code with the redirect URI recovered from state.
  9. Server-only credentials: never return or log client secrets, codes, refresh tokens, access tokens, ciphertext, or nonces.
  10. Inert completion page: return generic no-store HTML with no request-controlled redirect.

Shared runtime and table

crates/crawlkit-api/src/oauth_state.rs
public.oauth_authorization_states
FieldPurpose
token_hashUnique 32-byte SHA-256 hash; raw state is never stored.
account_idAuthenticated tenant that initiated authorization.
providerStable identifier such as google_search_console.
subject_type, subject_idTenant-owned resource receiving the connection.
redirect_uriExact URI used for authorization and token exchange.
metadataNon-secret provider context.
expires_at, consumed_atTen-minute expiry and atomic replay protection.

RLS is enabled. Browser roles cannot inspect or modify state bindings.

Standard flow

Authenticated client
  → POST provider /auth/url with tenant-owned subject
  → oauth_state::issue(...)
  → provider authorization URL + opaque state

Provider
  → GET public callback?code=...&state=...
  → oauth_state::consume(provider, state)
  → recover tenant + subject + exact redirect URI
  → exchange code server-side
  → encrypt/store durable credentials
  → optional provider-resource discovery
  → no-store completion page

Provider implementation checklist

  1. Choose lowercase provider and subject identifiers.
  2. Define distinct configure, connect, read, and submit permissions.
  3. Verify subject ownership before issuing state.
  4. Call oauth_state::issue and append the returned state to the provider URL.
  5. Merge the GET callback outside the bearer-authenticated router.
  6. Call oauth_state::consume, confirm subject type, and use the stored redirect URI.
  7. Encrypt durable client secrets with provider/account/client domain-separated AES-GCM AAD.
  8. Return redacted status only.
  9. Add OpenAPI, SDK/agent wrappers, setup and usage guides, and callback security tests.

Required tests

Do not

For implementation-level detail and non-OAuth adaptations, see docs/guides/authorization-integration-pattern.md in the CrawlKit repository.

PreviousUse GSC NextAPI Authentication