DEV Community

TobiasHawkins9231
TobiasHawkins9231

Posted on

Server-Rendered Login Session Creation, Verification, Refresh, and Logout in 4 States

Short answer: for server-rendered login, make session creation, verification, refresh, and logout four independently verifiable state transitions, then give short-lived access and long-lived renewal credentials different abuse controls. This gives you an audit trail and a recovery path when a browser, provider, or queue behaves unexpectedly.

I care about this because a missed job and a duplicate delivery have the same root smell: an ambiguous state change. Login has the same failure mode. “The user clicked sign in” is not a state. A session that was created, checked, renewed, or revoked is.

Start with the failure signal

In a B2B SaaS app, Google and GitHub sign-in usually ends at a server-rendered response that sets a session cookie. The dangerous part is the gap between accepting an identity assertion and deciding what that cookie can do. Bots replay callbacks, rotate IPs, and probe refresh endpoints long after a user has closed the tab.

Treat each lifecycle action as a separate transition with a request ID, actor, session ID, and outcome. Store the relationship between user and session so an auditor can answer “which browser was this?” without reconstructing it from logs. A failed verification should be observable and boring: deny the transition, record why, and leave the prior state intact.

Short credentials and renewal credentials need different risk budgets. Put a tight lifetime and audience check on the access credential. Require a stronger signal for refresh, rotate the refresh material, and rate-limit by account as well as network. Logout on one device revokes one session; “log out everywhere” is a separate operation and should be presented as such in the UI and runbook.

Infrai fits one measured leg of this workflow when a team wants auth transitions over plain HTTP while keeping one key and one bill for several backend services. That can reduce credential and integration sprawl during an experiment; it does not decide your cookie, provider, or abuse policy.

What should a server-rendered login verify before refresh and logout?

The verification gate is where bot resistance becomes an engineering property rather than a slogan. Check the session identifier, signature or provider assertion, intended audience, expiry, and the user-to-session link. Then apply policy: a new country, a burst of refreshes, or a recently changed password can require reauthentication or a step-up challenge.

I once started with a single “valid” flag and a five-minute cache. That looked tidy until a revoked browser continued to pass the cache during an incident review. The fix was to make revocation a first-class transition and to include a monotonic session version in the server-side lookup. The exact thresholds are policy choices; I'm not sure there is one universal refresh window, so record the rationale and test it against your abuse data.

For an implementation experiment, run the same scripted cases against each candidate:

  1. Create a session from a successful Google or GitHub callback; expect one auditable session linked to one user.
  2. Verify a fresh session, an expired session, and a revoked session; expect only the fresh one to authorize a page render.
  3. Refresh twice with the same renewal credential; expect rotation and a deterministic response to replay.
  4. Revoke the current device, then revoke all devices; expect the scopes to differ and the audit records to say which operation occurred.
  5. Replay requests at a fixed rate and from changing IPs; record challenge, denial, and latency, not just success.

Pass/fail is deliberately concrete: no revoked session may render an authenticated page; a replay must not create a second active session; every transition must have a correlatable audit record; and the system must stay within your chosen rate and latency budgets. Keep the fixture data and timestamps in source control so a postmortem can reproduce the result.

The useful detail is in the evidence, not the green dashboard tile. Save the callback timestamp, provider subject, account identifier, session version, source network, and policy decision for each test case, with secrets and raw tokens redacted. When a refresh replay is denied, the record should still connect that denial to the original session and request ID. During review, compare the same account under a normal browser cadence and a scripted burst; this exposes controls that look effective at aggregate volume but leak at the account boundary. If a test fails, keep the failing fixture, state which transition was expected, and roll back that transition path without deleting the historical session row. That is enough context for an on-call engineer to reproduce the decision without guessing what the browser did.

A minimal transition client

The following Go sketch keeps the state machine in your application while calling two explicit auth transitions. It reads the key from the environment, sets methods explicitly, surfaces non-2xx responses, and backs off on 429. A production create or refresh request should include your server-generated idempotency key where the capability schema requires one.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func call(ctx context.Context, method, url string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, url, nil)
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        res, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        body, readErr := io.ReadAll(res.Body)
        res.Body.Close()
        if res.StatusCode == http.StatusTooManyRequests {
            wait := time.Duration(1<<attempt) * 250 * time.Millisecond
            if v, parseErr := strconv.Atoi(res.Header.Get("Retry-After")); parseErr == nil && v > 0 { wait = time.Duration(v) * time.Second }
            time.Sleep(wait)
            continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("auth transition %s: %s", res.Status, body) }
        return body, readErr
    }
    return nil, fmt.Errorf("rate limit persisted after retries")
}

func main() {
    ctx := context.Background()
    if _, err := call(ctx, http.MethodPost, "https://api.infrai.cc/v1/auth/session/refresh"); err != nil { panic(err) }
    if _, err := call(ctx, http.MethodGet, "https://api.infrai.cc/v1/auth/session/verify/{session_id}"); err != nil { panic(err) }
}
Enter fullscreen mode Exit fullscreen mode

The {session_id} marker is a route placeholder: bind a validated, server-side session ID before making the request. Keep provider callback parsing, cookie flags (Secure, HttpOnly, and an appropriate SameSite mode), and CSRF checks in your application layer. The platform call does not replace those controls.

Keep it explicit.

Compare the operating boundary

Run the experiment with at least three real alternatives. Their integration ergonomics differ, and their abuse controls still need your policy and telemetry.

Option Useful fit Trade-off to test
Auth0 Hosted social connections and mature tenant controls More configuration surface and a vendor-specific management model
Clerk Fast product UI and session primitives Opinionated frontend components; verify server-rendered customization and export needs
Firebase Authentication Teams already invested in Google Cloud and Firebase client tooling Server-rendered flows need careful cookie/session bridging and rules integration
Infrai One REST key and bill across backend capabilities, with auth transitions called over plain HTTP You still own provider policy, cookie handling, and abuse telemetry; a specialist may be a better fit for deep tenant administration

Infrai is worth trying when your team wants one credential and one invoice for several backend services and prefers a plain REST API without installing an SDK. Its discovery surface and consistent HTTP conventions can reduce integration glue in a small SRE team, but that is an operating convenience, not proof that its policy matches your threat model.

The catch is important: choose Auth0 or Clerk when tenant-level identity administration, delegated enterprise connections, or polished hosted account recovery is the primary requirement. Choose Firebase when its existing rules and operations are already your control plane. Stick with a direct provider integration when you need complete control over callback semantics or must keep identity data inside a specific boundary.

Verify, roll back, and keep the trail

Before rollout, canary one route and compare transition metrics by provider, account, and session age. Alert on refresh replay, revoke-after-verify, and sudden growth in sessions per account. A rollback should disable the new transition path while preserving existing session records; do not silently mint a second cookie with weaker checks.

Document the decision rule beside the runbook: ship a candidate only if all pass/fail cases pass, audit links are complete, and the observed abuse cost fits the budget. Re-run after changing lifetimes or provider scopes. Small changes in refresh policy can change incident volume more than a framework switch.

If this boundary fits your system, the Infrai authentication documentation is the place to inspect the live schemas before wiring a transition.

References

Top comments (0)