Files
2026-07-02 15:59:46 +03:30

172 lines
6.1 KiB
Markdown

# Authentication & Authorization
Negareh API supports two actor types — **users** (customers) and **admins** (back-office). Both authenticate via **phone OTP** and receive **JWT access tokens** with **refresh tokens**.
## Overview
```
┌─────────────┐ OTP request ┌─────────────┐
│ Client │ ──────────────────► │ Auth API │
│ │ ◄────────────────── │ (SMS/Redis)│
└─────────────┘ OTP code └─────────────┘
│ │
│ OTP verify │
└──────────────────────────────────► │
┌─────────────────────────┐
│ Access JWT + Refresh │
│ token (hashed in DB) │
└─────────────────────────┘
```
## OTP flow
### User (public)
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/public/auth/otp/request` | POST | Send OTP to phone (creates user on verify) |
| `/public/auth/otp/verify` | POST | Verify OTP → returns tokens + user profile |
| `/public/auth/refresh` | POST | Exchange refresh token for new token pair |
### Admin
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/admin/auth/otp/request` | POST | Send OTP (phone must exist in `admins` table) |
| `/admin/auth/otp/verify` | POST | Verify OTP → returns tokens + admin profile |
| `/admin/auth/refresh` | POST | Exchange refresh token for new token pair |
### OTP details
- Phone numbers are normalized to local format via `normalizePhoneToLocal()` (`09xxxxxxxxx`).
- OTP codes are 5-digit numeric values stored in **Redis** with TTL (`OTP_EXPIRATION_TIME`, default 240 seconds).
- OTP request endpoints are rate-limited (`@Throttle`: 3 requests per 180 seconds).
- SMS delivery uses SMS.ir patterns (configured via `SMS_*` env vars).
### Request / verify DTOs
```typescript
// Request OTP
{ "phone": "09362532122" }
// Verify OTP
{ "phone": "09362532122", "otp": "12345" }
```
## JWT tokens
### Access token
- Signed with `JWT_SECRET`.
- Expiration: `JWT_EXPIRATION_TIME` (seconds).
- Sent as `Authorization: Bearer <token>`.
**User payload:**
```typescript
{ userId: string }
```
**Admin payload:**
```typescript
{ adminId: string }
```
### Refresh token
- Opaque 64-character hex string (not a JWT).
- Stored **hashed** (SHA-256) in the `refresh_tokens` table.
- Expiration: `REFRESH_TOKEN_EXPIRE` (days).
- Rotated on each refresh — old token is deleted in the same transaction as the new pair.
- Type field distinguishes `user` vs `admin` tokens.
### Refresh response shape
```json
{
"accessToken": { "token": "...", "expire": 1710000000000 },
"refreshToken": { "token": "...", "expire": 1711000000000 }
}
```
`expire` values are Unix timestamps in milliseconds.
## Guards
### `AuthGuard` (users)
- Applied with `@UseGuards(AuthGuard)` on public routes that require a logged-in user.
- Verifies JWT from `Authorization` header.
- Sets `request.userId` from the token payload.
- Extract identity in controllers with `@UserId()`.
### `AdminAuthGuard` (admins)
- Applied with `@UseGuards(AdminAuthGuard)` on admin routes.
- Verifies JWT and sets `request.adminId`.
- Extract identity with `@AdminId()`.
- Reads `@Permissions(...)` metadata from the handler/class for RBAC checks.
## Role-based access control (RBAC)
Admins have a **role** with attached **permissions**. Permission names are defined in `PermissionEnum` (e.g. `view_orders`, `create_invoice`, `manage_admins`).
### Declaring required permissions
```typescript
@Get('admin/orders')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.VIEW_ORDERS)
findAllAsAdmin(@Query() dto: FindOrdersDto) {
return this.orderService.findOrdersAsAdmin(dto);
}
```
The `@Permissions()` decorator stores required permission keys in metadata. `AdminAuthGuard` reads them via `Reflector`.
> **Note:** Permission enforcement against the database/cache is partially implemented. The guard structure and decorators are in place; ensure permission checks are active before relying on them in production.
## Route prefixes
Routes are namespaced by audience in the controller path (not a global prefix):
| Prefix | Audience | Guard |
|--------|----------|-------|
| `public/` | End users | `AuthGuard` (when protected) |
| `admin/` | Back-office | `AdminAuthGuard` |
Unauthenticated routes use `public/auth/...` or `admin/auth/...` without guards.
## WebSocket auth
Admin WebSocket connections use `WsAdminAuthGuard` in the notifications module. Follow the same JWT verification pattern as HTTP admin auth.
## Environment variables
| Variable | Purpose |
|----------|---------|
| `JWT_SECRET` | Signing key for access tokens |
| `JWT_EXPIRATION_TIME` | Access token TTL (seconds) |
| `REFRESH_TOKEN_EXPIRE` | Refresh token TTL (days) |
| `OTP_EXPIRATION_TIME` | OTP cache TTL (seconds) |
| `REDIS_URI` / `REDIS_HOST` | OTP and cache storage |
| `SMS_BASE_URL`, `SMS_API_KEY`, `SMS_PATTERN_OTP` | SMS delivery |
## Security practices
- Refresh tokens are hashed at rest; raw tokens are only returned once to the client.
- Token refresh runs inside a database transaction to prevent race conditions.
- Rate limiting on OTP and refresh endpoints reduces brute-force risk.
- Never log tokens, OTP codes, or secrets.
- Use HTTPS in production; CORS is enabled with `credentials: true` for cookie support if needed.
## Client integration checklist
1. Request OTP with normalized Iranian mobile number.
2. Verify OTP and store both `accessToken.token` and `refreshToken.token`.
3. Send `Authorization: Bearer <accessToken>` on protected requests.
4. On 401, call the refresh endpoint with the refresh token.
5. Replace stored tokens with the new pair from the refresh response.