Skip to main content

Overview

MediaStream supports Time-based One-Time Password (TOTP) two-factor authentication using authenticator apps like Google Authenticator, Authy, or 1Password. When enabled, users must provide both their password and a 6-digit code from their authenticator app to login.
Two-factor authentication is configured per user and is completely optional.

2FA Endpoints

Enable 2FA

POST /user/two-factor-authentication

Disable 2FA

DELETE /user/two-factor-authentication

Get QR Code

GET /user/two-factor-qr-code

Get Secret Key

GET /user/two-factor-secret-key

Get Recovery Codes

GET /user/two-factor-recovery-codes

Regenerate Codes

POST /user/two-factor-recovery-codes

Confirm 2FA

POST /user/confirmed-two-factor-authentication

2FA Challenge

POST /two-factor-challenge

Enable Two-Factor Authentication

User must be authenticated to enable 2FA. Password confirmation may be required based on Fortify configuration.

Endpoint

POST /user/two-factor-authentication

Request Headers

X-XSRF-TOKEN
string
required
CSRF token
Accept
string
required
Must be application/json

Response

Two-factor authentication enabled. Secret and recovery codes generated.
{
  "success": true
}
After enabling, retrieve the QR code and recovery codes for the user.

Example

await fetch('https://your-domain.com/user/two-factor-authentication', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include'
});

Get QR Code

Retrieve a QR code for scanning with an authenticator app.

Endpoint

GET /user/two-factor-qr-code

Response

Returns an SVG QR code that users can scan with their authenticator app.
{
  "svg": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>...</svg>"
}
svg
string
SVG markup for the QR code

Example

const response = await fetch('https://your-domain.com/user/two-factor-qr-code', {
  headers: {
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include'
});

const data = await response.json();
// Display the SVG QR code
document.getElementById('qr-code').innerHTML = data.svg;

Get Secret Key

Retrieve the plain text secret key for manual entry into authenticator apps.

Endpoint

GET /user/two-factor-secret-key

Response

{
  "secretKey": "JBSWY3DPEHPK3PXP"
}
secretKey
string
Base32-encoded secret key for manual entry

Example

const response = await fetch('https://your-domain.com/user/two-factor-secret-key', {
  headers: {
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include'
});

const data = await response.json();
console.log('Secret Key:', data.secretKey);

Get Recovery Codes

Retrieve recovery codes that can be used if the user loses access to their authenticator device.
Recovery codes should be stored securely. Each code can only be used once.

Endpoint

GET /user/two-factor-recovery-codes

Response

[
  "a1b2c3d4e5f6g7h8",
  "i9j0k1l2m3n4o5p6",
  "q7r8s9t0u1v2w3x4",
  "y5z6a7b8c9d0e1f2",
  "g3h4i5j6k7l8m9n0",
  "o1p2q3r4s5t6u7v8",
  "w9x0y1z2a3b4c5d6",
  "e7f8g9h0i1j2k3l4"
]
Returns an array of 8 single-use recovery codes.

Example

const response = await fetch('https://your-domain.com/user/two-factor-recovery-codes', {
  headers: {
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include'
});

const codes = await response.json();
codes.forEach((code, index) => {
  console.log(`Recovery Code ${index + 1}: ${code}`);
});

Regenerate Recovery Codes

Generate a new set of recovery codes. This invalidates all previous codes.

Endpoint

POST /user/two-factor-recovery-codes

Response

{
  "success": true
}
After regenerating, immediately fetch the new codes with GET /user/two-factor-recovery-codes.

Example

// Regenerate codes
await fetch('https://your-domain.com/user/two-factor-recovery-codes', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include'
});

// Fetch new codes
const response = await fetch('https://your-domain.com/user/two-factor-recovery-codes', {
  headers: {
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include'
});

const newCodes = await response.json();

Confirm Two-Factor Authentication

Confirm that 2FA is working correctly by verifying a code from the authenticator app.
This step is required when 'confirm' => true is set in the Fortify configuration.

Endpoint

POST /user/confirmed-two-factor-authentication

Request Body

code
string
required
6-digit code from authenticator app

Response

Two-factor authentication confirmed and fully enabled.
{
  "success": true
}

Example

const response = await fetch('https://your-domain.com/user/confirmed-two-factor-authentication', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include',
  body: JSON.stringify({
    code: '123456'
  })
});

Two-Factor Challenge

Complete the two-factor authentication challenge during login.

Endpoint

POST /two-factor-challenge

Request Body

Provide either a code from the authenticator app or a recovery code:
code
string
6-digit code from authenticator app
recovery_code
string
Single-use recovery code (alternative to code)
Only send one field: either code OR recovery_code, not both.

Response

Authentication successful. User is now logged in.
{
  "two_factor": false
}
Session cookie is set and user can access protected routes.

Examples

await fetch('https://your-domain.com/two-factor-challenge', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include',
  body: JSON.stringify({
    code: '123456'
  })
});

Disable Two-Factor Authentication

Disable two-factor authentication for the authenticated user.

Endpoint

DELETE /user/two-factor-authentication

Request Headers

X-XSRF-TOKEN
string
required
CSRF token

Response

{
  "success": true
}
Two-factor authentication is disabled and all recovery codes are invalidated.

Example

await fetch('https://your-domain.com/user/two-factor-authentication', {
  method: 'DELETE',
  headers: {
    'Accept': 'application/json',
    'X-XSRF-TOKEN': csrfToken
  },
  credentials: 'include'
});

Complete 2FA Setup Flow

1

Enable 2FA

POST /user/two-factor-authentication
2

Get QR Code

GET /user/two-factor-qr-code
Display the QR code for the user to scan with their authenticator app.
3

Show Secret Key (Optional)

GET /user/two-factor-secret-key
Provide the secret key for manual entry.
4

Confirm Setup

POST /user/confirmed-two-factor-authentication
User enters a code from their app to confirm it’s working.
5

Show Recovery Codes

GET /user/two-factor-recovery-codes
Display the 8 recovery codes for the user to save securely.

Login Flow with 2FA

Database Fields

Two-factor authentication data is stored in the users table:
two_factor_secret TEXT NULL,
two_factor_recovery_codes TEXT NULL,
two_factor_confirmed_at TIMESTAMP NULL
All 2FA fields are encrypted at rest and never exposed in API responses.

Security Best Practices

  • Instruct users to save recovery codes in a secure location
  • Each recovery code can only be used once
  • Regenerate codes if user suspects they’ve been compromised
  • 2FA challenge endpoint is rate-limited to 5 attempts per minute
  • This prevents brute-force attacks on 2FA codes
  • TOTP codes are time-based (30-second window)
  • Ensure server time is synchronized with NTP
  • Codes have a small grace period for clock drift
  • Require password confirmation before enabling/disabling 2FA
  • Set 'confirmPassword' => true in Fortify configuration

Common Issues

  • Ensure device time is synchronized correctly
  • Code may have expired (codes change every 30 seconds)
  • Verify the secret was scanned correctly
  • Try using a recovery code instead
  • User must enable 2FA first (POST /user/two-factor-authentication)
  • Check that Features::twoFactorAuthentication() is enabled
  • Ensure user is authenticated
  • User can login using a recovery code
  • After login, they can disable and re-enable 2FA
  • If recovery codes are also lost, admin intervention is required

Login

Initial authentication

Profile Settings

Manage user settings

Build docs developers (and LLMs) love