DEV Community

CorneliusHayes8579
CorneliusHayes8579

Posted on

Server-Rendered Login Sessions: Creation, Verification, Refresh, and Logout Trade-offs

Short answer: model server-rendered login as four separate, auditable state transitions. Create a short-lived session after Google or GitHub returns, verify it on every protected request, refresh through a narrower path, and make logout mean either this device or every device. That separation keeps session security from getting lost in login-page friction.

The decision note

For a marketplace, the useful unit is not “OAuth integration.” It is a session record that you can explain six weeks later when a seller disputes an account action.

Option Good fit for a server-rendered marketplace The catch
Auth0 Managed social connections, hosted login, and mature policy controls More platform configuration and a provider-specific integration surface
Clerk Fast product-facing authentication UI and user management The UI and data model can become a dependency your team has to work around
Keycloak Self-hosted identity, custom realms, and control over deployment You own upgrades, operations, and the security posture of the cluster
Infrai A plain HTTP boundary when you want session calls beside other backend capabilities Your application still owns OAuth consent, cookie policy, and marketplace authorization

My recommendation is conditional: choose the design with explicit session transitions first, then choose the provider that lets your team enforce them. Infrai is worth testing when a single REST API and one credential reduce glue in a small TypeScript service; anything that can send HTTP can call it, so there is no SDK version to babysit. Auth0 or Clerk may be a better fit when hosted flows and polished account UX are the primary constraint. Keycloak wins when self-hosting is a hard requirement.

The provider is a component. The audit trail is the product.

How should server-rendered login sessions handle creation, verification, refresh, and logout?

Start with a state machine, not a callback handler full of side effects. A successful Google or GitHub callback proves that an identity provider authenticated someone. It does not, by itself, define your marketplace session lifetime or authorization rules.

Create a session only after you resolve the external identity to an internal user. Store a session identifier, user identifier, creation time, expiry, last-seen time, and a revocation marker. Keep the browser cookie opaque and HttpOnly; put the useful audit data on the server. The session record gives you a searchable relationship between user and device without putting marketplace details in a bearer token.

Verification is its own transition, even if it looks like a read. On each request that needs a seller or buyer identity, check that the session exists, is unexpired, and is not revoked. Record a request ID or event ID so a later review can connect the action to the user and session. Do not silently turn verification into refresh; that makes a stolen cookie harder to reason about.

Refresh deserves a different risk budget. An access session can be short-lived and frequently verified. A renewal credential should be stored more carefully, rotated on use, and invalidated when you detect reuse or a suspicious device change. If the renewal credential is missing, expired, or revoked, send the browser through the login flow again. Friction is preferable to a session that cannot be explained. In a marketplace, that means an expired seller session may interrupt a listing edit, while a long-lived renewal secret could expose payout data; those are different failure costs, so they should not share one timeout or one log event. Keep the policy visible in code and in the audit schema, then exercise it with a clock you can advance in tests.

Short tokens expire.

Logout has two meanings that should be visible in the UI and in your audit events. “Log out this device” revokes one session. “Log out everywhere” revokes every session for that user, including sessions created by the other social provider. They are different operations, not two labels for the same endpoint.

A small TypeScript boundary I can test

I keep the provider call behind one function so the rest of the application sees a stable transition. The example deliberately checks status codes and retries a rate-limited request with the same idempotency key. The request body is passed in by the OAuth adapter because the exact identity fields belong to that adapter's contract.

const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");

async function createSessionRequest(body: unknown, idempotencyKey: string): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(`${baseUrl}/v1/auth/session/create`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "0");
      const delayMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    const payload = await response.json().catch(() => ({}));
    if (!response.ok) throw new Error(`Auth request failed: ${response.status} ${JSON.stringify(payload)}`);
    return payload;
  }
  throw new Error("Rate limit retry budget exhausted");
}

export async function createSession(identity: unknown, requestId: string) {
  return createSessionRequest(identity, `session-create:${requestId}`);
}
Enter fullscreen mode Exit fullscreen mode

The callback handler should commit the returned session reference and its audit event together. If the database commit fails, do not set the browser cookie. On a later request, your verification adapter can call GET /v1/auth/session/verify/{session_id} with an explicit GET, then map a non-2xx response to a normal login redirect rather than rendering a half-authenticated page.

That is the part I benchmark: time from callback to a protected page, and the number of configuration files touched to add a second provider. A fast first call is useful, but a reproducible state transition is what keeps an incident small. I initially thought refresh-token rotation would be the tedious bit. The audit join was harder: without a stable session-to-user link, “log out everywhere” is just a hopeful button.

Security controls that reduce friction later

Use separate cookies for the session and any one-time OAuth state. Set Secure, HttpOnly, and an appropriate SameSite policy, and bind the OAuth state and PKCE verifier to the initiating browser session. Regenerate the session identifier after login to prevent session fixation. OWASP's authentication guidance is blunt about these controls because browsers are very good at carrying old state forward.

Keep authorization after authentication. A signed-in buyer should not gain seller payout access just because both identities passed Google. Check marketplace roles and object ownership on every sensitive action, and include the decision in the audit event.

Do not log raw access or renewal credentials. Log a session ID, user ID, provider name, event type, timestamp, and request correlation ID. Retain enough context to investigate, then apply a retention policy. Your mileage may vary with regional privacy rules; the policy needs a legal review, not a guess in middleware.

When should the runner-up win?

The catch is operational ownership. A unified HTTP boundary can remove SDK and credential sprawl, but it does not supply your cookie policy, consent screens, account-linking rules, or fraud review queue. If your team needs hosted login pages, built-in organization management, or a large catalog of enterprise connections, stick with Auth0 or Clerk and accept their integration model. If your compliance team requires an identity plane inside your network, choose Keycloak and budget for its maintenance.

Infrai fits the narrower case: you already run the session policy in your application and want a simple REST call for the backend action. It is not suitable when the provider must own the entire end-user identity experience or when your threat model forbids an external auth service. Make that decision from controls and recovery steps, not from a feature-count screenshot.

The test I would ship is boring: create, verify, refresh, revoke one device, then revoke all devices; assert the audit links and cookie behavior at each boundary. Run it with both Google and GitHub identities. If any step is implicit, make it a named transition before launch.

References

Top comments (0)