CorebanqCorebanq Developer Docs
Auth

Description

Overview

The Authentication API provides comprehensive authentication and authorization functionality:

  • User authentication with username/password
  • Multi-factor authentication (2FA)
  • JWT token management (access & refresh tokens)
  • API key authentication
  • CORS configuration
  • Kubernetes-friendly session management
  • Device tracking and security

Core Concepts

Authentication Methods

1. Bearer Token

  • Access token (JWT, 15 minutes)
  • Refresh token (14 days, HTTP-only cookie)
  • Token rotation on refresh
  • Refresh-token idle timeout policy (configurable inactivity window)

2. API Key

  • Static keys from configuration
  • Request header: X-API-Key
  • Used for service-to-service communication

3. Application Identity (X-App-ID)

  • Deterministic identifier sent via X-App-ID header (e.g. web-app, admin-app, configurator-app)
  • Scopes the refresh-token cookie per application (refresh_token_web-app, refresh_token_admin-app, etc.)
  • Prevents cross-app token collisions when multiple frontends share the same domain
  • Backend validates the header against a configurable whitelist (auth.allowed_app_ids)
  • The App ID is stored inside the refresh token in Redis; on refresh the value must match
  • When no whitelist is configured, any non-empty value is accepted (backward-compatible)

4. Multi-Factor Authentication (2FA)

  • Email OTP
  • Phone OTP
  • TOTP (Time-based One-Time Password)
    • RFC 6238 compliant
    • 30-second time window
    • 6-digit codes
    • QR code generation for setup
    • Backup codes for recovery

5. MFA Session Boundary

  • challenge_token is a pre-auth token used only for MFA/TOTP challenge flows.
  • access_token is a post-auth token with sub=user_auth and is the only bearer token accepted on protected routes.
  • For MFA-enabled users, the initial /v1/authenticate response does not issue an authenticated access token or refresh-token cookie.
  • The authenticated session is created only after successful /v1/verify-2FA.
  • Replaying challenge_token on protected routes or /v1/refresh-token is rejected.

6. MFA mode semantics (mfa_mode)

  • off: After a successful password check, authentication completes without an interactive MFA step (no challenge payload); the API issues normal session artifacts (access_token, refresh cookie when applicable).
  • email / phone / totp: Successful password verification yields a pre-auth challenge (credential_type, challenge_token, localized message) until /v1/verify-2FA succeeds.
  • totp_setup_required: When TOTP is required but not yet enrolled, the challenge response uses credential_type: "totp_setup_required" and a localized message (see auth_m.totp_setup_required in auth_m.yaml). This applies in particular to Internal-role users with mfa_mode: totp and no TOTP secret yet (enrollment must finish before a full session is granted). Non-internal users in totp mode without a secret may receive the same payload when no validated email/phone fallback is available.

7. Internal invitation default MFA (users module)

Users created via /v1/users/admin/invite get their stored mfa_mode from auth.internal_invite.mfa_mode in the auth app-config module (totp, email, or phone), so privileged invites can enforce stricter MFA than the tenant default.

Security Features

Brute Force Protection

  • Maximum login attempts (configurable)
  • Cooling-off period
  • IP-based rate limiting
  • Device tracking

Admin Account Protection

  • Email Alert System: Failed admin login attempts trigger immediate email notifications
  • Alert Throttling: Email notifications have configurable cooldown periods to prevent spam
  • Enhanced Logging: Admin access attempts include detailed metadata (IP, user agent, timestamps)
  • Escalated Security: Admin accounts receive stricter security policies and monitoring
  • Real-time Notifications: Security teams receive instant alerts for suspicious admin activity

Token Security

  • HTTP-only cookies for refresh tokens
  • Secure token rotation
  • Device binding
  • IP tracking

Cache Degradation (Fail-Closed)

Authentication depends on cache availability for refresh token validation and access token blacklist checks. If the cache is unavailable, auth endpoints and protected routes return 503 Service Unavailable. This prevents accepting stale access tokens when revocation cannot be enforced.

