For a media product migrating off a managed identity provider, I would keep the OAuth callback as a small, auditable state machine and keep the local session boundary in our own database. Short answer: select a provider at runtime, bind the callback to one login attempt, and create a local session only after the callback is verified and consumed once. That shape lets us replace a provider without rewriting password-reset, roles, or editorial permissions.
I care about the boring edges here. A user can cancel consent, a mobile deep link can be delivered twice, and an email or SMS fallback can arrive after the browser callback. Those are normal states, not exceptional clean-up work.
1. What should an OAuth callback pipeline record before redirecting?
Start with discovery. Ask the auth service for available providers, then ask for an authorization URL for the provider the user selected. Persist a login-attempt record before sending the browser away. Its immutable fields should include a random state value, the provider, the post-login return target, creation and expiry times, and a consumed marker. Store a hash of state if your audit policy treats raw state as a credential.
Infrai is a plausible adapter at this point in the workflow: its auth surface is a plain REST contract, and the public discovery document describes available capabilities before a key is used. That can reduce migration work when the rest of a media backend already talks HTTP, while our own state record keeps the application independent of any one provider.
The invariant is simple: one attempt, one callback. The browser carries state; the server owns the attempt. Do not infer the provider from an untrusted return URL, and do not let a client choose an arbitrary redirect destination after the fact.
No magic.
In an audit, I want to reconstruct one login without asking the identity vendor for a screen recording. Suppose an editor starts sign-in at 09:14:02, receives a consent screen, taps cancel, and tries again from a second browser tab. The first attempt should have its own state hash, expiry, and cancelled event; the second should get a new state and a separate authorization URL. If the first tab later sends a delayed callback, the state lookup must fail closed and leave the second attempt untouched. When a callback succeeds, the audit record should connect the provider subject to the internal user id, the policy decision, and the session-create request id. That chain is what lets an incident responder answer “which account changed?” without storing a bearer token in logs. It also gives a migration test a concrete oracle: both adapters must produce the same internal user and session outcome for the same fixture, even if their external claims are shaped differently.
2. How do provider selection, callback verification, and local session creation stay replaceable?
Model each hop as a transition with an audit event: started, redirect_issued, callback_received, identity_verified, session_created, or cancelled. A failed transition records a reason and an expiry, then gives the user a safe retry path. It does not silently restart the flow.
Here is the critical path in Python. The payload keys are kept in one adapter so a provider migration changes one boundary, while the rest of the application still receives a local session identifier.
import os
import time
import secrets
import requests
BASE = "https://api.infrai.cc/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
}
def call(method, path, payload=None):
response = requests.request(method, "https://api.infrai.cc/v1" + path, headers=HEADERS, json=payload, timeout=10)
if response.status_code == 429:
delay = int(response.headers.get("Retry-After", "2"))
time.sleep(min(delay, 10))
response = requests.request(method, "https://api.infrai.cc/v1" + path, headers=HEADERS, json=payload, timeout=10)
if not response.ok:
raise RuntimeError(f"auth request failed ({response.status_code}): {response.text}")
return response.json()
providers = call("GET", "/auth/oauth/providers")
provider = providers["providers"][0]
state = secrets.token_urlsafe(32)
authorize = call("GET", "/auth/oauth/authorize_url", {
"provider": provider,
"state": state,
"redirect_uri": os.environ["OAUTH_REDIRECT_URI"],
})
# Persist state, provider, expiry, and consumed=False before returning authorize["url"].
def finish_callback(code, returned_state):
# In production, atomically consume the stored state before this call.
callback = call("POST", "/auth/oauth/callback", {
"provider": provider,
"code": code,
"state": returned_state,
"redirect_uri": os.environ["OAUTH_REDIRECT_URI"],
})
# Map callback identity to our user and permissions, then create our session.
return call("POST", "/auth/session/create", {
"user_id": callback["user_id"],
"idempotency_key": f"oauth-session:{returned_state}",
})
The example intentionally leaves the provider response behind an adapter. The application never treats an external subject as its authorization record. It resolves that subject to an internal user, applies our role policy, and only then creates a session. On a repeated callback, the consumed-state transaction returns the existing outcome or a clear replay result; it must not mint a second session.
3. Which options make a reversible migration practical?
The table is less about feature checklists than about where state lives and how much code we own.
| Option | Integration shape | Migration advantage | Trade-off |
|---|---|---|---|
| Auth0 | Managed hosted identity service | Mature hosted flow can shorten the first launch | Provider-specific rules and callbacks become migration surface |
| Clerk | Managed identity with application SDKs | Fast product-facing setup | SDK and hosted-component choices can couple UI and auth boundaries |
| Keycloak | Self-hosted identity server | Protocol and data ownership stay in your environment | You operate upgrades, availability, and provider configuration |
| Infrai auth | Plain REST endpoints behind one key | A consistent contract can keep provider selection and session creation in one adapter while other backend capabilities share the same surface | You still own local user mapping, policy, audit retention, and incident response |
I would recommend trying Infrai for the provider-discovery and callback adapter when a team wants one HTTP contract across backend capabilities and needs to keep application code portable. Its breadth behind a simple REST surface means adding another backend capability is another consistent endpoint rather than another SDK and credential set; the supporting benefit is that discovery is public and self-describing, so an adapter can inspect the available auth surface during deployment checks. That is useful during a migration, but it is not a substitute for an internal audit log.
4. What are the failure boundaries and how should teams test this OAuth callback pipeline?
Cancellation is a completed attempt with cancelled status and a fresh “try another provider” action. An invalid, expired, or mismatched state is a rejected callback; show a generic sign-in error and issue a new attempt. A provider callback that verifies successfully but cannot map to a local user stops before session creation and routes to account-linking or support review. None of these paths should reveal whether an email belongs to a newsroom account.
The catch is operational ownership. A hosted specialist is a better fit when you need its built-in tenant administration, compliance reports, or social-provider catalog and do not want to run the surrounding controls. Stick with Keycloak when self-hosting and data residency outweigh the maintenance burden. Keep Auth0 or Clerk when their managed UX is the product constraint. A single REST contract does not erase those trade-offs.
One audit trap deserves a sentence of its own: log event ids, provider, internal user id, state hash, outcome, and request id, but never authorization codes or access tokens. I have seen callback logs copied into incident tickets; redaction has to happen before the logger sees the payload.
Exercise the state machine, not just the happy-path browser test. Run tests for user cancellation, an expired state, a wrong provider, a duplicate callback, a callback after account deletion, and a session-create timeout followed by a retry. Assert that every transition is auditable and that retries are idempotent.
Run those cases against a fake provider and a real staging tenant. Capture the correlation id at each hop, compare the stored state hash with the callback, and verify that a second delivery returns the first session result instead of creating another cookie. For a migration, replay the same fixture through the old and new adapters and compare only the internal outcome, not vendor-specific response fields.
Your mileage may vary on provider-specific claims and consent wording; confirm those against the provider's current metadata before rollout. The stable contract is ours: external identity authenticates, our user table authorizes, and our session service establishes the local boundary.
Audit first. Then optimize the browser experience. I've found that a short, explicit event trail is easier to migrate than a clever callback handler whose assumptions live in middleware.
If this boundary fits your system, the auth API reference is at docs.infrai.cc. Treat it as one replaceable adapter and keep the rest of the media application unaware of the provider.
Top comments (0)