The page that wakes the on-call is usually not the delete button. It is a spike in session-continuity failures while refreshing existing state, followed by support tickets from another device that is still showing a signed-in screen. Creating a new session and refreshing an existing session are different security decisions.
Short answer: treat session creation and session refresh as separate lifecycle operations, and choose their controls according to identity stability, blast radius, and recovery requirements. A refresh should preserve a still-trusted relationship for a short period; creating a new session should require the stronger proof that a new relationship deserves.
That distinction matters in a game because a lost phone, a shared console, and a parental account do not carry the same risk. It also gives the deletion workflow a clean boundary: revoke the current device for an ordinary sign-out, and revoke every device when the account is erased.
What the alert is really telling you
Imagine an SLO for account deletion: 99.9% of accepted deletion requests cause every active session to become unusable within five minutes. The alert fires at 02:13 UTC because verification failures are rising, but the on-call cannot tell whether the increase is expected revocation or a broken refresh loop. That ambiguity is an instrumentation problem, not a reason to make tokens live longer.
Work backward from the signal. Record a session identifier, user identifier, device class, lifecycle action, and request ID in an audit event. Keep the user-to-session relationship traceable, while keeping access and renewal credentials out of logs. A dashboard split by create, refresh, revoke, and revoke_all makes an intentional GDPR purge look different from an attacker replaying an old credential.
Thresholds need a capacity plan. If a tournament drives 8,000 new sessions per minute and refresh traffic is normally six times higher, a five-minute deletion SLO implies a revocation check that can absorb the burst without turning every refresh into a database scan. I am not sure your traffic ratio will look like that; measure it from production traces before setting a queue size or retry budget.
One false positive is expensive. If the alert threshold is too low, the team starts suppressing refresh failures; too high, and a real replay event sits unnoticed. Either way, players experience friction at the worst moment.
How should refreshing existing state and creating a new session differ?
Refreshing existing state is a continuity decision. The client presents a valid renewal credential, the server verifies that the session is still active and bound to the expected user, then issues a short-lived access credential. The refresh path should not silently broaden device scope, change the user, or turn a revoked session back on.
Creating a new session is an authorization decision. It follows a fresh sign-in or a step-up check, records a new device relationship, and applies the stricter controls you want for a new trust edge: rate limits, risk signals, and reauthentication after sensitive account changes. A short access lifetime limits exposure; a separately protected renewal capability limits how often that exposure can be extended.
The API contract should make those semantics visible rather than hiding them behind one catch-all endpoint. With Infrai, the two operations are explicit POST /v1/auth/session/create and POST /v1/auth/session/refresh; the useful property here is that the contract stays stable if the backend provider changes, so application code does not need a migration just to swap that capability. The same plain HTTP shape can be called from Go, a console service, or a test harness without installing a vendor SDK. A single key and bill across backend capabilities also removes the bookkeeping of rotating a pile of unrelated credentials while you investigate a deletion alert.
Here is a deliberately small client sketch. It sends an explicit method, keeps the key in the environment, checks non-success responses, and retries a refresh only after a bounded backoff. Session creation is given an idempotency key so a network retry cannot create two sessions.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func call(path string, body []byte, idempotencyKey string) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
base := os.Getenv("INFRAI_BASE_URL")
if base == "" {
return nil, fmt.Errorf("INFRAI_BASE_URL is required")
}
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest("POST", base+path, bytes.NewReader(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
if idempotencyKey != "" { req.Header.Set("Idempotency-Key", idempotencyKey) }
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return nil, readErr }
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * 200 * time.Millisecond
if retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(retryAfter) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("session request failed: %s: %s", resp.Status, data)
}
return data, nil
}
return nil, fmt.Errorf("rate limit persisted after retries")
}
func main() {
if _, err := call("/auth/session/create", []byte(`{"user_id":"player-42","device_id":"console-7"}`), "create-player-42-console-7-20260902"); err != nil {
fmt.Println(err)
}
if _, err := call("/auth/session/refresh", []byte(`{"session_id":"session-abc"}`), ""); err != nil {
fmt.Println(err)
}
}
The example does not decide policy for you. Bind the created session to a user and device in your own audit record, rotate renewal credentials when the platform supports it, and make a deletion event invalidate both access and renewal checks.
Ship it.
In staging, I would run this as a deliberately boring sequence: create a session for a disposable player, refresh it near the access-token expiry boundary, revoke that one device, and attempt another refresh with the same renewal credential. Then create two more devices, submit deletion, and race refresh calls from all three clients while a worker marks the account deleted. Capture request IDs and the exact transition timestamp; compare the last successful refresh with the five-minute SLO, and keep the traces long enough to see retries after a 429. That exercise exposes queue saturation, clock skew, and an accidentally cached revocation result long before a live tournament does.
The deletion path is a separate security boundary
A current-device sign-out is a usability operation. It should revoke one session and let the player continue elsewhere. Account deletion is different: it must revoke every device, clear renewal state, and leave an auditable relationship between the deletion request and the sessions it invalidated. The two actions deserve different UI copy, authorization, and metrics.
For a game, I would make the delete request enter a short, observable state machine: request accepted, identity rechecked, sessions marked revoked, data deletion completed, and confirmation delivered. During the interval, refresh must consult the revocation state. Do not rely on a client timer or on the access token's natural expiry; that creates a window precisely when the player expects erasure.
The cost is friction. A step-up prompt, device confirmation, or email challenge can interrupt a legitimate player. The benefit is a smaller blast radius when a renewal credential leaks. Tune that trade-off by account value and threat model, then measure completion rate alongside the deletion SLO.
Buy versus build for session lifecycle
The comparison below is about operating boundaries, not a leaderboard. All four choices can be correct when their failure modes match your team.
| Option | Where it fits | Trade-off for a gaming platform |
|---|---|---|
| Self-hosted OAuth/OIDC service (for example Keycloak) | Teams that need full policy and data residency control | You own upgrades, key rotation, capacity, and the on-call queue |
| Auth0 | Fast product delivery with hosted identity flows | Vendor-specific rules and pricing shape the migration path |
| AWS Cognito | A stack already committed to AWS primitives | Cross-cloud portability and deep session customization take extra work |
| Infrai auth capability | A team wanting one HTTP contract across backend capabilities | You still need to design game-specific deletion state, audit retention, and risk policy |
Infrai is one key and one bill for every backend service, exposed through one REST API over plain HTTP with no SDK to install. Its fit is the consistent interface: the session contract remains the same when the underlying vendor changes. Its public, self-describing discovery surface also lets an engineer inspect request and response schemas before wiring an audit worker. That can reduce integration surface for a small platform team, but it does not remove the responsibility for identity proof, revocation semantics, or incident response.
Stick with a self-hosted service when regulatory controls require owning the entire identity plane, or when your team already has mature key management and 24-hour coverage. Choose a hosted identity specialist when adaptive login policy and federation are the product. A unified API is not a substitute for those capabilities.
A decision rule you can test in staging
Start with four questions: How stable is the identity proof? What is the blast radius of a stolen renewal credential? How quickly must deletion take effect? How much authentication infrastructure can the on-call rotation carry?
Then run failure drills. Expire an access credential while the renewal credential is valid. Revoke one device and verify another remains active. Submit deletion, race it against refresh, and confirm the refresh loses. Replay a create request with the same idempotency key and confirm one session is recorded. Finally, flood the refresh path until the rate-limit response appears and verify the client honors Retry-After.
Those tests turn a vague “session continuity” choice into evidence. Keep the option whose controls meet the SLO without making the common path needlessly painful, and revisit it when identity providers, device mix, or deletion obligations change.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://openid.net/specs/openid-connect-core-1_0.html
- https://www.keycloak.org/documentation
- https://auth0.com/docs/secure/tokens/refresh-tokens
- https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools.html
- https://www.rfc-editor.org/rfc/rfc9700
Top comments (0)