The OTP cooling-off gate follows the same principle on its own store: a read it cannot complete is not evidence that no lockout is active, so the request is refused with otp_m.cache_error instead of issuing a fresh code and clearing the lockout. This covers the e-mail, phone, TOTP and TOTP-recovery paths alike.

Endpoints

Authentication

Login

POST /v1/authenticate

Authenticate user and get tokens.

If the user's mfa_mode is off, the response contains the authenticated access_token and the server sets the refresh-token cookie (no MFA challenge).

If mfa_mode is email, phone, or totp (and TOTP is already enrolled where applicable), the response is a challenge response instead: no authenticated session artifacts are issued yet, and the client must complete /v1/verify-2FA first.

If TOTP is required but not enrolled, the challenge may use credential_type: "totp_setup_required" so the client can complete TOTP setup before verification.

When the challenge is delivered by e-mail or phone, this endpoint issues a one-time code, so the OTP module's refusals surface here rather than only on the verification endpoints. The attempt budget is counted per credential and shared with registration and credential verification, so a login can be refused with 429 otp_m.cooling_period_active because of failures made in another flow; the remaining seconds are interpolated into message and reported as retry_after. A resend does not reset that budget. Sends are paced on top of that budget, and pacing bounds sending a code, not logging in: a login that arrives too soon after the last one, or one for a credential that has spent its window, issues no new code but still answers 200 with the challenge token and retry_after, because the code issued earlier is still valid and the caller needs the token to submit it. The window outlives the code it capped, so a credential can be barred from new sends for minutes while holding one it can still use. Such an answer says so: message is auth_m.otp_previously_sent rather than auth_m.otp_email_send_successfully, so a client tells the user to enter the code they already have instead of waiting for one that is not coming. That holds only while the user can be assumed to hold the code: a delivery no channel confirmed keeps the interval running and stops offering its code, so a login paced after one is refused 429 otp_m.resend_too_soon or 429 otp_m.resend_limit_exceeded rather than promising a code nobody sent; both carry retry_after. A successful verification is the other way round — it consumes the code and releases the interval with it, so the next login issues a fresh code rather than being refused for the rest of an interval it can no longer answer. The e-mail and phone challenge responses carry it too, so a client seeds its resend countdown from the server rather than from a build-time constant. If the OTP store cannot be read the cooling-off gate fails closed and the login answers 500 otp_m.cache_error rather than issuing a code no verification could match. A delivery failure after the code was generated answers 500 otp_m.failed_send_otp, and a stored credential the OTP module cannot deliver to answers 400 otp_m.unsupported_credential_type — the only 400 on this endpoint that is not about the submitted credentials.

Request Body:

{
  "username": "user@example.com",
  "password": "secure_password"
}

Response:

{
  "credential_type": "email",
  "message": "OTP sent successfully",
  "challenge_token": "eyJ0eXAi...",
  "user_id": "123e4567-e89b-12d3-a456-426614174000",
  "retry_after": 30
}

Admin Login

POST /v1/authenticate/admin

Authenticate admin user with Internal role and get tokens. This endpoint requires the user to have the Internal role and includes enhanced security monitoring.

When mfa_mode is not off, the initial response is a pre-auth challenge; authenticated bearer/session artifacts are issued only after /v1/verify-2FA succeeds (or after TOTP enrollment when credential_type is totp_setup_required).

Request Body:

{
  "username": "admin@example.com",
  "password": "secure_password"
}

Response:

{
  "access_token": "eyJ0eXAi...",
  "expires_in": 900,
  "idle_timeout_seconds": 900,
  "user_id": "123e4567-e89b-12d3-a456-426614174000"
}

Error Response (403):

{
  "code": "auth.unauthorized_role",
  "message": "User does not have the required Internal role"
}

