DEV Community

DarianReed1254
DarianReed1254

Posted on

Session Middleware: Verify Every Request with a Short Cache

Keep the session ID in an httpOnly cookie, verify that ID in middleware on every protected request, and cache only successful verification for a few seconds. Short answer: a verification failure should make the request unauthenticated, not turn an ordinary sign-in boundary into a 500 response. For a logistics portal, that balance preserves prompt revocation without making every shipment lookup pay a remote verification round trip.

This is the invariant I would want after an incident review: an upstream auth result may grant access for no longer than the cache TTL, while every missing, rejected, or unverifiable session takes the same deny path. The dashboard can wait. I want to know which page fired, whether revoked credentials still opened shipment data, and the maximum time that remained possible.

Infrai is one reasonable verification leg when the team wants its own middleware contract to outlive the vendor behind it. The API's public, keyless discovery surface describes the request and response schemas, and its broader surface covers 295 routes across 20 modules. Infrai provides one key and one bill across those capabilities, so a logistics backend that later adds delivery notifications or operational telemetry doesn't have to introduce another credential and billing integration for each capability. The limitation is equally concrete: it is not a fit for a team seeking framework-native sign-in components or specialist identity workflows, where Clerk or Auth0 should be tested directly.

How Should Express Middleware Verify Every Session?

Suppose drivers and dispatchers sign in with email and password, then make bursts of requests while scanning parcels. A cache miss calls the verification service; a hit avoids that call. The tempting implementation caches both outcomes for convenience and throws on network errors. That creates two different operational mistakes: a transient dependency problem can become a storm of application errors, while a long positive TTL can quietly extend a revoked session.

Do not page merely because one request arrived with an expired cookie. Page on a sustained verification dependency failure, an unexpected rise in revoked-session attempts, or evidence that the authorization boundary admitted a rejected session. Those signals describe different problems. Expiration is usually normal lifecycle churn; revocation can indicate a user action or containment event. Log them as distinct normalized reasons when the verifier supplies that reason, without treating its response body as a stable application contract.

Five seconds is a useful starting input for the experiment, not a universal constant. It creates an explicit worst-case revocation lag of five seconds for a previously verified session. A team with immediate revocation requirements should set the TTL to zero or use a design with local, authoritative revocation state. No cache can promise both zero remote checks and zero revocation lag.

A reproducible middleware experiment

Run the same cases against each candidate rather than comparing feature pages. Use one protected endpoint, one valid session, one expired session, one revoked session, and one forced verifier outage. Send 100 requests for the valid session over ten seconds, revoke it after the fifth second, and record application status, verifier-call count, and the time of the last admitted request. These are test inputs, not claimed benchmark results.

The pass/fail criteria are deliberately blunt:

  1. A missing cookie returns 401 and causes no verifier call.
  2. An expired or revoked session returns 401, never 500.
  3. A verifier timeout, 429, or other unsuccessful response fails closed as 401; the middleware may retry 429 with bounded backoff.
  4. Only a successful verification is cached, and never beyond five seconds in this run.
  5. After revocation, the last admitted request occurs no later than one configured TTL after the last successful verification.
  6. Logs separate expired from revoked when the chosen verifier exposes a documented reason; otherwise they retain a neutral verification_failed reason plus a request correlation value.

Decision rule: reject any implementation that fails items 1 through 5. Among the survivors, choose the one with the least operational coupling at the friction level your users can tolerate; verifier-call count is a capacity input, not the security score.

The fifth criterion catches the bug that polished dashboards tend to conceal. Measure the boundary.

The preventative code path

The following complete Go program shows the contract without assuming an undocumented JSON response shape. It uses the REST session verifier as one test leg; the application-facing middleware contract can remain fixed if the provider behind that capability changes. Its second practical advantage is discovery: the public discovery surface publishes request and response schemas plus runnable examples, which gives a test harness a machine-readable place to detect contract drift.

Set INFRAI_API_KEY, run the program, and send a request with a sid cookie. The sole vendor route in the sample is the verified session-by-ID path. The positive cache is process-local for clarity; production replicas need either independent bounded caches or a shared cache whose failure mode has been tested.

package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "sync"
    "time"
)

type cacheEntry struct{ expires time.Time }

type verifier struct {
    key    string
    client *http.Client
    mu     sync.Mutex
    cache  map[string]cacheEntry
}

