Short answer: For a recovered gaming account, create a new authenticated session and carry forward only explicitly approved game context; refresh an existing session only when the same login is still trusted and merely needs newer application state. The deciding constraint is the recovery path: a password reset changes the trust boundary, while a lobby refresh does not.
That distinction keeps two meanings of "session" from colliding. Authentication state answers who may act. Game state answers which lobby, character, region, or pending match the player was using. Treating them as one object makes recovery deceptively convenient, but it can also preserve access that the password change was meant to revoke.
The runbook rule is short: preserve continuity as data, never as inherited proof of identity.
Should account recovery refresh existing session state or create a new session?
Create a new session after password recovery, an email ownership change, a suspicious-login response, or any other event that requires the player to prove identity again. Invalidate the old authentication sessions, mint a fresh opaque identifier after successful authentication, and rebuild the player experience from server-side records. OWASP recommends reauthentication after high-risk events such as password recovery and rotating tokens after reauthentication. That guidance is the clean dividing line here. Refresh existing state when identity assurance has not changed. A player returning from a brief network interruption may need current inventory, party membership, or matchmaking status, but that read does not justify extending or replacing authentication on its own. Keep the credential decision separate from the data refresh. If the current session has expired, the refresh stops and the sign-in flow begins. Do not copy every field from the old session into the new one. Recovery should restore low-risk context such as locale, accessibility preferences, and a last-selected character identifier after authorization checks. It should not restore a remembered recovery challenge, a pending email change, a privileged support mode, or a flag saying that recent authentication has already occurred. Those fields are evidence or authority, not convenience. For a concrete check, imagine that a player starts recovery while character c-1842 is queued for a ranked match: the account transition may retain that character identifier as a hint, yet the new session must ask the current roster and matchmaking stores whether the account still owns the character and whether the queue entry remains eligible. A copied boolean such as in_ranked_queue=true is stale authority. A revalidated lookup is continuity.
This is where missed-job instincts help: an account-recovery completion can be delivered twice because a client retries, a queue redelivers, or the first response disappears. The operation must be idempotent. A repeated completion with the same recovery grant should return the already-created session result or a terminal conflict defined by the service contract; it must not create an unbounded row of valid sessions.
Separate continuity data from authentication authority
A practical model uses three records with different lifetimes. The recovery grant is single-use evidence tied to an account and purpose. The authentication session is the server-controlled authority associated with an opaque browser cookie. The continuity snapshot is non-authoritative game context that can be validated and copied into a new session.
| Record | May survive recovery? | Operational rule |
|---|---|---|
| Recovery grant | No | Consume once and retain only audit metadata |
| Old authentication session | No | Revoke as part of the recovery transition |
| New authentication session | Yes | Create only after the grant and new password are accepted |
| Lobby or character context | Sometimes | Reload from its source and reauthorize before attaching |
| Recent-authentication marker | No | Derive from the new authentication event |
The catch is that global revocation is disruptive. A player who resets a password on a phone may be signed out of a console during a match. For an account with valuable virtual goods, revoking all sessions is the conservative default because recovery often follows lost access or suspected compromise. A lower-risk game might revoke only sessions created before the recovery event, but that choice needs a documented threat model and support playbook. Don't let client convenience make the policy accidentally.
Cookie handling belongs in the same boundary. The browser receives only the opaque session identifier; account data and recovery state remain server-side. Set the cookie's security attributes according to the deployment, avoid identifiers in URLs, and rotate the identifier when authentication occurs. The application can still restore the player's lobby after it reads the new session, checks current membership, and confirms that the match accepts reconnection.
One more separation matters: password storage is not session storage. Password verification should use a password-hashing scheme and a work factor selected by the authentication system's security policy. A session database compromise and a password database compromise have different response procedures, so combining their records makes containment harder.
Make recovery completion one idempotent transition
The safest implementation has one server-side command that consumes the recovery grant, updates the password verifier, revokes prior sessions, and creates one replacement session. The exact transaction mechanism depends on the datastore. The invariant does not.
The Go sketch below uses generic interfaces and makes the idempotency key part of the command. Complete is responsible for atomicity; no HTTP route or vendor contract is implied.
package recovery
import (
"context"
"errors"
"time"
)
var (
ErrGrantInvalid = errors.New("recovery grant is invalid")
ErrConflict = errors.New("recovery completion conflicts with stored result")
)
type Command struct {
Grant string
PasswordDigest []byte
IdempotencyKey string
Now time.Time
}
type Continuity struct {
Locale string
SelectedCharacter string
}
type Result struct {
SessionID string
Context Continuity
}
type Store interface {
// Complete atomically consumes the grant, changes the password verifier,
// revokes prior sessions, and creates or returns one replacement session.
Complete(ctx context.Context, cmd Command) (Result, error)
}
type Service struct {
store Store
now func() time.Time
}
func (s Service) Recover(
ctx context.Context,
grant string,
passwordDigest []byte,
idempotencyKey string,
) (Result, error) {
if grant == "" || len(passwordDigest) == 0 || idempotencyKey == "" {
return Result{}, ErrGrantInvalid
}
return s.store.Complete(ctx, Command{
Grant: grant,
PasswordDigest: passwordDigest,
IdempotencyKey: idempotencyKey,
Now: s.now().UTC(),
})
}
There are two traps in this small interface. First, the idempotency key must be scoped to the account recovery operation, not trusted as a global identity supplied by the player. Second, returning the prior result is safe only when the stored command fingerprint matches. If the same key arrives with a different password digest or grant, return a conflict and record the event. In an HTTP adapter, 409 Conflict is a reasonable local contract for that mismatch; a malformed or consumed grant should produce the service's chosen authentication-safe response without revealing whether an email address exists.
Keep raw session identifiers, password material, and recovery grants out of logs. Observability still needs a correlation identifier, account pseudonym, transition name, result class, revocation count, and latency. Those fields let an operator distinguish "the client retried" from "the datastore transaction did not commit" without turning logs into a credential cache.
Fast is good. Atomic is better.
Verify the failure modes before deployment
Test the transition as a state machine, not as a happy-path form submission. Start with a valid old session, complete recovery, then prove that the old credential can no longer authorize a request and that exactly one new session exists. Repeat the identical completion and verify the idempotent result. Repeat the key with different input and verify the conflict. Advance the clock past the grant expiry and verify that no password or session state changes.
Concurrency deserves its own test. Launch two goroutines against the same grant and idempotency key, synchronize their start, and assert that both observe one durable outcome. Then use two different keys against the same single-use grant. Only one transition may win. It's easy to pass sequential tests while leaving a race between "grant is unused" and "mark grant used"; the storage boundary must serialize that decision.
Exercise the gaming context too. Delete the selected character between snapshot and recovery, remove the player from the party, or close the lobby. The new session should still be valid, while stale context is omitted or replaced by a neutral destination. This is an authorization check, not a recovery failure.
The deployment can be canaried by transition type. Watch the rate of successful recovery completions, conflicts, rejected grants, session revocations, and post-recovery sign-ins. A spike in repeated completions may indicate an impatient client or delayed response rather than an attack, so alerts should combine rate, account spread, and source signals. I'm not sure one threshold transfers between a small cooperative title and a launch-day competitive game; baseline each transition and write the threshold beside its owner.
No single metric closes the loop. Support reports that players are immediately signed out, security signals showing use of pre-recovery sessions, and a widening gap between consumed grants and created sessions each point to a different failed invariant. The dashboard should preserve that distinction.
Roll back code without restoring old trust
Rollback is asymmetric. Reverting an application release is acceptable; re-enabling sessions revoked by account recovery is not. Once the trust transition commits, treat it as durable even if the release is rolled back. Otherwise a routine deployment action can resurrect the access the player was trying to remove.
Before rollout, make the previous application version able to read the new session record shape, or use an additive schema change. If the recovery command itself must be disabled, stop accepting new completions and keep sign-in plus existing-session validation available. Queueing password changes for later is not suitable because the user may believe the account has been secured when it has not.
For systems that cannot atomically update the password, revoke sessions, and create a new one, use an explicit recovery state with compensating actions and block authorization until the transition reaches its committed state. The trade-off is more machinery and a harder runbook. Stick with a single transactional datastore when the account and session volume allows it; choose an orchestrated state machine only when ownership boundaries or storage topology make that transaction impossible.
The final decision rule remains plain. Refresh data to continue play when trust is unchanged. After account recovery, establish new trust, then selectively reconstruct the experience around it.
Top comments (0)