350 lines
7.1 KiB
Markdown
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`
|