Skip to content

Managed tracker OAuth storage

preloop.models.crud.crud_managed_oauth owns the provider-neutral storage contract. Import model types through from preloop.models import models. Provider services, callbacks, HTTP refresh clients and console flows are separate work. Existing GitHub user tokens and pasted oauth_token trackers remain unmanaged; the migration does not convert credentials.

Configuration and connection

Create an account-scoped OAuthProviderConfiguration with provider, canonical HTTPS instance (including a DC context path), context, client ID, optional client secret, exact callback URI and selected permissions. Consumer secrets use dedicated tenant-owned encrypted SecretReference rows. Grant tokens remain encrypted in OAuthToken, never copied into tracker api_key or SecretReference. Configuration metadata excludes the secret reference and reports only a has_client_secret readiness boolean. Provider/instance/context are immutable consumer identity; use a new configuration to change them. Credential/callback/permission replacement increments the version, invalidates old handshakes (including completed callback retries), erases old grant secrets, disables their trackers and requires reconnect. Omitted enabled preserves the configuration's existing state. Unsupported tracker providers are rejected at configuration creation. It does not delete the provider consumer.

begin_connection returns metadata and a cryptographically random state once. Storage contains hashes of state and session identity and an optional encrypted PKCE verifier, with a ten-minute UTC expiry. The caller supplies an authenticated account/user and stable login-session identity. claim_callback verifies that binding and the exact callback URI, consumes the state once, and returns an explicit internal-only CallbackSecrets value for provider I/O. PKCE is erased when claimed. A failed exchange requires a new connection transaction.

After exchanging the code, call store_pending_grant with provider subject and a TokenPair. Services derive timezone-aware UTC expiry from expires_in; naive expiry is rejected. complete_connection creates a tracker or reconnects an existing managed tracker. Same-owner completion retries return its original tracker, including after expiry. Reconnect to an attached grant requires its current rotation version and configuration identity. A disconnected tracker takes a new grant; its old grant ID remains an unusable tombstone. Tenant equality is also enforced by composite database foreign keys. Workspace identities never need an OAuth app installation.

Rotation and lifecycle

Every mutation creates and commits its own session; it cannot commit unrelated caller work. Do not pass uncommitted caller-created records into this contract. The lock order is configuration, connection transaction, tracker, grant. Ordinary operations use a shared configuration row lock; replacement uses an exclusive lock. Rotation locks and rereads the grant before invoking the provider callback, then atomically persists both encrypted tokens and an incremented version. TokenPair.issued_at records the provider response receipt anchor separately from mutable row timestamps. Services compute bounded expiry skew from expires_at - issued_at without inventing refresh-token lifetimes. This nullable field preserves legacy consumer behavior, rotates with the pair, survives metadata-only updates, and is erased on disconnect or configuration replacement. Competing calls with the old version fail before provider I/O. Different grants can rotate independently, including under one configuration.

rotate is synchronous: async services must dispatch it to a worker thread. Its callback receives detached TokenPair/CallbackSecrets values and a transport timeout (default ten seconds, maximum sixty). The storage wait has the same hard bound. A late result has no database session and cannot persist credentials; providers must honor the supplied transport timeout to release network resources. Exceptions and timeouts roll back. A provider that invalidates a refresh token before a local timeout/commit failure may require reconnect; storage cannot roll back provider-side rotation. Never log provider inputs or results.

disconnect locks the grant, erases its secrets, increments its version, detaches and disables its tracker. Configuration and consumer survive. Account deletion cascades grants and handshakes. Run tenant-scoped cleanup_expired periodically to remove unfinished transactions and their pending secrets. Completed transactions retain the owner-bound retry mapping. All model to_dict implementations expose metadata only. Plaintext token values never enter the tracker ORM credential cache.

The nullable schema upgrade preserves legacy tokens and interprets their naive expiry timestamps as UTC. Rollback drops the new storage and erases managed grants; managed connections require reconnect after a subsequent upgrade. Legacy GitHub tokens and manual tracker credentials remain intact. PostgreSQL tests exercise upgrade/rollback, tenant constraints, replay, concurrent completion/refresh, configuration replacement, stale writes, ciphertext and rollback isolation.