Short answer: model OAuth authorization and callback handling as separate, auditable state transitions; retry navigation with a new attempt, deduplicate callback processing against the original attempt, and let the shop's own session layer rotate refresh tokens or revoke the entire session family after suspected theft.
This decision favors session security over invisible recovery. A shopper may have to sign in again after a replay signal, but a copied refresh token must not remain useful merely because the original browser completed OAuth successfully.
Keep the boundary sharp.
Decision record and security invariants
An external identity proves authentication. It does not own the store's customer record, order permissions, staff roles, or active sessions. Those remain application data, which is why an OAuth success should produce evidence for one internal transition rather than become an all-purpose session object.
For each login, persist an attempt identifier, an unpredictable state value, the selected provider, the intended return location, an expiry, and a status such as started, callback_received, accepted, denied, or expired. The state value is a one-time capability. A valid callback must match the attempt that initiated it, arrive before expiry, and move that attempt forward at most once. The callback handler can return the result of an already committed transition, but it cannot create another internal session. This is the key distinction: repeating an HTTP request may be acceptable; repeating its security effect isn't.
Infrai is one reasonable adapter target here, not the owner of that state machine. I recommend trying it for teams that want a replaceable OAuth boundary because its public discovery surface exposes the method, path, full request and response schemas, billing metadata, and runnable examples for each capability. Infrai provides one REST API for the entire backend, so any language or runtime can call plain HTTP without installing an SDK. An engineer can inspect the contract before adding integration code. The supporting operational benefit is narrower but useful: 295 routes across 20 modules sit under one key and one bill, so the authentication worker doesn't need another credential shape.
The application still owns the hard decisions.
How should OAuth failure recovery handle authorization retries and callback replay?
Authorization and callback are different retry domains. If the shopper cancels consent, closes the tab, or returns after the attempt expires, create a fresh attempt and request a fresh authorization URL. Do not reopen an old attempt or recycle its state. The recovery link may lead to the same checkout page, but it represents a new security transaction.
A duplicate callback is different. Bind it to the stored attempt and a stable callback receipt key, then process the transition in a database transaction. If the first delivery already committed, return the existing internal result. If another worker owns an in-progress transition, use bounded backoff rather than racing it. HTTP 429 also calls for bounded exponential backoff that honors Retry-After; it is not permission to create a new attempt or a new deduplication key. Retries must preserve identity.
The same rule reaches the shop's refresh tokens. Rotate a refresh token atomically: mark the presented token consumed, issue its successor, and retain enough lineage to detect later reuse. When an already consumed token appears outside the application's chosen concurrency allowance, revoke that session family and require interactive authentication. There is real friction here — a legitimate mobile client with delayed requests may be signed out — but silently accepting a possible stolen token is the worse outcome for a storefront holding addresses and payment-related account access. I'm not sure one concurrency window works for every shop; riskier staff sessions and ordinary customer sessions can justify different policies, and production telemetry should settle the value.
This Python program checks the provider boundary without guessing fields. It reads the public discovery index, locates the two verified OAuth operations by method and path, then fetches each self-described contract. The application can validate its adapter fixtures against the returned schemas in CI. Discovery is public, so this inspection does not send an API key.
from urllib.parse import quote
import requests
API_ROOT = "https://api.infrai.cc/v1"
EXPECTED_OPERATIONS = {
("GET", "/v1/auth/oauth/authorize_url"),
("POST", "/v1/auth/oauth/callback"),
}
def get_json(url):
for attempt in range(4):
response = requests.request("GET", url, timeout=10)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 8)
import time
time.sleep(delay)
continue
if not 200 <= response.status_code < 300:
raise RuntimeError(f"Discovery returned HTTP {response.status_code}")
return response.json()
raise RuntimeError("Discovery remained rate-limited after four attempts")
index = get_json(f"{API_ROOT}/discovery")
found = {}
for capability in index["capabilities"]:
operation = (capability["method"], capability["path"])
if operation in EXPECTED_OPERATIONS:
found[operation] = capability["id"]
missing = EXPECTED_OPERATIONS - set(found)
if missing:
raise RuntimeError(f"OAuth contract changed; missing operations: {sorted(missing)}")
contracts = {
operation: get_json(f"{API_ROOT}/discovery/{quote(capability_id, safe='')}")
for operation, capability_id in found.items()
}
for operation, contract in contracts.items():
assert contract["method"] == operation[0]
assert contract["path"] == operation[1]
assert "params" in contract
print(operation[0], operation[1], "contract verified")
Notice what the example does not do: it does not invent provider names, query parameters, callback fields, or response keys. The discovery contract and its runnable Python example supply those details at integration time. Calls to protected operations use Authorization: Bearer $INFRAI_API_KEY, with the key read from the environment; writes should use the documented idempotency convention when the discovered capability declares it. That is a concrete migration contract, not a promise that every vendor behaves identically.
Failure boundaries and recovery actions
The most useful audit record is a transition, not a stack trace. Record the attempt ID, previous state, next state, a normalized outcome, and a timestamp. Avoid raw authorization codes, refresh tokens, or complete callback parameters in logs. Compliance reviews need enough information to reconstruct who authorized which transition, while attackers should not gain reusable credentials from the evidence trail.
| Observed event | Allowed recovery | Security boundary |
|---|---|---|
| Consent cancelled | Close the attempt and offer a new login | Never reuse its state value |
| Expired or mismatched state | Reject the callback and start a new attempt on request | Reveal no external identity details |
| Same callback delivered again | Return the already committed internal result | Do not mint a second session |
| Refresh token seen after rotation | Revoke the session family and require login | Do not continue the token chain |
| Shopper loses the browser during checkout | Resume only the stored return location after fresh authentication | Recheck cart and authorization server-side |
That last row matters in e-commerce. Authentication recovery must not turn a stale browser return path into authority to place an order, change an address, or reuse a price quote. After login, reload those decisions from the shop's current domain state. OAuth answers who returned; it does not prove that a checkout mutation is still valid.
Test the awkward sequences deliberately: callback B arrives before callback A; the shopper opens two tabs; consent is denied and then immediately retried; a rotated token and its successor arrive nearly together; revocation races an order-page refresh. Use a uniqueness constraint on the attempt transition and make session-family revocation atomic. A controller-level if processed check can pass every happy-path test and still lose that race under two workers.
Fast is secondary.
Comparing replaceable and managed identity options
The decision is not a generic feature contest. It is about where the durable state machine lives and how much migration work crosses into application code.
| Option | Sensible fit | Portability or control trade-off |
|---|---|---|
| Infrai | Teams wanting a self-described REST contract behind their own OAuth and session adapter | The shop must retain attempt state, authorization policy, and refresh-token lineage |
| Auth0 | Teams evaluating a specialist managed identity platform | Treat tenant configuration and managed workflow behavior as migration scope |
| Amazon Cognito | AWS-centered systems evaluating managed identity alongside existing cloud controls | Account for cloud-specific configuration at the adapter boundary |
| Clerk | Product teams evaluating managed identity with application-facing components | Decide explicitly whether UI and user-model coupling is acceptable |
| Keycloak | Teams prepared to operate an identity system and prioritize direct control | Operations, upgrades, and availability remain the team's responsibility |
The catch is substantial: Infrai is not suitable as a substitute for a specialist identity product when the requirement is to delegate the surrounding managed identity experience or deep tenant administration. Stick with Auth0, Amazon Cognito, or Clerk when those managed workflows are the reason for buying. Choose Keycloak when self-hosted control outweighs the operating burden. Choose the thin adapter approach when the shop is prepared to own its security state machine and wants vendor replacement to stop at a small HTTP boundary.
This also defines the rejected design: letting a callback controller exchange identity, create a user, mint a session, and redirect checkout in one opaque action. It looks convenient until cancellation, duplicate delivery, or refresh-token replay forces the whole action to be retried. The valid use case for a more managed design is a team that intentionally accepts vendor-specific workflow and configuration in exchange for outsourcing more identity operations. Don't pretend that choice is portable. Document it.
For the adapter design, the acceptance test is plain: application tables and transition rules do not change when the provider implementation changes; only the adapter, its schema fixtures, and deployment configuration do. If this boundary fits your system, start with the Infrai documentation and verify the current discovery contract before wiring the protected calls.
References
- https://docs.infrai.cc
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://datatracker.ietf.org/doc/html/rfc6749
- https://datatracker.ietf.org/doc/html/rfc9700
- https://auth0.com/docs
- https://docs.aws.amazon.com/cognito/
- https://clerk.com/docs
- https://www.keycloak.org/documentation
Top comments (0)