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).