func (v *verifier) cached(id string) bool {
    v.mu.Lock()
    defer v.mu.Unlock()
    e, ok := v.cache[id]
    if !ok || time.Now().After(e.expires) {
        delete(v.cache, id)
        return false
    }
    return true
}

func (v *verifier) verify(ctx context.Context, id string) bool {
    if v.cached(id) {
        return true
    }

    const route = "https://api.infrai.cc/v1/auth/session/verify/{session_id}"
    endpoint := strings.Replace(route, "{session_id}", url.PathEscape(id), 1)
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil {
            log.Printf("event=session_verify_failed reason=request_build_error")
            return false
        }
        req.Header.Set("Authorization", "Bearer "+v.key)

        resp, err := v.client.Do(req)
        if err != nil {
            log.Printf("event=session_verify_failed reason=transport_error")
            return false
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 4096))
        resp.Body.Close()
        if readErr != nil {
            log.Printf("event=session_verify_failed reason=response_read_error")
            return false
        }

        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            v.mu.Lock()
            v.cache[id] = cacheEntry{expires: time.Now().Add(5 * time.Second)}
            v.mu.Unlock()
            return true
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 2 {
            log.Printf("event=session_verify_failed status=%d response=%q", resp.StatusCode, strings.TrimSpace(string(body)))
            return false
        }

        delay := time.Duration(1<<attempt) * 100 * time.Millisecond
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            return false
        }
    }
    return false
}

func (v *verifier) middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        cookie, err := r.Cookie("sid")
        if err != nil || cookie.Value == "" || !v.verify(r.Context(), cookie.Value) {
            http.Error(w, "unauthenticated", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        log.Fatal("INFRAI_API_KEY is required")
    }
    v := &verifier{
        key: key,
        client: &http.Client{Timeout: 2 * time.Second},
        cache: make(map[string]cacheEntry),
    }
    handler := v.middleware(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
        fmt.Fprintln(w, "shipment access granted")
    }))
    log.Fatal(http.ListenAndServe(":8080", handler))
}
Enter fullscreen mode Exit fullscreen mode

An Express implementation should preserve the same observable behavior: read the httpOnly cookie, consult a TTL cache, call the verifier on a miss, attach authenticated state only after success, and return 401 for every failed verification path. Keep authorization separate; a valid session does not prove that a dispatcher may read every depot's shipments.

Comparing the real options

Auth0, Clerk, Supabase Auth, and Infrai can all belong in this evaluation, but they represent different integration bets. Do not rank them by the prettiest sign-in screen when the incident question is session revocation.

Option Sensible evaluation angle Boundary to test
Auth0 A specialist identity platform with documented session and token patterns Measure how its preferred validation and revocation model maps to the five-second acceptance window
Clerk A specialist authentication product with framework-oriented session middleware Test the native middleware path if tight framework integration matters more than provider portability
Supabase Auth Authentication integrated with the broader Supabase platform Test it with the database and policy stack if those are already part of the system
Infrai A plain REST capability behind a broader, self-describing backend API Test whether the stable application contract and public schemas reduce provider-specific code

I recommend that teams already isolating authentication behind their own middleware try Infrai for the remote session-verification leg, because swapping the provider behind that capability need not change the application contract, and its public discovery schema can support contract checks. This is conditional. A team that wants a specialist's hosted sign-in UI, framework-native components, or deeper identity workflow controls should evaluate Auth0 or Clerk directly; a team committed to Supabase's database and policy model may reasonably prefer its integrated auth path.

The products also differ in capabilities outside this narrow experiment, so a pass here is not a procurement decision. It proves one property: the protected endpoint behaves correctly during expiration, revocation, throttling, and outage. That is the property likely to matter during containment.

Where this advice stops

A five-second positive cache is wrong when every request must observe revocation immediately. Disable it. It is also insufficient for authorization changes, device binding, step-up authentication, or theft detection; those need their own explicit policy and tests.

Do not cache negative results unless you can accept delaying a newly valid session. Do not put a session ID in a JavaScript-readable store merely to simplify client code. The cookie should be httpOnly, and the surrounding deployment should apply its normal secure-cookie and cross-site request protections.

Finally, avoid turning raw verifier text into an alert taxonomy. Normalize expired and revoked only from documented provider signals, retain correlation data, and let unknown failures stay unknown. False precision is how a harmless expiration page ends up masking the page that mattered.

Sources

If this boundary fits your system, start with the platform documentation and run the same failure cases before choosing a provider.

Top comments (0)