@venizia/ignis-docs 0.2.0 → 0.2.1-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/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
|
@@ -1,89 +1,73 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Authentication Usage
|
|
3
|
+
description: Securing routes, implementing IAuthService, JWKS microservice patterns, and the built-in auth controller endpoints
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# Authentication Usage
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Task-oriented examples for the Authentication component. See the [Overview](./) for initial setup and the [API Reference](./api) for every option and class.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## Securing routes
|
|
12
|
+
|
|
13
|
+
**Require one strategy.** Add `authenticate` to the route config.
|
|
8
14
|
|
|
9
15
|
```typescript
|
|
10
|
-
// Single strategy
|
|
11
16
|
const SECURE_ROUTE_CONFIG = {
|
|
12
17
|
path: '/secure-data',
|
|
13
18
|
method: HTTP.Methods.GET,
|
|
14
19
|
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
15
|
-
responses: jsonResponse({
|
|
16
|
-
description: 'Protected data',
|
|
17
|
-
schema: z.object({ message: z.string() }),
|
|
18
|
-
}),
|
|
20
|
+
responses: jsonResponse({ description: 'Protected data', schema: z.object({ message: z.string() }) }),
|
|
19
21
|
} as const;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Accept multiple strategies with fallback.** `mode: 'any'` (default) tries each in order; the first success wins.
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
```typescript
|
|
22
27
|
const FALLBACK_AUTH_CONFIG = {
|
|
23
28
|
path: '/api/data',
|
|
24
29
|
method: HTTP.Methods.GET,
|
|
25
|
-
authenticate: {
|
|
26
|
-
|
|
27
|
-
mode: AuthenticationModes.ANY,
|
|
28
|
-
},
|
|
29
|
-
responses: jsonResponse({
|
|
30
|
-
description: 'Data accessible via JWT or Basic auth',
|
|
31
|
-
schema: z.object({ data: z.any() }),
|
|
32
|
-
}),
|
|
30
|
+
authenticate: { strategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC], mode: AuthenticationModes.ANY },
|
|
31
|
+
responses: jsonResponse({ description: 'Data via JWT or Basic', schema: z.object({ data: z.any() }) }),
|
|
33
32
|
} as const;
|
|
33
|
+
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
**Make a route public.** `skip: true` bypasses authentication entirely.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
36
38
|
const PUBLIC_ROUTE_CONFIG = {
|
|
37
39
|
path: '/public',
|
|
38
40
|
method: HTTP.Methods.GET,
|
|
39
41
|
authenticate: { skip: true },
|
|
40
|
-
responses: jsonResponse({
|
|
41
|
-
description: 'Public endpoint',
|
|
42
|
-
schema: z.object({ message: z.string() }),
|
|
43
|
-
}),
|
|
42
|
+
responses: jsonResponse({ description: 'Public endpoint', schema: z.object({ message: z.string() }) }),
|
|
44
43
|
} as const;
|
|
45
44
|
```
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
The `authenticate()` function creates an `AuthenticationProvider` instance and uses its middleware factory. It returns a Hono `MiddlewareHandler` suitable for direct middleware usage:
|
|
46
|
+
**Use `authenticate()` as raw Hono middleware.** Outside of route configs - e.g. for a plain Hono sub-app.
|
|
50
47
|
|
|
51
48
|
```typescript
|
|
52
49
|
import { authenticate, Authentication, AuthenticationModes } from '@venizia/ignis';
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
const authMiddleware = authenticate({
|
|
56
|
-
strategies: [Authentication.STRATEGY_JWT],
|
|
57
|
-
mode: AuthenticationModes.ANY,
|
|
58
|
-
});
|
|
51
|
+
const authMiddleware = authenticate({ strategies: [Authentication.STRATEGY_JWT], mode: AuthenticationModes.ANY });
|
|
59
52
|
|
|
60
|
-
|
|
61
|
-
app.get('/protected', authMiddleware, (c) => {
|
|
53
|
+
app.get('/protected', authMiddleware, c => {
|
|
62
54
|
const user = c.get(Authentication.CURRENT_USER);
|
|
63
55
|
return c.json({ userId: user.userId });
|
|
64
56
|
});
|
|
65
57
|
```
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
After authentication, the user payload is available on the Hono `Context`:
|
|
59
|
+
**Read the authenticated user in a handler.**
|
|
70
60
|
|
|
71
61
|
```typescript
|
|
72
|
-
import { Context } from 'hono';
|
|
73
62
|
import { Authentication, IJWTTokenPayload } from '@venizia/ignis';
|
|
74
63
|
|
|
75
|
-
// Inside a route handler
|
|
76
64
|
const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload | undefined;
|
|
77
|
-
|
|
78
65
|
if (user) {
|
|
79
|
-
console.log('
|
|
80
|
-
console.log('User roles:', user.roles);
|
|
66
|
+
console.log('User ID:', user.userId, 'Roles:', user.roles);
|
|
81
67
|
}
|
|
82
68
|
```
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Use `Authentication.SKIP_AUTHENTICATION` to dynamically skip auth in middleware:
|
|
70
|
+
**Skip authentication dynamically from a preceding middleware.** Useful for internal API keys or webhooks.
|
|
87
71
|
|
|
88
72
|
```typescript
|
|
89
73
|
import { Authentication } from '@venizia/ignis';
|
|
@@ -97,35 +81,23 @@ const conditionalAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
97
81
|
});
|
|
98
82
|
```
|
|
99
83
|
|
|
100
|
-
## Implementing
|
|
84
|
+
## Implementing IAuthService
|
|
101
85
|
|
|
102
|
-
The
|
|
86
|
+
The built-in auth controller (`useAuthController: true`) delegates every route to a service you provide, implementing `IAuthService`.
|
|
103
87
|
|
|
104
|
-
|
|
88
|
+
**JWS-backed service.**
|
|
105
89
|
|
|
106
90
|
```typescript
|
|
107
91
|
import {
|
|
108
|
-
BaseService,
|
|
109
|
-
|
|
110
|
-
IAuthService,
|
|
111
|
-
IJWTTokenPayload,
|
|
112
|
-
JWSTokenService,
|
|
113
|
-
BindingKeys,
|
|
114
|
-
BindingNamespaces,
|
|
115
|
-
TSignInRequest,
|
|
116
|
-
TContext,
|
|
92
|
+
BaseService, inject, IAuthService, IJWTTokenPayload, JWSTokenService,
|
|
93
|
+
BindingKeys, BindingNamespaces, TSignInRequest, TContext,
|
|
117
94
|
} from '@venizia/ignis';
|
|
118
95
|
import { getError } from '@venizia/ignis-helpers';
|
|
119
96
|
import { Env } from 'hono';
|
|
120
97
|
|
|
121
98
|
export class AuthenticationService extends BaseService implements IAuthService {
|
|
122
99
|
constructor(
|
|
123
|
-
@inject({
|
|
124
|
-
key: BindingKeys.build({
|
|
125
|
-
namespace: BindingNamespaces.SERVICE,
|
|
126
|
-
key: JWSTokenService.name,
|
|
127
|
-
}),
|
|
128
|
-
})
|
|
100
|
+
@inject({ key: BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: JWSTokenService.name }) })
|
|
129
101
|
private _tokenService: JWSTokenService,
|
|
130
102
|
) {
|
|
131
103
|
super({ scope: AuthenticationService.name });
|
|
@@ -139,611 +111,195 @@ export class AuthenticationService extends BaseService implements IAuthService {
|
|
|
139
111
|
throw getError({ message: 'Invalid credentials' });
|
|
140
112
|
}
|
|
141
113
|
|
|
142
|
-
const payload: IJWTTokenPayload = {
|
|
143
|
-
userId: user.id,
|
|
144
|
-
roles: user.roles,
|
|
145
|
-
};
|
|
146
|
-
|
|
114
|
+
const payload: IJWTTokenPayload = { userId: user.id, roles: user.roles };
|
|
147
115
|
const token = await this._tokenService.generate({ payload });
|
|
148
116
|
return { token };
|
|
149
117
|
}
|
|
150
118
|
|
|
151
|
-
async signUp(context: TContext<Env>, opts: any): Promise<any> {
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async changePassword(context: TContext<Env>, opts: any): Promise<any> {
|
|
156
|
-
// Implement your change password logic
|
|
157
|
-
}
|
|
119
|
+
async signUp(context: TContext<Env>, opts: any): Promise<any> { /* your logic */ }
|
|
120
|
+
async changePassword(context: TContext<Env>, opts: any): Promise<any> { /* your logic */ }
|
|
158
121
|
}
|
|
159
122
|
```
|
|
160
123
|
|
|
161
|
-
|
|
124
|
+
**JWKS-backed service.** Same shape, inject `JWKSIssuerTokenService` instead.
|
|
162
125
|
|
|
163
126
|
```typescript
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
JWKSIssuerTokenService,
|
|
170
|
-
BindingKeys,
|
|
171
|
-
BindingNamespaces,
|
|
172
|
-
TSignInRequest,
|
|
173
|
-
TContext,
|
|
174
|
-
} from '@venizia/ignis';
|
|
175
|
-
import { getError } from '@venizia/ignis-helpers';
|
|
176
|
-
import { Env } from 'hono';
|
|
127
|
+
constructor(
|
|
128
|
+
@inject({ key: BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: JWKSIssuerTokenService.name }) })
|
|
129
|
+
private _tokenService: JWKSIssuerTokenService,
|
|
130
|
+
) { super({ scope: AuthenticationService.name }); }
|
|
131
|
+
```
|
|
177
132
|
|
|
178
|
-
|
|
179
|
-
constructor(
|
|
180
|
-
@inject({
|
|
181
|
-
key: BindingKeys.build({
|
|
182
|
-
namespace: BindingNamespaces.SERVICE,
|
|
183
|
-
key: JWKSIssuerTokenService.name,
|
|
184
|
-
}),
|
|
185
|
-
})
|
|
186
|
-
private _tokenService: JWKSIssuerTokenService,
|
|
187
|
-
) {
|
|
188
|
-
super({ scope: AuthenticationService.name });
|
|
189
|
-
}
|
|
133
|
+
**Implement `refreshToken` (optional).** Re-issues a token from the currently valid one - there is no separate refresh token.
|
|
190
134
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
135
|
+
```typescript
|
|
136
|
+
async refreshToken(context: TContext<Env>): Promise<{ token: string }> {
|
|
137
|
+
const currentUser = context.get(Authentication.CURRENT_USER);
|
|
138
|
+
const token = await this._tokenService.generate({ payload: currentUser });
|
|
139
|
+
return { token };
|
|
140
|
+
}
|
|
141
|
+
```
|
|
194
142
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
roles: user.roles,
|
|
198
|
-
};
|
|
143
|
+
> [!NOTE]
|
|
144
|
+
> IGNIS does not enforce rotation or revocation policy. If you need to invalidate old tokens after refresh, implement that inside your `refreshToken`.
|
|
199
145
|
|
|
200
|
-
|
|
201
|
-
return { token };
|
|
202
|
-
}
|
|
146
|
+
**Implement `getUserInformation` (optional).** Backs both `GET /me` and `GET /who-am-i?withUserInformation=true`.
|
|
203
147
|
|
|
204
|
-
|
|
148
|
+
```typescript
|
|
149
|
+
async getUserInformation(context: TContext<Env>, _opts: AnyObject): Promise<AnyObject> {
|
|
150
|
+
const currentUser = context.get(Authentication.CURRENT_USER);
|
|
151
|
+
return this.userRepository.findById({ id: currentUser.userId });
|
|
205
152
|
}
|
|
206
153
|
```
|
|
207
154
|
|
|
208
|
-
## JWKS
|
|
209
|
-
|
|
210
|
-
### Issuer + Verifier Architecture
|
|
155
|
+
## JWKS microservice patterns
|
|
211
156
|
|
|
212
|
-
|
|
157
|
+
**Issuer + verifier split.** One service signs, others only verify - no shared secret to distribute.
|
|
213
158
|
|
|
214
159
|
```mermaid
|
|
215
160
|
flowchart LR
|
|
216
161
|
CLIENT["Client App"]
|
|
217
|
-
|
|
218
162
|
subgraph AUTH["Auth Service (JWKS Issuer)"]
|
|
219
163
|
SIGNIN["POST /auth/sign-in"]
|
|
220
164
|
CERTS["GET /certs"]
|
|
221
165
|
end
|
|
222
|
-
|
|
223
166
|
subgraph API["API Service (JWKS Verifier)"]
|
|
224
167
|
DATA["GET /api/data"]
|
|
225
168
|
end
|
|
226
|
-
|
|
227
169
|
CLIENT -->|"1. Sign in"| SIGNIN
|
|
228
170
|
SIGNIN -->|"2. JWT token"| CLIENT
|
|
229
171
|
CLIENT -->|"3. Request + Bearer token"| DATA
|
|
230
172
|
DATA -->|"4. Fetch JWKS"| CERTS
|
|
231
173
|
CERTS -->|"5. Public keys"| DATA
|
|
232
174
|
DATA -->|"6. Verified response"| CLIENT
|
|
233
|
-
|
|
234
|
-
style AUTH fill:#e8f4fd,stroke:#0d6efd
|
|
235
|
-
style API fill:#d4edda,stroke:#28a745
|
|
236
175
|
```
|
|
237
176
|
|
|
238
|
-
**Auth Service (Issuer):**
|
|
239
177
|
```typescript
|
|
178
|
+
// Auth service (issuer)
|
|
240
179
|
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
241
180
|
standard: JOSEStandards.JWKS,
|
|
242
181
|
options: {
|
|
243
|
-
mode: JWKSModes.ISSUER,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
driver: JWKSKeyDrivers.FILE,
|
|
247
|
-
format: JWKSKeyFormats.PEM,
|
|
248
|
-
private: './keys/private.pem',
|
|
249
|
-
public: './keys/public.pem',
|
|
250
|
-
},
|
|
251
|
-
kid: 'auth-key-1',
|
|
252
|
-
getTokenExpiresFn: () => 86400,
|
|
182
|
+
mode: JWKSModes.ISSUER, algorithm: 'ES256',
|
|
183
|
+
keys: { driver: JWKSKeyDrivers.FILE, format: JWKSKeyFormats.PEM, private: './keys/private.pem', public: './keys/public.pem' },
|
|
184
|
+
kid: 'auth-key-1', getTokenExpiresFn: () => 86400,
|
|
253
185
|
},
|
|
254
186
|
});
|
|
255
|
-
```
|
|
256
187
|
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
188
|
+
// API service (verifier)
|
|
259
189
|
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
260
190
|
standard: JOSEStandards.JWKS,
|
|
261
|
-
options: {
|
|
262
|
-
mode: JWKSModes.VERIFIER,
|
|
263
|
-
jwksUrl: 'https://auth-service.internal/certs',
|
|
264
|
-
cacheTtlMs: 43_200_000, // Cache for 12 hours
|
|
265
|
-
cooldownMs: 30_000, // Min 30s between refreshes
|
|
266
|
-
},
|
|
191
|
+
options: { mode: JWKSModes.VERIFIER, jwksUrl: 'https://auth-service.internal/certs', cacheTtlMs: 43_200_000, cooldownMs: 30_000 },
|
|
267
192
|
});
|
|
268
193
|
```
|
|
269
194
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
When using AES payload encryption across services, **both issuer and verifier must share the same `applicationSecret`**:
|
|
273
|
-
|
|
274
|
-
**Issuer:**
|
|
275
|
-
```typescript
|
|
276
|
-
{
|
|
277
|
-
mode: JWKSModes.ISSUER,
|
|
278
|
-
algorithm: 'ES256',
|
|
279
|
-
keys: { /* ... */ },
|
|
280
|
-
kid: 'auth-key-1',
|
|
281
|
-
getTokenExpiresFn: () => 86400,
|
|
282
|
-
applicationSecret: process.env.APP_ENV_APPLICATION_SECRET,
|
|
283
|
-
}
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
**Verifier:**
|
|
287
|
-
```typescript
|
|
288
|
-
{
|
|
289
|
-
mode: JWKSModes.VERIFIER,
|
|
290
|
-
jwksUrl: 'https://auth-service.internal/certs',
|
|
291
|
-
applicationSecret: process.env.APP_ENV_APPLICATION_SECRET, // Must match issuer
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
### JWKS Key Generation
|
|
296
|
-
|
|
297
|
-
Generate ES256 keys for JWKS:
|
|
195
|
+
**Generate ES256 or RS256 keys.**
|
|
298
196
|
|
|
299
197
|
```bash
|
|
300
|
-
#
|
|
198
|
+
# ES256
|
|
301
199
|
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
|
|
302
|
-
|
|
303
|
-
# Generate public key from private key
|
|
304
200
|
openssl ec -in private.pem -pubout -out public.pem
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Generate RS256 keys:
|
|
308
201
|
|
|
309
|
-
|
|
310
|
-
# Generate private key
|
|
202
|
+
# RS256
|
|
311
203
|
openssl genrsa -out private.pem 2048
|
|
312
|
-
|
|
313
|
-
# Generate public key from private key
|
|
314
204
|
openssl rsa -in private.pem -pubout -out public.pem
|
|
315
205
|
```
|
|
316
206
|
|
|
317
207
|
> [!WARNING]
|
|
318
|
-
> Never commit private keys to version control.
|
|
319
|
-
|
|
320
|
-
### Inline Keys (Text Driver)
|
|
208
|
+
> Never commit private keys to version control.
|
|
321
209
|
|
|
322
|
-
|
|
210
|
+
**Use inline keys instead of files.** For serverless or restricted-filesystem environments, switch `driver` to `text`.
|
|
323
211
|
|
|
324
212
|
```typescript
|
|
325
|
-
{
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
format: JWKSKeyFormats.PEM,
|
|
331
|
-
private: process.env.JWKS_PRIVATE_KEY!, // PEM string from env
|
|
332
|
-
public: process.env.JWKS_PUBLIC_KEY!, // PEM string from env
|
|
333
|
-
},
|
|
334
|
-
kid: 'auth-key-1',
|
|
335
|
-
getTokenExpiresFn: () => 86400,
|
|
213
|
+
keys: {
|
|
214
|
+
driver: JWKSKeyDrivers.TEXT,
|
|
215
|
+
format: JWKSKeyFormats.PEM,
|
|
216
|
+
private: process.env.JWKS_PRIVATE_KEY!, // PEM string from env
|
|
217
|
+
public: process.env.JWKS_PUBLIC_KEY!,
|
|
336
218
|
}
|
|
337
219
|
```
|
|
338
220
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
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.
|
|
342
|
-
|
|
343
|
-
### Pattern
|
|
344
|
-
|
|
345
|
-
Each helper function returns an object of Drizzle column builders that you spread into your `pgTable()` call alongside any custom columns:
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
import { pgTable, serial, text } from 'drizzle-orm/pg-core';
|
|
349
|
-
import {
|
|
350
|
-
extraUserColumns,
|
|
351
|
-
extraRoleColumns,
|
|
352
|
-
extraPermissionColumns,
|
|
353
|
-
extraPolicyDefinitionColumns,
|
|
354
|
-
} from '@venizia/ignis';
|
|
355
|
-
import { withSerialId, withTimestamps } from '@venizia/ignis';
|
|
356
|
-
|
|
357
|
-
// User table with auth columns
|
|
358
|
-
export const users = pgTable('users', {
|
|
359
|
-
...withSerialId(),
|
|
360
|
-
...withTimestamps(),
|
|
361
|
-
...extraUserColumns(),
|
|
362
|
-
username: text('username').unique().notNull(),
|
|
363
|
-
passwordHash: text('password_hash').notNull(),
|
|
364
|
-
email: text('email').unique(),
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// Role table with auth columns
|
|
368
|
-
export const roles = pgTable('roles', {
|
|
369
|
-
...withSerialId(),
|
|
370
|
-
...withTimestamps(),
|
|
371
|
-
...extraRoleColumns(),
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
// Permission table
|
|
375
|
-
export const permissions = pgTable('permissions', {
|
|
376
|
-
...withSerialId(),
|
|
377
|
-
...withTimestamps(),
|
|
378
|
-
...extraPermissionColumns(),
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// Policy definition table (Casbin-style policies)
|
|
382
|
-
export const policyDefinitions = pgTable('policy_definitions', {
|
|
383
|
-
...withSerialId(),
|
|
384
|
-
...withTimestamps(),
|
|
385
|
-
...extraPolicyDefinitionColumns(),
|
|
386
|
-
});
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
### extraUserColumns
|
|
390
|
-
|
|
391
|
-
Returns columns for user-related fields with status and type defaults from `UserStatuses` and `UserTypes`.
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
extraUserColumns(opts?: { idType: 'string' | 'number' })
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
| Column | Type | Default | Description |
|
|
398
|
-
|--------|------|---------|-------------|
|
|
399
|
-
| `realm` | `text` | `''` | Multi-tenancy realm identifier |
|
|
400
|
-
| `status` | `text` | `UserStatuses.UNKNOWN` (`'000_UNKNOWN'`) | User status |
|
|
401
|
-
| `type` | `text` | `UserTypes.SYSTEM` (`'SYSTEM'`) | User type |
|
|
402
|
-
| `activatedAt` | `timestamp (tz)` | `null` | Activation timestamp |
|
|
403
|
-
| `lastLoginAt` | `timestamp (tz)` | `null` | Last login timestamp |
|
|
404
|
-
| `parentId` | `text` or `integer` | `null` | Parent user ID (type depends on `idType`) |
|
|
405
|
-
|
|
406
|
-
### extraRoleColumns
|
|
407
|
-
|
|
408
|
-
Returns columns for role definitions. No options parameter.
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
extraRoleColumns()
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
| Column | Type | Default | Description |
|
|
415
|
-
|--------|------|---------|-------------|
|
|
416
|
-
| `identifier` | `text` | -- | Unique role identifier (e.g., `'admin'`, `'user'`) |
|
|
417
|
-
| `name` | `text` | -- | Human-readable role name |
|
|
418
|
-
| `description` | `text` | `null` | Optional role description |
|
|
419
|
-
| `priority` | `integer` | -- | Role priority (lower = higher priority) |
|
|
420
|
-
| `status` | `text` | `RoleStatuses.ACTIVATED` (`'201_ACTIVATED'`) | Role status |
|
|
421
|
-
|
|
422
|
-
### extraPermissionColumns
|
|
423
|
-
|
|
424
|
-
Returns columns for permission definitions. Supports `idType` option for the `parentId` column type.
|
|
425
|
-
|
|
426
|
-
```typescript
|
|
427
|
-
extraPermissionColumns(opts?: { idType: 'string' | 'number' })
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
| Column | Type | Default | Description |
|
|
431
|
-
|--------|------|---------|-------------|
|
|
432
|
-
| `code` | `text` (unique) | -- | Unique permission code |
|
|
433
|
-
| `name` | `text` | -- | Permission name |
|
|
434
|
-
| `subject` | `text` | -- | Permission subject (e.g., `'User'`, `'Order'`) |
|
|
435
|
-
| `method` | `text` | -- | HTTP method (e.g., `'GET'`, `'POST'`) |
|
|
436
|
-
| `action` | `text` | -- | Permitted action (e.g., `'read'`, `'write'`) |
|
|
437
|
-
| `scope` | `text` | -- | Permission scope |
|
|
438
|
-
| `description` | `text` | `null` | Optional permission description |
|
|
439
|
-
| `parentId` | `text` or `integer` | `null` | Parent permission ID |
|
|
440
|
-
|
|
441
|
-
### extraPolicyDefinitionColumns
|
|
442
|
-
|
|
443
|
-
Returns columns for Casbin-style policy definitions that map subjects (users/roles) to targets (resources/permissions).
|
|
221
|
+
**Share AES payload encryption across issuer and verifier.** Both sides need the identical `applicationSecret` - the verifier decrypts what the issuer encrypted.
|
|
444
222
|
|
|
445
223
|
```typescript
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
| Column | DB Column | Type | Nullable | Default | Description |
|
|
450
|
-
|--------|-----------|------|----------|---------|-------------|
|
|
451
|
-
| `variant` | `variant` | `text` | No | -- | Policy variant (e.g., `'p'` for policy, `'g'` for grouping) |
|
|
452
|
-
| `subjectType` | `subject_type` | `text` | No | -- | Type of subject (e.g., `'user'`, `'role'`) |
|
|
453
|
-
| `targetType` | `target_type` | `text` | No | -- | Type of target (e.g., `'permission'`, `'role'`) |
|
|
454
|
-
| `action` | `action` | `text` | Yes | `null` | Policy action |
|
|
455
|
-
| `effect` | `effect` | `text` | Yes | `null` | Policy effect (e.g., `'allow'`, `'deny'`) |
|
|
456
|
-
| `domain` | `domain` | `text` | Yes | `null` | Policy domain for multi-tenancy |
|
|
457
|
-
| `subjectId` | `subject_id` | `text` or `integer` | No | -- | Subject ID (type depends on `idType`) |
|
|
458
|
-
| `targetId` | `target_id` | `text` or `integer` | No | -- | Target ID (type depends on `idType`) |
|
|
224
|
+
// Issuer
|
|
225
|
+
{ mode: JWKSModes.ISSUER, /* ... */ applicationSecret: process.env.APP_ENV_APPLICATION_SECRET }
|
|
459
226
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
All column helpers that accept `opts.idType` default to `'number'` (producing `integer` columns). Pass `'string'` to use `text` columns instead:
|
|
463
|
-
|
|
464
|
-
```typescript
|
|
465
|
-
// Number IDs (default) -- uses integer columns for FK references
|
|
466
|
-
extraUserColumns()
|
|
467
|
-
extraPermissionColumns()
|
|
468
|
-
|
|
469
|
-
// String IDs (e.g., UUID) -- uses text columns for FK references
|
|
470
|
-
extraUserColumns({ idType: 'string' })
|
|
471
|
-
extraPermissionColumns({ idType: 'string' })
|
|
227
|
+
// Verifier - must match
|
|
228
|
+
{ mode: JWKSModes.VERIFIER, jwksUrl: '...', applicationSecret: process.env.APP_ENV_APPLICATION_SECRET }
|
|
472
229
|
```
|
|
473
230
|
|
|
474
|
-
##
|
|
475
|
-
|
|
476
|
-
The authentication module uses status classes from `@/common/statuses`. These extend `CommonStatuses` and provide lifecycle state management for auth entities.
|
|
231
|
+
## Auth flows
|
|
477
232
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
| Constant | Value | Description |
|
|
483
|
-
|----------|-------|-------------|
|
|
484
|
-
| `UserStatuses.UNKNOWN` | `'000_UNKNOWN'` | Initial/unverified state |
|
|
485
|
-
| `UserStatuses.ACTIVATED` | `'201_ACTIVATED'` | Active user |
|
|
486
|
-
| `UserStatuses.DEACTIVATED` | `'401_DEACTIVATED'` | Deactivated user |
|
|
487
|
-
| `UserStatuses.BLOCKED` | `'403_BLOCKED'` | Blocked user |
|
|
488
|
-
| `UserStatuses.ARCHIVED` | `'405_ARCHIVED'` | Archived user |
|
|
489
|
-
|
|
490
|
-
### UserTypes
|
|
491
|
-
|
|
492
|
-
| Constant | Value | Description |
|
|
493
|
-
|----------|-------|-------------|
|
|
494
|
-
| `UserTypes.SYSTEM` | `'SYSTEM'` | System-created user (default) |
|
|
495
|
-
| `UserTypes.LINKED` | `'LINKED'` | Linked/external user |
|
|
496
|
-
|
|
497
|
-
### RoleStatuses
|
|
498
|
-
|
|
499
|
-
Inherits all statuses from `CommonStatuses` (same values as `UserStatuses`):
|
|
500
|
-
|
|
501
|
-
| Constant | Value | Description |
|
|
502
|
-
|----------|-------|-------------|
|
|
503
|
-
| `RoleStatuses.UNKNOWN` | `'000_UNKNOWN'` | Initial state |
|
|
504
|
-
| `RoleStatuses.ACTIVATED` | `'201_ACTIVATED'` | Active role (default for `extraRoleColumns`) |
|
|
505
|
-
| `RoleStatuses.DEACTIVATED` | `'401_DEACTIVATED'` | Deactivated role |
|
|
506
|
-
| `RoleStatuses.BLOCKED` | `'403_BLOCKED'` | Blocked role |
|
|
507
|
-
| `RoleStatuses.ARCHIVED` | `'405_ARCHIVED'` | Archived role |
|
|
508
|
-
|
|
509
|
-
## Auth Flows
|
|
510
|
-
|
|
511
|
-
### JWS Authentication Flow
|
|
233
|
+
- **JWS:** extract Bearer token -> `jose.jwtVerify()` with the shared secret -> decrypt payload (if AES configured) -> set `CURRENT_USER`.
|
|
234
|
+
- **JWKS Issuer:** extract Bearer token -> `ensureInitialized()` (lazy-loads keys once) -> `jwtVerify()` with the public key -> decrypt payload -> set `CURRENT_USER`.
|
|
235
|
+
- **JWKS Verifier:** extract Bearer token -> `ensureInitialized()` (creates the remote JWKS verifier once) -> `jwtVerify()` with the remote JWKS -> decrypt payload -> set `CURRENT_USER`.
|
|
236
|
+
- **Basic:** decode `Authorization: Basic <base64>` -> call your `verifyCredentials` callback -> on `null`, throw `401`; on a user, set `CURRENT_USER`.
|
|
512
237
|
|
|
513
238
|
```mermaid
|
|
514
239
|
sequenceDiagram
|
|
515
240
|
participant C as Client
|
|
516
241
|
participant MW as Auth Middleware
|
|
517
|
-
participant S as
|
|
518
|
-
participant SVC as
|
|
519
|
-
participant JOSE as jose library
|
|
242
|
+
participant S as Strategy
|
|
243
|
+
participant SVC as TokenService
|
|
520
244
|
|
|
521
|
-
C->>MW: Request + Authorization
|
|
245
|
+
C->>MW: Request + Authorization header
|
|
522
246
|
MW->>S: authenticate(context)
|
|
523
247
|
S->>SVC: extractCredentials(context)
|
|
524
|
-
SVC-->>S: { type
|
|
525
|
-
S->>SVC: verify(
|
|
526
|
-
SVC
|
|
527
|
-
JOSE-->>SVC: JWTVerifyResult
|
|
528
|
-
SVC->>SVC: decryptPayload() (if AES configured)
|
|
529
|
-
SVC-->>S: IJWTTokenPayload
|
|
248
|
+
SVC-->>S: { type, token } | { username, password }
|
|
249
|
+
S->>SVC: verify(...)
|
|
250
|
+
SVC-->>S: IAuthUser
|
|
530
251
|
S-->>MW: IAuthUser
|
|
531
252
|
MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
|
|
532
253
|
MW->>C: Continue to handler
|
|
533
254
|
```
|
|
534
255
|
|
|
535
|
-
|
|
536
|
-
2. **JWSAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
537
|
-
3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
|
|
538
|
-
4. **JWSTokenService.doVerify()** verifies the JWT signature using `jose.jwtVerify()` with the shared `jwtSecret`
|
|
539
|
-
5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
|
|
540
|
-
6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
|
|
256
|
+
## Multi-strategy authentication
|
|
541
257
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
participant C as Client
|
|
547
|
-
participant MW as Auth Middleware
|
|
548
|
-
participant S as JWKSIssuerStrategy
|
|
549
|
-
participant SVC as JWKSIssuerTokenService
|
|
550
|
-
participant INIT as Lazy Init
|
|
551
|
-
participant JOSE as jose library
|
|
552
|
-
|
|
553
|
-
C->>MW: Request + Authorization: Bearer <token>
|
|
554
|
-
MW->>S: authenticate(context)
|
|
555
|
-
S->>SVC: extractCredentials(context)
|
|
556
|
-
SVC-->>S: { type: "Bearer", token }
|
|
557
|
-
S->>SVC: verify({ type, token })
|
|
558
|
-
SVC->>INIT: ensureInitialized()
|
|
559
|
-
Note over INIT: Load keys from file/text<br/>Parse PEM/JWK<br/>Cache JWKS
|
|
560
|
-
INIT-->>SVC: initialized
|
|
561
|
-
SVC->>JOSE: jwtVerify(token, publicKey)
|
|
562
|
-
JOSE-->>SVC: JWTVerifyResult
|
|
563
|
-
SVC->>SVC: decryptPayload() (if AES configured)
|
|
564
|
-
SVC-->>S: IJWTTokenPayload
|
|
565
|
-
S-->>MW: IAuthUser
|
|
566
|
-
MW->>C: Continue to handler
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
1. **Client sends request** with <code v-pre>Authorization: Bearer <token></code> header
|
|
570
|
-
2. **JWKSIssuerAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
571
|
-
3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
|
|
572
|
-
4. **JWKSIssuerTokenService.doVerify()** calls `ensureInitialized()` (lazy-loads keys on first call), then verifies the JWT using the public key
|
|
573
|
-
5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
|
|
574
|
-
6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
|
|
575
|
-
|
|
576
|
-
### JWKS Verifier Authentication Flow
|
|
577
|
-
|
|
578
|
-
```mermaid
|
|
579
|
-
sequenceDiagram
|
|
580
|
-
participant C as Client
|
|
581
|
-
participant MW as Auth Middleware
|
|
582
|
-
participant S as JWKSVerifierStrategy
|
|
583
|
-
participant SVC as JWKSVerifierTokenService
|
|
584
|
-
participant INIT as Lazy Init
|
|
585
|
-
participant REMOTE as Remote JWKS URL
|
|
586
|
-
|
|
587
|
-
C->>MW: Request + Authorization: Bearer <token>
|
|
588
|
-
MW->>S: authenticate(context)
|
|
589
|
-
S->>SVC: extractCredentials(context)
|
|
590
|
-
SVC-->>S: { type: "Bearer", token }
|
|
591
|
-
S->>SVC: verify({ type, token })
|
|
592
|
-
SVC->>INIT: ensureInitialized()
|
|
593
|
-
INIT->>REMOTE: createRemoteJWKSet(jwksUrl)
|
|
594
|
-
REMOTE-->>INIT: JWKS verifier function
|
|
595
|
-
INIT-->>SVC: initialized
|
|
596
|
-
SVC->>SVC: jwtVerify(token, jwksVerifier)
|
|
597
|
-
SVC->>SVC: decryptPayload() (if AES configured)
|
|
598
|
-
SVC-->>S: IJWTTokenPayload
|
|
599
|
-
S-->>MW: IAuthUser
|
|
600
|
-
MW->>C: Continue to handler
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
1. **Client sends request** with <code v-pre>Authorization: Bearer <token></code> header
|
|
604
|
-
2. **JWKSVerifierAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
605
|
-
3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
|
|
606
|
-
4. **JWKSVerifierTokenService.doVerify()** calls `ensureInitialized()` (creates remote JWKS verifier on first call), then verifies the JWT using the remote JWKS
|
|
607
|
-
5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
|
|
608
|
-
6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
|
|
609
|
-
|
|
610
|
-
### Basic Authentication Flow
|
|
611
|
-
|
|
612
|
-
```mermaid
|
|
613
|
-
sequenceDiagram
|
|
614
|
-
participant C as Client
|
|
615
|
-
participant MW as Auth Middleware
|
|
616
|
-
participant S as BasicAuthStrategy
|
|
617
|
-
participant SVC as BasicTokenService
|
|
618
|
-
participant CB as verifyCredentials callback
|
|
619
|
-
|
|
620
|
-
C->>MW: Request + Authorization: Basic <base64>
|
|
621
|
-
MW->>S: authenticate(context)
|
|
622
|
-
S->>SVC: extractCredentials(context)
|
|
623
|
-
SVC->>SVC: Base64 decode
|
|
624
|
-
SVC-->>S: { username, password }
|
|
625
|
-
S->>SVC: verify({ credentials, context })
|
|
626
|
-
SVC->>CB: verifyCredentials({ credentials, context })
|
|
627
|
-
CB-->>SVC: IAuthUser | null
|
|
628
|
-
alt valid user
|
|
629
|
-
SVC-->>S: IAuthUser
|
|
630
|
-
S-->>MW: IAuthUser
|
|
631
|
-
MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
|
|
632
|
-
MW->>C: Continue to handler
|
|
633
|
-
else null (invalid)
|
|
634
|
-
SVC-->>S: throw 401
|
|
635
|
-
S-->>MW: throw 401
|
|
636
|
-
MW->>C: 401 Unauthorized
|
|
637
|
-
end
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
1. **Client sends request** with <code v-pre>Authorization: Basic <base64(username:password)></code> header
|
|
641
|
-
2. **BasicAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
642
|
-
3. **BasicTokenService.extractCredentials()** decodes the Base64 credentials
|
|
643
|
-
4. **BasicTokenService.verify()** calls the user-provided `verifyCredentials` callback with `{ credentials, context }`
|
|
644
|
-
5. **User payload is set** on `context.get(Authentication.CURRENT_USER)` if verification succeeds
|
|
645
|
-
|
|
646
|
-
> [!IMPORTANT]
|
|
647
|
-
> The `verifyCredentials` callback must perform all necessary validation (password hashing comparison, user lookup, etc.) and return an `IAuthUser` object or `null`.
|
|
648
|
-
|
|
649
|
-
## Multi-Strategy Authentication
|
|
650
|
-
|
|
651
|
-
When multiple strategies are configured on a route via `authenticate: { strategies: ['jwt', 'basic'] }`:
|
|
258
|
+
| Mode | Behavior | Use case |
|
|
259
|
+
|------|----------|----------|
|
|
260
|
+
| `'any'` (default) | Strategies tried in order; first success wins; failures discarded (debug log); `401` with the tried-strategy list only if all fail | Fallback auth (JWT primary, Basic for legacy clients) |
|
|
261
|
+
| `'all'` | Every strategy must pass; first failure rejects immediately; the **first** strategy's user payload is the identity source | Multi-factor authentication |
|
|
652
262
|
|
|
653
263
|
```mermaid
|
|
654
264
|
flowchart TD
|
|
655
265
|
REQ["Request arrives"] --> MODE{"mode?"}
|
|
656
|
-
|
|
657
|
-
MODE -->|"any (default)"| ANY["Try strategies in order"]
|
|
658
|
-
ANY --> S1{"Strategy 1"}
|
|
266
|
+
MODE -->|"any"| S1{"Strategy 1"}
|
|
659
267
|
S1 -->|"Success"| WIN["Set user, continue"]
|
|
660
268
|
S1 -->|"Fail"| S2{"Strategy 2"}
|
|
661
269
|
S2 -->|"Success"| WIN
|
|
662
|
-
S2 -->|"Fail"| FAIL_ANY["401: Tried strategies
|
|
663
|
-
|
|
664
|
-
MODE -->|"all"| ALL["Run all strategies"]
|
|
665
|
-
ALL --> A1{"Strategy 1"}
|
|
270
|
+
S2 -->|"Fail"| FAIL_ANY["401: Tried strategies"]
|
|
271
|
+
MODE -->|"all"| A1{"Strategy 1"}
|
|
666
272
|
A1 -->|"Fail"| FAIL_ALL["Exception propagates"]
|
|
667
273
|
A1 -->|"Pass"| A2{"Strategy 2"}
|
|
668
274
|
A2 -->|"Fail"| FAIL_ALL
|
|
669
275
|
A2 -->|"Pass"| CHECK{"userId?"}
|
|
670
276
|
CHECK -->|"Yes"| WIN2["Set user, continue"]
|
|
671
277
|
CHECK -->|"No"| FAIL_ID["401: Failed to identify user"]
|
|
672
|
-
|
|
673
|
-
style WIN fill:#d4edda,stroke:#28a745
|
|
674
|
-
style WIN2 fill:#d4edda,stroke:#28a745
|
|
675
|
-
style FAIL_ANY fill:#f8d7da,stroke:#dc3545
|
|
676
|
-
style FAIL_ALL fill:#f8d7da,stroke:#dc3545
|
|
677
|
-
style FAIL_ID fill:#f8d7da,stroke:#dc3545
|
|
678
278
|
```
|
|
679
279
|
|
|
680
|
-
|
|
681
|
-
- Strategies are tried in the order specified
|
|
682
|
-
- The first successful strategy wins
|
|
683
|
-
- Errors from failing strategies are **discarded** (logged at debug level)
|
|
684
|
-
- If all strategies fail, a `401 Unauthorized` error is thrown listing all tried strategies
|
|
685
|
-
- **Use case:** Fallback authentication (try JWT, fallback to Basic)
|
|
686
|
-
|
|
687
|
-
**`all` mode:**
|
|
688
|
-
- Every strategy must pass successfully
|
|
689
|
-
- If any strategy fails, the request is immediately rejected (exception propagates)
|
|
690
|
-
- The **first** strategy's user payload is used as the identity source
|
|
691
|
-
- **Use case:** Multi-factor authentication (both JWT and Basic required)
|
|
692
|
-
|
|
693
|
-
> [!TIP]
|
|
694
|
-
> 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.
|
|
695
|
-
|
|
696
|
-
## Token Encryption (Optional AES)
|
|
280
|
+
## Token encryption (optional AES)
|
|
697
281
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
P --> CHECK1{"applicationSecret?"}
|
|
704
|
-
CHECK1 -->|"Yes"| ENC["encryptPayload()"]
|
|
705
|
-
ENC --> E1["Keep: iss, sub, aud, exp, iat"]
|
|
706
|
-
ENC --> E2["Encrypt keys + values"]
|
|
707
|
-
CHECK1 -->|"No"| PLAIN1["Use payload as-is"]
|
|
708
|
-
end
|
|
282
|
+
- **Off by default.** AES payload encryption only activates when `applicationSecret` is set on the JWS/JWKS options - otherwise payloads are standard plaintext JWT.
|
|
283
|
+
- **Standard fields untouched.** `iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat` are never encrypted, on either side.
|
|
284
|
+
- **Everything else, key and value.** Every other payload field has both its key and its value AES-encrypted; `null`/`undefined` values are skipped.
|
|
285
|
+
- **Serialization is `JSON.stringify` unless you supply a codec.** `AuthenticationFieldCodecs.ROLES_CODEC` is a ready-made codec that serializes `roles` as pipe-separated `id|identifier|priority` strings - it is opt-in, not automatic. Pass it via `fieldCodecs: [AuthenticationFieldCodecs.ROLES_CODEC]` if you want that format instead of the default JSON array.
|
|
286
|
+
- **Secret must stay constant.** Changing `applicationSecret` invalidates every existing token. Issuer and verifier must share the identical secret (and identical `fieldCodecs`, if used).
|
|
709
287
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
style GENERATE fill:#e8f4fd,stroke:#0d6efd
|
|
721
|
-
style VERIFY fill:#d4edda,stroke:#28a745
|
|
288
|
+
```typescript
|
|
289
|
+
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
290
|
+
standard: JOSEStandards.JWS,
|
|
291
|
+
options: {
|
|
292
|
+
jwtSecret: process.env.APP_ENV_JWT_SECRET!,
|
|
293
|
+
getTokenExpiresFn: () => 86400,
|
|
294
|
+
applicationSecret: process.env.APP_ENV_APPLICATION_SECRET, // enables AES encryption
|
|
295
|
+
fieldCodecs: [AuthenticationFieldCodecs.ROLES_CODEC], // optional, opt-in
|
|
296
|
+
},
|
|
297
|
+
});
|
|
722
298
|
```
|
|
723
299
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
> [!NOTE]
|
|
727
|
-
> AES payload encryption is **optional** for all JOSE standards (JWS and JWKS). When `applicationSecret` is not provided, payloads are stored in standard plaintext JWT format.
|
|
728
|
-
|
|
729
|
-
**Encryption process (when `applicationSecret` is provided):**
|
|
730
|
-
1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are preserved as-is
|
|
731
|
-
2. All other fields have both their **keys** and **values** AES-encrypted
|
|
732
|
-
3. The `roles` field is serialized as `id|identifier|priority` pipe-separated strings before encryption
|
|
733
|
-
4. `null` and `undefined` values are skipped during encryption
|
|
300
|
+
## Hono context extension
|
|
734
301
|
|
|
735
|
-
|
|
736
|
-
1. If AES is not configured (`this.aes` is null), the payload is returned as-is
|
|
737
|
-
2. Standard JWT fields are extracted directly
|
|
738
|
-
3. Encrypted fields have their keys decrypted first, then their values
|
|
739
|
-
4. 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()`)
|
|
740
|
-
|
|
741
|
-
> [!WARNING]
|
|
742
|
-
> 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. In JWKS microservice setups, the issuer and all verifiers must share the same `applicationSecret`.
|
|
743
|
-
|
|
744
|
-
## Hono Context Extension
|
|
745
|
-
|
|
746
|
-
The Authentication module extends Hono's `ContextVariableMap` to provide type-safe access to auth data. Note: `ContextVariableMap` does **not** take a generic parameter - it is a plain interface augmentation:
|
|
302
|
+
The module augments Hono's `ContextVariableMap` (a plain interface, not generic) so `c.get()` is type-safe:
|
|
747
303
|
|
|
748
304
|
```typescript
|
|
749
305
|
declare module 'hono' {
|
|
@@ -754,435 +310,130 @@ declare module 'hono' {
|
|
|
754
310
|
}
|
|
755
311
|
```
|
|
756
312
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
**Context variable keys (from `Authentication` constants):**
|
|
765
|
-
|
|
766
|
-
| Key | Constant | Type | Description |
|
|
767
|
-
|-----|----------|------|-------------|
|
|
768
|
-
| `'auth.current.user'` | `Authentication.CURRENT_USER` | `IAuthUser` | The authenticated user payload |
|
|
769
|
-
| `'audit.user.id'` | `Authentication.AUDIT_USER_ID` | `IdType` | The authenticated user's ID (extracted from `userId`) |
|
|
770
|
-
| `'authentication.skip'` | `Authentication.SKIP_AUTHENTICATION` | `boolean` | Set to `true` to bypass authentication on a request |
|
|
771
|
-
|
|
772
|
-
## Request Schemas
|
|
773
|
-
|
|
774
|
-
### SignInRequestSchema
|
|
775
|
-
|
|
776
|
-
The built-in schema uses a nested `identifier` + `credential` structure:
|
|
777
|
-
|
|
778
|
-
```typescript
|
|
779
|
-
const SignInRequestSchema = z.object({
|
|
780
|
-
identifier: z.object({
|
|
781
|
-
scheme: requiredString({ min: 4 }), // e.g., 'username', 'email'
|
|
782
|
-
value: requiredString({ min: 8 }), // the actual identifier value
|
|
783
|
-
}),
|
|
784
|
-
credential: z.object({
|
|
785
|
-
scheme: requiredString(), // e.g., 'basic', 'password'
|
|
786
|
-
value: requiredString({ min: 8 }), // the actual credential value
|
|
787
|
-
}),
|
|
788
|
-
clientId: z.string().optional(), // optional auth provider
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
type TSignInRequest = z.infer<typeof SignInRequestSchema>;
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
### SignUpRequestSchema
|
|
795
|
-
|
|
796
|
-
The built-in schema uses a **flat structure**:
|
|
797
|
-
|
|
798
|
-
```typescript
|
|
799
|
-
const SignUpRequestSchema = z.object({
|
|
800
|
-
username: z.string().nonempty().min(8),
|
|
801
|
-
credential: z.string().nonempty().min(8),
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
type TSignUpRequest = z.infer<typeof SignUpRequestSchema>;
|
|
805
|
-
```
|
|
806
|
-
|
|
807
|
-
### ChangePasswordRequestSchema
|
|
808
|
-
|
|
809
|
-
```typescript
|
|
810
|
-
const ChangePasswordRequestSchema = z.object({
|
|
811
|
-
scheme: z.string(),
|
|
812
|
-
oldCredential: requiredString({ min: 8 }),
|
|
813
|
-
newCredential: requiredString({ min: 8 }),
|
|
814
|
-
userId: z.string().or(z.number()),
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
type TChangePasswordRequest = z.infer<typeof ChangePasswordRequestSchema>;
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
### JWTTokenPayloadSchema
|
|
821
|
-
|
|
822
|
-
Exported from the controller factory module. Used as the response schema for the `/who-am-i` endpoint:
|
|
823
|
-
|
|
824
|
-
```typescript
|
|
825
|
-
const JWTTokenPayloadSchema = z.object({
|
|
826
|
-
userId: z.string().or(z.number()),
|
|
827
|
-
roles: z.array(
|
|
828
|
-
z.object({
|
|
829
|
-
id: z.string().or(z.number()),
|
|
830
|
-
identifier: z.string(),
|
|
831
|
-
priority: z.number().int(),
|
|
832
|
-
}),
|
|
833
|
-
),
|
|
834
|
-
clientId: z.string().optional(),
|
|
835
|
-
provider: z.string().optional(),
|
|
836
|
-
email: z.email().optional(),
|
|
837
|
-
});
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
For the `/who-am-i` response, this schema is extended at runtime with an optional `userInformation` field (typed from `payload.getUserInformation.response.schema`, falling back to `AnyObjectSchema`) so the `withUserInformation` shape is OpenAPI-documented.
|
|
313
|
+
| Constant | Key string | Type | Description |
|
|
314
|
+
|----------|-----------|------|-------------|
|
|
315
|
+
| `Authentication.CURRENT_USER` | `auth.current.user` | `IAuthUser` | Authenticated user payload |
|
|
316
|
+
| `Authentication.AUDIT_USER_ID` | `audit.user.id` | `IdType` | Authenticated user's ID |
|
|
317
|
+
| `Authentication.SKIP_AUTHENTICATION` | `authentication.skip` | `boolean` | Set `true` to bypass authentication |
|
|
841
318
|
|
|
842
|
-
## API
|
|
319
|
+
## API endpoints
|
|
843
320
|
|
|
844
|
-
The built-in auth controller
|
|
321
|
+
The built-in auth controller exists only when `REST_OPTIONS.useAuthController: true` is set - see [Setup](./#common-tasks).
|
|
845
322
|
|
|
846
|
-
| Method | Path | Auth
|
|
323
|
+
| Method | Path | Auth required | Description |
|
|
847
324
|
|--------|------|---------------|-------------|
|
|
848
|
-
| `POST` | `/auth/sign-in` | No | Authenticate
|
|
849
|
-
| `POST` | `/auth/sign-up` | Configurable | Create a
|
|
325
|
+
| `POST` | `/auth/sign-in` | No | Authenticate, receive a JWT |
|
|
326
|
+
| `POST` | `/auth/sign-up` | Configurable (`requireAuthenticatedSignUp`) | Create a user account |
|
|
850
327
|
| `POST` | `/auth/change-password` | JWT | Change the authenticated user's password |
|
|
851
|
-
| `POST` | `/auth/token/refresh` | JWT | Re-issue
|
|
852
|
-
| `GET` | `/auth/who-am-i` | JWT | Return the
|
|
853
|
-
| `GET` | `/auth/me` | JWT | Return
|
|
854
|
-
| `GET` | `/certs` | No | JWKS endpoint (
|
|
328
|
+
| `POST` | `/auth/token/refresh` | JWT | Re-issue a token from the caller's valid JWT |
|
|
329
|
+
| `GET` | `/auth/who-am-i` | JWT | Return the JWT payload, optionally merged with `getUserInformation` |
|
|
330
|
+
| `GET` | `/auth/me` | JWT | Return `getUserInformation` result directly |
|
|
331
|
+
| `GET` | `/certs` | No | JWKS endpoint (Issuer mode only) |
|
|
855
332
|
|
|
856
333
|
> [!NOTE]
|
|
857
|
-
>
|
|
858
|
-
|
|
859
|
-
### POST /auth/sign-in
|
|
334
|
+
> `/auth` is configurable via `controllerOpts.restPath`; `/certs` via `rest.path` in `IJWKSIssuerOptions`. `/certs` is intentionally unauthenticated.
|
|
860
335
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
**Request Body:**
|
|
864
|
-
|
|
865
|
-
Uses `SignInRequestSchema` by default, or a custom schema via `payload.signIn.request.schema`.
|
|
866
|
-
|
|
867
|
-
**Response 200:**
|
|
868
|
-
|
|
869
|
-
Uses `payload.signIn.response.schema` if provided, otherwise `AnyObjectSchema`.
|
|
336
|
+
**`POST /auth/sign-in`** - body defaults to `SignInRequestSchema` (nested `identifier`/`credential`), overridable via `payload.signIn`.
|
|
870
337
|
|
|
871
338
|
```json
|
|
872
|
-
{
|
|
873
|
-
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..."
|
|
874
|
-
}
|
|
339
|
+
{ "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..." }
|
|
875
340
|
```
|
|
876
341
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
**Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
|
|
880
|
-
|
|
881
|
-
When `requireAuthenticatedSignUp: true`, requires JWT authentication. When `false`, the endpoint is public.
|
|
882
|
-
|
|
883
|
-
### POST /auth/change-password
|
|
884
|
-
|
|
885
|
-
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
886
|
-
|
|
887
|
-
### POST /auth/token/refresh
|
|
888
|
-
|
|
889
|
-
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
890
|
-
|
|
891
|
-
Re-issues an access token using the caller's currently valid JWT. There is no separate refresh token - the caller must send a valid Bearer token in the `Authorization` header.
|
|
892
|
-
|
|
893
|
-
**Request Body:** None
|
|
894
|
-
|
|
895
|
-
**Response 200:**
|
|
896
|
-
|
|
897
|
-
Uses `payload.refreshToken.response.schema` if provided, otherwise `AnyObjectSchema`. The response shape is entirely defined by your `IAuthService.refreshToken()` implementation.
|
|
898
|
-
|
|
899
|
-
**Response 501 (Not Implemented):**
|
|
900
|
-
|
|
901
|
-
Returned when the bound `IAuthService` does not implement the optional `refreshToken` method.
|
|
902
|
-
|
|
903
|
-
**Implementing `refreshToken` in your service:**
|
|
904
|
-
|
|
905
|
-
```typescript
|
|
906
|
-
export class AuthenticationService extends BaseService implements IAuthService {
|
|
907
|
-
async refreshToken(context: TContext<Env>): Promise<{ token: string }> {
|
|
908
|
-
// The current user is already verified by JWT middleware
|
|
909
|
-
const currentUser = context.get(Authentication.CURRENT_USER);
|
|
910
|
-
|
|
911
|
-
// Re-issue a new token with the same payload
|
|
912
|
-
const token = await this._tokenService.generate({ payload: currentUser });
|
|
913
|
-
return { token };
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// ... signIn, signUp, changePassword ...
|
|
917
|
-
}
|
|
918
|
-
```
|
|
919
|
-
|
|
920
|
-
> [!NOTE]
|
|
921
|
-
> The framework does not enforce rotation or revocation policy. If you need to invalidate old tokens after refresh, implement that logic (e.g., a token blocklist or short expiry) inside your `refreshToken` implementation.
|
|
922
|
-
|
|
923
|
-
### GET /auth/who-am-i
|
|
924
|
-
|
|
925
|
-
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
926
|
-
|
|
927
|
-
Returns the current user's decrypted JWT payload directly from context.
|
|
342
|
+
**`POST /auth/sign-up`** - public unless `requireAuthenticatedSignUp: true`. Body defaults to a flat `SignUpRequestSchema` (`username`, `credential` - unlike sign-in's nested shape).
|
|
928
343
|
|
|
929
|
-
|
|
930
|
-
{
|
|
931
|
-
"userId": "123",
|
|
932
|
-
"roles": [
|
|
933
|
-
{ "id": "1", "identifier": "admin", "priority": 0 }
|
|
934
|
-
],
|
|
935
|
-
"clientId": "optional-client-id",
|
|
936
|
-
"provider": "optional-provider",
|
|
937
|
-
"email": "user@example.com"
|
|
938
|
-
}
|
|
939
|
-
```
|
|
344
|
+
**`POST /auth/change-password`** - always requires JWT. Body defaults to `ChangePasswordRequestSchema` (`scheme`, `oldCredential`, `newCredential`, `userId`).
|
|
940
345
|
|
|
941
|
-
|
|
346
|
+
**`POST /auth/token/refresh`** - always requires JWT, no request body. Returns `501` if `IAuthService.refreshToken` isn't implemented.
|
|
942
347
|
|
|
943
|
-
|
|
944
|
-
|------|------|-------------|
|
|
945
|
-
| `withUserInformation` | `true` \| `false` \| `1` \| `0` | When truthy, attaches a `userInformation` field built from `IAuthService.getUserInformation`. Defaults to `false`. |
|
|
946
|
-
|
|
947
|
-
When `withUserInformation` is truthy, the response merges the `getUserInformation` result into the payload:
|
|
348
|
+
**`GET /auth/who-am-i`** - always requires JWT. Query param `withUserInformation` (`true`/`false`/`1`/`0`, default `false`) attaches a `userInformation` field from `getUserInformation`; returns `501` if that method is truthy-requested but not implemented.
|
|
948
349
|
|
|
949
350
|
```json
|
|
950
|
-
{
|
|
951
|
-
"userId": "123",
|
|
952
|
-
"roles": [{ "id": "1", "identifier": "admin", "priority": 0 }],
|
|
953
|
-
"userInformation": { "fullName": "Ada Lovelace", "department": "R&D" }
|
|
954
|
-
}
|
|
351
|
+
{ "userId": "123", "roles": [{ "id": "1", "identifier": "admin", "priority": 0 }] }
|
|
955
352
|
```
|
|
956
353
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
Returned when `withUserInformation` is truthy but the bound `IAuthService` does not implement the optional `getUserInformation` method.
|
|
960
|
-
|
|
961
|
-
### GET /auth/me
|
|
962
|
-
|
|
963
|
-
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
964
|
-
|
|
965
|
-
Returns the current user's information by delegating to `IAuthService.getUserInformation(context, {})`. Unlike `who-am-i`, the response is entirely the service result -- it is not merged with the JWT payload.
|
|
966
|
-
|
|
967
|
-
**Response 200:**
|
|
968
|
-
|
|
969
|
-
Uses `payload.getUserInformation.response.schema` if provided, otherwise `AnyObjectSchema`. The shape is defined by your `getUserInformation` implementation.
|
|
970
|
-
|
|
971
|
-
**Response 501 (Not Implemented):**
|
|
972
|
-
|
|
973
|
-
Returned when the bound `IAuthService` does not implement the optional `getUserInformation` method.
|
|
974
|
-
|
|
975
|
-
**Implementing `getUserInformation` in your service:**
|
|
976
|
-
|
|
977
|
-
```typescript
|
|
978
|
-
export class AuthenticationService extends BaseService implements IAuthService {
|
|
979
|
-
async getUserInformation(context: TContext<Env>, _opts: AnyObject): Promise<AnyObject> {
|
|
980
|
-
// The current user is already verified by JWT middleware
|
|
981
|
-
const currentUser = context.get(Authentication.CURRENT_USER);
|
|
982
|
-
return this.userRepository.findById({ id: currentUser.userId });
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// ... signIn, signUp, changePassword ...
|
|
986
|
-
}
|
|
987
|
-
```
|
|
354
|
+
**`GET /auth/me`** - always requires JWT. Delegates entirely to `getUserInformation(context, {})` - the response is not merged with the JWT payload. Returns `501` if not implemented.
|
|
988
355
|
|
|
989
356
|
> [!TIP]
|
|
990
|
-
> One `getUserInformation` implementation backs both routes
|
|
991
|
-
|
|
992
|
-
### GET /certs (JWKS Issuer Only)
|
|
357
|
+
> One `getUserInformation` implementation backs both routes: use `GET /me` for the raw profile, `GET /who-am-i?withUserInformation=true` to get it merged with the principal in one round-trip.
|
|
993
358
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
Returns the JSON Web Key Set for external verifiers.
|
|
359
|
+
**`GET /certs`** (Issuer mode only) - public, returns the JSON Web Key Set with `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`.
|
|
997
360
|
|
|
998
361
|
```json
|
|
999
|
-
{
|
|
1000
|
-
"keys": [
|
|
1001
|
-
{
|
|
1002
|
-
"kty": "EC",
|
|
1003
|
-
"kid": "my-key-id-1",
|
|
1004
|
-
"use": "sig",
|
|
1005
|
-
"alg": "ES256",
|
|
1006
|
-
"crv": "P-256",
|
|
1007
|
-
"x": "...",
|
|
1008
|
-
"y": "..."
|
|
1009
|
-
}
|
|
1010
|
-
]
|
|
1011
|
-
}
|
|
362
|
+
{ "keys": [{ "kty": "EC", "kid": "my-key-id-1", "use": "sig", "alg": "ES256", "crv": "P-256", "x": "...", "y": "..." }] }
|
|
1012
363
|
```
|
|
1013
364
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
## Auth Entity Column Helpers
|
|
1017
|
-
|
|
1018
|
-
IGNIS provides column helper functions that return pre-configured Drizzle column objects for common auth-related database tables. These functions are designed to be spread into `pgTable()` definitions, giving you standardized columns for User, Role, Permission, and PolicyDefinition entities without manually defining each column.
|
|
1019
|
-
|
|
1020
|
-
All helpers that accept an `opts` parameter support `{ idType: 'string' | 'number' }` to control whether foreign key columns use `text` (for UUIDs) or `integer` (for serial IDs). The default is `'number'`.
|
|
1021
|
-
|
|
1022
|
-
### extraUserColumns
|
|
1023
|
-
|
|
1024
|
-
**Import:** `import { extraUserColumns } from '@venizia/ignis';`
|
|
1025
|
-
|
|
1026
|
-
**Signature:** `extraUserColumns(opts?: { idType: 'string' | 'number' })`
|
|
1027
|
-
|
|
1028
|
-
| Column | DB Column | Type | Nullable | Default | Description |
|
|
1029
|
-
|--------|-----------|------|----------|---------|-------------|
|
|
1030
|
-
| `realm` | `realm` | `text` | Yes | `''` | Multi-tenancy realm identifier |
|
|
1031
|
-
| `status` | `status` | `text` | No | `UserStatuses.UNKNOWN` | User lifecycle status |
|
|
1032
|
-
| `type` | `type` | `text` | No | `UserTypes.SYSTEM` | User type (`SYSTEM` or `LINKED`) |
|
|
1033
|
-
| `activatedAt` | `activated_at` | `timestamp (tz)` | Yes | `null` | When the user was activated |
|
|
1034
|
-
| `lastLoginAt` | `last_login_at` | `timestamp (tz)` | Yes | `null` | Last login timestamp |
|
|
1035
|
-
| `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent user ID (type depends on `idType`) |
|
|
1036
|
-
|
|
1037
|
-
### extraRoleColumns
|
|
1038
|
-
|
|
1039
|
-
**Import:** `import { extraRoleColumns } from '@venizia/ignis';`
|
|
1040
|
-
|
|
1041
|
-
**Signature:** `extraRoleColumns()`
|
|
1042
|
-
|
|
1043
|
-
| Column | DB Column | Type | Nullable | Default | Description |
|
|
1044
|
-
|--------|-----------|------|----------|---------|-------------|
|
|
1045
|
-
| `identifier` | `identifier` | `text` (unique) | No | -- | Unique role identifier (e.g., `'admin'`, `'editor'`) |
|
|
1046
|
-
| `name` | `name` | `text` | No | -- | Human-readable role name |
|
|
1047
|
-
| `description` | `description` | `text` | Yes | `null` | Optional role description |
|
|
1048
|
-
| `priority` | `priority` | `integer` | No | -- | Role priority (lower = higher priority) |
|
|
1049
|
-
| `status` | `status` | `text` | No | `RoleStatuses.ACTIVATED` | Role lifecycle status |
|
|
1050
|
-
|
|
1051
|
-
### extraPermissionColumns
|
|
1052
|
-
|
|
1053
|
-
**Import:** `import { extraPermissionColumns } from '@venizia/ignis';`
|
|
1054
|
-
|
|
1055
|
-
**Signature:** `extraPermissionColumns(opts?: { idType: 'string' | 'number' })`
|
|
1056
|
-
|
|
1057
|
-
| Column | DB Column | Type | Nullable | Default | Description |
|
|
1058
|
-
|--------|-----------|------|----------|---------|-------------|
|
|
1059
|
-
| `code` | `code` | `text` (unique) | No | -- | Unique permission code |
|
|
1060
|
-
| `name` | `name` | `text` | No | -- | Permission display name |
|
|
1061
|
-
| `subject` | `subject` | `text` | No | -- | Permission subject (e.g., `'User'`, `'Order'`) |
|
|
1062
|
-
| `method` | `method` | `text` | No | -- | HTTP method (e.g., `'GET'`, `'POST'`) |
|
|
1063
|
-
| `action` | `action` | `text` | No | -- | Permitted action (e.g., `'read'`, `'write'`) |
|
|
1064
|
-
| `scope` | `scope` | `text` | No | -- | Permission scope |
|
|
1065
|
-
| `description` | `description` | `text` | Yes | `null` | Optional permission description |
|
|
1066
|
-
| `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent permission ID (type depends on `idType`) |
|
|
1067
|
-
|
|
1068
|
-
### extraPolicyDefinitionColumns
|
|
1069
|
-
|
|
1070
|
-
**Import:** `import { extraPolicyDefinitionColumns } from '@venizia/ignis';`
|
|
1071
|
-
|
|
1072
|
-
**Signature:** `extraPolicyDefinitionColumns(opts?: { idType: 'string' | 'number' })`
|
|
1073
|
-
|
|
1074
|
-
Provides columns for Casbin-style policy definitions that map subjects (users/roles) to targets (resources/permissions).
|
|
365
|
+
## Entity column helpers
|
|
1075
366
|
|
|
1076
|
-
|
|
1077
|
-
|--------|-----------|------|----------|---------|-------------|
|
|
1078
|
-
| `variant` | `variant` | `text` | No | -- | Policy variant (e.g., `'p'` for policy, `'g'` for grouping) |
|
|
1079
|
-
| `subjectType` | `subject_type` | `text` | No | -- | Type of subject (e.g., `'user'`, `'role'`) |
|
|
1080
|
-
| `targetType` | `target_type` | `text` | No | -- | Type of target (e.g., `'permission'`, `'role'`) |
|
|
1081
|
-
| `action` | `action` | `text` | Yes | `null` | Policy action |
|
|
1082
|
-
| `effect` | `effect` | `text` | Yes | `null` | Policy effect (e.g., `'allow'`, `'deny'`) |
|
|
1083
|
-
| `domain` | `domain` | `text` | Yes | `null` | Policy domain for multi-tenancy |
|
|
1084
|
-
| `subjectId` | `subject_id` | `text` or `integer` | No | -- | Subject ID (type depends on `idType`) |
|
|
1085
|
-
| `targetId` | `target_id` | `text` or `integer` | No | -- | Target ID (type depends on `idType`) |
|
|
1086
|
-
|
|
1087
|
-
### Usage Example
|
|
367
|
+
Column helper functions return pre-configured Drizzle columns for auth-related tables - spread them into `pgTable()` alongside your own columns.
|
|
1088
368
|
|
|
1089
369
|
```typescript
|
|
1090
|
-
import { pgTable,
|
|
1091
|
-
import {
|
|
1092
|
-
extraUserColumns,
|
|
1093
|
-
extraRoleColumns,
|
|
1094
|
-
extraPermissionColumns,
|
|
1095
|
-
extraPolicyDefinitionColumns,
|
|
1096
|
-
} from '@venizia/ignis';
|
|
1097
|
-
import { withSerialId, withTimestamps } from '@venizia/ignis';
|
|
370
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
371
|
+
import { extraUserColumns, extraRoleColumns, extraPermissionColumns, extraPolicyDefinitionColumns, withSerialId, withTimestamps } from '@venizia/ignis';
|
|
1098
372
|
|
|
1099
|
-
// User table
|
|
1100
373
|
export const users = pgTable('users', {
|
|
1101
374
|
...withSerialId(),
|
|
1102
375
|
...withTimestamps(),
|
|
1103
376
|
...extraUserColumns(),
|
|
1104
377
|
username: text('username').unique().notNull(),
|
|
1105
378
|
passwordHash: text('password_hash').notNull(),
|
|
1106
|
-
email: text('email').unique(),
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
// Role table
|
|
1110
|
-
export const roles = pgTable('roles', {
|
|
1111
|
-
...withSerialId(),
|
|
1112
|
-
...withTimestamps(),
|
|
1113
|
-
...extraRoleColumns(),
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
// Permission table
|
|
1117
|
-
export const permissions = pgTable('permissions', {
|
|
1118
|
-
...withSerialId(),
|
|
1119
|
-
...withTimestamps(),
|
|
1120
|
-
...extraPermissionColumns(),
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
// Policy definition table (Casbin-style policies)
|
|
1124
|
-
export const policyDefinitions = pgTable('policy_definitions', {
|
|
1125
|
-
...withSerialId(),
|
|
1126
|
-
...withTimestamps(),
|
|
1127
|
-
...extraPolicyDefinitionColumns(),
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
// With UUID-based IDs
|
|
1131
|
-
export const uuidUsers = pgTable('users', {
|
|
1132
|
-
...withUuidId(),
|
|
1133
|
-
...withTimestamps(),
|
|
1134
|
-
...extraUserColumns({ idType: 'string' }),
|
|
1135
|
-
username: text('username').unique().notNull(),
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
export const uuidPolicies = pgTable('policy_definitions', {
|
|
1139
|
-
...withUuidId(),
|
|
1140
|
-
...withTimestamps(),
|
|
1141
|
-
...extraPolicyDefinitionColumns({ idType: 'string' }),
|
|
1142
379
|
});
|
|
1143
|
-
```
|
|
1144
|
-
|
|
1145
|
-
### Context Variables
|
|
1146
|
-
|
|
1147
|
-
The auth middleware sets several variables on the Hono `Context` object during request processing. These are declared via a `ContextVariableMap` module augmentation and can be accessed with `c.get()` / `c.set()`.
|
|
1148
|
-
|
|
1149
|
-
| Constant | Key String | Type | Description |
|
|
1150
|
-
|----------|-----------|------|-------------|
|
|
1151
|
-
| `Authentication.CURRENT_USER` | `'auth.current.user'` | `IAuthUser` | The authenticated user payload, set after successful authentication |
|
|
1152
|
-
| `Authentication.AUDIT_USER_ID` | `'audit.user.id'` | `IdType` | The authenticated user's ID, extracted from the user payload |
|
|
1153
|
-
| `Authentication.SKIP_AUTHENTICATION` | `'authentication.skip'` | `boolean` | Set to `true` in a preceding middleware to bypass authentication for the current request |
|
|
1154
|
-
| `Authorization.RULES` | `'authorization.rules'` | `unknown` | Authorization rules resolved for the current request |
|
|
1155
|
-
| `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | `boolean` | Set to `true` to bypass authorization checks for the current request |
|
|
1156
|
-
|
|
1157
|
-
**Reading context variables in a handler:**
|
|
1158
|
-
|
|
1159
|
-
```typescript
|
|
1160
|
-
import { Authentication, Authorization } from '@venizia/ignis';
|
|
1161
|
-
|
|
1162
|
-
// Inside a route handler
|
|
1163
|
-
const currentUser = c.get(Authentication.CURRENT_USER);
|
|
1164
|
-
const userId = c.get(Authentication.AUDIT_USER_ID);
|
|
1165
|
-
const skipAuth = c.get(Authentication.SKIP_AUTHENTICATION);
|
|
1166
|
-
const authzRules = c.get(Authorization.RULES);
|
|
1167
|
-
```
|
|
1168
|
-
|
|
1169
|
-
**Skipping auth dynamically from middleware:**
|
|
1170
|
-
|
|
1171
|
-
```typescript
|
|
1172
|
-
import { Authentication, Authorization } from '@venizia/ignis';
|
|
1173
|
-
import { createMiddleware } from 'hono/factory';
|
|
1174
|
-
|
|
1175
|
-
const apiKeyMiddleware = createMiddleware(async (c, next) => {
|
|
1176
|
-
if (c.req.header('X-API-Key') === process.env.INTERNAL_API_KEY) {
|
|
1177
|
-
c.set(Authentication.SKIP_AUTHENTICATION, true);
|
|
1178
|
-
c.set(Authorization.SKIP_AUTHORIZATION, true);
|
|
1179
|
-
}
|
|
1180
|
-
return next();
|
|
1181
|
-
});
|
|
1182
|
-
```
|
|
1183
|
-
|
|
1184
|
-
## See Also
|
|
1185
380
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
381
|
+
export const roles = pgTable('roles', { ...withSerialId(), ...withTimestamps(), ...extraRoleColumns() });
|
|
382
|
+
export const permissions = pgTable('permissions', { ...withSerialId(), ...withTimestamps(), ...extraPermissionColumns() });
|
|
383
|
+
export const policyDefinitions = pgTable('policy_definitions', { ...withSerialId(), ...withTimestamps(), ...extraPolicyDefinitionColumns() });
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**`extraUserColumns(opts?: { idType })`**
|
|
387
|
+
|
|
388
|
+
| Column | DB column | Type | Default | Description |
|
|
389
|
+
|--------|-----------|------|---------|-------------|
|
|
390
|
+
| `realm` | `realm` | `text` | `''` | Multi-tenancy realm identifier |
|
|
391
|
+
| `status` | `status` | `text` | `UserStatuses.UNKNOWN` | User lifecycle status |
|
|
392
|
+
| `type` | `type` | `text` | `UserTypes.SYSTEM` | `SYSTEM` or `LINKED` |
|
|
393
|
+
| `activatedAt` | `activated_at` | `timestamp (tz)` | `null` | Activation timestamp |
|
|
394
|
+
| `lastLoginAt` | `last_login_at` | `timestamp (tz)` | `null` | Last login timestamp |
|
|
395
|
+
| `parentId` | `parent_id` | `text` or `integer` | `null` | Depends on `idType` |
|
|
396
|
+
|
|
397
|
+
**`extraRoleColumns()`** - no options.
|
|
398
|
+
|
|
399
|
+
| Column | DB column | Type | Default | Description |
|
|
400
|
+
|--------|-----------|------|---------|-------------|
|
|
401
|
+
| `identifier` | `identifier` | `text` (unique) | -- | e.g. `'admin'`, `'editor'` |
|
|
402
|
+
| `name` | `name` | `text` | -- | Human-readable name |
|
|
403
|
+
| `description` | `description` | `text` | `null` | Optional |
|
|
404
|
+
| `priority` | `priority` | `integer` | -- | Lower = higher priority |
|
|
405
|
+
| `status` | `status` | `text` | `RoleStatuses.ACTIVATED` | Role lifecycle status |
|
|
406
|
+
|
|
407
|
+
**`extraPermissionColumns(opts?: { idType })`**
|
|
408
|
+
|
|
409
|
+
| Column | DB column | Type | Default | Description |
|
|
410
|
+
|--------|-----------|------|---------|-------------|
|
|
411
|
+
| `code` | `code` | `text` (unique) | -- | Unique permission code |
|
|
412
|
+
| `name` | `name` | `text` | -- | Display name |
|
|
413
|
+
| `subject` | `subject` | `text` | -- | e.g. `'User'`, `'Order'` |
|
|
414
|
+
| `method` | `method` | `text` | -- | e.g. `'GET'`, `'POST'` |
|
|
415
|
+
| `action` | `action` | `text` | -- | e.g. `'read'`, `'write'` |
|
|
416
|
+
| `scope` | `scope` | `text` | -- | Permission scope |
|
|
417
|
+
| `description` | `description` | `text` | `null` | Optional |
|
|
418
|
+
| `parentId` | `parent_id` | `text` or `integer` | `null` | Depends on `idType` |
|
|
419
|
+
|
|
420
|
+
**`extraPolicyDefinitionColumns(opts?: { idType })`** - Casbin-style policies mapping subjects to targets.
|
|
421
|
+
|
|
422
|
+
| Column | DB column | Type | Nullable | Description |
|
|
423
|
+
|--------|-----------|------|----------|-------------|
|
|
424
|
+
| `variant` | `variant` | `text` | No | `'p'` (policy) or `'g'` (grouping) |
|
|
425
|
+
| `subjectType` | `subject_type` | `text` | No | e.g. `'user'`, `'role'` |
|
|
426
|
+
| `targetType` | `target_type` | `text` | No | e.g. `'permission'`, `'role'` |
|
|
427
|
+
| `action` | `action` | `text` | Yes | Policy action |
|
|
428
|
+
| `effect` | `effect` | `text` | Yes | `'allow'` / `'deny'` |
|
|
429
|
+
| `domain` | `domain` | `text` | Yes | Multi-tenancy domain |
|
|
430
|
+
| `subjectId` | `subject_id` | `text` or `integer` | No | Depends on `idType` |
|
|
431
|
+
| `targetId` | `target_id` | `text` or `integer` | No | Depends on `idType` |
|
|
432
|
+
|
|
433
|
+
All `idType` options default to `'number'` (`integer` columns); pass `'string'` for `text` (e.g. UUID) columns.
|
|
434
|
+
|
|
435
|
+
## See also
|
|
436
|
+
|
|
437
|
+
- [Overview](./) - initial setup and binding keys
|
|
438
|
+
- [API Reference](./api) - full option tables, service class hierarchy, strategy registry
|
|
439
|
+
- [Error Reference](./errors) - every error message and how to fix it
|