Sign in (passwordless)
Crowded Kingdoms is passwordless. There is no email + password login and no
registration form — accounts are created on first sign-in. A user authenticates
one of three ways, and every path returns an identity session token (an
AuthResponse):
- Magic link — a one-time link emailed to the address.
- Social / OIDC — a federated provider (e.g. Google).
- Dev bypass — a direct sign-in for local/dev/test only (never production).
The session token is a management-plane credential. It is not valid for gameplay: to play, mint a short-lived app-scoped token from it — see Portals & app-scoped tokens and Game API → Authentication.
CrowdyJS wraps every flow below behind client.auth (passwordless) — see
CrowdyJS → Authentication.
You rarely hand-write these mutations in a browser.
What you get back
A successful sign-in returns an AuthResponse:
type AuthResponse {
token: String! # identity SESSION token — send as Authorization: Bearer <token>
gameTokenId: String! # id of the underlying session row
user: User! # the authenticated (or just-created) account
}
Send Authorization: Bearer <token> on subsequent Management API requests. Resolve
the caller with me { userId email gamertag }.
Magic link (email)
Two steps. Request a link, then complete the sign-in with the token from it.
# 1) Email a one-time sign-in link. Always reports sent=true (no account
# enumeration). Creates the account on first sign-in. Public.
mutation Request($input: RequestLoginLinkInput!) {
requestLoginLink(input: $input) {
sent
devToken # DEV ONLY: present when DEV_AUTH_BYPASS is on (no email is sent); else null
}
}
# variables: { "input": { "email": "player@example.com", "redirectUri": "https://app.example.com/auth/callback" } }
# 2) Complete sign-in with the token from the link (or devToken in dev). Public —
# the token authorizes the call; throws if invalid/expired/already used.
mutation Complete($input: CompleteLoginLinkInput!) {
completeLoginLink(input: $input) { token gameTokenId user { userId email } }
}
# variables: { "input": { "token": "<one-time-token-from-the-link>" } }
The redirectUri origin must be an allowed app/UI origin; it defaults to the
platform sign-in page. In development (DEV_AUTH_BYPASS=true) no email is
delivered — requestLoginLink returns the devToken directly so tests and local
flows can call completeLoginLink without an inbox.
Social / OIDC
Providers are pluggable. List the enabled ones, then run a standard redirect-based OAuth/OIDC handshake.
# Which providers are enabled right now, e.g. ["google"].
query Providers { availableLoginProviders }
# 1) Begin: returns a URL to send the browser to and an opaque state to round-trip.
mutation Start($input: SocialLoginStartInput!) {
socialLoginStart(input: $input) { authorizeUrl state }
}
# variables: { "input": { "provider": "google", "redirectUri": "https://app.example.com/auth/google/callback" } }
Redirect the browser to authorizeUrl. The provider sends the user back to your
redirectUri with a code; complete the sign-in with that code and the
state you started with:
# 2) Complete: creates/links the account by provider identity, returns a session.
mutation Done($input: SocialLoginCompleteInput!) {
socialLoginComplete(input: $input) { token gameTokenId user { userId email } }
}
# variables: { "input": { "provider": "google", "code": "<from-provider>", "state": "<from-step-1>" } }
The framework is provider-agnostic. Today it ships:
| Provider | Enabled when |
|---|---|
google | GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are configured on the server. |
mock | Dev only — appears only when DEV_AUTH_BYPASS=true. Never in production. |
availableLoginProviders reflects exactly what is configured, so drive your
sign-in UI from it rather than hard-coding provider buttons.
Dev bypass
devLogin returns a session for an email without email or social
verification. It is active only when the server runs with
DEV_AUTH_BYPASS=true (local, dev, and test environments) and throws FORBIDDEN
otherwise. It is used by tests and fixtures and is never enabled in
production.
mutation Dev($input: DevLoginInput!) {
devLogin(input: $input) { token gameTokenId user { userId email } }
}
# variables: { "input": { "email": "player@example.com" } }
Federated identities (linking sign-in methods)
An account can have several linked sign-in identities (a Google identity, an email
magic-link identity, …). The account is created on first sign-in; on later
sign-ins an identity is matched by (provider, subject) and linked to an existing
account by verified email, so signing in with Google and with a magic link for
the same verified address resolves to one account.
# The signed-in user's linked identities (requires a session token).
query Mine { myIdentities { identityId provider subject email emailVerified lastLoginAt } }
linkIdentity(input: { provider, code, state })— link an additional identity from asocialLoginStartcallback to the signed-in account. Throws if that identity is already linked to another account.unlinkIdentity(identityId)— remove a linked identity. Refuses to remove your last remaining sign-in method (so you can never lock yourself out).
Sessions and sign-out
logoutends the current session (deletes thegame_tokenthat authenticated the request); other devices stay signed in. Signing out an identity session also revokes every app token it minted.logoutAllDevicesends every active session for the user.
Security notes
- Treat the session token (and any app token) as a secret; use HTTPS only in production.
requestLoginLinknever reveals whether an address has an account (sentis alwaystrue); one-time link tokens are single-use and short-lived.- Restrict your own frontends' CORS and redirect origins to trusted hosts; the
server validates
redirectUriorigins for both magic-link and social flows. - The dev bypass (
devLogin+ themockprovider) must never be enabled in production.availableLoginProvidersand the presence ofdevTokenare the machine-readable signals that it is on.