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-IDheader (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_tokenis a pre-auth token used only for MFA/TOTP challenge flows.access_tokenis a post-auth token withsub=user_authand is the only bearer token accepted on protected routes.- For MFA-enabled users, the initial
/v1/authenticateresponse does not issue an authenticated access token or refresh-token cookie. - The authenticated session is created only after successful
/v1/verify-2FA. - Replaying
challenge_tokenon protected routes or/v1/refresh-tokenis 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, localizedmessage) until/v1/verify-2FAsucceeds.totp_setup_required: When TOTP is required but not yet enrolled, the challenge response usescredential_type: "totp_setup_required"and a localizedmessage(seeauth_m.totp_setup_requiredinauth_m.yaml). This applies in particular to Internal-role users withmfa_mode: totpand no TOTP secret yet (enrollment must finish before a full session is granted). Non-internal users intotpmode 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
| Code | Description |
|---|---|
| auth.unauthorized | Invalid credentials |
| auth.account_locked | Account locked due to too many attempts |
| auth.unauthorized_role | User does not have the required role |
| auth.invalid_token | Invalid access token |
| auth.token_expired | Token has expired |
| auth.invalid_refresh_token | Invalid refresh token |
| auth.device_info_mismatch | Device mismatch detected |
| auth_m.invalid_app_id | X-App-ID value is not in the allowed whitelist |
| auth_m.app_id_mismatch | X-App-ID does not match the app that created the refresh token |
| auth_m.invalid_challenge | Invalid MFA challenge token |
| auth_m.challenge_already_used | MFA challenge token has already been used or expired from cache |
| otp_m.cooling_period_active | OTP attempt budget for the credential is exhausted; the wait is in message and retry_after (429) |
| otp_m.resend_too_soon | A code was issued for this credential less than otp.resend.min_interval ago (429) |
| otp_m.resend_limit_exceeded | The credential has spent otp.resend.max_per_window codes inside otp.resend.window (429) |
| otp_m.cache_error | The OTP store could not be read or written, so the code was not issued (500) |
| otp_m.generation_failed | The one-time code could not be generated or delivered (500) |
| otp_m.failed_send_otp | The code was generated but the e-mail, SMS or Telegram delivery failed (500) |
| otp_m.unsupported_credential_type | The stored MFA credential is of a type the OTP module cannot deliver to (400) |
Admin Account Errors
| Code | Description |
|---|---|
| auth.admin_locked | Admin account locked due to failed attempts |
| auth.unauthorized_role | User does not have required Internal role |
| auth.admin_alert_sent | Security alert sent to administrators |
TOTP Errors
| Code | Description |
|---|---|
| auth.totp_invalid_code | Invalid TOTP code (on /v1/verify-2FA a wrong code returns otp_m.otp_invalid, with the attempts left in message) |
| auth.totp_code_expired | TOTP code has expired |
| auth.totp_already_enabled | TOTP is already enabled for this user |
| auth.totp_not_enabled | TOTP is not enabled for this user |
| auth.totp_setup_required | TOTP setup must be completed first |
| auth.backup_code_invalid | Invalid backup code |
| auth.backup_code_used | Backup code has already been used |
| auth.backup_codes_exhausted | All backup codes have been used |
API Key Errors
| Code | Description |
|---|---|
| auth.api_key_required | Missing API key |
| auth.invalid_api_key | Invalid 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 usageRefresh Idle Timeout Behavior
refresh_token_idle_timeout_minutesdefines 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_secondsso clients can align UI timers with backend policy.
Cookie Settings
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-appWhen 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 usersCORS 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 # pixelsSecurity Best Practices
-
Token Management
- Use short-lived access tokens
- Secure refresh token storage
- Implement token rotation
- Monitor suspicious activity
-
Device Security
- Track device information
- Detect suspicious changes
- Implement device fingerprinting
- Alert on security events
-
Rate Limiting
- Per-IP rate limits
- Cooling-off periods
- Graduated response
- Admin notifications
-
TOTP Security
- Enforce unique codes (prevent replay attacks)
- Secure secret storage
- Backup code management
- QR code security
- Time synchronization
-
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
| Header | Description |
|---|---|
| Authorization | Bearer token |
| X-API-Key | API key |
| X-Device-ID | Device identifier |
| X-App-ID | Application identifier (web-app, admin-app, configurator-app). Scopes refresh-token cookies per app. |
| X-Request-ID | Optional client-provided request ID for tracing (auto-generated if not provided) |
| X-Idempotency-Key | Client-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-Language | Preferred language |
Response Headers
| Header | Description |
|---|---|
| X-Request-ID | Unique request identifier for tracing and correlation |
| Set-Cookie | Refresh 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
- Client-Provided ID: If you include an
X-Request-IDheader in your request, the API will use that ID - Auto-Generated ID: If no
X-Request-IDis provided, the API automatically generates a unique UUID - Response Header: The
X-Request-IDis always returned in the response headers - 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-446655440000Benefits
- 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
- User requests TOTP setup via
/v1/auth/totp/setup - System generates the secret, the QR code and the recovery codes; nothing is enabled yet
- User scans the QR code with an authenticator app
- User submits a code from the app to
/v1/auth/totp/verify - System enables TOTP for the user
Authentication with TOTP
- User provides username/password via
/v1/authenticate - System responds with the MFA challenge
- User provides the TOTP code via
/v1/auth/totp/verify - System validates and returns the access token
Recovery Code Usage
- User cannot reach the authenticator app
- User provides a recovery code via
/v1/auth/totp/recovery - System validates it and marks that code used
- To turn the second factor off from a recovery session, use
/v1/auth/totp/disable-recovery;/v1/auth/totp/disableis 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: 1616876520Handler: GetAllAddressesV2. Returns a get_all envelope (data/total/total_unfiltered/has_more) of address objects, with search/sort/pagination.
Authenticate user. When the user's mfa_mode is off, the response includes authenticated session artifacts and the refresh token cookie is scoped to the provided X-App-ID (e.g. refresh_token_web-app). When mfa_mode is email, phone, or totp (with TOTP enrolled), the response is a pre-auth challenge (MFAResponse) and no authenticated access token or refresh cookie is issued until /v1/verify-2FA succeeds. If mfa_mode is totp but TOTP is not enrolled—especially for Internal-role users—a challenge is returned with credential_type totp_setup_required and a localized message until enrollment completes.