Admin Security Features:

  • Enhanced Monitoring: Failed admin login attempts trigger immediate security alerts
  • Email Notifications: Administrators receive real-time email alerts for suspicious activity
  • Extended Cooling Periods: Admin accounts have longer lockout periods for security
  • IP Tracking: All admin access attempts are logged with IP addresses and user agents

Refresh Token

POST /v1/refresh-token

Get new access token using refresh token.

The Authorization header must contain a real authenticated user_auth access token. Pre-auth challenge_token values are rejected.

Response:

{
  "access_token": "eyJ0eXAi...",
  "expires_in": 900,
  "idle_timeout_seconds": 900,
  "user_id": "123e4567-e89b-12d3-a456-426614174000"
}

Verify 2FA

POST /v1/verify-2FA

Verify 2FA code and complete authentication.

This is the elevation point for MFA-enabled users. On success, the server issues the authenticated access_token and the refresh-token cookie.

When auth.2fa_challenge=true, clients must also send the challenge token returned by /v1/authenticate in X-MFA-Challenge.

Operational note — keep auth.2fa_challenge on. The setting defaults to false for backward compatibility, and with it off the challenge token is not checked at all: the endpoint is unauthenticated, so a caller can submit any user_id and a guessed code. Those failures are counted against the OTP attempt budget, which is keyed by the credential value and shared with registration and credential verification. Three of them put that user's e-mail or phone into the cooling-off period, and the user's next login is then refused by the same gate.

The budget no longer resets on a resend, so this costs more than it used to: previously the next Generate cleared the counter and the escalation index, and the effect lasted one minute; now each lockout is longer than the one before, up to otp.cooling.max (default one hour), for as long as otp.ttl.failed_attempts from the most recent lockout. That is a deliberate trade — the persistent budget is what bounds brute force against a six-digit code — but it makes the challenge token the control that keeps a stranger from spending someone else's budget.

Treat auth.2fa_challenge=true as a deployment requirement of otp.cooling.max, not a recommendation: the production values set CONFIG_AUTH_2FA_CHALLENGE: "true". An environment that leaves it off accepts that a caller who knows a user id can hold that user out of login for up to otp.cooling.max at a cost of three requests per lockout. A burst cannot shorten that: the escalation advances once per lockout actually served, so driving the wait to its ceiling takes as many separated lockouts as the backoff has steps, not one flood. The startup log names the state — a security notice is printed on every boot where the setting is off.

Request Body:

{
  "user_id": "123e4567-e89b-12d3-a456-426614174000",
  "otp": "123456"
}

Response:

{
  "access_token": "eyJ0eXAi...",
  "expires_in": 900,
  "idle_timeout_seconds": 900,
  "user_id": "123e4567-e89b-12d3-a456-426614174000"
}

Wrong code — attempt counting and cooling-off. A wrong TOTP (or recovery) code is a counted failure, not a plain validation error: the response is otp_m.otp_invalid (not otp_m.totp_invalid_code), and the remaining attempts are interpolated into the localized message. The error envelope carries no params object — branch on code, and show message as-is.

{
  "status": 400,
  "code": "otp_m.otp_invalid",
  "message": "Invalid OTP, 2 attempt(s) remaining"
}

Once the attempts are exhausted, further verification is refused for a cooling-off period (backoff grows with repeated lockouts) rather than returning the per-attempt error: the request that exhausts them answers otp_m.max_attempts_reached, and every request arriving while the period is still active answers otp_m.cooling_period_active with the time left in message.

{
  "status": 429,
  "code": "otp_m.cooling_period_active",
  "message": "In cooling period for 42 seconds, 42 second(s) left",
  "retry_after": 42
}

retry_after is whole seconds, rounded up, and is mirrored in the Retry-After header. Every OTP rate-limit refusal carries it, otp_m.max_attempts_reached included. Other 429s on this API do not carry it yet — auth.admin_locked and the IP rate limiter still state their wait in message only — so treat the field as optional on any 429. A wrong code that has not exhausted the budget carries none, so a client can leave a running resend countdown alone, and an answer that names no interval reports no retry_after at all rather than a zero or a one.

