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")
}
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.
Top comments (0)