Files
fm_be/docs/security-pin.md
2026-07-16 22:16:45 +07:00

7.1 KiB

Security PIN Flow

This document explains how to configure and use a per-user 6-digit Security PIN for sensitive actions. All API payloads follow JSON:API.

1) Setup Security PIN (first time)

Endpoint: POST /api/v1/auth/pin/setup

Auth:

  • Requires logged-in session (AUTH cookie / access token cookie)

Request:

{
  "data": {
    "type": "auth_pin_setup",
    "attributes": {
      "pin": "123456",
      "confirm_pin": "123456"
    }
  }
}

Success (200):

{
  "data": {
    "type": "auth_pin_setup",
    "attributes": {
      "configured": true
    }
  }
}

If PIN already exists (409):

{
  "errors": [
    {
      "status": "409",
      "title": "PIN already configured",
      "detail": "security PIN is already set"
    }
  ]
}

2) Verify PIN before sensitive action

Endpoint: POST /api/v1/auth/pin/verify

Auth:

  • Requires logged-in session

Request:

{
  "data": {
    "type": "auth_pin_verify",
    "attributes": {
      "pin": "123456",
      "action": "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74"
    }
  }
}

Success (200):

{
  "data": {
    "type": "auth_pin_verify",
    "attributes": {
      "verified": true,
      "action_token": "<one-time-token>",
      "action_token_ttl_ms": 300000
    }
  }
}

Use the returned token on the sensitive API call:

  • Header: X-PIN-Verification-Token: <one-time-token>

Example sensitive request:

DELETE /api/v1/users/delete/{uuid}
X-PIN-Verification-Token: <one-time-token>
Cookie: wucher_at=...

Important:

  • Token is action-bound (a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74 only for that action).
  • Token is one-time use.
  • Token expires by AUTH_SECURITY_PIN_ACTION_TOKEN_TTL.
  • Maximum failed attempts follow AUTH_SECURITY_PIN_MAX_ATTEMPTS (default 5).
  • When max attempts are reached, API returns 429 and PIN becomes blocked.
  • Block is permanent until PIN reset succeeds (AUTH_SECURITY_PIN_LOCK_DURATION=0 means no auto-unlock timeout).

3) Change Security PIN

Endpoint: POST /api/v1/auth/pin/change

Auth:

  • Requires logged-in session

Request:

{
  "data": {
    "type": "auth_pin_change",
    "attributes": {
      "current_pin": "123456",
      "new_pin": "654321",
      "confirm_pin": "654321"
    }
  }
}

Success (200):

{
  "data": {
    "type": "auth_pin_change",
    "attributes": {
      "updated": true
    }
  }
}

4) Reset Security PIN Flow

Endpoint: POST /api/v1/auth/pin/request-reset

Auth:

  • Requires logged-in session

Request:

{
  "data": {
    "type": "auth_pin_request_reset",
    "attributes": {
      "password": "<current-account-password>"
    }
  }
}

Success (200):

{
  "data": {
    "type": "auth_pin_request_reset",
    "attributes": {
      "message": "If allowed, a security PIN reset link has been sent to your email."
    }
  }
}

If PIN not blocked (409):

{
  "errors": [
    {
      "status": "409",
      "title": "PIN not blocked",
      "detail": "security PIN is not blocked"
    }
  ]
}

Invalid password (401):

{
  "errors": [
    {
      "status": "401",
      "title": "Unauthorized",
      "detail": "invalid password"
    }
  ]
}

Rate limited (429):

{
  "errors": [
    {
      "status": "429",
      "title": "Too many requests",
      "detail": "please wait before requesting another reset link"
    }
  ]
}

Endpoint: POST /api/v1/auth/pin/forgot

Request:

{
  "data": {
    "type": "auth_forgot_pin",
    "attributes": {
      "email": "user@example.com"
    }
  }
}

Response is always generic (200):

{
  "data": {
    "type": "auth_forgot_pin",
    "attributes": {
      "message": "If the account exists, a security PIN reset link has been sent."
    }
  }
}

