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

350 lines
7.1 KiB
Markdown

# 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:
```json
{
"data": {
"type": "auth_pin_setup",
"attributes": {
"pin": "123456",
"confirm_pin": "123456"
}
}
}
```
Success (`200`):
```json
{
"data": {
"type": "auth_pin_setup",
"attributes": {
"configured": true
}
}
}
```
If PIN already exists (`409`):
```json
{
"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:
```json
{
"data": {
"type": "auth_pin_verify",
"attributes": {
"pin": "123456",
"action": "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74"
}
}
}
```
Success (`200`):
```json
{
"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:
```http
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:
```json
{
"data": {
"type": "auth_pin_change",
"attributes": {
"current_pin": "123456",
"new_pin": "654321",
"confirm_pin": "654321"
}
}
}
```
Success (`200`):
```json
{
"data": {
"type": "auth_pin_change",
"attributes": {
"updated": true
}
}
}
```
## 4) Reset Security PIN Flow
### 4.1 Request reset link (blocked PIN button)
Endpoint:
`POST /api/v1/auth/pin/request-reset`
Auth:
- Requires logged-in session
Request:
```json
{
"data": {
"type": "auth_pin_request_reset",
"attributes": {
"password": "<current-account-password>"
}
}
}
```
Success (`200`):
```json
{
"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`):
```json
{
"errors": [
{
"status": "409",
"title": "PIN not blocked",
"detail": "security PIN is not blocked"
}
]
}
```
Invalid password (`401`):
```json
{
"errors": [
{
"status": "401",
"title": "Unauthorized",
"detail": "invalid password"
}
]
}
```
Rate limited (`429`):
```json
{
"errors": [
{
"status": "429",
"title": "Too many requests",
"detail": "please wait before requesting another reset link"
}
]
}
```
### 4.2 Request reset link (public forgot endpoint)
Endpoint:
`POST /api/v1/auth/pin/forgot`
Request:
```json
{
"data": {
"type": "auth_forgot_pin",
"attributes": {
"email": "user@example.com"
}
}
}
```
Response is always generic (`200`):
```json
{
"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:
```json
{
"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:
```json
{
"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`):
```json
{
"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`):
```json
{
"errors": [
{
"status": "400",
"title": "Invalid reset link",
"detail": "This reset link is invalid or expired."
}
]
}
```
Invalid password (`401`):
```json
{
"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`