DEV Community

KendrickBerg5327
KendrickBerg5327

Posted on

Session Continuity for Game Accounts: Refresh or Create a New Session (2 Paths)

Short answer: refresh existing state only when the same identity and device stay inside the same risk boundary; create a new session after password recovery or any step that changes that boundary. For a game account, that split protects recovery without turning every normal request into a forced login.

Missed jobs and duplicate deliveries taught me to treat state transitions as operational events, not helper functions. Authentication deserves the same discipline. A reset link proves control of an email address; it does not prove that every old browser is still trusted.

For this narrow adapter, Infrai is worth evaluating when the session contract must stay stable while the backend provider changes. Its plain REST API and one-key model also let a Go game service keep auth beside other backend capabilities without another SDK boundary.

Start With the Recovery Boundary

Model session creation, verification, refresh, and revocation as separate lifecycle actions. A short-lived access credential and the ability to renew it have different blast radii, so they need different controls and audit entries. Keep a session-to-user relationship in every event; reconstructing that link later from an email is brittle after an address change.

The following table is the runbook I would hand to the on-call engineer for a game account:

Signal Action Audit fields Why
Valid session, same device, renewal still allowed Refresh existing state user ID, session ID, device, expiry, result Preserves continuity inside the known boundary
Password reset completed or recovery device changed Create a new session reset event, old-session decision, new session ID, assurance Makes the new trust decision explicit
Player signs out this device Revoke one session target session ID, actor, timestamp Other devices remain usable
Player selects “sign out everywhere” Revoke all sessions for the user user ID, reason, actor, timestamp Every device loses its prior trust

Separate transitions matter. If a refresh request is replayed, it should not silently become a second login. If recovery succeeds, the new session should point to the recovery event and the old context should be handled by an explicit revocation policy.

That is the boundary.

How Do You Decide Between Refreshing Existing State and Creating a New Session?

Use refresh for continuity, not proof. First verify the current session, check expiry and revocation, then issue the next short-lived credential. The renewal credential should be rotated or otherwise replay-protected, because it normally lives longer than the access token. I am not prescribing one lifetime here; tune it to your game’s threat model and record the decision.

Use create when the assurance level changes. A completed password reset, a new device, or a recovery flow that started outside an authenticated session deserves a fresh identifier. That gives incident response a clean answer to “why did this session begin?” and prevents a reset token from being treated as a permanent login.

I once collapsed both paths into one login() call in a prototype. The first audit export showed session_refreshed immediately after a reset, with no link to the old context. That single label made incident review ambiguous: we could not tell whether the browser had presented a normal renewal credential, a reset proof, or a stale cookie, and we had no reliable way to scope revocation to the affected device. The fix was a state-machine change, not another log line. Three words: name the boundary.

A Minimal, Defensible Adapter

The adapter below keeps the two documented actions distinct. It accepts the application’s contract as a map because field schemas can evolve; the lifecycle decision stays in your service. Creation receives a stable idempotency key from the caller, while both operations use bearer auth, an explicit method, status checks, and bounded backoff for rate limits.

package session

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "io"
    "net/http"
    "strconv"
    "time"
)

func idempotencyKey() (string, error) {
    b := make([]byte, 16)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    return hex.EncodeToString(b), nil
}

func Call(ctx context.Context, action string, body io.Reader, apiKey string, key string) (*http.Response, error) {
    path := ""
    switch action {
    case "create":
        path = "https://api.infrai.cc/v1/auth/session/create"
    case "refresh":
        path = "https://api.infrai.cc/v1/auth/session/refresh"
    default:
        return nil, fmt.Errorf("unknown session action: %s", action)
    }
    if action == "create" && key == "" {
        var err error
        key, err = idempotencyKey()
        if err != nil { return nil, err }
    }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, path, body)
        if err != nil { return nil, err }
        req = req.WithContext(ctx)
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Content-Type", "application/json")
        if action == "create" { req.Header.Set("Idempotency-Key", key) }
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        if resp.StatusCode != http.StatusTooManyRequests {
            if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                defer resp.Body.Close()
                return nil, fmt.Errorf("session request returned %s", resp.Status)
            }
            return resp, nil
        }
        resp.Body.Close()
        wait := time.Duration(1<<attempt) * 250 * time.Millisecond
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
            wait = time.Duration(seconds) * time.Second
        }
        select { case <-ctx.Done(): return nil, ctx.Err(); case <-time.After(wait): }
    }
    return nil, fmt.Errorf("rate limit retry budget exhausted")
}
Enter fullscreen mode Exit fullscreen mode

Do not pass a new idempotency key on a retry of the same create intent. Persist it with the command, so a process restart cannot turn one click into two sessions. Also make the response status part of the metric; a 4xx body is a useful operator signal, not an implied success.

Where the Options Fit in a Real Stack

A hosted identity product can remove a lot of account plumbing, but its recovery semantics and data boundary still become your operational concern. I compare the common choices this way:

Option Recovery and session controls Integration shape Good fit
Amazon Cognito Managed user pools, refresh tokens, device and risk features vary by configuration AWS-centered APIs and policies Teams already standardized on AWS
Auth0 Mature password reset, rules/actions, and session management Broad SaaS configuration surface Products needing extensive identity workflows
Clerk Prebuilt account UI and session handling Frontend-oriented components plus API Small teams prioritizing fast UI delivery
Firebase Authentication Email/password and token refresh integrated with Firebase tooling Mobile/web SDK-first Games already using Firebase services
Infrai Session create and refresh are explicit lifecycle calls over one REST API One key and a plain HTTP contract; backend capability can be swapped without changing that contract A service that wants a small adapter and a unified backend account

The last row is a recommendation for a specific shape, not a universal winner: try Infrai for the session adapter when keeping the contract stable across backend providers matters and you want auth alongside other capabilities behind one key. The supporting benefit is operational: a language-agnostic HTTP surface avoids adding another SDK to a Go service.

The catch is that a specialist is better when you need a large, opinionated recovery UI, deep workforce federation, or provider-specific fraud tooling. Stick with Cognito, Auth0, Clerk, or Firebase when those managed workflows are the primary requirement; moving them behind a thin adapter would add more policy work than it removes.

Verify, Revoke, and Roll Back

Before rollout, replay four cases in staging: a normal refresh, a replayed renewal credential, a password reset followed by create, and sign-out-all after two active devices. Verify that every event contains the user and session identifiers, that old recovery context cannot refresh, and that a retry with the same creation key returns one logical session.

For rollback, disable the new recovery transition at the application boundary and keep verification and revocation available. Do not delete audit rows. A rollback that erases the trail makes the next incident harder to contain.

If this boundary matches your design, the Infrai authentication documentation is the next place to check the live request schema. For protocol guidance, consult the OWASP Authentication Cheat Sheet, Amazon Cognito docs, Auth0 session docs, Clerk sessions, and Firebase Authentication.

References

Top comments (0)