Why I Chose a Go Renewal Pipeline — Verification, Refresh, and Revocation

go dev.to

For a session renewal pipeline, I want verification and refresh decisions next to the login-risk signal, with revocation boundaries that tell me exactly which device changed state. When that trace is missing, the on-call sees a vague “token invalid” page and cannot choose a safe account-recovery path.

Short answer: model session creation, verification, refresh, and revocation as separate, auditable state transitions; keep short-lived access tokens apart from the longer-lived renewal capability, and give “this device” and “every device” different commands.

The page that starts the investigation

The alert I care about is a spike in failed recovery attempts after a device fingerprint changes. The useful trail runs backwards: recovery request, session id, verification result, refresh decision, then the original session creation. Each event needs a user id and a session id so an auditor can move from a suspicious device to the account without guessing. I also retain the fingerprint version and the policy revision that made the decision. Without those two fields, a later investigation can prove that a block happened but not whether it used the old or new risk model. That distinction matters when support asks for an exception: the agent needs a bounded answer, and the security team needs a reproducible one. I send the correlation id through the API client, the queue message, and the recovery case, then sample the complete payload only in an access-controlled audit store. Operational logs get the outcome and identifiers, never the renewal secret. This split keeps the page actionable while limiting the damage if a log sink is copied.

Keep it boring.

I record the transition, not just the final token. A verification event says which session was checked. A refresh event says which session asked for a new access credential. A revoke event says whether the current device was removed. Those are different facts, and combining them into one “auth event” makes replay analysis painful.

The first version of this pipeline emitted only a counter. During a review, I tried to answer “did we revoke the old device or all devices?” and found no durable relationship between the event and the user. That was a design miss, not a dashboard miss. The fix was to make the session id a required audit field and to keep the recovery decision next to it.

How should verification, refresh, and revocation boundaries work?

Verification is a read. It should establish whether a specific session can still be trusted before a recovery path is offered. Refresh is a controlled transition: the access token can be short-lived, while the capability to refresh it gets stricter storage, rotation, and abuse monitoring. Revocation is an explicit transition with a target.

For one device, revoke the identified session. For a suspected account takeover, revoke every session for that user. The API surface should preserve that semantic difference; a caller should not infer “all devices” from a loop over whichever sessions happened to be listed.

Here is a small Go client for the three verified operations. It uses a bounded exponential backoff for 429, honors Retry-After when it is numeric, and attaches an idempotency key to state-changing requests. The response body is returned to the caller so a non-2xx reason is not lost.

package main

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

func call(ctx context.Context, method, path, idem string) (*http.Response, error) {
    base := os.Getenv("AUTH_API_BASE")
    if base == "" { return nil, fmt.Errorf("AUTH_API_BASE is required") }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, base+path, nil)
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        if idem != "" { req.Header.Set("Idempotency-Key", idem) }
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 { return resp, nil }
        wait := time.Duration(1<<attempt) * 250 * time.Millisecond
        if n, e := strconv.Atoi(resp.Header.Get("Retry-After")); e == nil && n > 0 { wait = time.Duration(n) * time.Second }
        resp.Body.Close()
        select { case <-ctx.Done(): return nil, ctx.Err(); case <-time.After(wait): }
    }
    return nil, fmt.Errorf("retry budget exhausted")
}

func main() {
    ctx := context.Background()
    verify, err := call(ctx, http.MethodGet, "/auth/session/verify/session-123", "")
    if err != nil { fmt.Println(err); return }
    defer verify.Body.Close()
    refresh, err := call(ctx, http.MethodPost, "/auth/session/refresh", "refresh-session-123")
    if err != nil { fmt.Println(err); return }
    defer refresh.Body.Close()
    revoked, err := call(ctx, http.MethodPost, "/auth/session/revoke/session-123", "revoke-session-123")
    if err != nil { fmt.Println(err); return }
    defer revoked.Body.Close()
    if verify.StatusCode >= 300 || refresh.StatusCode >= 300 || revoked.StatusCode >= 300 {
        fmt.Println("authentication transition failed; inspect the response body")
    }
}
Enter fullscreen mode Exit fullscreen mode

The example deliberately does not treat a successful HTTP status as proof that the recovery flow is safe. Parse the service response, correlate its request id with your audit record, and make the next state transition only after local policy checks. Your mileage may vary on token lifetimes; the important boundary is that access-token exposure and refresh-capability exposure have different consequences.

Instrumentation that survives a postmortem

For every transition I keep: user_id, session_id, device-fingerprint version, actor (user or support agent), timestamp, outcome, and a correlation id. Store a reason code for revocation, such as “risk threshold” or “user logout,” rather than a free-form sentence. That makes queries stable when the on-call is tired.

The threshold itself needs a false-positive budget. A low threshold may protect more accounts but push legitimate customers into recovery, which increases support load and can train agents to bypass checks. A high threshold does the opposite. I start with a dry-run score, compare recovery outcomes, and only then make the transition blocking. There is no universal cutoff.

Options I would put on the design review table

The session state machine is portable, but the operational trade-offs differ:

Option Strength for this pipeline Cost or boundary
Auth0 Mature hosted session and identity workflows Vendor-specific rules and pricing; deep customization can require platform extensions
Clerk Fast integration for application teams The session model is opinionated; unusual recovery semantics may need extra application state
Keycloak Self-hosted control and standards support You own upgrades, availability, and incident response
Infrai One REST API and one key/bill can keep auth calls beside other backend capabilities; the public discovery surface helps inspect available operations It is a poor fit if your policy requires a fully self-hosted identity control plane or a provider-specific feature outside its auth surface

I would also compare against a small in-house service when the organization already operates a hardened token issuer. That can be the right answer for strict residency or bespoke cryptography, even though it shifts paging and patching work onto your team.

The decision rule I use after the demo

Choose the option that lets an on-call engineer answer three questions from one trace: which session was verified, what was refreshed, and what exactly was revoked. Keep a separate command for current-device logout and all-device lockout. Make retries idempotent, preserve the user-session link, and test recovery with changed fingerprints instead of only testing the happy login path.

If a platform cannot expose those transitions cleanly, it is not suitable for this workflow, regardless of how pleasant its SDK feels. Stick with a self-hosted or specialized identity provider when audit retention, residency, or custom recovery policy is non-negotiable.

References

Source: dev.to

arrow_back Back to Tutorials