For a gaming signup flow, resolve the external identity first, inspect the account state second, and attach only after the invariants pass. The captcha is an abuse gate; identity linking is a state transition that must remain auditable and recoverable when a player retries, changes devices, or loses a login method.
Short answer: model each authentication action as its own validated transition, with an idempotency key and an audit record around the attach operation. Never merge two users because their email, display name, or other fuzzy attributes happen to look alike.
The invariants behind a safe link
The workflow starts with a verified captcha result and an external provider assertion. Those inputs are evidence, not an instruction to mutate a user record. Resolve the assertion into a stable identity record, then inspect the identities already attached to the candidate user. A user may have several identities, but one external identity must map to at most one user.
That distinction matters under retries. Mobile clients resend requests, queue workers replay messages, and a timeout can hide a successful write. Treat the attach command as an exactly-once intention even when the transport is at-least-once: carry a client-generated idempotency key, persist the decision, and make a repeated request return the original result. A common design-review failure is treating a resolver response as permission to attach, allowing a second device to race the first one and create a support ticket rather than a clean, explainable state. The fix is procedural, not clever. Resolve and inspect are reads; attach is a separately authorized write with a unique constraint and a durable audit event.
The audit record should keep four facts for every transition: who initiated it, which provider subject was resolved, which user was inspected, and why the policy allowed or rejected the attach. It should include a request ID and timestamps, while sensitive provider tokens stay out of application logs. OWASP's authentication guidance is a useful baseline for that separation of credentials, sessions, and account recovery.
Do not guess at a match.
If resolution fails, ask the player to authenticate an existing account or start a new one; an automatic merge based on a similar name is difficult to reverse and impossible to explain during a support investigation. Your mileage may vary on the exact risk thresholds, but the decision should be explicit and testable rather than hidden in a fuzzy matcher.
How should a gaming signup resolve, inspect, and attach identities safely?
The critical path is deliberately boring:
- Verify the captcha and rate-limit the signup attempt by IP, device signal, and account target.
- Resolve the provider assertion.
- Inspect the candidate user's current identity list and login methods.
- Attach once, recording the policy decision and idempotency key.
The following Go sketch shows the read side of that path. It uses the documented identity routes and leaves the final database transaction in the service that owns the user record. In production, the same transaction boundary should guard the uniqueness constraint on (provider, subject).
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type request struct {
Provider string `json:"provider"`
Token string `json:"token"`
}
func call(method, path, key, baseURL string, body any) ([]byte, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("identity request failed: %s: %s", res.Status, out)
}
return out, nil
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
baseURL := os.Getenv("INFRAI_BASE_URL")
if key == "" {
panic("INFRAI_API_KEY is required")
}
if baseURL == "" {
panic("INFRAI_BASE_URL is required")
}
resolved, err := call("POST", "/v1/auth/identity/resolve", key, baseURL, request{
Provider: "steam",
Token: os.Getenv("PROVIDER_ASSERTION"),
})
if err != nil {
panic(err)
}
fmt.Println(string(resolved))
// After parsing the resolved subject, inspect before any attach transaction.
inspected, err := call("POST", "/v1/auth/identity/get", key, baseURL, map[string]any{
"provider": "steam",
"subject": "resolved-subject",
})
if err != nil {
panic(err)
}
fmt.Println(string(inspected))
}
The example intentionally stops before mutation because the attach policy belongs beside the user database's uniqueness constraint. A retrying client should add an idempotency key to its write request and back off on HTTP 429, honoring Retry-After; it must surface any 4xx response instead of treating it as a successful link.
What the alternatives optimize for
There is no universal winner. The right choice depends on where you want identity policy, abuse controls, and operational ownership to live.
| Option | Typical fit | Trade-off for this workflow |
|---|---|---|
| Auth0 | Hosted social and enterprise identity | Broad provider integrations, with policy and cost tied to a hosted tenant model |
| Firebase Authentication | Mobile-first games already using Firebase | Fast client integration, but account data and adjacent services stay in the Firebase ecosystem |
| Amazon Cognito | Teams standardized on AWS IAM and managed pools | Strong AWS alignment, with more AWS-specific configuration to carry across environments |
| A focused service behind your API | Teams requiring domain-owned ledger and audit rules | Maximum control, but you own provider adapters, recovery UX, and abuse operations |
| Infrai | A single HTTP surface for several backend capabilities | One key and one bill can cover auth plus other services, and the plain REST interface avoids an SDK dependency; your service still owns the attach transaction and policy |
The last row is useful when a small team wants one credential and billing boundary across backend capabilities while keeping its own user database authoritative. It is not suitable when you need a turnkey hosted console, built-in game-specific risk scoring, or a provider contract your compliance team already mandates. Stick with Auth0, Firebase, or Cognito when their existing operational controls are the requirement rather than an implementation detail.
Unlinking is a separate safety check
Removal deserves its own transition. Before detaching an identity, count the remaining usable login methods and require a fresh authentication for a high-risk account change. A player who unlinks the only provider can lock themselves out; a support agent who cannot reconstruct the decision leaves an audit gap.
Keep the operation reversible in your domain model with a tombstoned link record and an actor, reason, and request ID. A scheduled reconciliation job can compare provider identities with local records, but it should report discrepancies for review rather than silently merge accounts.
Top comments (0)