SeptemCore LogoSeptemCore
PrimitivesAuth

Multi-Factor Authentication (TOTP)

TOTP-based MFA: enabling 2FA with QR codes, verifying during login, disabling, recovery code usage, and regeneration. All recovery codes are Argon2id-hashed server-side.

SeptemCore MFA uses TOTP (Time-based One-Time Passwords) defined in RFC 6238. Authenticator apps — Google Authenticator, Authy, 1Password, Bitwarden — generate a fresh 6-digit code every 30 seconds. The server verifies it using the shared secret without any network round-trip to the authenticator app.


MFA Lifecycle

Enable → (Verify during login) → Disable  ← optional
           ↑                         ↓
      Recovery Code             Regenerate Codes

1. Enable MFA

POST /auth/mfa/enable initiates the TOTP setup. The IAM service generates a new TOTP secret and returns a provisioning URI that authenticator apps can parse from a QR code.

Request

POST https://api.septemcore.com/v1/auth/mfa/enable
Authorization: Bearer <access_token>

Response 200 OK

{
  "secret": "JBSWY3DPEHPK3PXP",
  "otpauth_uri": "otpauth://totp/SeptemCore:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=SeptemCore&algorithm=SHA1&digits=6&period=30",
  "qr_code_svg": "<svg>...</svg>",
  "recovery_codes": [
    "AAAA-BBBB-CCCC",
    "DDDD-EEEE-FFFF",
    "GGGG-HHHH-IIII",
    "JJJJ-KKKK-LLLL",
    "MMMM-NNNN-OOOO",
    "PPPP-QQQQ-RRRR",
    "SSSS-TTTT-UUUU",
    "VVVV-WWWW-XXXX",
    "YYYY-ZZZZ-0000",
    "1111-2222-3333"
  ]
}

MFA is NOT yet active after this call. The user must scan the QR code and call POST /auth/mfa/verify with a valid TOTP code to confirm the secret was saved correctly and activate MFA. This prevents lockout due to a failed scan.

The secret and recovery_codes in the enable response are displayed only once. The secret is stored server-side as a Vault-encrypted value. The raw recovery codes are never stored — only their Argon2id hashes are persisted. The user must save them before closing the setup screen.


2. Verify — Activate MFA

POST https://api.septemcore.com/v1/auth/mfa/verify
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "code": "123456"
}

Response — MFA Activated

{
  "mfa_enabled": true,
  "recovery_codes_remaining": 10
}

This call activates MFA on the account. All subsequent logins require a TOTP code after the password step.


3. Login with MFA Enabled

When a user with MFA enabled submits correct credentials, the login endpoint returns an intermediate challenge instead of the token pair:

{
  "mfa_required": true,
  "mfa_token": "mfa_01j9p4mn3c00000000000000",
  "mfa_type": "totp"
}

The mfa_token is a short-lived opaque token (TTL: 5 minutes). It identifies the pending authentication session. The client must call:

POST https://api.septemcore.com/v1/auth/mfa/verify
Content-Type: application/json

{
  "mfa_token": "mfa_01j9p4mn3c00000000000000",
  "code": "523917"
}

On success, the IAM service issues the full JWT pair:

{
  "access_token": "eyJhbGciOiJFUzI1NiJ9...",
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
  "token_type": "Bearer",
  "expires_in": 900,
  "user": {
    "id": "01j9p3kx2e00000000000000",
    "mfa_enabled": true
  }
}

TOTP timing window

The IAM service accepts the code for the current 30-second window and the immediately preceding window (60 seconds total). This accommodates clock skew between the user's device and the server. Each code is accepted exactly once — replay attacks are blocked by a nonce cache in Valkey (TTL 90 seconds).


4. Disable MFA

POST https://api.septemcore.com/v1/auth/mfa/disable
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "code": "523917",
  "password": "S3cure-P@ssw0rd!"
}

Both the current TOTP code and the account password are required to disable MFA. This prevents an attacker with a stolen access token (but without physical access to the authenticator) from silently removing 2FA protection.

Response — MFA Disabled

{
  "mfa_enabled": false
}

5. Recovery Codes

Recovery codes allow account access when the authenticator device is unavailable (lost, stolen, factory reset). Each user receives 10 single-use codes when MFA is enabled.

Using a recovery code

POST https://api.septemcore.com/v1/auth/recovery
Content-Type: application/json

{
  "email": "[email protected]",
  "recovery_code": "AAAA-BBBB-CCCC"
}

On success, the IAM service:

  1. Marks the used code as consumed (Argon2id verification then immediate deletion — codes cannot be reused).
  2. Issues the full JWT pair.
  3. Sends a security notification to the user's email: "A recovery code was used to access your account."

After recovery login, MFA remains enabled. The user should regenerate codes and reconfigure the authenticator immediately.

Checking remaining code count

GET https://api.septemcore.com/v1/auth/recovery-codes
Authorization: Bearer <access_token>
{
  "remaining": 7
}

The endpoint returns only the count — never the code values. Codes are one-way hashed; the server cannot reverse them.

Regenerating codes

When fewer codes remain, the user should regenerate a fresh set of 10:

POST https://api.septemcore.com/v1/auth/recovery-codes/regenerate
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "password": "S3cure-P@ssw0rd!",
  "code": "523917"
}

Both the account password and a valid TOTP code are required. On success:

{
  "recovery_codes": [
    "NEW1-CODE-AAAA",
    "NEW2-CODE-BBBB",
    "...",
    "NEW10-CODEZZZZ"
  ]
}

All previous recovery codes are immediately invalidated. The new codes are shown once — never again. They are stored only as Argon2id hashes.


Implementation Details

PropertyValue
AlgorithmTOTP (RFC 6238) — SHA-1, 6 digits, 30-second period
Server librarypquerna/otp (Go)
TOTP secret storageVault-encrypted at rest (shared/crypto AES-256-GCM)
Recovery code formatXXXX-XXXX-XXXX (16 characters, uppercase alphanumeric)
Recovery code count10 per user
Recovery code hashingArgon2id (same parameters as passwords) — one-way, irreversible
Replay protectionValkey nonce cache, TTL 90 seconds — each code accepted once
MFA token TTL5 minutes (intermediate challenge after correct password)
Clock skew tolerance±30 seconds (current + previous window)

SDK

import { useAuth } from '@platform/sdk-auth';

const { enableMFA, verifyMFA, disableMFA } = useAuth();

// Step 1 — initiate setup
const setup = await enableMFA();
// setup.otpauthUri → render QR code
// setup.recoveryCodes → show once, user must save

// Step 2 — confirm with code from authenticator app
await verifyMFA({ code: '523917' });
// MFA is now active

// Disable
await disableMFA({ code: '523917', password: 'S3cure-P@ssw0rd!' });
// During login — SDK handles the MFA challenge automatically
// if mfaRequired is true, sdk-auth triggers this internally:
const result = await kernel.auth().completeMfaChallenge({
  mfaToken: 'mfa_01j9p4...',
  code: userEnteredCode,
});

Error Reference

ScenarioHTTPtype URI suffix
Invalid TOTP code401unauthorized
TOTP code already used (replay)401unauthorized
MFA token expired401token-expired
Invalid recovery code401unauthorized
All recovery codes exhausted401unauthorized
MFA already enabled409conflict
MFA not enabled (disable attempt)400validation-error

On this page