On the TOTP branch of /v1/verify-2FA the wrong-code states are reported as 400 (the web middleware treats 401 as a token-refresh signal), but the two lockout states keep 429 so the client can tell "wait" from "wrong code" and read the time left. The email/phone branch and /v1/auth/totp/recovery use 401/429 — so branch on code, not on the status.

Recovery codes are single-use, and failures are indistinguishable. /v1/auth/totp/recovery answers 401 for every rejected code, whatever the reason: an unknown username, a user who has no TOTP enrolled (common.invalid_credentials), a code that never existed, and a code already spent by an earlier request (auth.totp_invalid_recovery_code). Nothing in the response tells an unauthenticated caller which of those it hit, so the endpoint cannot be used to learn whether an account exists or whether it has TOTP. The rate-limited states are the exception and are meant to be told apart: exhausting the attempts (otp_m.max_attempts_reached) and calling during an active cooling-off period (otp_m.cooling_period_active) both answer 429 with the time left.

A successful recovery consumes exactly one code. The consume is atomic — the row is locked, and the code is cleared only if it is still present — so two requests racing with the same code produce one success and one 401, never two.

Logout

POST /v1/logout

Invalidate tokens and end session.

When the Authorization: Bearer {access_token} header is provided, the access token is immediately invalidated. Any subsequent requests using that token will return 401 Unauthorized, even if the token has not yet expired.

TOTP Management

Five routes, all under /v1/auth/totp/. The page previously described them under /v1/totp/, along with a setup-verification, a backup-code and a code-regeneration endpoint; none of those paths was ever registered, and a caller following them got a 404. The full request and response schemas, and every refusal each route can answer, are in OpenAPI schema.

Setup TOTP

POST /v1/auth/totp/setup

Start enrolment: generates the secret, the QR code and the recovery codes. Nothing is enabled yet — the secret is held in the cache until a code is verified against it.

Authenticated with an access token, or with the MFA challenge token from a login that has TOTP pending. The body's user_id must be the authenticated user; enrolling a second factor for somebody else is refused with 403.

Request Body:

{
  "user_id": "123e4567-e89b-12d3-a456-426614174000"
}

Response:

{
  "success": true,
  "data": {
    "secret": "JBSWY3DPEHPK3PXP",
    "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
    "recovery_codes": ["ABCDEFGH12345678", "..."],
    "backup_url": "otpauth://totp/CoreBanq:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=CoreBanq"
  }
}

Verify TOTP

POST /v1/auth/totp/verify

One route for both cases. While a setup is pending in the cache it completes the enrolment and answers setup_completed; afterwards it verifies an ordinary code. Wrong codes are counted, and the attempt budget is shared with the other TOTP routes: once it is spent, every one of them answers 429 until the cooling-off period elapses.

Request Body:

{
  "code": "123456"
}

Response:

{
  "success": true,
  "data": {
    "valid": true,
    "setup_completed": true
  }
}

Use a recovery code

POST /v1/auth/totp/recovery

For a user who cannot produce a code. Unauthenticated — the caller has no token at this point — so the user is identified by e-mail, and an unknown user, one without TOTP, a wrong code and a code already spent all get the same 401: a distinguishable refusal would tell an anonymous caller which accounts exist and which carry a second factor.

A code is spent by the request that uses it, and the session token it returns is what disables TOTP without a second code. There is no endpoint that issues new recovery codes: a user who has spent them all recovers by having TOTP disabled and enrolling again.

Request Body:

{
  "username": "user@example.com",
  "recovery_code": "ABCDEFGH12345678"
}

Response:

