Skip to main content

Authentication Endpoints

This page documents the email/password credential and session-based authentication endpoints: registration, email verification, login (including the 2FA challenge), logout, and password reset.

These endpoints are the primary surface used by platform/id for local account creation and sign-in. They are the credential layer that runs before the OAuth consent flow.

Related documentation

Social / OAuth provider sign-in (Twitch, Discord, GitHub, etc.) is handled separately via NextAuth callbacks and the oauthSignin GraphQL mutation, which provisions a user from an external provider. The credential endpoints below cover only email-platform (password) authentication.

Overview​

  • Base path: all endpoints are mounted under the /v1 scope.
  • Credential storage: passwords are hashed with Argon2id (heimdall-auth); a minimum length of 8 characters is enforced on register and reset.
  • Sessions: a successful login creates a database Session and returns a session token of the form ses_<id> that is valid for 30 days.
  • 2FA challenge: if the account has 2FA enabled, login does not return a session token. Instead it returns requiresTwoFactor = true plus a short-lived temporary token of the form 2fa_<id> (valid for 10 minutes), which must be exchanged via the 2FA verify endpoint.
  • Bot protection: when Cloudflare Turnstile is enabled in config, register, login, reset-password, and reset-password/complete require a valid turnstileToken. When Turnstile is disabled the field is ignored.

Field conventions: Rust/REST JSON uses snake_case; GraphQL/TypeScript uses camelCase (auto-converted). The tables below list REST snake_case field names.

Shared response: AuthResponse​

login, register/verify, and 2fa/verify all return an AuthResponse:

FieldTypeNotes
user_idstringUser UUID.
emailstringEmail-platform address (may be empty if none).
usernamestringCanonical username.
session_tokenstring?Session token (ses_…). Omitted when 2FA is required.
session_idstring?DB session ID (for WebSocket session targeting). Omitted when 2FA is required.
expires_atdatetime?Session expiry. Omitted when 2FA is required.
providerstring?Primary login provider slug (e.g. email, twitch, discord).
requires_2fabooltrue when a 2FA challenge must be completed.
temp_tokenstring?Temporary 2FA token (2fa_…). Only set when requires_2fa is true.
preferred_localestring?User locale (e.g. en, de).

Register​

POST /v1/auth/register
Content-Type: application/json

Initiates registration by sending a verification email. No account is created yet — a PendingRegistration row is stored and the user must verify their email to complete sign-up.

Auth: none.

Request body:

FieldTypeRequiredNotes
emailstringyesLowercased server-side.
usernamestringyes3–30 chars; letters, numbers, and underscores only. Lowercased server-side.
passwordstringyesMinimum 8 characters.
turnstile_tokenstringconditionalRequired when Turnstile is enabled.

Response — 200 OK:

{
"message": "Verification email sent. Please check your inbox to complete registration.",
"expires_at": "2026-06-28T12:00:00Z"
}

The pending registration (and its verification token) expires after 24 hours.

Error cases:

StatusCause
400 Bad RequestRegistration disabled (global registration_enabled setting or email platform), username length 3–30 violated, invalid username characters, password shorter than 8, or Turnstile verification failed.
409 ConflictEmail already registered, username already taken, or a pending registration already exists for that email/username.
500 Internal Server ErrorFailed to send the verification email (the pending row is rolled back).

Verify Registration​

POST /v1/auth/register/verify
Content-Type: application/json

Completes registration after email verification: creates the User, the email PlatformAccount, assigns the default role_user, and immediately creates a session (new users do not yet have 2FA).

Auth: none.

Request body:

FieldTypeRequired
tokenstringyes

Response — 201 Created: an AuthResponse with session_token, session_id, expires_at populated, provider = "email", and requires_2fa = false.

Error cases:

StatusCause
400 Bad RequestInvalid or expired verification token.
409 ConflictEmail or username became taken between request and verification.

Verify Email (unified)​

POST /v1/auth/verify-email
Content-Type: application/json

A unified endpoint that auto-detects the token type and handles three flows:

  • Registration — creates a new account (same as register/verify).
  • Email link — links an additional email to an existing account.
  • Email change — changes the primary email of an existing account.

Auth: none.

Request body:

FieldTypeRequired
tokenstringyes

Response — 200 OK:

FieldTypeNotes
successboolAlways true on success.
messagestringHuman-readable result.
verification_typeenumOne of registration, email_link, email_change.
user_idstring?Affected user UUID.
redirect_urlstringWhere the frontend should redirect (e.g. /login?verified=true).

Error cases:

StatusCause
400 Bad RequestToken not found in any table, or expired.
409 ConflictEmail/username already in use for the detected flow.

Unlike register/verify, this endpoint does not return a session — it returns a redirect_url for the frontend to follow.


Login​

POST /v1/auth/login
Content-Type: application/json

Authenticates with an email or username plus password. The identifier is matched against User.username first, then against the email platform account.

Auth: none.

Request body:

FieldTypeRequiredNotes
identifierstringyesEmail or username. Lowercased server-side.
passwordstringyes
turnstile_tokenstringconditionalRequired when Turnstile is enabled.

Response — 200 OK: an AuthResponse.

  • No 2FA: session_token, session_id, expires_at are set, requires_2fa = false, temp_token = null.
  • 2FA enabled: session_token/session_id/expires_at are null, requires_2fa = true, and temp_token (2fa_…, valid 10 minutes) is returned. The client must then call /v1/auth/2fa/verify.

