All routes below are relative to the namespace this module is mounted under (see AuthModule::config(cfg, namespace) in the README). Examples assume a namespace of /api/v1.
Unless noted otherwise, responses use this envelope (ApiResponse<T>):
{
"success": true,
"message": "human-readable summary",
"data": { }
}On error, success is false, data is null, and message describes the problem. Passkey-specific error responses (see below) instead return { "error": "..." } — these are the prototype's original shape and haven't been changed to avoid a breaking change to that module's contract.
Routes marked 🔐 require a valid access token. This module doesn't dictate how the token is transmitted (bearer header vs. cookie) — that's handled by whatever Validate<Identity> implementation and middleware the consuming app wires up around actixutils::Auth.
Create a new account.
Body
{ "username": "alice", "password": "hunter2plus" }201 Created
{
"success": true,
"message": "User registered successfully",
"data": {
"access_token": "…",
"refresh_token": "…",
"expires_in": 600
}
}Errors: 400 missing credentials / password under 6 characters, 409 username already exists.
Publishes an auth.user.created event.
Body
{ "identifier": "alice@example.com", "password": "hunter2plus" }200 OK — same AuthResult shape as register. Also sets an access_token httpOnly cookie.
Errors: 401 invalid credentials.
Identical contract to /auth/login/email, but looks the account up by username instead of email.
Exchange a refresh token for a new access/refresh pair. The old refresh token is deleted (rotation) — it cannot be reused even if the request otherwise fails partway.
Body
{ "refresh_token": "…" }200 OK — new AuthResult.
Errors: 401 token not found, revoked, or expired.
Revokes a refresh token. Idempotent — calling it twice with the same (now-revoked) token still returns success.
Body
{ "refresh_token": "…" }200 OK, empty data.
Body
{ "email": "alice@example.com" }200 OK always, regardless of whether the email exists — this endpoint never reveals whether an address is registered.
Body
{ "token": "…", "new_password": "newpassword123" }200 OK on success. Revokes all of the user's existing sessions (refresh tokens) as a side effect.
Errors: 400 password too short, 401 invalid/expired/already-used token.
Returns the identity embedded in the caller's access token.
200 OK
{
"success": true,
"message": "Protected data retrieved successfully",
"data": { "user_id": "…", "message": "Access granted to protected route" }
}Body
{ "current_password": "hunter2plus", "new_password": "newpassword123" }200 OK on success. Revokes all existing sessions.
Errors: 400 new password too short, 401 current password incorrect.
Base path: /passwordless. Challenges are short-lived (2 minutes) and held in memory (not the database).
Body
{ "email": "alice@example.com" }201 Created on success — a challenge token/link pair is generated and published as an auth.2fa.challenge.requested event for your notification service to deliver (email/SMS/etc). Nothing is returned in the HTTP response body itself.
Errors: 404 user not found.
Same as above, looked up by username instead of email.
Confirm via the link half of the challenge pair.
200 OK
{ "access_token": "…", "refresh_token": "…", "expires_in": 600 }Errors: 400 invalid/expired link.
Confirm via the numeric one-time code half of the pair.
Body
{ "token": 123456 }200 OK — same shape as confirm_link.
Base path: /passkey. All bodies are the standard WebAuthn Level 2/3 JSON shapes produced by navigator.credentials.create() / .get() — pass what the browser gives you straight through, no reshaping needed.
Error responses on this sub-module use { "error": "message" } rather than the ApiResponse envelope.
Begin registering a new passkey for the calling account.
Body: none.
200 OK — a WebAuthn PublicKeyCredentialCreationOptions object. Pass directly to:
const options = await res.json();
const credential = await navigator.credentials.create({ publicKey: options.publicKey });Errors: 400 WebAuthn/config error, 401 missing/invalid access token, 500 internal error.
Complete registration and store the credential. label is optional.
Body: the PublicKeyCredential object returned by navigator.credentials.create(), JSON-serialized (typically via a small helper that base64url-encodes the ArrayBuffer fields).
200 OK
{ "status": "success", "message": "Passkey registered" }Errors: 400 no registration in progress / expired (registrations expire after 5 minutes) or the authenticator's response failed verification.
List the calling account's registered passkeys.
200 OK
[
{
"id": "5a4e...",
"label": "My Laptop",
"created_at": "2026-07-10T12:00:00Z",
"last_used_at": "2026-07-14T08:30:00Z"
}
]Remove a passkey by its id (from the list endpoint above).
200 OK { "status": "success" } · 404 if that id doesn't belong to the caller.
Begin a passkey login. Public — no access token required or available yet.
Body
{ "username": "alice" }200 OK — a WebAuthn PublicKeyCredentialRequestOptions object:
const options = await res.json();
const credential = await navigator.credentials.get({ publicKey: options.publicKey });Errors: 400 unknown username or the account has no passkeys registered (same message either way, to avoid username enumeration).
Complete the login.
Body: the PublicKeyCredential object returned by navigator.credentials.get(), JSON-serialized.
200 OK
{
"success": true,
"message": "Passkey login successful",
"data": { "access_token": "…", "refresh_token": "…", "expires_in": 600 }
}Also sets the access_token httpOnly cookie, same as password login.
Errors: 400 no login in progress for that username / expired (5 minute window) / assertion failed verification.
Look up a user's id by username. Returns the raw id as the response body (not JSON-wrapped).
200 OK — plain text UUID · 404 if not found.