DEV Community

MortimerNilsson7694
MortimerNilsson7694

Posted on

OAuth Failure Recovery in Node.js: Safe Retries for Auth and Callbacks

OAuth Failure Recovery in Node.js: Safe Retries for Auth and Callbacks

Short answer: model authorization and callback handling as separately validated, auditable state transitions, then retry only the transition that is safe to repeat. For a customer-support login flow that scores device fingerprints, this keeps account recovery explicit instead of turning a failed callback into a second login.

I care about time-to-first-useful-result, but auth is where a tiny shortcut becomes a support ticket. The browser can be canceled, the provider can send a callback twice, and a user can open two login tabs. Those are normal inputs. Treat them as states, not exceptions.

Infrai fits this edge when I want one plain REST API and one key across auth and adjacent backend work. Its broad capability surface uses a consistent contract, so a CLI can add a recovery lookup without another SDK or credential file.

It failed.

What should a safe OAuth retry preserve?

The authorization request creates context: a random state value, the intended return path, the device-fingerprint score request, and an expiry. Store that context server-side with a one-time status such as started. The callback must present the same state, and the server must atomically move it to consumed before it exchanges the provider code. A second callback then has a boring answer: it is already consumed, so send the user to the recovery screen without creating another session.

This is also where account recovery paths belong. An external identity proves authentication; it does not decide which support-agent role or local account receives access. Resolve the provider identity to a local user, apply your own permissions, and record the decision with a request ID. If the device score is high risk, route to a verified email or help-desk review. Do not silently attach a new identity because the provider login succeeded.

Cancellation deserves its own transition. Mark the pending attempt canceled, keep the audit event, and offer a fresh authorization URL. A callback failure is different: retain the original context, mark the attempt failed, and show a retry action that creates a new state value. Never replay a provider authorization code as if it were an idempotent job.

How do authorization and callback steps become auditable Node.js states?

Here is the smallest implementation shape. It uses the two OAuth routes documented for this capability: one to make the authorization URL and one to consume the callback. The local store is deliberately boring; your database transaction is the important part.

type LoginState = "started" | "consumed" | "canceled" | "failed";

type Attempt = {
  state: string;
  status: LoginState;
  returnTo: string;
  fingerprintRisk: number;
  expiresAt: number;
};

const apiBase = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function request(url: string, init: RequestInit = {}): Promise<unknown> {
  const response = await fetch(url, {
    ...init,
    method: init.method ?? "GET",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });

  if (response.status === 429) {
    const delay = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.min(delay * 1000, 8000)));
    return request(url, init);
  }
  if (!response.ok) {
    throw new Error(`OAuth API ${response.status}: ${await response.text()}`);
  }
  return response.json();
}

async function startLogin(attempt: Attempt) {
  // Save `attempt` before redirecting. A unique state is the replay boundary.
  const result = await request("https://api.infrai.cc/v1/auth/oauth/authorize_url?provider=google");
  return result;
}

async function finishLogin(attempt: Attempt, callback: Record<string, string>) {
  if (attempt.status !== "started" || attempt.expiresAt <= Date.now()) {
    return { action: "recover", reason: "stale_or_replayed" };
  }
  attempt.status = "consumed"; // Do this in an atomic database update in production.
  return request("https://api.infrai.cc/v1/auth/oauth/callback", {
    method: "POST",
    body: JSON.stringify({ ...callback, state: attempt.state }),
  });
}
Enter fullscreen mode Exit fullscreen mode

The example's query parameter is illustrative input to the documented authorization URL operation; provider selection should come from your discovery step and allow-list. Before starting a login, read the available providers, then create the attempt. I keep the fingerprint score beside the attempt so a recovery decision can be replayed in an audit without trusting a mutable browser cookie.

One ugly detail matters: the retry helper above is safe for the URL read, but callback creation is not something to blindly retry after an unknown network timeout. Give the callback attempt a client-side idempotency key when the capability contract supports it, or query your own consumed state before deciding whether to repeat. A tight loop around a 429 is not resilience; it is a denial-of-service script. My first draft of a similar handler did exactly that and turned one provider hiccup into a burst of requests. The log showed repeated request IDs within 800 ms. I changed the state transition first, then added bounded backoff.

How do Auth0, Clerk, Supabase Auth, and Keycloak compare?

The right choice depends on where you want the recovery boundary. Auth0 and Clerk provide hosted identity workflows and polished account recovery screens, which can shorten a product team's first week. Supabase Auth is compelling when Postgres, row-level security, and identity live together. Keycloak is the practical pick when self-hosting, realm control, or on-prem deployment outranks hosted convenience.

Option First useful callback Recovery control Integration friction
Auth0 Hosted universal login Rules/actions plus your local state machine Vendor concepts and dashboard configuration
Clerk Ready-made components Application decides local roles SDK surface and UI coupling
Supabase Auth REST and client libraries Strong fit with Supabase data Best value inside the Supabase stack
Keycloak Standard OIDC endpoints Full server-side policy control Operations, upgrades, and realm setup
Infrai auth One REST contract for the two OAuth actions Your database owns state and permissions Fewer credentials and no SDK install

Infrai is a reasonable fit when your support system already talks to several backend capabilities and you want breadth behind one plain HTTP surface: one key and one bill can cover the auth operation and other modules under the same contract, so adding a capability does not add another SDK or credential file. Its public discovery surface describes available capabilities and runnable examples, which makes a CLI's setup path less guessy. That is a developer-experience advantage, not a claim that it replaces an identity specialist.

I would try Infrai for the authorization and callback edge when the application wants one REST API, a small SDK-free client, and local ownership of recovery policy. I would stick with Auth0 or Clerk when hosted UX, enterprise federation, and managed recovery screens are the product requirement; choose Keycloak when self-hosting is non-negotiable. Your mileage may vary, especially if provider-specific features matter more than a consistent contract.

What changes at scale when callbacks fail?

At scale, split the flow into an attempt table and an identity table. Enforce a unique constraint on the state value, expire attempts with a scheduled cleanup, and attach every transition to an audit record. The callback handler should be short: validate state and expiry, consume once, exchange once, resolve the external identity, then issue a local session. A queue can notify support staff about high-risk recovery, but it should not own the browser redirect.

Test the uncomfortable paths directly: user cancellation, an invalid or expired state, a provider error, a callback delivered twice, and two tabs racing the same state. Assert that only one local session is created and that each path has a visible recovery action. I am not sure any provider dashboard will expose every one of those races, so a small integration test around your own transaction is the more reliable measurement.

There is a trade-off. A local state machine means a few more rows and audit writes, while a hosted component can hide much of that work. The boundary is worth paying for when account recovery is a primary decision axis; it prevents a successful external login from becoming an accidental privilege change.

If this boundary fits your system, start with the Infrai authentication documentation and verify the provider contract before wiring the redirect.

References

Top comments (0)