Error cases:

StatusCause
400 Bad RequestTurnstile verification failed.
401 UnauthorizedUnknown identifier, account has no password authentication, or wrong password (logged as a failed-login audit event).

Verify 2FA (at login)​

POST /v1/auth/2fa/verify
Content-Type: application/json

Completes a login that returned requires_2fa = true. Verifies the TOTP code (or a backup code), consumes the temporary session, and creates a full session preserving the original login provider.

Auth: none (the temp_token is the credential).

Request body:

FieldTypeRequiredNotes
temp_tokenstringyesThe 2fa_… token from login.
codestringyesTOTP code from the authenticator app, or a backup code.

Response — 200 OK: an AuthResponse with a full session (session_token, session_id, expires_at) and requires_2fa = false.

If a backup code is used and the user has 3 or fewer remaining, a "backup codes low" reminder email is sent.

Error cases:

StatusCause
401 Unauthorizedtemp_token does not start with 2fa_, is expired/invalid, or the code is wrong (logged as a failed-2FA audit event).

Check 2FA Status (internal)​

POST /v1/auth/2fa/check
Authorization: Bearer <system_api_key>
Content-Type: application/json

Used by OAuth/social providers (NextAuth callbacks) to check whether a freshly authenticated user needs a 2FA challenge, and to obtain a temp token if so. This is an internal endpoint intended for platform/id, not external consumers.

Auth: requires a valid system API key (is_system = true). Returns 403 for non-system keys.

Request body:

FieldTypeRequiredNotes
user_idstringyesUser to check.
oauth_providerstringnoProvider that initiated login (e.g. twitch); stored with the temp token.

Response — 200 OK:

FieldTypeNotes
requires_2faboolWhether 2FA is enabled for the user.
temp_tokenstring?2fa_… token, only present when requires_2fa is true.

Error cases:

StatusCause
400 Bad RequestInvalid user ID format.
401 UnauthorizedMissing API key.
403 ForbiddenAPI key is not a system key.

Logout​

POST /v1/auth/logout
Authorization: Bearer <session_token>

Deletes the session matching the bearer token, invalidates the Redis cache, and logs a logout audit event.

Auth: session token in the Authorization: Bearer header.

Response — 200 OK:

{ "message": "Logged out successfully" }

Error cases:

StatusCause
401 UnauthorizedMissing session token.

Request Password Reset​

POST /v1/auth/reset-password
Content-Type: application/json

Sends a password-reset email if an account exists for the address. Always returns 200 to prevent email enumeration.

Auth: none.

Request body:

FieldTypeRequiredNotes
emailstringyesLowercased server-side.
turnstile_tokenstringconditionalRequired when Turnstile is enabled.

Response — 200 OK:

{ "message": "If an account exists with this email, a reset link has been sent" }

The reset token expires after 1 hour.

Error cases:

StatusCause
400 Bad RequestTurnstile verification failed.

Complete Password Reset​

POST /v1/auth/reset-password/complete
Content-Type: application/json

Sets a new password using a reset token. On success, the password is updated, the token is marked used, and all existing sessions for the user are invalidated.

Auth: none (the token is the credential).

Request body:

FieldTypeRequiredNotes
tokenstringyesReset token from the email.
new_passwordstringyesMinimum 8 characters.
turnstile_tokenstringconditionalRequired when Turnstile is enabled.

Response — 200 OK:

{ "message": "Password reset successfully. Please login with your new password." }

Error cases:

StatusCause
400 Bad RequestInvalid or expired reset token, password shorter than 8, or Turnstile verification failed.

Not Implemented​

The following stub endpoints exist in the routing table but return 501 Not Implemented:

POST /v1/auth/signin
POST /v1/auth/validate

Both respond with:

{ "message": "Authentication not implemented yet" }

Use the credential endpoints documented above instead.


GraphQL Equivalents​

Most credential operations (register, login, verify-email, password reset) are REST-only; there are no GraphQL mutations for them. The GraphQL API exposes the following auth-related operations for parity where they exist:

verifyTwoFactorLogin mutation​

Equivalent to POST /v1/auth/2fa/verify.

mutation VerifyTwoFactorLogin($input: VerifyTwoFactorLoginInput!) {
verifyTwoFactorLogin(input: $input) {
userId
email
username
sessionToken
sessionId
expiresAt
provider
preferredLocale
}
}

Input (VerifyTwoFactorLoginInput):

FieldTypeNotes
tempTokenString!The 2fa_… token from login.
codeString!TOTP or backup code.

Returns GqlTwoFactorLoginResponse (a full session; sessionToken, sessionId, and expiresAt are non-nullable here). Requires no authentication — the temp token is the credential.

logout mutation​

mutation { logout }

Returns a Boolean. Note: unlike the REST logout (which deletes only the current session), the GraphQL logout deletes all sessions for the authenticated user and invalidates cached permissions. Requires a user session (API keys cannot log out).

oauthSignin mutation​

Provisions or updates a user from an external OAuth provider (social login). Requires a system API key and is used by NextAuth backend calls, not by external consumers.

mutation OauthSignin($input: OAuthSignInInput!) {
oauthSignin(input: $input) {
user { id username email avatarUrl platform preferredLocale }
message
}
}

Input (OAuthSignInInput): email (String), name (String!), oauthProvider (String!), oauthProviderId (String!), image (String).