@venizia/ignis-docs 0.0.5 → 0.0.6-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/package.json +1 -1
- package/wiki/best-practices/architecture-decisions.md +0 -8
- package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
- package/wiki/best-practices/performance-optimization.md +3 -3
- package/wiki/best-practices/security-guidelines.md +2 -2
- package/wiki/best-practices/troubleshooting-tips.md +1 -1
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/components.md +2 -2
- package/wiki/guides/core-concepts/dependency-injection.md +1 -1
- package/wiki/guides/core-concepts/services.md +1 -1
- package/wiki/guides/tutorials/building-a-crud-api.md +1 -1
- package/wiki/guides/tutorials/ecommerce-api.md +2 -2
- package/wiki/guides/tutorials/realtime-chat.md +6 -6
- package/wiki/guides/tutorials/testing.md +1 -1
- package/wiki/references/base/bootstrapping.md +0 -2
- package/wiki/references/base/components.md +2 -2
- package/wiki/references/base/controllers.md +0 -1
- package/wiki/references/base/datasources.md +1 -1
- package/wiki/references/base/dependency-injection.md +1 -1
- package/wiki/references/base/filter-system/quick-reference.md +0 -14
- package/wiki/references/base/middlewares.md +0 -8
- package/wiki/references/base/providers.md +0 -9
- package/wiki/references/base/services.md +0 -1
- package/wiki/references/components/authentication/api.md +444 -0
- package/wiki/references/components/authentication/errors.md +177 -0
- package/wiki/references/components/authentication/index.md +571 -0
- package/wiki/references/components/authentication/usage.md +781 -0
- package/wiki/references/components/health-check.md +292 -103
- package/wiki/references/components/index.md +14 -12
- package/wiki/references/components/mail/api.md +505 -0
- package/wiki/references/components/mail/errors.md +176 -0
- package/wiki/references/components/mail/index.md +535 -0
- package/wiki/references/components/mail/usage.md +404 -0
- package/wiki/references/components/request-tracker.md +229 -25
- package/wiki/references/components/socket-io/api.md +1051 -0
- package/wiki/references/components/socket-io/errors.md +119 -0
- package/wiki/references/components/socket-io/index.md +410 -0
- package/wiki/references/components/socket-io/usage.md +322 -0
- package/wiki/references/components/static-asset/api.md +261 -0
- package/wiki/references/components/static-asset/errors.md +89 -0
- package/wiki/references/components/static-asset/index.md +617 -0
- package/wiki/references/components/static-asset/usage.md +364 -0
- package/wiki/references/components/swagger.md +390 -110
- package/wiki/references/components/template/api-page.md +125 -0
- package/wiki/references/components/template/errors-page.md +100 -0
- package/wiki/references/components/template/index.md +104 -0
- package/wiki/references/components/template/setup-page.md +134 -0
- package/wiki/references/components/template/single-page.md +132 -0
- package/wiki/references/components/template/usage-page.md +127 -0
- package/wiki/references/components/websocket/api.md +508 -0
- package/wiki/references/components/websocket/errors.md +123 -0
- package/wiki/references/components/websocket/index.md +453 -0
- package/wiki/references/components/websocket/usage.md +475 -0
- package/wiki/references/helpers/cron/index.md +224 -0
- package/wiki/references/helpers/crypto/index.md +537 -0
- package/wiki/references/helpers/env/index.md +214 -0
- package/wiki/references/helpers/error/index.md +232 -0
- package/wiki/references/helpers/index.md +16 -15
- package/wiki/references/helpers/inversion/index.md +608 -0
- package/wiki/references/helpers/logger/index.md +600 -0
- package/wiki/references/helpers/network/api.md +986 -0
- package/wiki/references/helpers/network/index.md +620 -0
- package/wiki/references/helpers/queue/index.md +589 -0
- package/wiki/references/helpers/redis/index.md +495 -0
- package/wiki/references/helpers/socket-io/api.md +497 -0
- package/wiki/references/helpers/socket-io/index.md +513 -0
- package/wiki/references/helpers/storage/api.md +705 -0
- package/wiki/references/helpers/storage/index.md +583 -0
- package/wiki/references/helpers/template/index.md +66 -0
- package/wiki/references/helpers/template/single-page.md +126 -0
- package/wiki/references/helpers/testing/index.md +510 -0
- package/wiki/references/helpers/types/index.md +512 -0
- package/wiki/references/helpers/uid/index.md +272 -0
- package/wiki/references/helpers/websocket/api.md +736 -0
- package/wiki/references/helpers/websocket/index.md +574 -0
- package/wiki/references/helpers/worker-thread/index.md +470 -0
- package/wiki/references/quick-reference.md +3 -18
- package/wiki/references/utilities/jsx.md +1 -8
- package/wiki/references/utilities/statuses.md +0 -7
- package/wiki/references/components/authentication.md +0 -476
- package/wiki/references/components/mail.md +0 -687
- package/wiki/references/components/socket-io.md +0 -562
- package/wiki/references/components/static-asset.md +0 -1277
- package/wiki/references/helpers/cron.md +0 -108
- package/wiki/references/helpers/crypto.md +0 -132
- package/wiki/references/helpers/env.md +0 -83
- package/wiki/references/helpers/error.md +0 -97
- package/wiki/references/helpers/inversion.md +0 -176
- package/wiki/references/helpers/logger.md +0 -296
- package/wiki/references/helpers/network.md +0 -396
- package/wiki/references/helpers/queue.md +0 -150
- package/wiki/references/helpers/redis.md +0 -142
- package/wiki/references/helpers/socket-io.md +0 -932
- package/wiki/references/helpers/storage.md +0 -665
- package/wiki/references/helpers/testing.md +0 -133
- package/wiki/references/helpers/types.md +0 -167
- package/wiki/references/helpers/uid.md +0 -167
- package/wiki/references/helpers/worker-thread.md +0 -178
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
# Authentication -- Usage & Examples
|
|
2
|
+
|
|
3
|
+
> Securing routes, authentication flows, entity helpers, and API endpoint specifications. See [Setup & Configuration](./) for initial setup.
|
|
4
|
+
|
|
5
|
+
## Securing Routes
|
|
6
|
+
|
|
7
|
+
Use `authStrategies` and `authMode` in route configurations:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Single strategy
|
|
11
|
+
const SECURE_ROUTE_CONFIG = {
|
|
12
|
+
path: '/secure-data',
|
|
13
|
+
method: HTTP.Methods.GET,
|
|
14
|
+
authStrategies: [Authentication.STRATEGY_JWT],
|
|
15
|
+
responses: jsonResponse({
|
|
16
|
+
description: 'Protected data',
|
|
17
|
+
schema: z.object({ message: z.string() }),
|
|
18
|
+
}),
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
// Multiple strategies with fallback (any mode)
|
|
22
|
+
const FALLBACK_AUTH_CONFIG = {
|
|
23
|
+
path: '/api/data',
|
|
24
|
+
method: HTTP.Methods.GET,
|
|
25
|
+
authStrategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC],
|
|
26
|
+
authMode: 'any',
|
|
27
|
+
responses: jsonResponse({
|
|
28
|
+
description: 'Data accessible via JWT or Basic auth',
|
|
29
|
+
schema: z.object({ data: z.any() }),
|
|
30
|
+
}),
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
// Skip authentication
|
|
34
|
+
const PUBLIC_ROUTE_CONFIG = {
|
|
35
|
+
path: '/public',
|
|
36
|
+
method: HTTP.Methods.GET,
|
|
37
|
+
skipAuth: true,
|
|
38
|
+
responses: jsonResponse({
|
|
39
|
+
description: 'Public endpoint',
|
|
40
|
+
schema: z.object({ message: z.string() }),
|
|
41
|
+
}),
|
|
42
|
+
} as const;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Using the `authenticate()` Standalone Function
|
|
46
|
+
|
|
47
|
+
The `authenticate()` function is a convenience wrapper around `AuthenticationStrategyRegistry.getInstance().authenticate()`. It returns a Hono `MiddlewareHandler` suitable for direct middleware usage:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { authenticate, Authentication } from '@venizia/ignis';
|
|
51
|
+
|
|
52
|
+
// Use as Hono middleware directly
|
|
53
|
+
const authMiddleware = authenticate({
|
|
54
|
+
strategies: [Authentication.STRATEGY_JWT],
|
|
55
|
+
mode: 'any',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Apply to a Hono route
|
|
59
|
+
app.get('/protected', authMiddleware, (c) => {
|
|
60
|
+
const user = c.get(Authentication.CURRENT_USER);
|
|
61
|
+
return c.json({ userId: user.userId });
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Accessing the Current User
|
|
66
|
+
|
|
67
|
+
After authentication, the user payload is available on the Hono `Context`:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { Context } from 'hono';
|
|
71
|
+
import { Authentication, IJWTTokenPayload } from '@venizia/ignis';
|
|
72
|
+
|
|
73
|
+
// Inside a route handler
|
|
74
|
+
const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload | undefined;
|
|
75
|
+
|
|
76
|
+
if (user) {
|
|
77
|
+
console.log('Authenticated user ID:', user.userId);
|
|
78
|
+
console.log('User roles:', user.roles);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Dynamic Skip Authentication
|
|
83
|
+
|
|
84
|
+
Use `Authentication.SKIP_AUTHENTICATION` to dynamically skip auth in middleware:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { Authentication } from '@venizia/ignis';
|
|
88
|
+
import { createMiddleware } from 'hono/factory';
|
|
89
|
+
|
|
90
|
+
const conditionalAuthMiddleware = createMiddleware(async (c, next) => {
|
|
91
|
+
if (c.req.header('X-API-Key') === 'valid-api-key') {
|
|
92
|
+
c.set(Authentication.SKIP_AUTHENTICATION, true);
|
|
93
|
+
}
|
|
94
|
+
return next();
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Implementing an AuthenticationService
|
|
99
|
+
|
|
100
|
+
The `AuthenticateComponent` depends on a service implementing the `IAuthService` interface when using the built-in auth controller:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import {
|
|
104
|
+
BaseService,
|
|
105
|
+
inject,
|
|
106
|
+
IAuthService,
|
|
107
|
+
IJWTTokenPayload,
|
|
108
|
+
JWTTokenService,
|
|
109
|
+
TSignInRequest,
|
|
110
|
+
getError,
|
|
111
|
+
} from '@venizia/ignis';
|
|
112
|
+
import { Context } from 'hono';
|
|
113
|
+
|
|
114
|
+
export class AuthenticationService extends BaseService implements IAuthService {
|
|
115
|
+
constructor(
|
|
116
|
+
@inject({ key: 'services.JWTTokenService' })
|
|
117
|
+
private _jwtTokenService: JWTTokenService,
|
|
118
|
+
) {
|
|
119
|
+
super({ scope: AuthenticationService.name });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async signIn(context: Context, opts: TSignInRequest): Promise<{ token: string }> {
|
|
123
|
+
const { identifier, credential } = opts;
|
|
124
|
+
const user = await this.userRepo.findByIdentifier(identifier);
|
|
125
|
+
|
|
126
|
+
if (!user || !await this.verifyCredential(credential, user)) {
|
|
127
|
+
throw getError({ message: 'Invalid credentials' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const payload: IJWTTokenPayload = {
|
|
131
|
+
userId: user.id,
|
|
132
|
+
roles: user.roles,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const token = await this._jwtTokenService.generate({ payload });
|
|
136
|
+
return { token };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async signUp(context: Context, opts: any): Promise<any> {
|
|
140
|
+
// Implement your sign-up logic
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async changePassword(context: Context, opts: any): Promise<any> {
|
|
144
|
+
// Implement your change password logic
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Entity Column Helpers
|
|
150
|
+
|
|
151
|
+
The authentication module provides a set of **column helper functions** designed to be spread into Drizzle `pgTable()` definitions. These functions return pre-configured column objects for common auth-related entities, saving you from manually defining columns for users, roles, permissions, and their relationships.
|
|
152
|
+
|
|
153
|
+
### Pattern
|
|
154
|
+
|
|
155
|
+
Each helper function returns an object of Drizzle column builders that you spread into your `pgTable()` call alongside any custom columns:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { pgTable, serial, text } from 'drizzle-orm/pg-core';
|
|
159
|
+
import {
|
|
160
|
+
extraUserColumns,
|
|
161
|
+
extraRoleColumns,
|
|
162
|
+
extraPermissionColumns,
|
|
163
|
+
extraPermissionMappingColumns,
|
|
164
|
+
extraUserRoleColumns,
|
|
165
|
+
} from '@venizia/ignis';
|
|
166
|
+
import { withSerialId, withTimestamps } from '@venizia/ignis';
|
|
167
|
+
|
|
168
|
+
// User table with auth columns
|
|
169
|
+
export const users = pgTable('users', {
|
|
170
|
+
...withSerialId(),
|
|
171
|
+
...withTimestamps(),
|
|
172
|
+
...extraUserColumns(),
|
|
173
|
+
username: text('username').unique().notNull(),
|
|
174
|
+
passwordHash: text('password_hash').notNull(),
|
|
175
|
+
email: text('email').unique(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Role table with auth columns
|
|
179
|
+
export const roles = pgTable('roles', {
|
|
180
|
+
...withSerialId(),
|
|
181
|
+
...withTimestamps(),
|
|
182
|
+
...extraRoleColumns(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Permission table
|
|
186
|
+
export const permissions = pgTable('permissions', {
|
|
187
|
+
...withSerialId(),
|
|
188
|
+
...withTimestamps(),
|
|
189
|
+
...extraPermissionColumns(),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Permission mapping (role-to-permission or user-to-permission)
|
|
193
|
+
export const permissionMappings = pgTable('permission_mappings', {
|
|
194
|
+
...withSerialId(),
|
|
195
|
+
...extraPermissionMappingColumns(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// User-role junction table
|
|
199
|
+
export const userRoles = pgTable('user_roles', {
|
|
200
|
+
...withSerialId(),
|
|
201
|
+
...extraUserRoleColumns(),
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### extraUserColumns
|
|
206
|
+
|
|
207
|
+
Returns columns for user-related fields with status and type defaults from `UserStatuses` and `UserTypes`.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
extraUserColumns(opts?: { idType: 'string' | 'number' })
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
| Column | Type | Default | Description |
|
|
214
|
+
|--------|------|---------|-------------|
|
|
215
|
+
| `realm` | `text` | `''` | Multi-tenancy realm identifier |
|
|
216
|
+
| `status` | `text` | `UserStatuses.UNKNOWN` (`'000_UNKNOWN'`) | User status |
|
|
217
|
+
| `type` | `text` | `UserTypes.SYSTEM` (`'SYSTEM'`) | User type |
|
|
218
|
+
| `activatedAt` | `timestamp (tz)` | `null` | Activation timestamp |
|
|
219
|
+
| `lastLoginAt` | `timestamp (tz)` | `null` | Last login timestamp |
|
|
220
|
+
| `parentId` | `text` or `integer` | `null` | Parent user ID (type depends on `idType`) |
|
|
221
|
+
|
|
222
|
+
### extraRoleColumns
|
|
223
|
+
|
|
224
|
+
Returns columns for role definitions. No options parameter.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
extraRoleColumns()
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
| Column | Type | Default | Description |
|
|
231
|
+
|--------|------|---------|-------------|
|
|
232
|
+
| `identifier` | `text` | -- | Unique role identifier (e.g., `'admin'`, `'user'`) |
|
|
233
|
+
| `name` | `text` | -- | Human-readable role name |
|
|
234
|
+
| `description` | `text` | `null` | Optional role description |
|
|
235
|
+
| `priority` | `integer` | -- | Role priority (lower = higher priority) |
|
|
236
|
+
| `status` | `text` | `RoleStatuses.ACTIVATED` (`'201_ACTIVATED'`) | Role status |
|
|
237
|
+
|
|
238
|
+
### extraPermissionColumns
|
|
239
|
+
|
|
240
|
+
Returns columns for permission definitions. Supports `idType` option for the `parentId` column type.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
extraPermissionColumns(opts?: { idType: 'string' | 'number' })
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
| Column | Type | Default | Description |
|
|
247
|
+
|--------|------|---------|-------------|
|
|
248
|
+
| `code` | `text` | -- | Unique permission code |
|
|
249
|
+
| `name` | `text` | -- | Permission name |
|
|
250
|
+
| `subject` | `text` | -- | Permission subject (e.g., `'User'`, `'Order'`) |
|
|
251
|
+
| `pType` | `text` | -- | Permission type (maps to DB column `p_type`) |
|
|
252
|
+
| `action` | `text` | -- | Permitted action (e.g., `'read'`, `'write'`) |
|
|
253
|
+
| `scope` | `text` | -- | Permission scope |
|
|
254
|
+
| `parentId` | `text` or `integer` | `null` | Parent permission ID |
|
|
255
|
+
|
|
256
|
+
### extraPermissionMappingColumns
|
|
257
|
+
|
|
258
|
+
Returns columns for mapping permissions to users or roles.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
extraPermissionMappingColumns(opts?: { idType: 'string' | 'number' })
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
| Column | Type | Default | Description |
|
|
265
|
+
|--------|------|---------|-------------|
|
|
266
|
+
| `effect` | `text` | `null` | Permission effect (e.g., `'allow'`, `'deny'`) |
|
|
267
|
+
| `userId` | `text` or `integer` | `null` | Associated user ID |
|
|
268
|
+
| `roleId` | `text` or `integer` | `null` | Associated role ID |
|
|
269
|
+
| `permissionId` | `text` or `integer` | -- | Associated permission ID (not null) |
|
|
270
|
+
|
|
271
|
+
### extraUserRoleColumns
|
|
272
|
+
|
|
273
|
+
Returns columns for the user-role junction table. Includes principal columns (polymorphic type/ID fields) via `generatePrincipalColumnDefs` with a default polymorphic value of `'Role'`.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
extraUserRoleColumns(opts?: { idType: 'string' | 'number' })
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
| Column | Type | Default | Description |
|
|
280
|
+
|--------|------|---------|-------------|
|
|
281
|
+
| _(principal columns)_ | _various_ | -- | Polymorphic columns from `generatePrincipalColumnDefs` |
|
|
282
|
+
| `userId` | `text` or `integer` | -- | Associated user ID (not null) |
|
|
283
|
+
|
|
284
|
+
### ID Type Polymorphism
|
|
285
|
+
|
|
286
|
+
All column helpers that accept `opts.idType` default to `'number'` (producing `integer` columns). Pass `'string'` to use `text` columns instead:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Number IDs (default) -- uses integer columns for FK references
|
|
290
|
+
extraUserColumns()
|
|
291
|
+
extraPermissionColumns()
|
|
292
|
+
|
|
293
|
+
// String IDs (e.g., UUID) -- uses text columns for FK references
|
|
294
|
+
extraUserColumns({ idType: 'string' })
|
|
295
|
+
extraPermissionColumns({ idType: 'string' })
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Status Constants
|
|
299
|
+
|
|
300
|
+
The authentication module uses status classes from `@/common/statuses`. These extend `CommonStatuses` and provide lifecycle state management for auth entities.
|
|
301
|
+
|
|
302
|
+
### UserStatuses
|
|
303
|
+
|
|
304
|
+
Inherits all statuses from `CommonStatuses`:
|
|
305
|
+
|
|
306
|
+
| Constant | Value | Description |
|
|
307
|
+
|----------|-------|-------------|
|
|
308
|
+
| `UserStatuses.UNKNOWN` | `'000_UNKNOWN'` | Initial/unverified state |
|
|
309
|
+
| `UserStatuses.ACTIVATED` | `'201_ACTIVATED'` | Active user |
|
|
310
|
+
| `UserStatuses.DEACTIVATED` | `'401_DEACTIVATED'` | Deactivated user |
|
|
311
|
+
| `UserStatuses.BLOCKED` | `'403_BLOCKED'` | Blocked user |
|
|
312
|
+
| `UserStatuses.ARCHIVED` | `'405_ARCHIVED'` | Archived user |
|
|
313
|
+
|
|
314
|
+
### UserTypes
|
|
315
|
+
|
|
316
|
+
| Constant | Value | Description |
|
|
317
|
+
|----------|-------|-------------|
|
|
318
|
+
| `UserTypes.SYSTEM` | `'SYSTEM'` | System-created user (default) |
|
|
319
|
+
| `UserTypes.LINKED` | `'LINKED'` | Linked/external user |
|
|
320
|
+
|
|
321
|
+
### RoleStatuses
|
|
322
|
+
|
|
323
|
+
Inherits all statuses from `CommonStatuses` (same values as `UserStatuses`):
|
|
324
|
+
|
|
325
|
+
| Constant | Value | Description |
|
|
326
|
+
|----------|-------|-------------|
|
|
327
|
+
| `RoleStatuses.UNKNOWN` | `'000_UNKNOWN'` | Initial state |
|
|
328
|
+
| `RoleStatuses.ACTIVATED` | `'201_ACTIVATED'` | Active role (default for `extraRoleColumns`) |
|
|
329
|
+
| `RoleStatuses.DEACTIVATED` | `'401_DEACTIVATED'` | Deactivated role |
|
|
330
|
+
| `RoleStatuses.BLOCKED` | `'403_BLOCKED'` | Blocked role |
|
|
331
|
+
| `RoleStatuses.ARCHIVED` | `'405_ARCHIVED'` | Archived role |
|
|
332
|
+
|
|
333
|
+
## Auth Flows
|
|
334
|
+
|
|
335
|
+
### JWT Authentication Flow
|
|
336
|
+
|
|
337
|
+
1. **Client sends request** with <code v-pre>Authorization: Bearer <token></code> header
|
|
338
|
+
2. **JWTAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
339
|
+
3. **JWTTokenService.extractCredentials()** extracts the token from the Authorization header
|
|
340
|
+
4. **JWTTokenService.verify()** verifies the JWT signature using `jose.jwtVerify()`
|
|
341
|
+
5. **JWTTokenService.decryptPayload()** decrypts the AES-encrypted payload fields
|
|
342
|
+
6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
|
|
343
|
+
|
|
344
|
+
> [!NOTE]
|
|
345
|
+
> JWT payloads are encrypted field-by-field for additional security. Standard JWT fields (`iss`, `sub`, `aud`, etc.) remain unencrypted, while custom fields like `userId` and `roles` are AES-encrypted.
|
|
346
|
+
|
|
347
|
+
### Basic Authentication Flow
|
|
348
|
+
|
|
349
|
+
1. **Client sends request** with <code v-pre>Authorization: Basic <base64(username:password)></code> header
|
|
350
|
+
2. **BasicAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
351
|
+
3. **BasicTokenService.extractCredentials()** decodes the Base64 credentials
|
|
352
|
+
4. **BasicTokenService.verify()** calls the user-provided `verifyCredentials` callback
|
|
353
|
+
5. **User payload is set** on `context.get(Authentication.CURRENT_USER)` if verification succeeds
|
|
354
|
+
|
|
355
|
+
> [!IMPORTANT]
|
|
356
|
+
> The `verifyCredentials` callback must perform all necessary validation (password hashing comparison, user lookup, etc.) and return an `IAuthUser` object or `null`.
|
|
357
|
+
|
|
358
|
+
## Multi-Strategy Authentication
|
|
359
|
+
|
|
360
|
+
When multiple strategies are configured on a route via `authStrategies: ['jwt', 'basic']`:
|
|
361
|
+
|
|
362
|
+
**`any` mode (default):**
|
|
363
|
+
- Strategies are tried in the order specified
|
|
364
|
+
- The first successful strategy wins
|
|
365
|
+
- If all strategies fail, a `401 Unauthorized` error is thrown listing all tried strategies
|
|
366
|
+
- **Use case:** Fallback authentication (try JWT, fallback to Basic)
|
|
367
|
+
|
|
368
|
+
**`all` mode:**
|
|
369
|
+
- Every strategy must pass successfully
|
|
370
|
+
- If any strategy fails, the request is immediately rejected (exception propagates)
|
|
371
|
+
- The last strategy's user payload is used
|
|
372
|
+
- **Use case:** Multi-factor authentication (both JWT and Basic required)
|
|
373
|
+
|
|
374
|
+
> [!TIP]
|
|
375
|
+
> Use `'any'` mode for graceful fallback (e.g., allow mobile apps to use JWT while legacy systems use Basic). Use `'all'` mode for high-security endpoints requiring multiple forms of authentication.
|
|
376
|
+
|
|
377
|
+
## Token Encryption
|
|
378
|
+
|
|
379
|
+
JWT payloads are encrypted field-by-field using AES (default `aes-256-cbc`) via the `@venizia/ignis-helpers` AES utility:
|
|
380
|
+
|
|
381
|
+
**Encryption process:**
|
|
382
|
+
1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are preserved as-is
|
|
383
|
+
2. All other fields have both their **keys** and **values** AES-encrypted
|
|
384
|
+
3. The `roles` field is serialized as `id|identifier|priority` pipe-separated strings before encryption
|
|
385
|
+
4. `null` and `undefined` values are skipped during encryption
|
|
386
|
+
|
|
387
|
+
**Encryption code walkthrough:**
|
|
388
|
+
|
|
389
|
+
The `encryptPayload()` method processes each field:
|
|
390
|
+
1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are copied as-is
|
|
391
|
+
2. `null`/`undefined` values are skipped entirely
|
|
392
|
+
3. For the `roles` field: values are serialized as `"id|identifier|priority"` pipe-separated strings, then the array is JSON-stringified before encryption
|
|
393
|
+
4. For all other fields: values are converted to string via template literal (<code v-pre>`${value}`</code>), then both key and value are AES-encrypted independently
|
|
394
|
+
5. The encrypted key becomes the new field name, the encrypted value becomes its value
|
|
395
|
+
|
|
396
|
+
**Decryption process:**
|
|
397
|
+
1. Standard JWT fields are extracted directly
|
|
398
|
+
2. Encrypted fields have their keys decrypted first, then their values
|
|
399
|
+
3. The `roles` field is deserialized: JSON-parsed to a string array, then each entry is split on `|` to reconstruct objects with `id`, `identifier`, and `priority` (where `priority` is converted to integer via `int()`)
|
|
400
|
+
|
|
401
|
+
> [!WARNING]
|
|
402
|
+
> The `applicationSecret` must remain constant across all instances of your application. Changing it will invalidate all existing tokens, as they cannot be decrypted with a different secret.
|
|
403
|
+
|
|
404
|
+
## Hono Context Extension
|
|
405
|
+
|
|
406
|
+
The Authentication module extends Hono's `ContextVariableMap` to provide type-safe access to auth data:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
declare module 'hono' {
|
|
410
|
+
interface ContextVariableMap<User extends IAuthUser = IAuthUser> {
|
|
411
|
+
[Authentication.CURRENT_USER]: User;
|
|
412
|
+
[Authentication.AUDIT_USER_ID]: IdType;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
This enables type-safe access in route handlers:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// TypeScript knows this is IAuthUser
|
|
421
|
+
const user = c.get(Authentication.CURRENT_USER);
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Context variable keys (from `Authentication` constants):**
|
|
425
|
+
|
|
426
|
+
| Key | Constant | Type | Description |
|
|
427
|
+
|-----|----------|------|-------------|
|
|
428
|
+
| `'auth.current.user'` | `Authentication.CURRENT_USER` | `IAuthUser` | The authenticated user payload |
|
|
429
|
+
| `'audit.user.id'` | `Authentication.AUDIT_USER_ID` | `IdType` | The authenticated user's ID (extracted from `userId`) |
|
|
430
|
+
| `'authentication.skip'` | `Authentication.SKIP_AUTHENTICATION` | `boolean` | Set to `true` to bypass authentication on a request |
|
|
431
|
+
|
|
432
|
+
## Request Schemas
|
|
433
|
+
|
|
434
|
+
### SignInRequestSchema
|
|
435
|
+
|
|
436
|
+
The built-in schema uses a nested `identifier` + `credential` structure:
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
const SignInRequestSchema = z.object({
|
|
440
|
+
identifier: z.object({
|
|
441
|
+
scheme: requiredString({ min: 4 }), // e.g., 'username', 'email'
|
|
442
|
+
value: requiredString({ min: 8 }), // the actual identifier value
|
|
443
|
+
}),
|
|
444
|
+
credential: z.object({
|
|
445
|
+
scheme: requiredString(), // e.g., 'basic', 'password'
|
|
446
|
+
value: requiredString({ min: 8 }), // the actual credential value
|
|
447
|
+
}),
|
|
448
|
+
clientId: z.string().optional(), // optional auth provider
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
type TSignInRequest = z.infer<typeof SignInRequestSchema>;
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
| Field | Type | Constraints |
|
|
455
|
+
|-------|------|-------------|
|
|
456
|
+
| `identifier.scheme` | `string` | Non-empty, min 4 chars |
|
|
457
|
+
| `identifier.value` | `string` | Non-empty, min 8 chars |
|
|
458
|
+
| `credential.scheme` | `string` | Non-empty |
|
|
459
|
+
| `credential.value` | `string` | Non-empty, min 8 chars |
|
|
460
|
+
| `clientId` | `string` | Optional |
|
|
461
|
+
|
|
462
|
+
**OpenAPI examples** (from source):
|
|
463
|
+
```json
|
|
464
|
+
[
|
|
465
|
+
{
|
|
466
|
+
"identifier": { "scheme": "username", "value": "test_username" },
|
|
467
|
+
"credential": { "scheme": "basic", "value": "test_password" }
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"identifier": { "scheme": "username", "value": "test_username" },
|
|
471
|
+
"credential": { "scheme": "basic", "value": "test_password" },
|
|
472
|
+
"clientId": "auth-provider"
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### SignUpRequestSchema
|
|
478
|
+
|
|
479
|
+
The built-in schema uses a **flat structure** -- not the nested `identifier`/`credential` pattern used by sign-in:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const SignUpRequestSchema = z.object({
|
|
483
|
+
username: z.string().nonempty().min(8),
|
|
484
|
+
credential: z.string().nonempty().min(8),
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
type TSignUpRequest = z.infer<typeof SignUpRequestSchema>;
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
| Field | Type | Constraints |
|
|
491
|
+
|-------|------|-------------|
|
|
492
|
+
| `username` | `string` | Non-empty, min 8 chars |
|
|
493
|
+
| `credential` | `string` | Non-empty, min 8 chars |
|
|
494
|
+
|
|
495
|
+
**OpenAPI examples** (from source):
|
|
496
|
+
```json
|
|
497
|
+
[
|
|
498
|
+
{
|
|
499
|
+
"username": "example_username",
|
|
500
|
+
"credential": "example_credential"
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### ChangePasswordRequestSchema
|
|
506
|
+
|
|
507
|
+
The built-in schema uses scheme-based credential naming with a `userId` field:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
const ChangePasswordRequestSchema = z.object({
|
|
511
|
+
scheme: z.string(),
|
|
512
|
+
oldCredential: requiredString({ min: 8 }),
|
|
513
|
+
newCredential: requiredString({ min: 8 }),
|
|
514
|
+
userId: z.string().or(z.number()),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
type TChangePasswordRequest = z.infer<typeof ChangePasswordRequestSchema>;
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
| Field | Type | Constraints |
|
|
521
|
+
|-------|------|-------------|
|
|
522
|
+
| `scheme` | `string` | Required (e.g., `'basic'`) |
|
|
523
|
+
| `oldCredential` | `string` | Non-empty, min 8 chars |
|
|
524
|
+
| `newCredential` | `string` | Non-empty, min 8 chars |
|
|
525
|
+
| `userId` | `string \| number` | Required |
|
|
526
|
+
|
|
527
|
+
**OpenAPI examples** (from source):
|
|
528
|
+
```json
|
|
529
|
+
[
|
|
530
|
+
{
|
|
531
|
+
"scheme": "basic",
|
|
532
|
+
"oldCredential": "old_password",
|
|
533
|
+
"newCredential": "new_password"
|
|
534
|
+
}
|
|
535
|
+
]
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### JWTTokenPayloadSchema
|
|
539
|
+
|
|
540
|
+
Exported from the controller factory module. Used as the response schema for the `/who-am-i` endpoint:
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
const JWTTokenPayloadSchema = z.object({
|
|
544
|
+
userId: z.string().or(z.number()),
|
|
545
|
+
roles: z.array(
|
|
546
|
+
z.object({
|
|
547
|
+
id: z.string().or(z.number()),
|
|
548
|
+
identifier: z.string(),
|
|
549
|
+
priority: z.number().int(),
|
|
550
|
+
}),
|
|
551
|
+
),
|
|
552
|
+
clientId: z.string().optional(),
|
|
553
|
+
provider: z.string().optional(),
|
|
554
|
+
email: z.email().optional(),
|
|
555
|
+
});
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Custom Schema Example
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { z } from 'zod';
|
|
562
|
+
|
|
563
|
+
this.bind<TAuthenticationRestOptions>({ key: AuthenticateBindingKeys.REST_OPTIONS }).toValue({
|
|
564
|
+
useAuthController: true,
|
|
565
|
+
controllerOpts: {
|
|
566
|
+
restPath: '/auth',
|
|
567
|
+
payload: {
|
|
568
|
+
signIn: {
|
|
569
|
+
request: {
|
|
570
|
+
schema: z.object({
|
|
571
|
+
email: z.string().email(),
|
|
572
|
+
password: z.string().min(8),
|
|
573
|
+
}),
|
|
574
|
+
},
|
|
575
|
+
response: {
|
|
576
|
+
schema: z.object({
|
|
577
|
+
accessToken: z.string(),
|
|
578
|
+
refreshToken: z.string(),
|
|
579
|
+
expiresIn: z.number(),
|
|
580
|
+
}),
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## API Endpoints
|
|
589
|
+
|
|
590
|
+
The built-in auth controller is created by the `defineAuthController()` factory function and is only available when `useAuthController: true` is set in `REST_OPTIONS`.
|
|
591
|
+
|
|
592
|
+
| Method | Path | Auth Required | Description |
|
|
593
|
+
|--------|------|---------------|-------------|
|
|
594
|
+
| `POST` | `/auth/sign-in` | No | Authenticate and receive a JWT token |
|
|
595
|
+
| `POST` | `/auth/sign-up` | Configurable | Create a new user account |
|
|
596
|
+
| `POST` | `/auth/change-password` | JWT | Change the authenticated user's password |
|
|
597
|
+
| `GET` | `/auth/who-am-i` | JWT | Return the current user's JWT payload |
|
|
598
|
+
|
|
599
|
+
> [!NOTE]
|
|
600
|
+
> The base path `/auth` is configurable via `controllerOpts.restPath`. All paths shown above use the default.
|
|
601
|
+
|
|
602
|
+
### POST /auth/sign-in
|
|
603
|
+
|
|
604
|
+
**Authentication:** None
|
|
605
|
+
|
|
606
|
+
**Request Body:**
|
|
607
|
+
|
|
608
|
+
Uses `SignInRequestSchema` by default, or a custom schema via `payload.signIn.request.schema`.
|
|
609
|
+
|
|
610
|
+
Default schema:
|
|
611
|
+
```typescript
|
|
612
|
+
{
|
|
613
|
+
identifier: {
|
|
614
|
+
scheme: string; // min 4 chars, e.g., 'username', 'email'
|
|
615
|
+
value: string; // min 8 chars
|
|
616
|
+
};
|
|
617
|
+
credential: {
|
|
618
|
+
scheme: string; // e.g., 'basic', 'password'
|
|
619
|
+
value: string; // min 8 chars
|
|
620
|
+
};
|
|
621
|
+
clientId?: string;
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Response 200:**
|
|
626
|
+
|
|
627
|
+
Uses `payload.signIn.response.schema` if provided, otherwise `AnyObjectSchema`.
|
|
628
|
+
|
|
629
|
+
```json
|
|
630
|
+
{
|
|
631
|
+
"token": "eyJhbGciOiJIUzI1NiJ9..."
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Example:**
|
|
636
|
+
```typescript
|
|
637
|
+
const response = await fetch('/auth/sign-in', {
|
|
638
|
+
method: 'POST',
|
|
639
|
+
headers: { 'Content-Type': 'application/json' },
|
|
640
|
+
body: JSON.stringify({
|
|
641
|
+
identifier: { scheme: 'email', value: 'user@example.com' },
|
|
642
|
+
credential: { scheme: 'password', value: 'my-password' },
|
|
643
|
+
}),
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const { token } = await response.json();
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
### POST /auth/sign-up
|
|
651
|
+
|
|
652
|
+
**Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
|
|
653
|
+
|
|
654
|
+
When `requireAuthenticatedSignUp: true`, requires JWT authentication (strategy: `Authentication.STRATEGY_JWT`). When `false`, the `strategies` array is empty (public endpoint).
|
|
655
|
+
|
|
656
|
+
**Request Body:**
|
|
657
|
+
|
|
658
|
+
Uses `SignUpRequestSchema` by default, or a custom schema via `payload.signUp.request.schema`.
|
|
659
|
+
|
|
660
|
+
Default schema (flat structure):
|
|
661
|
+
```typescript
|
|
662
|
+
{
|
|
663
|
+
username: string; // non-empty, min 8 chars
|
|
664
|
+
credential: string; // non-empty, min 8 chars
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Response 200:**
|
|
669
|
+
|
|
670
|
+
Uses `payload.signUp.response.schema` if provided, otherwise `AnyObjectSchema`.
|
|
671
|
+
|
|
672
|
+
```json
|
|
673
|
+
{
|
|
674
|
+
"id": "user-id",
|
|
675
|
+
"username": "newuser123"
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Example:**
|
|
680
|
+
```typescript
|
|
681
|
+
const response = await fetch('/auth/sign-up', {
|
|
682
|
+
method: 'POST',
|
|
683
|
+
headers: { 'Content-Type': 'application/json' },
|
|
684
|
+
body: JSON.stringify({
|
|
685
|
+
username: 'newuser123',
|
|
686
|
+
credential: 'secure-password',
|
|
687
|
+
}),
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const user = await response.json();
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
### POST /auth/change-password
|
|
695
|
+
|
|
696
|
+
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
697
|
+
|
|
698
|
+
**Request Body:**
|
|
699
|
+
|
|
700
|
+
Uses `ChangePasswordRequestSchema` by default, or a custom schema via `payload.changePassword.request.schema`.
|
|
701
|
+
|
|
702
|
+
Default schema:
|
|
703
|
+
```typescript
|
|
704
|
+
{
|
|
705
|
+
scheme: string; // e.g., 'basic'
|
|
706
|
+
oldCredential: string; // non-empty, min 8 chars
|
|
707
|
+
newCredential: string; // non-empty, min 8 chars
|
|
708
|
+
userId: string | number;
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
**Response 200:**
|
|
713
|
+
|
|
714
|
+
Uses `payload.changePassword.response.schema` if provided, otherwise `AnyObjectSchema`.
|
|
715
|
+
|
|
716
|
+
```json
|
|
717
|
+
{
|
|
718
|
+
"success": true
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
**Example:**
|
|
723
|
+
```typescript
|
|
724
|
+
const response = await fetch('/auth/change-password', {
|
|
725
|
+
method: 'POST',
|
|
726
|
+
headers: {
|
|
727
|
+
'Content-Type': 'application/json',
|
|
728
|
+
'Authorization': `Bearer ${token}`,
|
|
729
|
+
},
|
|
730
|
+
body: JSON.stringify({
|
|
731
|
+
scheme: 'basic',
|
|
732
|
+
oldCredential: 'old-password',
|
|
733
|
+
newCredential: 'new-secure-password',
|
|
734
|
+
userId: '123',
|
|
735
|
+
}),
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const result = await response.json();
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
### GET /auth/who-am-i
|
|
743
|
+
|
|
744
|
+
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
745
|
+
|
|
746
|
+
**Request Body:** None
|
|
747
|
+
|
|
748
|
+
**Response 200:**
|
|
749
|
+
|
|
750
|
+
Uses the `JWTTokenPayloadSchema` Zod schema. Returns the current user's decrypted JWT payload directly from context:
|
|
751
|
+
|
|
752
|
+
```json
|
|
753
|
+
{
|
|
754
|
+
"userId": "123",
|
|
755
|
+
"roles": [
|
|
756
|
+
{ "id": "1", "identifier": "admin", "priority": 0 }
|
|
757
|
+
],
|
|
758
|
+
"clientId": "optional-client-id",
|
|
759
|
+
"provider": "optional-provider",
|
|
760
|
+
"email": "user@example.com"
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Example:**
|
|
765
|
+
```typescript
|
|
766
|
+
const response = await fetch('/auth/who-am-i', {
|
|
767
|
+
method: 'GET',
|
|
768
|
+
headers: {
|
|
769
|
+
'Authorization': `Bearer ${token}`,
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const user = await response.json();
|
|
774
|
+
console.log('Current user:', user);
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
## See Also
|
|
778
|
+
|
|
779
|
+
- [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
|
|
780
|
+
- [API Reference](./api) -- Architecture, service internals, and strategy registry
|
|
781
|
+
- [Error Reference](./errors) -- Error messages and troubleshooting
|