@appweaver/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +7 -0
- package/build/build-command.d.ts +2 -0
- package/build/build-command.js +15 -0
- package/build/build-project.d.ts +8 -0
- package/build/build-project.js +19 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +18 -0
- package/generate/generate-command.d.ts +2 -0
- package/generate/generate-command.js +38 -0
- package/generate/generate-schema.d.ts +12 -0
- package/generate/generate-schema.js +475 -0
- package/generate/generate-types.d.ts +10 -0
- package/generate/generate-types.js +86 -0
- package/generate/index.d.ts +3 -0
- package/generate/index.js +19 -0
- package/migrate/index.d.ts +1 -0
- package/migrate/index.js +17 -0
- package/migrate/migrate-command.d.ts +2 -0
- package/migrate/migrate-command.js +13 -0
- package/migration/index.d.ts +1 -0
- package/migration/index.js +17 -0
- package/migration/migration-command.d.ts +2 -0
- package/migration/migration-command.js +34 -0
- package/openapi/index.d.ts +1 -0
- package/openapi/index.js +17 -0
- package/openapi/openapi-command.d.ts +2 -0
- package/openapi/openapi-command.js +46 -0
- package/package.json +56 -0
- package/seed/index.d.ts +1 -0
- package/seed/index.js +17 -0
- package/seed/seed-command.d.ts +2 -0
- package/seed/seed-command.js +33 -0
- package/skill/GUIDELINES.md +298 -0
- package/skill/SKILL.md +593 -0
- package/skill/references/cache.md +207 -0
- package/skill/references/cli.md +213 -0
- package/skill/references/client.md +507 -0
- package/skill/references/configuration.md +402 -0
- package/skill/references/database.md +134 -0
- package/skill/references/dependency-injection.md +214 -0
- package/skill/references/events.md +152 -0
- package/skill/references/mailer.md +235 -0
- package/skill/references/queue.md +196 -0
- package/skill/references/resources.md +961 -0
- package/skill/references/scheduler.md +184 -0
- package/skill/references/security.md +694 -0
- package/skill/references/storage.md +251 -0
- package/start/index.d.ts +2 -0
- package/start/index.js +18 -0
- package/start/start-command.d.ts +2 -0
- package/start/start-command.js +17 -0
- package/start/start-project.d.ts +8 -0
- package/start/start-project.js +147 -0
- package/testing/index.d.ts +1 -0
- package/testing/index.js +17 -0
- package/testing/testing-command.d.ts +2 -0
- package/testing/testing-command.js +96 -0
- package/update/index.d.ts +2 -0
- package/update/index.js +18 -0
- package/update/update-command.d.ts +2 -0
- package/update/update-command.js +84 -0
- package/update/update-packages.d.ts +10 -0
- package/update/update-packages.js +45 -0
- package/update/update-skill.d.ts +8 -0
- package/update/update-skill.js +93 -0
- package/utils/index.d.ts +3 -0
- package/utils/index.js +19 -0
- package/utils/loader-util.d.ts +29 -0
- package/utils/loader-util.js +132 -0
- package/utils/path-util.d.ts +41 -0
- package/utils/path-util.js +98 -0
- package/utils/process-util.d.ts +39 -0
- package/utils/process-util.js +92 -0
- package/weaver.d.ts +2 -0
- package/weaver.js +53 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Appweaver provides a comprehensive security system with multiple authentication methods, role-based authorization,
|
|
4
|
+
OAuth2 social login, two-factor authentication, reCAPTCHA, and account management. All security features are
|
|
5
|
+
configurable and can be enabled/disabled independently.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Authentication methods
|
|
10
|
+
|
|
11
|
+
Appweaver supports four authentication methods that can be used independently or combined:
|
|
12
|
+
|
|
13
|
+
| Method | Config flag | Header/mechanism |
|
|
14
|
+
|--------------|----------------------------|--------------------------------------|
|
|
15
|
+
| JWT (Bearer) | Always enabled | `Authorization: Bearer <token>` |
|
|
16
|
+
| HTTP Basic | `SECURITY_BASIC_ENABLED` | `Authorization: Basic <base64>` |
|
|
17
|
+
| API Key | `SECURITY_API_KEY_ENABLED` | `x-api-key: <id><delimiter><secret>` |
|
|
18
|
+
| OAuth2 | Per-provider flags | Browser redirect flow |
|
|
19
|
+
|
|
20
|
+
When multiple methods are enabled, the authentication middleware tries each in order. A request is authenticated if
|
|
21
|
+
any one method succeeds. If no credentials are present for any method, a 401 error is returned.
|
|
22
|
+
|
|
23
|
+
### Route authentication configuration
|
|
24
|
+
|
|
25
|
+
Routes specify which auth methods they accept:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const config = {
|
|
29
|
+
// Accept only JWT
|
|
30
|
+
create: {
|
|
31
|
+
auth: ['jwt']
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Accept JWT or API key
|
|
35
|
+
query: {
|
|
36
|
+
auth: ['jwt', 'apiKey']
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Public route (no auth required)
|
|
40
|
+
find: {
|
|
41
|
+
public: true
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## JWT authentication
|
|
50
|
+
|
|
51
|
+
JWT is the primary authentication method. Appweaver uses RSA (RS256) by default but supports symmetric HMAC (HS256)
|
|
52
|
+
if `SECURITY_JWT_SECRET` is set.
|
|
53
|
+
|
|
54
|
+
### Key management
|
|
55
|
+
|
|
56
|
+
- RSA 2048-bit key pair generated automatically if `SECURITY_JWT_AUTO_GENERATE_KEYS` is `true` and key files are
|
|
57
|
+
missing
|
|
58
|
+
- Keys stored at `SECURITY_JWT_PUBLIC_KEY_PATH` and `SECURITY_JWT_PRIVATE_KEY_PATH` (default: `./storage/keys/`)
|
|
59
|
+
- If `SECURITY_JWT_SECRET` is set, HMAC signing is used instead of RSA
|
|
60
|
+
|
|
61
|
+
### Token types and scopes
|
|
62
|
+
|
|
63
|
+
| Scope | Purpose | Access |
|
|
64
|
+
|-----------|-----------------------|------------------------------------------------------------------------|
|
|
65
|
+
| `Auth` | Full API access | All routes except `/refresh`, `/2fa-send-code`, `/verify-2fa-code` |
|
|
66
|
+
| `Refresh` | Token renewal only | Only `POST /auth/refresh` |
|
|
67
|
+
| `TwoFA` | 2FA verification only | Only `POST /account/send-2fa-code` and `POST /account/verify-2fa-code` |
|
|
68
|
+
|
|
69
|
+
### JWT payload
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"scope": "auth | refresh | 2fa",
|
|
74
|
+
"source": "password | oauth2Google | oauth2Facebook | oauth2Custom | apiKey | basic",
|
|
75
|
+
"username": "User email (e.g. admin@example.com)",
|
|
76
|
+
"sub": "User ID (e.g. 123)",
|
|
77
|
+
"iat": "Issued at timestamp (e.g. 1774623924234)"
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Token validation
|
|
82
|
+
|
|
83
|
+
On every authenticated request, the server:
|
|
84
|
+
|
|
85
|
+
1. Verifies the JWT signature
|
|
86
|
+
2. Loads the user from the database by `sub` (user ID)
|
|
87
|
+
3. Checks that the user is enabled
|
|
88
|
+
4. Validates `logoutAt` is before the token's `iat` (tokens issued before logout are rejected)
|
|
89
|
+
5. Checks that the token scope allows access to the requested URL
|
|
90
|
+
|
|
91
|
+
### Auth routes
|
|
92
|
+
|
|
93
|
+
| Method | Path | Auth | Description |
|
|
94
|
+
|--------|-------------------------|---------------------|-------------------------------------------------------------------------------------------------------------|
|
|
95
|
+
| `POST` | `/auth/login` | Public | Login with email and password. Returns access + refresh tokens. Rate limited: 12/window. |
|
|
96
|
+
| `POST` | `/auth/refresh` | JWT (Refresh scope) | Exchange a refresh token for a new access token. Rate limited: 12/window. |
|
|
97
|
+
| `POST` | `/auth/logout` | JWT | Logout. Sets `logoutAt` timestamp to invalidate all existing tokens. |
|
|
98
|
+
| `GET` | `/auth/me` | JWT | Get the current authenticated user's profile. |
|
|
99
|
+
| `POST` | `/auth/change-password` | Any | Change password. Requires current password + new password. Invalidates all tokens. Rate limited: 12/window. |
|
|
100
|
+
| `POST` | `/auth/exchange-token` | Public | Exchange a one-time token (OTT) for JWT access + refresh tokens. Rate limited: 12/window. |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## HTTP Basic authentication
|
|
105
|
+
|
|
106
|
+
When enabled, requests with an `Authorization: Basic` header are authenticated against the user database.
|
|
107
|
+
|
|
108
|
+
**Configuration:**
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"config": {
|
|
113
|
+
"security": {
|
|
114
|
+
"basic": {
|
|
115
|
+
"enabled": true,
|
|
116
|
+
"realm": "My App",
|
|
117
|
+
"proxyMode": false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Uses `@fastify/basic-auth` plugin. Extracts Base64-encoded `username:password` from the header and validates against
|
|
125
|
+
the user's stored password hash.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## API key authentication
|
|
130
|
+
|
|
131
|
+
API keys provide long-lived, per-user credentials for programmatic access.
|
|
132
|
+
|
|
133
|
+
### How it works
|
|
134
|
+
|
|
135
|
+
1. An authenticated user creates an API key via the CRUD endpoints
|
|
136
|
+
2. The server generates a 64-character random secret and stores its SHA256 hash
|
|
137
|
+
3. The full key is returned once as `{id}{delimiter}{secret}` (e.g. `42AKa1b2c3d4...`)
|
|
138
|
+
4. Subsequent reads show only a masked version: `{id}...{last6chars}`
|
|
139
|
+
5. On each request, the server extracts the key from the header, parses `id` and `secret`, hashes the secret, and
|
|
140
|
+
compares against the stored hash
|
|
141
|
+
|
|
142
|
+
### API key format
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
{id}{SECURITY_API_KEY_DELIMITER}{secret}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Default delimiter is `AK`, so a key looks like: `42AKa1b2c3d4e5f6...`
|
|
149
|
+
|
|
150
|
+
### API key model
|
|
151
|
+
|
|
152
|
+
| Field | Type | Description |
|
|
153
|
+
|---------------|-----------|------------------------------------------------------|
|
|
154
|
+
| `id` | int | Auto-generated ID. |
|
|
155
|
+
| `key` | string | 64-char secret (shown only at creation). |
|
|
156
|
+
| `keyHash` | string | SHA256 hash of the key (stored, hidden from API). |
|
|
157
|
+
| `name` | string? | Optional friendly name. |
|
|
158
|
+
| `description` | string? | Optional description. |
|
|
159
|
+
| `enabled` | boolean | Whether the key is active. |
|
|
160
|
+
| `expiresAt` | dateTime? | Optional expiration date. Enforced on every request. |
|
|
161
|
+
|
|
162
|
+
### API key policy
|
|
163
|
+
|
|
164
|
+
- Users can only see and manage their own API keys
|
|
165
|
+
- `SECURITY_API_KEY_MAX_DURATION` limits how far in the future `expiresAt` can be set
|
|
166
|
+
|
|
167
|
+
**Configuration:**
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"config": {
|
|
172
|
+
"security": {
|
|
173
|
+
"apiKey": {
|
|
174
|
+
"enabled": true,
|
|
175
|
+
"headerName": "x-api-key",
|
|
176
|
+
"delimiter": "AK",
|
|
177
|
+
"maxDuration": 7776000000
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## OAuth2 authentication
|
|
187
|
+
|
|
188
|
+
Appweaver supports OAuth2 login with Google, Facebook, and a custom OpenID Connect provider. All OAuth2 providers
|
|
189
|
+
follow the same flow pattern.
|
|
190
|
+
|
|
191
|
+
### OAuth2 flow
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
1. Client redirects to:
|
|
195
|
+
GET /auth/login/{provider}?redirectToUrl=https://myapp.com/dashboard
|
|
196
|
+
|
|
197
|
+
2. Server validates redirectToUrl against SECURITY_ALLOWED_REDIRECT_HOSTS
|
|
198
|
+
|
|
199
|
+
3. Server generates a state token (OTT) and redirects to provider:
|
|
200
|
+
-> https://accounts.google.com/o/oauth2/v2/auth?
|
|
201
|
+
client_id=...&
|
|
202
|
+
redirect_uri=https://myapi.com/auth/login/google/callback&
|
|
203
|
+
state={stateToken}&
|
|
204
|
+
scope=profile email&
|
|
205
|
+
response_type=code
|
|
206
|
+
|
|
207
|
+
4. User authenticates with the provider
|
|
208
|
+
|
|
209
|
+
5. Provider redirects back to callback:
|
|
210
|
+
GET /auth/login/{provider}/callback?code={authCode}&state={stateToken}
|
|
211
|
+
|
|
212
|
+
6. Server verifies the state token (one-time use)
|
|
213
|
+
7. Server exchanges the code for an access token with the provider
|
|
214
|
+
8. Server fetches user info from the provider
|
|
215
|
+
9. Server creates or finds the user by email
|
|
216
|
+
10. Server generates an authentication OTT
|
|
217
|
+
11. Server redirects to the original URL with the token:
|
|
218
|
+
-> https://myapp.com/dashboard?token={ott}
|
|
219
|
+
|
|
220
|
+
12. Client exchanges the OTT for JWT tokens:
|
|
221
|
+
POST /auth/exchange-token { token: "{ott}" }
|
|
222
|
+
-> { accessToken, refreshToken }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Google OAuth2
|
|
226
|
+
|
|
227
|
+
**Configuration:**
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"config": {
|
|
232
|
+
"security": {
|
|
233
|
+
"oauth2": {
|
|
234
|
+
"google": {
|
|
235
|
+
"enabled": true,
|
|
236
|
+
"clientId": "your-google-client-id",
|
|
237
|
+
"clientSecret": "your-google-client-secret"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Or via environment variables:
|
|
246
|
+
|
|
247
|
+
```env
|
|
248
|
+
SECURITY_OAUTH2_GOOGLE_ENABLED=true
|
|
249
|
+
SECURITY_OAUTH2_GOOGLE_CLIENT_ID=your-google-client-id
|
|
250
|
+
SECURITY_OAUTH2_GOOGLE_CLIENT_SECRET=your-google-client-secret
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Routes:**
|
|
254
|
+
|
|
255
|
+
| Method | Path | Description |
|
|
256
|
+
|--------|-------------------------------|--------------------------------------------------------------------------|
|
|
257
|
+
| `GET` | `/auth/login/google` | Redirect to Google consent screen. Query: `redirectToUrl`. |
|
|
258
|
+
| `GET` | `/auth/login/google/callback` | Google callback. Exchanges code, creates/finds user, redirects with OTT. |
|
|
259
|
+
|
|
260
|
+
**Scopes**: `profile`, `email`
|
|
261
|
+
|
|
262
|
+
**User info extracted**: `email`, `given_name` (firstName), `family_name` (lastName)
|
|
263
|
+
|
|
264
|
+
**Google Cloud Console setup:**
|
|
265
|
+
|
|
266
|
+
1. Create OAuth 2.0 credentials in the Google Cloud Console
|
|
267
|
+
2. Set the authorized redirect URI to: `{APP_HOSTNAME}{SERVER_API_PREFIX}/auth/login/google/callback`
|
|
268
|
+
(e.g. `https://api.myapp.com/api/auth/login/google/callback`)
|
|
269
|
+
|
|
270
|
+
### Facebook OAuth2
|
|
271
|
+
|
|
272
|
+
**Configuration:**
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"config": {
|
|
277
|
+
"security": {
|
|
278
|
+
"oauth2": {
|
|
279
|
+
"facebook": {
|
|
280
|
+
"enabled": true,
|
|
281
|
+
"clientId": "your-facebook-app-id",
|
|
282
|
+
"clientSecret": "your-facebook-app-secret"
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Or via environment variables:
|
|
291
|
+
|
|
292
|
+
```env
|
|
293
|
+
SECURITY_OAUTH2_FACEBOOK_ENABLED=true
|
|
294
|
+
SECURITY_OAUTH2_FACEBOOK_CLIENT_ID=your-facebook-app-id
|
|
295
|
+
SECURITY_OAUTH2_FACEBOOK_CLIENT_SECRET=your-facebook-app-secret
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Routes:**
|
|
299
|
+
|
|
300
|
+
| Method | Path | Description |
|
|
301
|
+
|--------|---------------------------------|----------------------------------------------------------------------------|
|
|
302
|
+
| `GET` | `/auth/login/facebook` | Redirect to Facebook login. Query: `redirectToUrl`. |
|
|
303
|
+
| `GET` | `/auth/login/facebook/callback` | Facebook callback. Exchanges code, creates/finds user, redirects with OTT. |
|
|
304
|
+
|
|
305
|
+
**Scopes**: `public_profile`, `email`
|
|
306
|
+
|
|
307
|
+
**User info extracted**: `email`, `name` (split into firstName/lastName)
|
|
308
|
+
|
|
309
|
+
**Facebook Developer Console setup:**
|
|
310
|
+
|
|
311
|
+
1. Create an app in the Facebook Developer Console
|
|
312
|
+
2. Add Facebook Login product
|
|
313
|
+
3. Set the valid OAuth redirect URI to: `{APP_HOSTNAME}{SERVER_API_PREFIX}/auth/login/facebook/callback`
|
|
314
|
+
|
|
315
|
+
### Custom OAuth2 (OpenID Connect)
|
|
316
|
+
|
|
317
|
+
For any OpenID Connect-compatible provider (Keycloak, Auth0, etc.).
|
|
318
|
+
|
|
319
|
+
**Configuration:**
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"config": {
|
|
324
|
+
"security": {
|
|
325
|
+
"oauth2": {
|
|
326
|
+
"custom": {
|
|
327
|
+
"enabled": true,
|
|
328
|
+
"clientId": "your-client-id",
|
|
329
|
+
"clientSecret": "your-client-secret",
|
|
330
|
+
"issuer": "https://keycloak.example.com/realms/myrealm"
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Routes:**
|
|
339
|
+
|
|
340
|
+
| Method | Path | Description |
|
|
341
|
+
|--------|-------------------------------|------------------------------------------------------|
|
|
342
|
+
| `GET` | `/auth/login/custom` | Redirect to custom provider. Query: `redirectToUrl`. |
|
|
343
|
+
| `GET` | `/auth/login/custom/callback` | Custom provider callback. |
|
|
344
|
+
|
|
345
|
+
**Scopes**: `openid`, `profile`, `email`
|
|
346
|
+
|
|
347
|
+
**User info endpoint**: `{issuer}/protocol/openid-connect/userinfo`
|
|
348
|
+
|
|
349
|
+
**Standard claims expected**: `sub`, `email`, `given_name`, `family_name`
|
|
350
|
+
|
|
351
|
+
### Client-side OAuth2 integration example
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
// 1. Redirect user to OAuth2 login
|
|
355
|
+
window.location.href = 'https://api.myapp.com/api/auth/login/google?redirectToUrl=https://myapp.com/auth/callback';
|
|
356
|
+
|
|
357
|
+
// 2. On the callback page, extract the token from URL params
|
|
358
|
+
const params = new URLSearchParams(window.location.search);
|
|
359
|
+
const token = params.get('token');
|
|
360
|
+
|
|
361
|
+
// 3. Exchange the OTT for JWT tokens
|
|
362
|
+
const response = await fetch('https://api.myapp.com/api/auth/exchange-token', {
|
|
363
|
+
method: 'POST',
|
|
364
|
+
headers: { 'Content-Type': 'application/json' },
|
|
365
|
+
body: JSON.stringify({ token })
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const { accessToken, refreshToken } = await response.json();
|
|
369
|
+
|
|
370
|
+
// 4. Use the access token for subsequent requests
|
|
371
|
+
fetch('https://api.myapp.com/api/products/query', {
|
|
372
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Redirect URL validation
|
|
377
|
+
|
|
378
|
+
All redirect URLs (for OAuth2, email verification, password reset) are validated against
|
|
379
|
+
`SECURITY_ALLOWED_REDIRECT_HOSTS`. Set this to specific domains in production:
|
|
380
|
+
|
|
381
|
+
```json
|
|
382
|
+
{
|
|
383
|
+
"config": {
|
|
384
|
+
"security": {
|
|
385
|
+
"allowedRedirectHosts": [
|
|
386
|
+
"myapp.com",
|
|
387
|
+
"admin.myapp.com"
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Default is `['*']` (all hosts allowed).
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Authorization
|
|
399
|
+
|
|
400
|
+
### Role-based access control (RBAC)
|
|
401
|
+
|
|
402
|
+
Appweaver uses a role-permission model:
|
|
403
|
+
|
|
404
|
+
- **Roles** have a unique name and contain zero or more **permissions**
|
|
405
|
+
- **Users** are assigned zero or more roles
|
|
406
|
+
- Routes can require specific roles or permissions
|
|
407
|
+
|
|
408
|
+
### Route authorization
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
// Require any of these roles (OR logic)
|
|
412
|
+
create: {
|
|
413
|
+
roles: ['Admin', 'Editor']
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Require any of these permissions (OR logic)
|
|
417
|
+
update: {
|
|
418
|
+
permissions: ['product:update', 'product:manage']
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Authorization check order
|
|
423
|
+
|
|
424
|
+
On every authenticated request:
|
|
425
|
+
|
|
426
|
+
1. Verify user exists and is enabled
|
|
427
|
+
2. Verify `logoutAt` is before token `iat`
|
|
428
|
+
3. Verify JWT scope allows access to the URL
|
|
429
|
+
4. Verify the user has required roles (if configured)
|
|
430
|
+
5. Verify the user has required permissions (if configured)
|
|
431
|
+
|
|
432
|
+
### Helper functions
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import { hasRole, hasRoles, hasPermission, hasPermissions, currentAuthUser } from '@appweaver/core';
|
|
436
|
+
|
|
437
|
+
const user = currentAuthUser();
|
|
438
|
+
|
|
439
|
+
hasRole(user, 'Admin'); // boolean
|
|
440
|
+
hasRoles(user, ['Admin', 'Editor']); // OR: has at least one
|
|
441
|
+
hasRoles(user, ['Admin', 'Editor'], 'and'); // AND: has all
|
|
442
|
+
|
|
443
|
+
hasPermission(user, 'product:create'); // boolean
|
|
444
|
+
hasPermissions(user, ['product:create', 'product:update']); // OR
|
|
445
|
+
hasPermissions(user, ['product:create', 'product:update'], 'and'); // AND
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Request context helpers
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
import { currentAuthUser, currentAuthType, currentAuthSource } from '@appweaver/core';
|
|
452
|
+
|
|
453
|
+
const user = currentAuthUser(); // Current authenticated user
|
|
454
|
+
const type = currentAuthType(); // 'jwt' | 'apiKey' | 'basic'
|
|
455
|
+
const source = currentAuthSource(); // 'Password' | 'OAuth2Google' | 'ApiKey' | etc.
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Two-factor authentication (2FA)
|
|
461
|
+
|
|
462
|
+
When enabled, 2FA adds an extra verification step after password login using email-based one-time codes.
|
|
463
|
+
|
|
464
|
+
### Configuration
|
|
465
|
+
|
|
466
|
+
```json
|
|
467
|
+
{
|
|
468
|
+
"config": {
|
|
469
|
+
"security": {
|
|
470
|
+
"account": {
|
|
471
|
+
"2fa": {
|
|
472
|
+
"enabled": true,
|
|
473
|
+
"forced": false,
|
|
474
|
+
"ottTtl": 300000
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
- `enabled` - Allow users to opt into 2FA
|
|
483
|
+
- `forced` - Require 2FA for all users regardless of their preference
|
|
484
|
+
- `ottTtl` - Code expiration time in milliseconds (default 5 minutes)
|
|
485
|
+
|
|
486
|
+
### User setting
|
|
487
|
+
|
|
488
|
+
Users set their 2FA preference via the `twoFactorAuth` field on their profile:
|
|
489
|
+
|
|
490
|
+
- `'None'` - 2FA disabled for this user
|
|
491
|
+
- `'Email'` - 2FA enabled via email codes
|
|
492
|
+
|
|
493
|
+
### Login flow with 2FA
|
|
494
|
+
|
|
495
|
+
```
|
|
496
|
+
1. POST /auth/login { email, password }
|
|
497
|
+
-> If 2FA required: returns JWT with TwoFA scope (restricted access)
|
|
498
|
+
-> If 2FA not required: returns JWT with Auth scope (full access)
|
|
499
|
+
|
|
500
|
+
2. POST /account/send-2fa-code
|
|
501
|
+
-> Generates 6-digit code, emails it to user
|
|
502
|
+
-> Returns { challengeId }
|
|
503
|
+
|
|
504
|
+
3. POST /account/verify-2fa-code { challengeId, code }
|
|
505
|
+
-> Validates code against stored hash
|
|
506
|
+
-> Returns { token } (one-time authentication token)
|
|
507
|
+
|
|
508
|
+
4. POST /auth/exchange-token { token }
|
|
509
|
+
-> Returns full JWT with Auth scope { accessToken, refreshToken }
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### 2FA routes
|
|
513
|
+
|
|
514
|
+
| Method | Path | Auth | Rate limit | Description |
|
|
515
|
+
|--------|----------------------------|-------------------|------------|--------------------------------------------------|
|
|
516
|
+
| `POST` | `/account/send-2fa-code` | JWT (TwoFA scope) | 10/15min | Send 2FA code to user's email. |
|
|
517
|
+
| `POST` | `/account/verify-2fa-code` | JWT (TwoFA scope) | 12/window | Verify 2FA code, returns OTT for token exchange. |
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## reCAPTCHA
|
|
522
|
+
|
|
523
|
+
Appweaver integrates Google reCAPTCHA v3 for bot protection on sensitive endpoints.
|
|
524
|
+
|
|
525
|
+
### Configuration
|
|
526
|
+
|
|
527
|
+
```json
|
|
528
|
+
{
|
|
529
|
+
"config": {
|
|
530
|
+
"security": {
|
|
531
|
+
"recaptcha": {
|
|
532
|
+
"enabled": true,
|
|
533
|
+
"secret": "your-recaptcha-secret-key",
|
|
534
|
+
"headerName": "x-recaptcha-token",
|
|
535
|
+
"minScore": 0.4
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### How it works
|
|
543
|
+
|
|
544
|
+
1. Client gets a reCAPTCHA token from the Google reCAPTCHA v3 widget
|
|
545
|
+
2. Client includes the token in the request header: `x-recaptcha-token: <token>`
|
|
546
|
+
3. Server sends the token to Google's verification API along with the secret key and client IP
|
|
547
|
+
4. Server validates:
|
|
548
|
+
- `success` flag is `true`
|
|
549
|
+
- `action` matches the expected action (if configured on the route)
|
|
550
|
+
- `score` is at or above `SECURITY_RECAPTCHA_MIN_SCORE`
|
|
551
|
+
|
|
552
|
+
### Using reCAPTCHA on custom routes
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
registerRoute(
|
|
556
|
+
async (router) => {
|
|
557
|
+
router.post('/contact', { handler: contactHandler });
|
|
558
|
+
},
|
|
559
|
+
{ recaptcha: true, recaptchaAction: 'contact_form', public: true }
|
|
560
|
+
);
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
On resource routes:
|
|
564
|
+
|
|
565
|
+
```ts
|
|
566
|
+
createRoutes({
|
|
567
|
+
modelName: 'Comment',
|
|
568
|
+
create: { recaptcha: true, recaptchaAction: 'create_comment' }
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Routes with reCAPTCHA by default
|
|
573
|
+
|
|
574
|
+
- `POST /account/send-reset-password` (action: `send-reset-password`)
|
|
575
|
+
- `POST /account/reset-password` (action: `reset-password`)
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## Account management
|
|
580
|
+
|
|
581
|
+
### Email verification
|
|
582
|
+
|
|
583
|
+
| Method | Path | Auth | Description |
|
|
584
|
+
|--------|----------------------------------|--------|--------------------------------------------------------------------------------------------------------------------|
|
|
585
|
+
| `POST` | `/account/send-verify-email` | JWT | Send verification email. Takes `redirectUrl` and optional `type` (`'auto'` or `'manual'`). Rate limited: 10/15min. |
|
|
586
|
+
| `POST` | `/account/verify-email` | Public | Verify email with token from body. Rate limited: 12/window. |
|
|
587
|
+
| `GET` | `/account/verify-email-redirect` | Public | Auto-verify and redirect. Token from query param. Redirects to `{redirectUrl}?status=ok\|error&message=...`. |
|
|
588
|
+
|
|
589
|
+
**Verification types:**
|
|
590
|
+
|
|
591
|
+
- `auto` (default) – Generates a link that auto-verifies on click and redirects with a status query parameter
|
|
592
|
+
- `manual` - Generates a link where the client must POST the token to the verified endpoint
|
|
593
|
+
|
|
594
|
+
### Password reset
|
|
595
|
+
|
|
596
|
+
| Method | Path | Auth | reCAPTCHA | Description |
|
|
597
|
+
|--------|--------------------------------|--------|-----------|-------------------------------------------------------------------------------------|
|
|
598
|
+
| `POST` | `/account/send-reset-password` | Public | Yes | Send password reset email. Takes `email` and `redirectUrl`. Rate limited: 10/15min. |
|
|
599
|
+
| `POST` | `/account/reset-password` | Public | Yes | Reset password with token and new password. Rate limited: 12/window. |
|
|
600
|
+
|
|
601
|
+
**Reset flow:**
|
|
602
|
+
|
|
603
|
+
1. User requests reset: `POST /account/send-reset-password { email, redirectUrl }`
|
|
604
|
+
2. Server generates OTT, emails a link: `{redirectUrl}?token={ott}`
|
|
605
|
+
3. User clicks a link, enters a new password
|
|
606
|
+
4. Client sends: `POST /account/reset-password { token, password }`
|
|
607
|
+
5. Server validates password complexity, updates hash, sets `logoutAt` (invalidates all sessions)
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## One-time tokens (OTT)
|
|
612
|
+
|
|
613
|
+
One-time tokens are used internally for various verification flows. They are single-use, purpose-bound, and
|
|
614
|
+
time-limited.
|
|
615
|
+
|
|
616
|
+
### Purposes
|
|
617
|
+
|
|
618
|
+
| Purpose | TTL config | Used for |
|
|
619
|
+
|---------------------|---------------------------------------------------|---------------------------------------|
|
|
620
|
+
| `Authentication` | `SECURITY_AUTH_OTT_TTL` (120s) | OAuth2 token exchange, 2FA completion |
|
|
621
|
+
| `EmailVerification` | `SECURITY_ACCOUNT_VERIFY_EMAIL_OTT_TTL` (2h) | Email verification links |
|
|
622
|
+
| `PasswordReset` | `SECURITY_ACCOUNT_RESET_PASSWORD_OTT_TTL` (30min) | Password reset links |
|
|
623
|
+
| `TwoFAVerification` | `SECURITY_ACCOUNT_2FA_OTT_TTL` (5min) | 2FA code challenges |
|
|
624
|
+
| `OAuth2State` | `SECURITY_OAUTH2_STATE_TTL` (10min) | CSRF protection in OAuth2 flow |
|
|
625
|
+
|
|
626
|
+
### Storage
|
|
627
|
+
|
|
628
|
+
OTTs are stored in the configured security store:
|
|
629
|
+
|
|
630
|
+
- **Redis** (default): `@appweaver/core/security/store/redis-security-store`
|
|
631
|
+
- **Database**: `@appweaver/core/security/store/database-security-store`
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Auth user model
|
|
636
|
+
|
|
637
|
+
The security module automatically adds the following fields to the user model:
|
|
638
|
+
|
|
639
|
+
| Field | Type | Default | Description |
|
|
640
|
+
|-----------------|------------------|----------|--------------------------------------------------------------|
|
|
641
|
+
| `email` | string (unique) | - | User's email address. |
|
|
642
|
+
| `passwordHash` | string? (hidden) | - | Bcrypt password hash (never exposed in API). |
|
|
643
|
+
| `verifiedEmail` | boolean | `false` | Whether the user's email is verified. |
|
|
644
|
+
| `twoFactorAuth` | enum | `'None'` | 2FA setting. Values: `'None'`, `'Email'`. |
|
|
645
|
+
| `enabled` | boolean | `true` | Whether the account is active. |
|
|
646
|
+
| `logoutAt` | dateTime? | - | Timestamp used to invalidate tokens issued before this time. |
|
|
647
|
+
|
|
648
|
+
**Relations:**
|
|
649
|
+
|
|
650
|
+
| Relation | Type | Description |
|
|
651
|
+
|-----------|----------|---------------------------------------------------------------------|
|
|
652
|
+
| `roles` | Role[] | Assigned roles with nested permissions (always included in output). |
|
|
653
|
+
| `apiKeys` | ApiKey[] | User's API keys (if API key auth is enabled). |
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## Password handling
|
|
658
|
+
|
|
659
|
+
### Hashing
|
|
660
|
+
|
|
661
|
+
Passwords are hashed using bcrypt with automatic salt generation.
|
|
662
|
+
|
|
663
|
+
### Complexity validation
|
|
664
|
+
|
|
665
|
+
Password validation is configurable via `SECURITY_PASSWORD_*` properties:
|
|
666
|
+
|
|
667
|
+
- Minimum length (default: 8)
|
|
668
|
+
- Maximum length (default: 100)
|
|
669
|
+
- Require an uppercase letter (default: true)
|
|
670
|
+
- Require a lowercase letter (default: true)
|
|
671
|
+
- Require a digit (default: true)
|
|
672
|
+
- Require special character (default: true)
|
|
673
|
+
|
|
674
|
+
### Token invalidation on password change
|
|
675
|
+
|
|
676
|
+
Both `change-password` and `reset-password` set `logoutAt` to the current timestamp, which invalidates all existing
|
|
677
|
+
JWT tokens across all devices. The `change-password` endpoint returns new tokens so the current session stays active.
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## Rate limiting on security routes
|
|
682
|
+
|
|
683
|
+
| Endpoint | Limit |
|
|
684
|
+
|-------------------------------------|-------------------|
|
|
685
|
+
| `POST /auth/login` | 12 per window |
|
|
686
|
+
| `POST /auth/refresh` | 12 per window |
|
|
687
|
+
| `POST /auth/change-password` | 12 per window |
|
|
688
|
+
| `POST /auth/exchange-token` | 12 per window |
|
|
689
|
+
| `POST /account/send-verify-email` | 10 per 15 minutes |
|
|
690
|
+
| `POST /account/verify-email` | 12 per window |
|
|
691
|
+
| `POST /account/send-reset-password` | 10 per 15 minutes |
|
|
692
|
+
| `POST /account/reset-password` | 12 per window |
|
|
693
|
+
| `POST /account/send-2fa-code` | 10 per 15 minutes |
|
|
694
|
+
| `POST /account/verify-2fa-code` | 12 per window |
|