@venizia/ignis-docs 0.0.6-3 → 0.0.7-1
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 +125 -388
- package/dist/mcp-server/common/config.d.ts +0 -21
- package/dist/mcp-server/common/config.d.ts.map +1 -1
- package/dist/mcp-server/common/config.js +1 -36
- package/dist/mcp-server/common/config.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts +0 -24
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +0 -25
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.d.ts +0 -13
- package/dist/mcp-server/helpers/github.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +3 -20
- package/dist/mcp-server/helpers/github.helper.js.map +1 -1
- package/dist/mcp-server/index.js +0 -20
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +2 -79
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js +1 -38
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-content.tool.js +0 -9
- package/dist/mcp-server/tools/docs/get-document-content.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.js +0 -9
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +0 -6
- package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/get-package-overview.tool.js +1 -24
- package/dist/mcp-server/tools/docs/get-package-overview.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/list-categories.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/list-categories.tool.js +0 -9
- package/dist/mcp-server/tools/docs/list-categories.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/list-documents.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/list-documents.tool.js +0 -9
- package/dist/mcp-server/tools/docs/list-documents.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.js +0 -9
- package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +0 -9
- package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -13
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts +0 -4
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -18
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +0 -9
- package/dist/mcp-server/tools/github/view-source-file.tool.js.map +1 -1
- package/dist/mcp-server/tools/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/index.js +0 -2
- package/dist/mcp-server/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/wiki/best-practices/api-usage-examples.md +7 -5
- package/wiki/best-practices/code-style-standards/advanced-patterns.md +1 -1
- package/wiki/best-practices/code-style-standards/constants-configuration.md +1 -1
- package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
- package/wiki/best-practices/code-style-standards/function-patterns.md +1 -1
- package/wiki/best-practices/common-pitfalls.md +1 -1
- package/wiki/best-practices/data-modeling.md +33 -1
- package/wiki/best-practices/error-handling.md +7 -4
- package/wiki/best-practices/performance-optimization.md +1 -1
- package/wiki/best-practices/security-guidelines.md +5 -4
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/controllers.md +14 -8
- package/wiki/guides/core-concepts/persistent/models.md +32 -0
- package/wiki/guides/core-concepts/services.md +2 -1
- package/wiki/guides/get-started/5-minute-quickstart.md +1 -1
- package/wiki/guides/tutorials/building-a-crud-api.md +2 -1
- package/wiki/guides/tutorials/complete-installation.md +2 -2
- package/wiki/guides/tutorials/ecommerce-api.md +3 -3
- package/wiki/guides/tutorials/realtime-chat.md +7 -6
- package/wiki/index.md +2 -1
- package/wiki/references/base/application.md +28 -0
- package/wiki/references/base/components.md +2 -1
- package/wiki/references/base/controllers.md +31 -4
- package/wiki/references/base/datasources.md +6 -2
- package/wiki/references/base/dependency-injection.md +31 -0
- package/wiki/references/base/filter-system/fields-order-pagination.md +8 -1
- package/wiki/references/base/middlewares.md +2 -1
- package/wiki/references/base/models.md +144 -2
- package/wiki/references/base/repositories/advanced.md +2 -2
- package/wiki/references/base/repositories/index.md +24 -1
- package/wiki/references/base/repositories/soft-deletable.md +213 -0
- package/wiki/references/base/services.md +2 -1
- package/wiki/references/components/authentication/api.md +525 -205
- package/wiki/references/components/authentication/errors.md +502 -105
- package/wiki/references/components/authentication/index.md +388 -75
- package/wiki/references/components/authentication/usage.md +575 -247
- package/wiki/references/components/authorization/usage.md +62 -0
- package/wiki/references/components/health-check.md +2 -1
- package/wiki/references/components/socket-io/index.md +9 -4
- package/wiki/references/components/socket-io/usage.md +1 -1
- package/wiki/references/components/static-asset/index.md +3 -5
- package/wiki/references/components/swagger.md +2 -1
- package/wiki/references/configuration/environment-variables.md +2 -1
- package/wiki/references/configuration/index.md +40 -1
- package/wiki/references/helpers/error/index.md +1 -1
- package/wiki/references/helpers/inversion/index.md +1 -1
- package/wiki/references/helpers/redis/index.md +2 -9
- package/wiki/references/quick-reference.md +3 -5
- package/wiki/references/utilities/crypto.md +2 -2
- package/wiki/references/utilities/date.md +5 -5
- package/wiki/references/utilities/index.md +3 -11
- package/wiki/references/utilities/jsx.md +4 -2
- package/wiki/references/utilities/module.md +1 -1
- package/wiki/references/utilities/parse.md +24 -4
- package/wiki/references/utilities/performance.md +2 -2
- package/wiki/references/utilities/promise.md +4 -4
- package/wiki/references/utilities/request.md +2 -2
- package/wiki/references/utilities/schema.md +17 -8
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Authentication -- Usage & Examples
|
|
2
2
|
|
|
3
|
-
> Securing routes, authentication flows, entity helpers, and API endpoint specifications. See [Setup & Configuration](./) for initial setup.
|
|
3
|
+
> Securing routes, authentication flows, JWKS microservice patterns, entity helpers, and API endpoint specifications. See [Setup & Configuration](./) for initial setup.
|
|
4
4
|
|
|
5
5
|
## Securing Routes
|
|
6
6
|
|
|
7
|
-
Use `
|
|
7
|
+
Use the `authenticate` field in route configurations. The field accepts `TRouteAuthenticateConfig`:
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
10
|
// Single strategy
|
|
11
11
|
const SECURE_ROUTE_CONFIG = {
|
|
12
12
|
path: '/secure-data',
|
|
13
13
|
method: HTTP.Methods.GET,
|
|
14
|
-
|
|
14
|
+
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
15
15
|
responses: jsonResponse({
|
|
16
16
|
description: 'Protected data',
|
|
17
17
|
schema: z.object({ message: z.string() }),
|
|
@@ -22,8 +22,10 @@ const SECURE_ROUTE_CONFIG = {
|
|
|
22
22
|
const FALLBACK_AUTH_CONFIG = {
|
|
23
23
|
path: '/api/data',
|
|
24
24
|
method: HTTP.Methods.GET,
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
authenticate: {
|
|
26
|
+
strategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC],
|
|
27
|
+
mode: AuthenticationModes.ANY,
|
|
28
|
+
},
|
|
27
29
|
responses: jsonResponse({
|
|
28
30
|
description: 'Data accessible via JWT or Basic auth',
|
|
29
31
|
schema: z.object({ data: z.any() }),
|
|
@@ -34,7 +36,7 @@ const FALLBACK_AUTH_CONFIG = {
|
|
|
34
36
|
const PUBLIC_ROUTE_CONFIG = {
|
|
35
37
|
path: '/public',
|
|
36
38
|
method: HTTP.Methods.GET,
|
|
37
|
-
|
|
39
|
+
authenticate: { skip: true },
|
|
38
40
|
responses: jsonResponse({
|
|
39
41
|
description: 'Public endpoint',
|
|
40
42
|
schema: z.object({ message: z.string() }),
|
|
@@ -44,7 +46,7 @@ const PUBLIC_ROUTE_CONFIG = {
|
|
|
44
46
|
|
|
45
47
|
## Using the `authenticate()` Standalone Function
|
|
46
48
|
|
|
47
|
-
The `authenticate()` function
|
|
49
|
+
The `authenticate()` function creates an `AuthenticationProvider` instance and uses its middleware factory. It returns a Hono `MiddlewareHandler` suitable for direct middleware usage:
|
|
48
50
|
|
|
49
51
|
```typescript
|
|
50
52
|
import { authenticate, Authentication, AuthenticationModes } from '@venizia/ignis';
|
|
@@ -97,7 +99,9 @@ const conditionalAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
97
99
|
|
|
98
100
|
## Implementing an AuthenticationService
|
|
99
101
|
|
|
100
|
-
The `AuthenticateComponent` depends on a service implementing the `IAuthService` interface when using the built-in auth controller
|
|
102
|
+
The `AuthenticateComponent` depends on a service implementing the `IAuthService` interface when using the built-in auth controller.
|
|
103
|
+
|
|
104
|
+
### JWS Example
|
|
101
105
|
|
|
102
106
|
```typescript
|
|
103
107
|
import {
|
|
@@ -105,21 +109,29 @@ import {
|
|
|
105
109
|
inject,
|
|
106
110
|
IAuthService,
|
|
107
111
|
IJWTTokenPayload,
|
|
108
|
-
|
|
112
|
+
JWSTokenService,
|
|
113
|
+
BindingKeys,
|
|
114
|
+
BindingNamespaces,
|
|
109
115
|
TSignInRequest,
|
|
110
|
-
|
|
116
|
+
TContext,
|
|
111
117
|
} from '@venizia/ignis';
|
|
112
|
-
import {
|
|
118
|
+
import { getError } from '@venizia/ignis-helpers';
|
|
119
|
+
import { Env } from 'hono';
|
|
113
120
|
|
|
114
121
|
export class AuthenticationService extends BaseService implements IAuthService {
|
|
115
122
|
constructor(
|
|
116
|
-
@inject({
|
|
117
|
-
|
|
123
|
+
@inject({
|
|
124
|
+
key: BindingKeys.build({
|
|
125
|
+
namespace: BindingNamespaces.SERVICE,
|
|
126
|
+
key: JWSTokenService.name,
|
|
127
|
+
}),
|
|
128
|
+
})
|
|
129
|
+
private _tokenService: JWSTokenService,
|
|
118
130
|
) {
|
|
119
131
|
super({ scope: AuthenticationService.name });
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
async signIn(context:
|
|
134
|
+
async signIn(context: TContext<Env>, opts: TSignInRequest): Promise<{ token: string }> {
|
|
123
135
|
const { identifier, credential } = opts;
|
|
124
136
|
const user = await this.userRepo.findByIdentifier(identifier);
|
|
125
137
|
|
|
@@ -132,20 +144,198 @@ export class AuthenticationService extends BaseService implements IAuthService {
|
|
|
132
144
|
roles: user.roles,
|
|
133
145
|
};
|
|
134
146
|
|
|
135
|
-
const token = await this.
|
|
147
|
+
const token = await this._tokenService.generate({ payload });
|
|
136
148
|
return { token };
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
async signUp(context:
|
|
151
|
+
async signUp(context: TContext<Env>, opts: any): Promise<any> {
|
|
140
152
|
// Implement your sign-up logic
|
|
141
153
|
}
|
|
142
154
|
|
|
143
|
-
async changePassword(context:
|
|
155
|
+
async changePassword(context: TContext<Env>, opts: any): Promise<any> {
|
|
144
156
|
// Implement your change password logic
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
159
|
```
|
|
148
160
|
|
|
161
|
+
### JWKS Issuer Example
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import {
|
|
165
|
+
BaseService,
|
|
166
|
+
inject,
|
|
167
|
+
IAuthService,
|
|
168
|
+
IJWTTokenPayload,
|
|
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';
|
|
177
|
+
|
|
178
|
+
export class AuthenticationService extends BaseService implements IAuthService {
|
|
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
|
+
}
|
|
190
|
+
|
|
191
|
+
async signIn(context: TContext<Env>, opts: TSignInRequest): Promise<{ token: string }> {
|
|
192
|
+
const { identifier, credential } = opts;
|
|
193
|
+
// ... lookup and verify user ...
|
|
194
|
+
|
|
195
|
+
const payload: IJWTTokenPayload = {
|
|
196
|
+
userId: user.id,
|
|
197
|
+
roles: user.roles,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const token = await this._tokenService.generate({ payload });
|
|
201
|
+
return { token };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ... signUp, changePassword ...
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## JWKS Microservice Patterns
|
|
209
|
+
|
|
210
|
+
### Issuer + Verifier Architecture
|
|
211
|
+
|
|
212
|
+
In a microservice architecture, one service issues tokens (issuer) and other services verify them (verifier):
|
|
213
|
+
|
|
214
|
+
```mermaid
|
|
215
|
+
flowchart LR
|
|
216
|
+
CLIENT["Client App"]
|
|
217
|
+
|
|
218
|
+
subgraph AUTH["Auth Service (JWKS Issuer)"]
|
|
219
|
+
SIGNIN["POST /auth/sign-in"]
|
|
220
|
+
CERTS["GET /certs"]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
subgraph API["API Service (JWKS Verifier)"]
|
|
224
|
+
DATA["GET /api/data"]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
CLIENT -->|"1. Sign in"| SIGNIN
|
|
228
|
+
SIGNIN -->|"2. JWT token"| CLIENT
|
|
229
|
+
CLIENT -->|"3. Request + Bearer token"| DATA
|
|
230
|
+
DATA -->|"4. Fetch JWKS"| CERTS
|
|
231
|
+
CERTS -->|"5. Public keys"| DATA
|
|
232
|
+
DATA -->|"6. Verified response"| CLIENT
|
|
233
|
+
|
|
234
|
+
style AUTH fill:#e8f4fd,stroke:#0d6efd
|
|
235
|
+
style API fill:#d4edda,stroke:#28a745
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Auth Service (Issuer):**
|
|
239
|
+
```typescript
|
|
240
|
+
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
241
|
+
standard: JOSEStandards.JWKS,
|
|
242
|
+
options: {
|
|
243
|
+
mode: JWKSModes.ISSUER,
|
|
244
|
+
algorithm: 'ES256',
|
|
245
|
+
keys: {
|
|
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,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**API Service (Verifier):**
|
|
258
|
+
```typescript
|
|
259
|
+
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
260
|
+
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
|
+
},
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### JWKS with AES Payload Encryption
|
|
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:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# Generate private key
|
|
301
|
+
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
|
|
302
|
+
|
|
303
|
+
# Generate public key from private key
|
|
304
|
+
openssl ec -in private.pem -pubout -out public.pem
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Generate RS256 keys:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
# Generate private key
|
|
311
|
+
openssl genrsa -out private.pem 2048
|
|
312
|
+
|
|
313
|
+
# Generate public key from private key
|
|
314
|
+
openssl rsa -in private.pem -pubout -out public.pem
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
> [!WARNING]
|
|
318
|
+
> Never commit private keys to version control. The `.gitignore` includes patterns for `*.pem`, `*.key`, and `keys/` directories.
|
|
319
|
+
|
|
320
|
+
### Inline Keys (Text Driver)
|
|
321
|
+
|
|
322
|
+
For environments where file access is restricted (e.g., serverless), use the `text` driver:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
{
|
|
326
|
+
mode: JWKSModes.ISSUER,
|
|
327
|
+
algorithm: 'ES256',
|
|
328
|
+
keys: {
|
|
329
|
+
driver: JWKSKeyDrivers.TEXT,
|
|
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,
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
149
339
|
## Entity Column Helpers
|
|
150
340
|
|
|
151
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.
|
|
@@ -332,24 +522,139 @@ Inherits all statuses from `CommonStatuses` (same values as `UserStatuses`):
|
|
|
332
522
|
|
|
333
523
|
## Auth Flows
|
|
334
524
|
|
|
335
|
-
###
|
|
525
|
+
### JWS Authentication Flow
|
|
526
|
+
|
|
527
|
+
```mermaid
|
|
528
|
+
sequenceDiagram
|
|
529
|
+
participant C as Client
|
|
530
|
+
participant MW as Auth Middleware
|
|
531
|
+
participant S as JWSAuthenticationStrategy
|
|
532
|
+
participant SVC as JWSTokenService
|
|
533
|
+
participant JOSE as jose library
|
|
534
|
+
|
|
535
|
+
C->>MW: Request + Authorization: Bearer <token>
|
|
536
|
+
MW->>S: authenticate(context)
|
|
537
|
+
S->>SVC: extractCredentials(context)
|
|
538
|
+
SVC-->>S: { type: "Bearer", token }
|
|
539
|
+
S->>SVC: verify({ type, token })
|
|
540
|
+
SVC->>JOSE: jwtVerify(token, jwtSecret)
|
|
541
|
+
JOSE-->>SVC: JWTVerifyResult
|
|
542
|
+
SVC->>SVC: decryptPayload() (if AES configured)
|
|
543
|
+
SVC-->>S: IJWTTokenPayload
|
|
544
|
+
S-->>MW: IAuthUser
|
|
545
|
+
MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
|
|
546
|
+
MW->>C: Continue to handler
|
|
547
|
+
```
|
|
336
548
|
|
|
337
549
|
1. **Client sends request** with <code v-pre>Authorization: Bearer <token></code> header
|
|
338
|
-
2. **
|
|
339
|
-
3. **
|
|
340
|
-
4. **
|
|
341
|
-
5. **
|
|
550
|
+
2. **JWSAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
551
|
+
3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
|
|
552
|
+
4. **JWSTokenService.doVerify()** verifies the JWT signature using `jose.jwtVerify()` with the shared `jwtSecret`
|
|
553
|
+
5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
|
|
342
554
|
6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
|
|
343
555
|
|
|
344
|
-
|
|
345
|
-
|
|
556
|
+
### JWKS Issuer Authentication Flow
|
|
557
|
+
|
|
558
|
+
```mermaid
|
|
559
|
+
sequenceDiagram
|
|
560
|
+
participant C as Client
|
|
561
|
+
participant MW as Auth Middleware
|
|
562
|
+
participant S as JWKSIssuerStrategy
|
|
563
|
+
participant SVC as JWKSIssuerTokenService
|
|
564
|
+
participant INIT as Lazy Init
|
|
565
|
+
participant JOSE as jose library
|
|
566
|
+
|
|
567
|
+
C->>MW: Request + Authorization: Bearer <token>
|
|
568
|
+
MW->>S: authenticate(context)
|
|
569
|
+
S->>SVC: extractCredentials(context)
|
|
570
|
+
SVC-->>S: { type: "Bearer", token }
|
|
571
|
+
S->>SVC: verify({ type, token })
|
|
572
|
+
SVC->>INIT: ensureInitialized()
|
|
573
|
+
Note over INIT: Load keys from file/text<br/>Parse PEM/JWK<br/>Cache JWKS
|
|
574
|
+
INIT-->>SVC: initialized
|
|
575
|
+
SVC->>JOSE: jwtVerify(token, publicKey)
|
|
576
|
+
JOSE-->>SVC: JWTVerifyResult
|
|
577
|
+
SVC->>SVC: decryptPayload() (if AES configured)
|
|
578
|
+
SVC-->>S: IJWTTokenPayload
|
|
579
|
+
S-->>MW: IAuthUser
|
|
580
|
+
MW->>C: Continue to handler
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
1. **Client sends request** with <code v-pre>Authorization: Bearer <token></code> header
|
|
584
|
+
2. **JWKSIssuerAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
585
|
+
3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
|
|
586
|
+
4. **JWKSIssuerTokenService.doVerify()** calls `ensureInitialized()` (lazy-loads keys on first call), then verifies the JWT using the public key
|
|
587
|
+
5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
|
|
588
|
+
6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
|
|
589
|
+
|
|
590
|
+
### JWKS Verifier Authentication Flow
|
|
591
|
+
|
|
592
|
+
```mermaid
|
|
593
|
+
sequenceDiagram
|
|
594
|
+
participant C as Client
|
|
595
|
+
participant MW as Auth Middleware
|
|
596
|
+
participant S as JWKSVerifierStrategy
|
|
597
|
+
participant SVC as JWKSVerifierTokenService
|
|
598
|
+
participant INIT as Lazy Init
|
|
599
|
+
participant REMOTE as Remote JWKS URL
|
|
600
|
+
|
|
601
|
+
C->>MW: Request + Authorization: Bearer <token>
|
|
602
|
+
MW->>S: authenticate(context)
|
|
603
|
+
S->>SVC: extractCredentials(context)
|
|
604
|
+
SVC-->>S: { type: "Bearer", token }
|
|
605
|
+
S->>SVC: verify({ type, token })
|
|
606
|
+
SVC->>INIT: ensureInitialized()
|
|
607
|
+
INIT->>REMOTE: createRemoteJWKSet(jwksUrl)
|
|
608
|
+
REMOTE-->>INIT: JWKS verifier function
|
|
609
|
+
INIT-->>SVC: initialized
|
|
610
|
+
SVC->>SVC: jwtVerify(token, jwksVerifier)
|
|
611
|
+
SVC->>SVC: decryptPayload() (if AES configured)
|
|
612
|
+
SVC-->>S: IJWTTokenPayload
|
|
613
|
+
S-->>MW: IAuthUser
|
|
614
|
+
MW->>C: Continue to handler
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
1. **Client sends request** with <code v-pre>Authorization: Bearer <token></code> header
|
|
618
|
+
2. **JWKSVerifierAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
619
|
+
3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
|
|
620
|
+
4. **JWKSVerifierTokenService.doVerify()** calls `ensureInitialized()` (creates remote JWKS verifier on first call), then verifies the JWT using the remote JWKS
|
|
621
|
+
5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
|
|
622
|
+
6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
|
|
346
623
|
|
|
347
624
|
### Basic Authentication Flow
|
|
348
625
|
|
|
626
|
+
```mermaid
|
|
627
|
+
sequenceDiagram
|
|
628
|
+
participant C as Client
|
|
629
|
+
participant MW as Auth Middleware
|
|
630
|
+
participant S as BasicAuthStrategy
|
|
631
|
+
participant SVC as BasicTokenService
|
|
632
|
+
participant CB as verifyCredentials callback
|
|
633
|
+
|
|
634
|
+
C->>MW: Request + Authorization: Basic <base64>
|
|
635
|
+
MW->>S: authenticate(context)
|
|
636
|
+
S->>SVC: extractCredentials(context)
|
|
637
|
+
SVC->>SVC: Base64 decode
|
|
638
|
+
SVC-->>S: { username, password }
|
|
639
|
+
S->>SVC: verify({ credentials, context })
|
|
640
|
+
SVC->>CB: verifyCredentials({ credentials, context })
|
|
641
|
+
CB-->>SVC: IAuthUser | null
|
|
642
|
+
alt valid user
|
|
643
|
+
SVC-->>S: IAuthUser
|
|
644
|
+
S-->>MW: IAuthUser
|
|
645
|
+
MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
|
|
646
|
+
MW->>C: Continue to handler
|
|
647
|
+
else null (invalid)
|
|
648
|
+
SVC-->>S: throw 401
|
|
649
|
+
S-->>MW: throw 401
|
|
650
|
+
MW->>C: 401 Unauthorized
|
|
651
|
+
end
|
|
652
|
+
```
|
|
653
|
+
|
|
349
654
|
1. **Client sends request** with <code v-pre>Authorization: Basic <base64(username:password)></code> header
|
|
350
655
|
2. **BasicAuthenticationStrategy.authenticate()** is called by the Hono middleware
|
|
351
656
|
3. **BasicTokenService.extractCredentials()** decodes the Base64 credentials
|
|
352
|
-
4. **BasicTokenService.verify()** calls the user-provided `verifyCredentials` callback
|
|
657
|
+
4. **BasicTokenService.verify()** calls the user-provided `verifyCredentials` callback with `{ credentials, context }`
|
|
353
658
|
5. **User payload is set** on `context.get(Authentication.CURRENT_USER)` if verification succeeds
|
|
354
659
|
|
|
355
660
|
> [!IMPORTANT]
|
|
@@ -357,11 +662,39 @@ Inherits all statuses from `CommonStatuses` (same values as `UserStatuses`):
|
|
|
357
662
|
|
|
358
663
|
## Multi-Strategy Authentication
|
|
359
664
|
|
|
360
|
-
When multiple strategies are configured on a route via `
|
|
665
|
+
When multiple strategies are configured on a route via `authenticate: { strategies: ['jwt', 'basic'] }`:
|
|
666
|
+
|
|
667
|
+
```mermaid
|
|
668
|
+
flowchart TD
|
|
669
|
+
REQ["Request arrives"] --> MODE{"mode?"}
|
|
670
|
+
|
|
671
|
+
MODE -->|"any (default)"| ANY["Try strategies in order"]
|
|
672
|
+
ANY --> S1{"Strategy 1"}
|
|
673
|
+
S1 -->|"Success"| WIN["Set user, continue"]
|
|
674
|
+
S1 -->|"Fail"| S2{"Strategy 2"}
|
|
675
|
+
S2 -->|"Success"| WIN
|
|
676
|
+
S2 -->|"Fail"| FAIL_ANY["401: Tried strategies: jwt, basic"]
|
|
677
|
+
|
|
678
|
+
MODE -->|"all"| ALL["Run all strategies"]
|
|
679
|
+
ALL --> A1{"Strategy 1"}
|
|
680
|
+
A1 -->|"Fail"| FAIL_ALL["Exception propagates"]
|
|
681
|
+
A1 -->|"Pass"| A2{"Strategy 2"}
|
|
682
|
+
A2 -->|"Fail"| FAIL_ALL
|
|
683
|
+
A2 -->|"Pass"| CHECK{"userId?"}
|
|
684
|
+
CHECK -->|"Yes"| WIN2["Set user, continue"]
|
|
685
|
+
CHECK -->|"No"| FAIL_ID["401: Failed to identify user"]
|
|
686
|
+
|
|
687
|
+
style WIN fill:#d4edda,stroke:#28a745
|
|
688
|
+
style WIN2 fill:#d4edda,stroke:#28a745
|
|
689
|
+
style FAIL_ANY fill:#f8d7da,stroke:#dc3545
|
|
690
|
+
style FAIL_ALL fill:#f8d7da,stroke:#dc3545
|
|
691
|
+
style FAIL_ID fill:#f8d7da,stroke:#dc3545
|
|
692
|
+
```
|
|
361
693
|
|
|
362
694
|
**`any` mode (default):**
|
|
363
695
|
- Strategies are tried in the order specified
|
|
364
696
|
- The first successful strategy wins
|
|
697
|
+
- Errors from failing strategies are **discarded** (logged at debug level)
|
|
365
698
|
- If all strategies fail, a `401 Unauthorized` error is thrown listing all tried strategies
|
|
366
699
|
- **Use case:** Fallback authentication (try JWT, fallback to Basic)
|
|
367
700
|
|
|
@@ -374,41 +707,62 @@ When multiple strategies are configured on a route via `authStrategies: ['jwt',
|
|
|
374
707
|
> [!TIP]
|
|
375
708
|
> 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
709
|
|
|
377
|
-
## Token Encryption
|
|
710
|
+
## Token Encryption (Optional AES)
|
|
711
|
+
|
|
712
|
+
```mermaid
|
|
713
|
+
flowchart LR
|
|
714
|
+
subgraph GENERATE["generate() — Token Creation"]
|
|
715
|
+
direction TB
|
|
716
|
+
P["Payload: { userId, roles, email }"]
|
|
717
|
+
P --> CHECK1{"applicationSecret?"}
|
|
718
|
+
CHECK1 -->|"Yes"| ENC["encryptPayload()"]
|
|
719
|
+
ENC --> E1["Keep: iss, sub, aud, exp, iat"]
|
|
720
|
+
ENC --> E2["Encrypt keys + values"]
|
|
721
|
+
CHECK1 -->|"No"| PLAIN1["Use payload as-is"]
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
subgraph VERIFY["verify() — Token Verification"]
|
|
725
|
+
direction TB
|
|
726
|
+
T["Verified JWT payload"]
|
|
727
|
+
T --> CHECK2{"applicationSecret?"}
|
|
728
|
+
CHECK2 -->|"Yes"| DEC["decryptPayload()"]
|
|
729
|
+
DEC --> D1["Extract: iss, sub, aud, exp, iat"]
|
|
730
|
+
DEC --> D2["Decrypt keys + values"]
|
|
731
|
+
CHECK2 -->|"No"| PLAIN2["Use payload as-is"]
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
style GENERATE fill:#e8f4fd,stroke:#0d6efd
|
|
735
|
+
style VERIFY fill:#d4edda,stroke:#28a745
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
JWT payloads can optionally be encrypted field-by-field using AES (default `aes-256-cbc`) via the `@venizia/ignis-helpers` AES utility. This is configured by providing `applicationSecret` in the service options.
|
|
378
739
|
|
|
379
|
-
|
|
740
|
+
> [!NOTE]
|
|
741
|
+
> 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.
|
|
380
742
|
|
|
381
|
-
**Encryption process:**
|
|
743
|
+
**Encryption process (when `applicationSecret` is provided):**
|
|
382
744
|
1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are preserved as-is
|
|
383
745
|
2. All other fields have both their **keys** and **values** AES-encrypted
|
|
384
746
|
3. The `roles` field is serialized as `id|identifier|priority` pipe-separated strings before encryption
|
|
385
747
|
4. `null` and `undefined` values are skipped during encryption
|
|
386
748
|
|
|
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
749
|
**Decryption process:**
|
|
397
|
-
1.
|
|
398
|
-
2.
|
|
399
|
-
3.
|
|
750
|
+
1. If AES is not configured (`this.aes` is null), the payload is returned as-is
|
|
751
|
+
2. Standard JWT fields are extracted directly
|
|
752
|
+
3. Encrypted fields have their keys decrypted first, then their values
|
|
753
|
+
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()`)
|
|
400
754
|
|
|
401
755
|
> [!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.
|
|
756
|
+
> 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`.
|
|
403
757
|
|
|
404
758
|
## Hono Context Extension
|
|
405
759
|
|
|
406
|
-
The Authentication module extends Hono's `ContextVariableMap` to provide type-safe access to auth data:
|
|
760
|
+
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:
|
|
407
761
|
|
|
408
762
|
```typescript
|
|
409
763
|
declare module 'hono' {
|
|
410
|
-
interface ContextVariableMap
|
|
411
|
-
[Authentication.CURRENT_USER]:
|
|
764
|
+
interface ContextVariableMap {
|
|
765
|
+
[Authentication.CURRENT_USER]: IAuthUser;
|
|
412
766
|
[Authentication.AUDIT_USER_ID]: IdType;
|
|
413
767
|
}
|
|
414
768
|
}
|
|
@@ -451,32 +805,9 @@ const SignInRequestSchema = z.object({
|
|
|
451
805
|
type TSignInRequest = z.infer<typeof SignInRequestSchema>;
|
|
452
806
|
```
|
|
453
807
|
|
|
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
808
|
### SignUpRequestSchema
|
|
478
809
|
|
|
479
|
-
The built-in schema uses a **flat structure
|
|
810
|
+
The built-in schema uses a **flat structure**:
|
|
480
811
|
|
|
481
812
|
```typescript
|
|
482
813
|
const SignUpRequestSchema = z.object({
|
|
@@ -487,25 +818,8 @@ const SignUpRequestSchema = z.object({
|
|
|
487
818
|
type TSignUpRequest = z.infer<typeof SignUpRequestSchema>;
|
|
488
819
|
```
|
|
489
820
|
|
|
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
821
|
### ChangePasswordRequestSchema
|
|
506
822
|
|
|
507
|
-
The built-in schema uses scheme-based credential naming with a `userId` field:
|
|
508
|
-
|
|
509
823
|
```typescript
|
|
510
824
|
const ChangePasswordRequestSchema = z.object({
|
|
511
825
|
scheme: z.string(),
|
|
@@ -517,24 +831,6 @@ const ChangePasswordRequestSchema = z.object({
|
|
|
517
831
|
type TChangePasswordRequest = z.infer<typeof ChangePasswordRequestSchema>;
|
|
518
832
|
```
|
|
519
833
|
|
|
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
834
|
### JWTTokenPayloadSchema
|
|
539
835
|
|
|
540
836
|
Exported from the controller factory module. Used as the response schema for the `/who-am-i` endpoint:
|
|
@@ -555,36 +851,6 @@ const JWTTokenPayloadSchema = z.object({
|
|
|
555
851
|
});
|
|
556
852
|
```
|
|
557
853
|
|
|
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
854
|
## API Endpoints
|
|
589
855
|
|
|
590
856
|
The built-in auth controller is created by the `defineAuthController()` factory function and is only available when `useAuthController: true` is set in `REST_OPTIONS`.
|
|
@@ -595,9 +861,10 @@ The built-in auth controller is created by the `defineAuthController()` factory
|
|
|
595
861
|
| `POST` | `/auth/sign-up` | Configurable | Create a new user account |
|
|
596
862
|
| `POST` | `/auth/change-password` | JWT | Change the authenticated user's password |
|
|
597
863
|
| `GET` | `/auth/who-am-i` | JWT | Return the current user's JWT payload |
|
|
864
|
+
| `GET` | `/certs` | No | JWKS endpoint (JWKS Issuer mode only) |
|
|
598
865
|
|
|
599
866
|
> [!NOTE]
|
|
600
|
-
> The base path `/auth` is configurable via `controllerOpts.restPath`.
|
|
867
|
+
> The base path `/auth` is configurable via `controllerOpts.restPath`. The `/certs` path is configurable via `rest.path` in `IJWKSIssuerOptions`. The `/certs` endpoint is intentionally unauthenticated — it serves the public keys needed by external verifiers.
|
|
601
868
|
|
|
602
869
|
### POST /auth/sign-in
|
|
603
870
|
|
|
@@ -607,171 +874,232 @@ The built-in auth controller is created by the `defineAuthController()` factory
|
|
|
607
874
|
|
|
608
875
|
Uses `SignInRequestSchema` by default, or a custom schema via `payload.signIn.request.schema`.
|
|
609
876
|
|
|
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
877
|
**Response 200:**
|
|
626
878
|
|
|
627
879
|
Uses `payload.signIn.response.schema` if provided, otherwise `AnyObjectSchema`.
|
|
628
880
|
|
|
629
881
|
```json
|
|
630
882
|
{
|
|
631
|
-
"token": "
|
|
883
|
+
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..."
|
|
632
884
|
}
|
|
633
885
|
```
|
|
634
886
|
|
|
635
|
-
|
|
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
|
-
});
|
|
887
|
+
### POST /auth/sign-up
|
|
645
888
|
|
|
646
|
-
|
|
647
|
-
```
|
|
889
|
+
**Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
|
|
648
890
|
|
|
891
|
+
When `requireAuthenticatedSignUp: true`, requires JWT authentication. When `false`, the endpoint is public.
|
|
649
892
|
|
|
650
|
-
### POST /auth/
|
|
893
|
+
### POST /auth/change-password
|
|
651
894
|
|
|
652
|
-
**Authentication:**
|
|
895
|
+
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
653
896
|
|
|
654
|
-
|
|
897
|
+
### GET /auth/who-am-i
|
|
655
898
|
|
|
656
|
-
**
|
|
899
|
+
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
657
900
|
|
|
658
|
-
|
|
901
|
+
Returns the current user's decrypted JWT payload directly from context.
|
|
659
902
|
|
|
660
|
-
|
|
661
|
-
```typescript
|
|
903
|
+
```json
|
|
662
904
|
{
|
|
663
|
-
|
|
664
|
-
|
|
905
|
+
"userId": "123",
|
|
906
|
+
"roles": [
|
|
907
|
+
{ "id": "1", "identifier": "admin", "priority": 0 }
|
|
908
|
+
],
|
|
909
|
+
"clientId": "optional-client-id",
|
|
910
|
+
"provider": "optional-provider",
|
|
911
|
+
"email": "user@example.com"
|
|
665
912
|
}
|
|
666
913
|
```
|
|
667
914
|
|
|
668
|
-
|
|
915
|
+
### GET /certs (JWKS Issuer Only)
|
|
916
|
+
|
|
917
|
+
**Authentication:** None (intentionally public)
|
|
669
918
|
|
|
670
|
-
|
|
919
|
+
Returns the JSON Web Key Set for external verifiers.
|
|
671
920
|
|
|
672
921
|
```json
|
|
673
922
|
{
|
|
674
|
-
"
|
|
675
|
-
|
|
923
|
+
"keys": [
|
|
924
|
+
{
|
|
925
|
+
"kty": "EC",
|
|
926
|
+
"kid": "my-key-id-1",
|
|
927
|
+
"use": "sig",
|
|
928
|
+
"alg": "ES256",
|
|
929
|
+
"crv": "P-256",
|
|
930
|
+
"x": "...",
|
|
931
|
+
"y": "..."
|
|
932
|
+
}
|
|
933
|
+
]
|
|
676
934
|
}
|
|
677
935
|
```
|
|
678
936
|
|
|
679
|
-
**
|
|
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
|
-
});
|
|
937
|
+
**Cache headers:** `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`
|
|
689
938
|
|
|
690
|
-
|
|
691
|
-
```
|
|
939
|
+
## Auth Entity Column Helpers
|
|
692
940
|
|
|
941
|
+
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.
|
|
693
942
|
|
|
694
|
-
|
|
943
|
+
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'`.
|
|
695
944
|
|
|
696
|
-
|
|
945
|
+
### extraUserColumns
|
|
697
946
|
|
|
698
|
-
**
|
|
947
|
+
**Import:** `import { extraUserColumns } from '@venizia/ignis';`
|
|
699
948
|
|
|
700
|
-
|
|
949
|
+
**Signature:** `extraUserColumns(opts?: { idType: 'string' | 'number' })`
|
|
701
950
|
|
|
702
|
-
Default
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
```
|
|
951
|
+
| Column | DB Column | Type | Nullable | Default | Description |
|
|
952
|
+
|--------|-----------|------|----------|---------|-------------|
|
|
953
|
+
| `realm` | `realm` | `text` | Yes | `''` | Multi-tenancy realm identifier |
|
|
954
|
+
| `status` | `status` | `text` | No | `UserStatuses.UNKNOWN` | User lifecycle status |
|
|
955
|
+
| `type` | `type` | `text` | No | `UserTypes.SYSTEM` | User type (`SYSTEM` or `LINKED`) |
|
|
956
|
+
| `activatedAt` | `activated_at` | `timestamp (tz)` | Yes | `null` | When the user was activated |
|
|
957
|
+
| `lastLoginAt` | `last_login_at` | `timestamp (tz)` | Yes | `null` | Last login timestamp |
|
|
958
|
+
| `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent user ID (type depends on `idType`) |
|
|
711
959
|
|
|
712
|
-
|
|
960
|
+
### extraRoleColumns
|
|
713
961
|
|
|
714
|
-
|
|
962
|
+
**Import:** `import { extraRoleColumns } from '@venizia/ignis';`
|
|
715
963
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
964
|
+
**Signature:** `extraRoleColumns()`
|
|
965
|
+
|
|
966
|
+
| Column | DB Column | Type | Nullable | Default | Description |
|
|
967
|
+
|--------|-----------|------|----------|---------|-------------|
|
|
968
|
+
| `identifier` | `identifier` | `text` (unique) | No | -- | Unique role identifier (e.g., `'admin'`, `'editor'`) |
|
|
969
|
+
| `name` | `name` | `text` | No | -- | Human-readable role name |
|
|
970
|
+
| `description` | `description` | `text` | Yes | `null` | Optional role description |
|
|
971
|
+
| `priority` | `priority` | `integer` | No | -- | Role priority (lower = higher priority) |
|
|
972
|
+
| `status` | `status` | `text` | No | `RoleStatuses.ACTIVATED` | Role lifecycle status |
|
|
973
|
+
|
|
974
|
+
### extraPermissionColumns
|
|
975
|
+
|
|
976
|
+
**Import:** `import { extraPermissionColumns } from '@venizia/ignis';`
|
|
977
|
+
|
|
978
|
+
**Signature:** `extraPermissionColumns(opts?: { idType: 'string' | 'number' })`
|
|
979
|
+
|
|
980
|
+
| Column | DB Column | Type | Nullable | Default | Description |
|
|
981
|
+
|--------|-----------|------|----------|---------|-------------|
|
|
982
|
+
| `code` | `code` | `text` (unique) | No | -- | Unique permission code |
|
|
983
|
+
| `name` | `name` | `text` | No | -- | Permission display name |
|
|
984
|
+
| `subject` | `subject` | `text` | No | -- | Permission subject (e.g., `'User'`, `'Order'`) |
|
|
985
|
+
| `action` | `action` | `text` | No | -- | Permitted action (e.g., `'read'`, `'write'`) |
|
|
986
|
+
| `scope` | `scope` | `text` | No | -- | Permission scope |
|
|
987
|
+
| `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent permission ID (type depends on `idType`) |
|
|
988
|
+
|
|
989
|
+
### extraPolicyDefinitionColumns
|
|
990
|
+
|
|
991
|
+
**Import:** `import { extraPolicyDefinitionColumns } from '@venizia/ignis';`
|
|
992
|
+
|
|
993
|
+
**Signature:** `extraPolicyDefinitionColumns(opts?: { idType: 'string' | 'number' })`
|
|
994
|
+
|
|
995
|
+
Provides columns for Casbin-style policy definitions that map subjects (users/roles) to targets (resources/permissions).
|
|
996
|
+
|
|
997
|
+
| Column | DB Column | Type | Nullable | Default | Description |
|
|
998
|
+
|--------|-----------|------|----------|---------|-------------|
|
|
999
|
+
| `variant` | `variant` | `text` | No | -- | Policy variant (e.g., `'p'` for policy, `'g'` for grouping) |
|
|
1000
|
+
| `subjectType` | `subject_type` | `text` | No | -- | Type of subject (e.g., `'user'`, `'role'`) |
|
|
1001
|
+
| `targetType` | `target_type` | `text` | No | -- | Type of target (e.g., `'permission'`, `'role'`) |
|
|
1002
|
+
| `action` | `action` | `text` | Yes | `null` | Policy action |
|
|
1003
|
+
| `effect` | `effect` | `text` | Yes | `null` | Policy effect (e.g., `'allow'`, `'deny'`) |
|
|
1004
|
+
| `domain` | `domain` | `text` | Yes | `null` | Policy domain for multi-tenancy |
|
|
1005
|
+
| `subjectId` | `subject_id` | `text` or `integer` | No | -- | Subject ID (type depends on `idType`) |
|
|
1006
|
+
| `targetId` | `target_id` | `text` or `integer` | No | -- | Target ID (type depends on `idType`) |
|
|
1007
|
+
|
|
1008
|
+
### Usage Example
|
|
721
1009
|
|
|
722
|
-
**Example:**
|
|
723
1010
|
```typescript
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1011
|
+
import { pgTable, serial, text } from 'drizzle-orm/pg-core';
|
|
1012
|
+
import {
|
|
1013
|
+
extraUserColumns,
|
|
1014
|
+
extraRoleColumns,
|
|
1015
|
+
extraPermissionColumns,
|
|
1016
|
+
extraPolicyDefinitionColumns,
|
|
1017
|
+
} from '@venizia/ignis';
|
|
1018
|
+
import { withSerialId, withTimestamps } from '@venizia/ignis';
|
|
1019
|
+
|
|
1020
|
+
// User table
|
|
1021
|
+
export const users = pgTable('users', {
|
|
1022
|
+
...withSerialId(),
|
|
1023
|
+
...withTimestamps(),
|
|
1024
|
+
...extraUserColumns(),
|
|
1025
|
+
username: text('username').unique().notNull(),
|
|
1026
|
+
passwordHash: text('password_hash').notNull(),
|
|
1027
|
+
email: text('email').unique(),
|
|
736
1028
|
});
|
|
737
1029
|
|
|
738
|
-
|
|
739
|
-
|
|
1030
|
+
// Role table
|
|
1031
|
+
export const roles = pgTable('roles', {
|
|
1032
|
+
...withSerialId(),
|
|
1033
|
+
...withTimestamps(),
|
|
1034
|
+
...extraRoleColumns(),
|
|
1035
|
+
});
|
|
740
1036
|
|
|
1037
|
+
// Permission table
|
|
1038
|
+
export const permissions = pgTable('permissions', {
|
|
1039
|
+
...withSerialId(),
|
|
1040
|
+
...withTimestamps(),
|
|
1041
|
+
...extraPermissionColumns(),
|
|
1042
|
+
});
|
|
741
1043
|
|
|
742
|
-
|
|
1044
|
+
// Policy definition table (Casbin-style policies)
|
|
1045
|
+
export const policyDefinitions = pgTable('policy_definitions', {
|
|
1046
|
+
...withSerialId(),
|
|
1047
|
+
...withTimestamps(),
|
|
1048
|
+
...extraPolicyDefinitionColumns(),
|
|
1049
|
+
});
|
|
743
1050
|
|
|
744
|
-
|
|
1051
|
+
// With UUID-based IDs
|
|
1052
|
+
export const uuidUsers = pgTable('users', {
|
|
1053
|
+
...withUuidId(),
|
|
1054
|
+
...withTimestamps(),
|
|
1055
|
+
...extraUserColumns({ idType: 'string' }),
|
|
1056
|
+
username: text('username').unique().notNull(),
|
|
1057
|
+
});
|
|
745
1058
|
|
|
746
|
-
|
|
1059
|
+
export const uuidPolicies = pgTable('policy_definitions', {
|
|
1060
|
+
...withUuidId(),
|
|
1061
|
+
...withTimestamps(),
|
|
1062
|
+
...extraPolicyDefinitionColumns({ idType: 'string' }),
|
|
1063
|
+
});
|
|
1064
|
+
```
|
|
747
1065
|
|
|
748
|
-
|
|
1066
|
+
### Context Variables
|
|
749
1067
|
|
|
750
|
-
|
|
1068
|
+
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()`.
|
|
751
1069
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1070
|
+
| Constant | Key String | Type | Description |
|
|
1071
|
+
|----------|-----------|------|-------------|
|
|
1072
|
+
| `Authentication.CURRENT_USER` | `'auth.current.user'` | `IAuthUser` | The authenticated user payload, set after successful authentication |
|
|
1073
|
+
| `Authentication.AUDIT_USER_ID` | `'audit.user.id'` | `IdType` | The authenticated user's ID, extracted from the user payload |
|
|
1074
|
+
| `Authentication.SKIP_AUTHENTICATION` | `'authentication.skip'` | `boolean` | Set to `true` in a preceding middleware to bypass authentication for the current request |
|
|
1075
|
+
| `Authorization.RULES` | `'authorization.rules'` | `unknown` | Authorization rules resolved for the current request |
|
|
1076
|
+
| `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | `boolean` | Set to `true` to bypass authorization checks for the current request |
|
|
1077
|
+
|
|
1078
|
+
**Reading context variables in a handler:**
|
|
1079
|
+
|
|
1080
|
+
```typescript
|
|
1081
|
+
import { Authentication, Authorization } from '@venizia/ignis';
|
|
1082
|
+
|
|
1083
|
+
// Inside a route handler
|
|
1084
|
+
const currentUser = c.get(Authentication.CURRENT_USER);
|
|
1085
|
+
const userId = c.get(Authentication.AUDIT_USER_ID);
|
|
1086
|
+
const skipAuth = c.get(Authentication.SKIP_AUTHENTICATION);
|
|
1087
|
+
const authzRules = c.get(Authorization.RULES);
|
|
762
1088
|
```
|
|
763
1089
|
|
|
764
|
-
**
|
|
1090
|
+
**Skipping auth dynamically from middleware:**
|
|
1091
|
+
|
|
765
1092
|
```typescript
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
headers: {
|
|
769
|
-
'Authorization': `Bearer ${token}`,
|
|
770
|
-
},
|
|
771
|
-
});
|
|
1093
|
+
import { Authentication, Authorization } from '@venizia/ignis';
|
|
1094
|
+
import { createMiddleware } from 'hono/factory';
|
|
772
1095
|
|
|
773
|
-
const
|
|
774
|
-
|
|
1096
|
+
const apiKeyMiddleware = createMiddleware(async (c, next) => {
|
|
1097
|
+
if (c.req.header('X-API-Key') === process.env.INTERNAL_API_KEY) {
|
|
1098
|
+
c.set(Authentication.SKIP_AUTHENTICATION, true);
|
|
1099
|
+
c.set(Authorization.SKIP_AUTHORIZATION, true);
|
|
1100
|
+
}
|
|
1101
|
+
return next();
|
|
1102
|
+
});
|
|
775
1103
|
```
|
|
776
1104
|
|
|
777
1105
|
## See Also
|