4.3 Reset PIN with password or Microsoft re-auth

Endpoint: POST /api/v1/auth/pin/reset

Notes:

  • method is optional. Default is password.
  • For method=password, token is required and must be the raw single-use reset token from the email reset link, then send password.
  • For method=microsoft, token is not required. Send reauth_token from Microsoft re-auth directly.

Request with password re-auth:

{
  "data": {
    "type": "auth_reset_pin",
    "attributes": {
      "token": "<raw-token-from-email-link>",
      "password": "<current-account-password>",
      "new_pin": "123456",
      "confirm_pin": "123456"
    }
  }
}

Request with Microsoft re-auth:

{
  "data": {
    "type": "auth_reset_pin",
    "attributes": {
      "method": "microsoft",
      "reauth_token": "<reauth-token-from-/auth/microsoft/reauth?purpose=pin_reset_request>",
      "new_pin": "123456",
      "confirm_pin": "123456"
    }
  }
}

Success (200):

{
  "data": {
    "type": "auth_reset_pin",
    "attributes": {
      "message": "Security PIN has been reset successfully.",
      "reset_success": true,
      "redirect_url": "https://app.example.com"
    }
  }
}

Invalid/expired token (400):

{
  "errors": [
    {
      "status": "400",
      "title": "Invalid reset link",
      "detail": "This reset link is invalid or expired."
    }
  ]
}

Invalid password (401):

{
  "errors": [
    {
      "status": "401",
      "title": "Unauthorized",
      "detail": "invalid password"
    }
  ]
}

5) Sensitive endpoints currently protected by PIN token

The following routes require X-PIN-Verification-Token:

  • POST /api/v1/auth/invite (action=2a59e8b4-5a72-4be3-b8fc-4f49941181c3)
  • POST /api/v1/users/create (action=2a59e8b4-5a72-4be3-b8fc-4f49941181c3)
  • PATCH /api/v1/users/update/:uuid (action=428fdf48-3f99-46ea-bc47-8fb2e312be08)
  • DELETE /api/v1/users/delete/:uuid (action=a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74)
  • POST /api/v1/roles/create (action=7fb1d693-8692-4e8f-be86-fd0e29e96bc3)
  • PATCH /api/v1/roles/update/:uuid (action=18ca5bbf-ce9d-4e24-8ace-793de56c1de0)
  • DELETE /api/v1/roles/delete/:uuid (action=b87fca76-783d-45e7-9022-b2d9c95f6da7)
  • POST /api/v1/roles/:uuid/permissions (action=48e523f8-d86f-4b24-b0e3-e213e4d33db4)
  • DELETE /api/v1/roles/:uuid/permissions/:permission_uuid (action=f2bf59f8-0783-40fc-94ce-a79e40f84c44)
  • POST /api/v1/roles/:uuid/permissions/assign-bulk (action=48e523f8-d86f-4b24-b0e3-e213e4d33db4)
  • POST /api/v1/roles/:uuid/permissions/remove-bulk (action=f2bf59f8-0783-40fc-94ce-a79e40f84c44)

6) Security controls

  • PIN must be exactly 6 digits.
  • PIN is encrypted at rest (users.security_pin_encrypted), not stored in plaintext.
  • Forgot/reset tokens are random, hashed, single-use, and expiring.
  • Verify attempts are rate-limited per user with lockout (AUTH_SECURITY_PIN_MAX_ATTEMPTS, AUTH_SECURITY_PIN_LOCK_DURATION).
  • Forgot endpoint response is generic to reduce account enumeration risk.

7) Environment variables

  • AUTH_SECURITY_PIN_RESET_URL
  • AUTH_SECURITY_PIN_RESET_TTL
  • AUTH_FORGOT_SECURITY_PIN_EMAIL_COOLDOWN
  • AUTH_FORGOT_SECURITY_PIN_MIN_DURATION
  • AUTH_SECURITY_PIN_MAX_ATTEMPTS
  • AUTH_SECURITY_PIN_LOCK_DURATION
  • AUTH_SECURITY_PIN_ACTION_TOKEN_TTL