{
  "success": true,
  "data": {
    "valid": true,
    "remaining_codes": 8,
    "recovery_session_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Disable TOTP

POST /v1/auth/totp/disable

Requires a valid access token. code is either a six-digit TOTP code or a recovery code; the length decides which, and anything longer than six characters is read as a recovery code.

Request Body:

{
  "code": "123456"
}

Response:

{
  "success": true,
  "message": "TOTP disabled"
}

Disable TOTP with a recovery session

POST /v1/auth/totp/disable-recovery

The continuation of the recovery flow, for a user who has no token to authenticate with. The session token is consumed on use, so a repeat of the same request answers 401.

Request Body:

{
  "recovery_session_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Response:

{
  "success": true,
  "message": "TOTP disabled"
}

Error Handling

All errors follow a standard format:

{
  "status": 401,
  "message": "Invalid credentials"
}

All error responses include the X-Request-ID header for tracing:

HTTP/1.1 401 Unauthorized
Content-Type: application/json
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

{"status":401,"message":"Invalid credentials"}

Tip: When reporting issues, always include the X-Request-ID from the response header for faster debugging.

Error Codes

Authentication Errors

CodeDescription
auth.unauthorizedInvalid credentials
auth.account_lockedAccount locked due to too many attempts
auth.unauthorized_roleUser does not have the required role
auth.invalid_tokenInvalid access token
auth.token_expiredToken has expired
auth.invalid_refresh_tokenInvalid refresh token
auth.device_info_mismatchDevice mismatch detected
auth_m.invalid_app_idX-App-ID value is not in the allowed whitelist
auth_m.app_id_mismatchX-App-ID does not match the app that created the refresh token
auth_m.invalid_challengeInvalid MFA challenge token
auth_m.challenge_already_usedMFA challenge token has already been used or expired from cache
otp_m.cooling_period_activeOTP attempt budget for the credential is exhausted; the wait is in message and retry_after (429)
otp_m.resend_too_soonA code was issued for this credential less than otp.resend.min_interval ago (429)
otp_m.resend_limit_exceededThe credential has spent otp.resend.max_per_window codes inside otp.resend.window (429)
otp_m.cache_errorThe OTP store could not be read or written, so the code was not issued (500)
otp_m.generation_failedThe one-time code could not be generated or delivered (500)
otp_m.failed_send_otpThe code was generated but the e-mail, SMS or Telegram delivery failed (500)
otp_m.unsupported_credential_typeThe stored MFA credential is of a type the OTP module cannot deliver to (400)

Admin Account Errors

CodeDescription
auth.admin_lockedAdmin account locked due to failed attempts
auth.unauthorized_roleUser does not have required Internal role
auth.admin_alert_sentSecurity alert sent to administrators

TOTP Errors

CodeDescription
auth.totp_invalid_codeInvalid TOTP code (on /v1/verify-2FA a wrong code returns otp_m.otp_invalid, with the attempts left in message)
auth.totp_code_expiredTOTP code has expired
auth.totp_already_enabledTOTP is already enabled for this user
auth.totp_not_enabledTOTP is not enabled for this user
auth.totp_setup_requiredTOTP setup must be completed first
auth.backup_code_invalidInvalid backup code
auth.backup_code_usedBackup code has already been used
auth.backup_codes_exhaustedAll backup codes have been used

API Key Errors

CodeDescription
auth.api_key_requiredMissing API key
auth.invalid_api_keyInvalid API key

Configuration

JWT Settings

auth:
  jwt_secret: "your_secret_key"
  access_token_ttl_minutes: 15
  refresh_token_ttl_minutes: 20160  # 14 days
  refresh_token_idle_timeout_minutes: 15  # inactivity timeout for refresh usage

Refresh Idle Timeout Behavior

  • refresh_token_idle_timeout_minutes defines the inactivity window for refresh-token usage.
  • If the refresh token is not used within this window, refresh validation fails and re-authentication is required.
  • API token responses include idle_timeout_seconds so clients can align UI timers with backend policy.
auth:
  cookie:
    # Allow insecure cookies (HTTP) for local development (default: false)
    allow_insecure: false
    
    # SameSite mode: strict, lax, or none (default: none)
    # 'none' is required for cross-origin requests (e.g., frontend on separate domain/port)
    same_site: "none"
    
    # Domain for the cookie (default: empty/host-only)
    # - Empty: Cookie is set for the specific host only (best for production)
    # - "localhost": Auto-set when SameSite=None and host is localhost/127.0.0.1 (fixes cross-port dev)
    # - Explicit Value: Set if valid for subdomains (e.g., ".example.com")
    domain: ""

App ID Whitelist

auth:
  allowed_app_ids:
    - web-app
    - admin-app
    - configurator-app

When the whitelist is configured, only the listed values are accepted in the X-App-ID header. An unknown value returns 400 Bad Request. When the list is empty or absent, any non-empty X-App-ID value is accepted (backward-compatible).

The App ID is persisted in the refresh token stored in Redis. During token refresh, the backend compares the stored App ID with the incoming X-App-ID header. A mismatch immediately revokes the token and returns 401 Unauthorized. Tokens created before this feature (no App ID stored) are not affected; the check is skipped for them.

API Key Settings

auth:
  api_keys:
    - "key1"
    - "key2"

Admin Security Settings

auth:
  admin_email: "security@company.com"
  brute_force:
    alert:
      ttl_minutes: 60  # Email notification cooldown
  audit_fields_internal: true  # Remove audit fields for non-internal users

CORS Settings

auth:
  cors:
    allowed_origins: ["https://example.com"]
    allowed_methods: "GET,POST,PUT,DELETE,OPTIONS"
    allowed_headers: "Content-Type,Authorization,X-API-Key"
    exposed_headers: "X-Request-ID,Idempotency-Replayed"
    allow_credentials: "true"

allowed_headers is not the final list. X-App-ID and X-Idempotency-Key are appended unconditionally, because omitting either breaks the client in a way that is almost impossible to diagnose from the outside: the OPTIONS preflight succeeds, the browser then drops the POST itself, so no request reaches the server, nothing is logged, there is no status code, and the SPA can only report a bare network error — while curl works fine. Idempotent POSTs (transfer create / finalize / quotes) all carry X-Idempotency-Key, so a stale environment config would otherwise disable the whole transfer flow in the browser only.

exposed_headers is configurable, but X-Request-ID and Idempotency-Replayed are always appended so browser clients can read response correlation IDs and idempotent replay state.

TOTP Settings

auth:
  totp:
    issuer: "CoreBanq"
    digits: 6
    period: 30  # seconds
    skew: 1     # allow 1 period of drift
    backup_codes_count: 5
    qr_code_size: 256  # pixels

Security Best Practices

  1. Token Management

    • Use short-lived access tokens
    • Secure refresh token storage
    • Implement token rotation
    • Monitor suspicious activity
  2. Device Security

    • Track device information
    • Detect suspicious changes
    • Implement device fingerprinting
    • Alert on security events
  3. Rate Limiting

    • Per-IP rate limits
    • Cooling-off periods
    • Graduated response
    • Admin notifications
  4. TOTP Security

    • Enforce unique codes (prevent replay attacks)
    • Secure secret storage
    • Backup code management
    • QR code security
    • Time synchronization
  5. Admin Account Security

    • Real-time email alerts for failed login attempts
    • Enhanced monitoring and logging
    • Stricter cooling-off periods
    • IP address and user agent tracking
    • Configurable notification thresholds

Headers

Request Headers

HeaderDescription
AuthorizationBearer token
X-API-KeyAPI key
X-Device-IDDevice identifier
X-App-IDApplication identifier (web-app, admin-app, configurator-app). Scopes refresh-token cookies per app.
X-Request-IDOptional client-provided request ID for tracing (auto-generated if not provided)
X-Idempotency-KeyClient-chosen key that makes a retried write land once. Always advertised in the CORS allow-list, so a browser client can send it without a preflight failure.
Accept-LanguagePreferred language

Response Headers

HeaderDescription
X-Request-IDUnique request identifier for tracing and correlation
Set-CookieRefresh token

Authorization on protected routes and refresh must always carry a post-auth access_token with sub=user_auth. challenge_token values are accepted only by explicit MFA/TOTP challenge handlers. | Access-Control-Allow-* | CORS headers |

Request Tracing

All API requests include automatic request ID tracing for debugging and support purposes.

How It Works

  1. Client-Provided ID: If you include an X-Request-ID header in your request, the API will use that ID
  2. Auto-Generated ID: If no X-Request-ID is provided, the API automatically generates a unique UUID
  3. Response Header: The X-Request-ID is always returned in the response headers
  4. Log Correlation: All server-side logs include the request ID for easy debugging

Usage Example

# With client-provided request ID
curl -X POST https://api.example.com/v1/authenticate \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: my-custom-trace-123" \
  -d '{"username": "user@example.com", "password": "password"}'

# Response headers will include:
# X-Request-ID: my-custom-trace-123
# Without request ID (auto-generated)
curl -X POST https://api.example.com/v1/authenticate \
  -H "Content-Type: application/json" \
  -d '{"username": "user@example.com", "password": "password"}'

# Response headers will include:
# X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

Benefits

  • Debugging: Easily trace requests through server logs
  • Support: Provide the request ID when reporting issues for faster resolution
  • Correlation: Track related requests across multiple API calls
  • Monitoring: Use request IDs in observability tools for request tracing

Language Support

The API supports localization through the Accept-Language header:

  • English (en)
  • German (de)
  • French (fr)
  • Italian (it)

TOTP Integration Flow

There are five TOTP routes and they all live under /v1/auth/totp/. There is no separate setup-verification route and no separate backup-code route: enrolment and login are verified through the same verify, and a recovery code goes to recovery.

Initial Setup

  1. User requests TOTP setup via /v1/auth/totp/setup
  2. System generates the secret, the QR code and the recovery codes; nothing is enabled yet
  3. User scans the QR code with an authenticator app
  4. User submits a code from the app to /v1/auth/totp/verify
  5. System enables TOTP for the user

Authentication with TOTP

  1. User provides username/password via /v1/authenticate
  2. System responds with the MFA challenge
  3. User provides the TOTP code via /v1/auth/totp/verify
  4. System validates and returns the access token

Recovery Code Usage

  1. User cannot reach the authenticator app
  2. User provides a recovery code via /v1/auth/totp/recovery
  3. System validates it and marks that code used
  4. To turn the second factor off from a recovery session, use /v1/auth/totp/disable-recovery; /v1/auth/totp/disable is the authenticated equivalent

Example Integration

// Setup TOTP
const setupResponse = await fetch('/v1/auth/totp/setup', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  }
});

const { secret, qr_code, recovery_codes } = await setupResponse.json();

// Display QR code to user
displayQRCode(qr_code);

// Verify enrolment through the same route a login uses
const verifyResponse = await fetch('/v1/auth/totp/verify', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ code: userEnteredCode })
});

// Store recovery codes securely
storeRecoveryCodes(recovery_codes);

Admin Security Monitoring

// Example admin login with security monitoring
const adminLoginResponse = await fetch('/v1/authenticate/admin', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'admin@company.com',
    password: adminPassword
  })
});

if (!adminLoginResponse.ok) {
  // Failed admin login triggers:
  // 1. Enhanced logging with IP and user agent
  // 2. Email notification to security team
  // 3. Extended cooling-off period
  // 4. Real-time alert system activation
  
  const error = await adminLoginResponse.json();
  if (error.code === 'auth.admin_locked') {
    // Admin account is locked - security team has been notified
    showSecurityLockoutMessage();
  }
}

Rate Limiting

The API implements rate limiting per IP:

  • 1000 requests per minute per IP
  • 100 requests per minute per endpoint
  • 5 failed login attempts before cooling-off
  • Admin Protection: Failed admin login attempts trigger immediate email alerts to security teams

Rate Limit Headers

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1616876520

On this page