@venizia/ignis-docs 0.2.0 → 0.2.1-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 +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- 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 +27 -3
- 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 +8 -4
- 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 +247 -153
- 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 +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- 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/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- 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 +58 -218
- package/content/references/utilities/retry.md +139 -0
- 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/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +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 +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +24 -23
|
@@ -1,89 +1,87 @@
|
|
|
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
|
+
## Find what you need
|
|
12
|
+
|
|
13
|
+
| You want to | Go to |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Require auth on a route, or make one public | [Securing routes](#securing-routes) |
|
|
16
|
+
| Implement sign-in, sign-up, or change-password | [Implementing IAuthService](#implementing-iauthservice) |
|
|
17
|
+
| Split issuer and verifier across services | [JWKS microservice patterns](#jwks-microservice-patterns) |
|
|
18
|
+
| See what happens on each request, step by step | [Auth flows](#auth-flows) |
|
|
19
|
+
| Accept both JWT and Basic on one route | [Multi-strategy authentication](#multi-strategy-authentication) |
|
|
20
|
+
| Encrypt JWT payload fields | [Token encryption (optional AES)](#token-encryption-optional-aes) |
|
|
21
|
+
| Read `CURRENT_USER` in a handler, with types | [Hono context extension](#hono-context-extension) |
|
|
22
|
+
| Call the built-in `/auth` endpoints | [API endpoints](#api-endpoints) |
|
|
23
|
+
| Add auth columns to a Drizzle table | [Entity column helpers](#entity-column-helpers) |
|
|
24
|
+
|
|
25
|
+
## Securing routes
|
|
26
|
+
|
|
27
|
+
**Require one strategy.** Add `authenticate` to the route config.
|
|
8
28
|
|
|
9
29
|
```typescript
|
|
10
|
-
// Single strategy
|
|
11
30
|
const SECURE_ROUTE_CONFIG = {
|
|
12
31
|
path: '/secure-data',
|
|
13
32
|
method: HTTP.Methods.GET,
|
|
14
33
|
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
15
|
-
responses: jsonResponse({
|
|
16
|
-
description: 'Protected data',
|
|
17
|
-
schema: z.object({ message: z.string() }),
|
|
18
|
-
}),
|
|
34
|
+
responses: jsonResponse({ description: 'Protected data', schema: z.object({ message: z.string() }) }),
|
|
19
35
|
} as const;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Accept multiple strategies with fallback.** `mode: 'any'` (default) tries each strategy in order. The first success wins.
|
|
20
39
|
|
|
21
|
-
|
|
40
|
+
```typescript
|
|
22
41
|
const FALLBACK_AUTH_CONFIG = {
|
|
23
42
|
path: '/api/data',
|
|
24
43
|
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
|
-
}),
|
|
44
|
+
authenticate: { strategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC], mode: AuthenticationModes.ANY },
|
|
45
|
+
responses: jsonResponse({ description: 'Data via JWT or Basic', schema: z.object({ data: z.any() }) }),
|
|
33
46
|
} as const;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Make a route public.** `skip: true` bypasses authentication entirely.
|
|
34
50
|
|
|
35
|
-
|
|
51
|
+
```typescript
|
|
36
52
|
const PUBLIC_ROUTE_CONFIG = {
|
|
37
53
|
path: '/public',
|
|
38
54
|
method: HTTP.Methods.GET,
|
|
39
55
|
authenticate: { skip: true },
|
|
40
|
-
responses: jsonResponse({
|
|
41
|
-
description: 'Public endpoint',
|
|
42
|
-
schema: z.object({ message: z.string() }),
|
|
43
|
-
}),
|
|
56
|
+
responses: jsonResponse({ description: 'Public endpoint', schema: z.object({ message: z.string() }) }),
|
|
44
57
|
} as const;
|
|
45
58
|
```
|
|
46
59
|
|
|
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:
|
|
60
|
+
**Use `authenticate()` as raw Hono middleware.** Use this outside route configs - for a plain Hono sub-app, for example.
|
|
50
61
|
|
|
51
62
|
```typescript
|
|
52
63
|
import { authenticate, Authentication, AuthenticationModes } from '@venizia/ignis';
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
const authMiddleware = authenticate({
|
|
56
|
-
strategies: [Authentication.STRATEGY_JWT],
|
|
57
|
-
mode: AuthenticationModes.ANY,
|
|
58
|
-
});
|
|
65
|
+
const authMiddleware = authenticate({ strategies: [Authentication.STRATEGY_JWT], mode: AuthenticationModes.ANY });
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
app.get('/protected', authMiddleware, (c) => {
|
|
67
|
+
app.get('/protected', authMiddleware, c => {
|
|
62
68
|
const user = c.get(Authentication.CURRENT_USER);
|
|
63
69
|
return c.json({ userId: user.userId });
|
|
64
70
|
});
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
After authentication, the user payload is available on the Hono `Context`:
|
|
73
|
+
**Read the authenticated user in a handler.**
|
|
70
74
|
|
|
71
75
|
```typescript
|
|
72
|
-
import { Context } from 'hono';
|
|
73
76
|
import { Authentication, IJWTTokenPayload } from '@venizia/ignis';
|
|
74
77
|
|
|
75
|
-
// Inside a route handler
|
|
76
78
|
const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload | undefined;
|
|
77
|
-
|
|
78
79
|
if (user) {
|
|
79
|
-
console.log('
|
|
80
|
-
console.log('User roles:', user.roles);
|
|
80
|
+
console.log('User ID:', user.userId, 'Roles:', user.roles);
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Use `Authentication.SKIP_AUTHENTICATION` to dynamically skip auth in middleware:
|
|
84
|
+
**Skip authentication dynamically from a preceding middleware.** Useful for internal API keys or webhooks.
|
|
87
85
|
|
|
88
86
|
```typescript
|
|
89
87
|
import { Authentication } from '@venizia/ignis';
|
|
@@ -97,35 +95,23 @@ const conditionalAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
97
95
|
});
|
|
98
96
|
```
|
|
99
97
|
|
|
100
|
-
## Implementing
|
|
98
|
+
## Implementing IAuthService
|
|
101
99
|
|
|
102
|
-
The `
|
|
100
|
+
The built-in auth controller (`useAuthController: true`) delegates every route to a service you provide. That service implements `IAuthService`.
|
|
103
101
|
|
|
104
|
-
|
|
102
|
+
**JWS-backed service.**
|
|
105
103
|
|
|
106
104
|
```typescript
|
|
107
105
|
import {
|
|
108
|
-
BaseService,
|
|
109
|
-
|
|
110
|
-
IAuthService,
|
|
111
|
-
IJWTTokenPayload,
|
|
112
|
-
JWSTokenService,
|
|
113
|
-
BindingKeys,
|
|
114
|
-
BindingNamespaces,
|
|
115
|
-
TSignInRequest,
|
|
116
|
-
TContext,
|
|
106
|
+
BaseService, inject, IAuthService, IJWTTokenPayload, JWSTokenService,
|
|
107
|
+
BindingKeys, BindingNamespaces, TSignInRequest, TContext,
|
|
117
108
|
} from '@venizia/ignis';
|
|
118
109
|
import { getError } from '@venizia/ignis-helpers';
|
|
119
110
|
import { Env } from 'hono';
|
|
120
111
|
|
|
121
112
|
export class AuthenticationService extends BaseService implements IAuthService {
|
|
122
113
|
constructor(
|
|
123
|
-
@inject({
|
|
124
|
-
key: BindingKeys.build({
|
|
125
|
-
namespace: BindingNamespaces.SERVICE,
|
|
126
|
-
key: JWSTokenService.name,
|
|
127
|
-
}),
|
|
128
|
-
})
|
|
114
|
+
@inject({ key: BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: JWSTokenService.name }) })
|
|
129
115
|
private _tokenService: JWSTokenService,
|
|
130
116
|
) {
|
|
131
117
|
super({ scope: AuthenticationService.name });
|
|
@@ -139,611 +125,200 @@ export class AuthenticationService extends BaseService implements IAuthService {
|
|
|
139
125
|
throw getError({ message: 'Invalid credentials' });
|
|
140
126
|
}
|
|
141
127
|
|
|
142
|
-
const payload: IJWTTokenPayload = {
|
|
143
|
-
userId: user.id,
|
|
144
|
-
roles: user.roles,
|
|
145
|
-
};
|
|
146
|
-
|
|
128
|
+
const payload: IJWTTokenPayload = { userId: user.id, roles: user.roles };
|
|
147
129
|
const token = await this._tokenService.generate({ payload });
|
|
148
130
|
return { token };
|
|
149
131
|
}
|
|
150
132
|
|
|
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
|
-
}
|
|
133
|
+
async signUp(context: TContext<Env>, opts: any): Promise<any> { /* your logic */ }
|
|
134
|
+
async changePassword(context: TContext<Env>, opts: any): Promise<any> { /* your logic */ }
|
|
158
135
|
}
|
|
159
136
|
```
|
|
160
137
|
|
|
161
|
-
|
|
138
|
+
**JWKS-backed service.** Same shape, inject `JWKSIssuerTokenService` instead.
|
|
162
139
|
|
|
163
140
|
```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';
|
|
141
|
+
constructor(
|
|
142
|
+
@inject({ key: BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: JWKSIssuerTokenService.name }) })
|
|
143
|
+
private _tokenService: JWKSIssuerTokenService,
|
|
144
|
+
) { super({ scope: AuthenticationService.name }); }
|
|
145
|
+
```
|
|
177
146
|
|
|
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
|
-
}
|
|
147
|
+
**Implement `refreshToken` (optional).** It re-issues a token from the caller's currently valid one. IGNIS has no separate refresh token.
|
|
190
148
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
149
|
+
```typescript
|
|
150
|
+
async refreshToken(context: TContext<Env>): Promise<{ token: string }> {
|
|
151
|
+
const currentUser = context.get(Authentication.CURRENT_USER);
|
|
152
|
+
const token = await this._tokenService.generate({ payload: currentUser });
|
|
153
|
+
return { token };
|
|
154
|
+
}
|
|
155
|
+
```
|
|
194
156
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
roles: user.roles,
|
|
198
|
-
};
|
|
157
|
+
> [!NOTE]
|
|
158
|
+
> IGNIS does not enforce rotation or revocation policy. If you need to invalidate old tokens after refresh, implement that inside your `refreshToken`.
|
|
199
159
|
|
|
200
|
-
|
|
201
|
-
return { token };
|
|
202
|
-
}
|
|
160
|
+
**Implement `getUserInformation` (optional).** Backs both `GET /me` and `GET /who-am-i?withUserInformation=true`.
|
|
203
161
|
|
|
204
|
-
|
|
162
|
+
```typescript
|
|
163
|
+
async getUserInformation(context: TContext<Env>, _opts: AnyObject): Promise<AnyObject> {
|
|
164
|
+
const currentUser = context.get(Authentication.CURRENT_USER);
|
|
165
|
+
return this.userRepository.findById({ id: currentUser.userId });
|
|
205
166
|
}
|
|
206
167
|
```
|
|
207
168
|
|
|
208
|
-
## JWKS
|
|
209
|
-
|
|
210
|
-
### Issuer + Verifier Architecture
|
|
169
|
+
## JWKS microservice patterns
|
|
211
170
|
|
|
212
|
-
|
|
171
|
+
**Issuer + verifier split.** One service signs, others only verify - no shared secret to distribute.
|
|
213
172
|
|
|
214
173
|
```mermaid
|
|
215
174
|
flowchart LR
|
|
216
175
|
CLIENT["Client App"]
|
|
217
|
-
|
|
218
176
|
subgraph AUTH["Auth Service (JWKS Issuer)"]
|
|
219
177
|
SIGNIN["POST /auth/sign-in"]
|
|
220
178
|
CERTS["GET /certs"]
|
|
221
179
|
end
|
|
222
|
-
|
|
223
180
|
subgraph API["API Service (JWKS Verifier)"]
|
|
224
181
|
DATA["GET /api/data"]
|
|
225
182
|
end
|
|
226
|
-
|
|
227
183
|
CLIENT -->|"1. Sign in"| SIGNIN
|
|
228
184
|
SIGNIN -->|"2. JWT token"| CLIENT
|
|
229
185
|
CLIENT -->|"3. Request + Bearer token"| DATA
|
|
230
186
|
DATA -->|"4. Fetch JWKS"| CERTS
|
|
231
187
|
CERTS -->|"5. Public keys"| DATA
|
|
232
188
|
DATA -->|"6. Verified response"| CLIENT
|
|
233
|
-
|
|
234
|
-
style AUTH fill:#e8f4fd,stroke:#0d6efd
|
|
235
|
-
style API fill:#d4edda,stroke:#28a745
|
|
236
189
|
```
|
|
237
190
|
|
|
238
|
-
**Auth Service (Issuer):**
|
|
239
191
|
```typescript
|
|
192
|
+
// Auth service (issuer)
|
|
240
193
|
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
241
194
|
standard: JOSEStandards.JWKS,
|
|
242
195
|
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,
|
|
196
|
+
mode: JWKSModes.ISSUER, algorithm: 'ES256',
|
|
197
|
+
keys: { driver: JWKSKeyDrivers.FILE, format: JWKSKeyFormats.PEM, private: './keys/private.pem', public: './keys/public.pem' },
|
|
198
|
+
kid: 'auth-key-1', getTokenExpiresFn: () => 86400,
|
|
253
199
|
},
|
|
254
200
|
});
|
|
255
|
-
```
|
|
256
201
|
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
202
|
+
// API service (verifier)
|
|
259
203
|
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
260
204
|
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
|
-
},
|
|
205
|
+
options: { mode: JWKSModes.VERIFIER, jwksUrl: 'https://auth-service.internal/certs', cacheTtlMs: 43_200_000, cooldownMs: 30_000 },
|
|
267
206
|
});
|
|
268
207
|
```
|
|
269
208
|
|
|
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:
|
|
209
|
+
**Generate ES256 or RS256 keys.**
|
|
298
210
|
|
|
299
211
|
```bash
|
|
300
|
-
#
|
|
212
|
+
# ES256
|
|
301
213
|
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
|
|
302
|
-
|
|
303
|
-
# Generate public key from private key
|
|
304
214
|
openssl ec -in private.pem -pubout -out public.pem
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Generate RS256 keys:
|
|
308
215
|
|
|
309
|
-
|
|
310
|
-
# Generate private key
|
|
216
|
+
# RS256
|
|
311
217
|
openssl genrsa -out private.pem 2048
|
|
312
|
-
|
|
313
|
-
# Generate public key from private key
|
|
314
218
|
openssl rsa -in private.pem -pubout -out public.pem
|
|
315
219
|
```
|
|
316
220
|
|
|
317
221
|
> [!WARNING]
|
|
318
|
-
> Never commit private keys to version control.
|
|
319
|
-
|
|
320
|
-
### Inline Keys (Text Driver)
|
|
222
|
+
> Never commit private keys to version control.
|
|
321
223
|
|
|
322
|
-
|
|
224
|
+
**Use inline keys instead of files.** For serverless or restricted-filesystem environments, switch `driver` to `text`.
|
|
323
225
|
|
|
324
226
|
```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,
|
|
227
|
+
keys: {
|
|
228
|
+
driver: JWKSKeyDrivers.TEXT,
|
|
229
|
+
format: JWKSKeyFormats.PEM,
|
|
230
|
+
private: process.env.JWKS_PRIVATE_KEY!, // PEM string from env
|
|
231
|
+
public: process.env.JWKS_PUBLIC_KEY!,
|
|
336
232
|
}
|
|
337
233
|
```
|
|
338
234
|
|
|
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).
|
|
235
|
+
**Share AES payload encryption across issuer and verifier.** Both sides need the identical `applicationSecret` - the verifier decrypts what the issuer encrypted.
|
|
444
236
|
|
|
445
237
|
```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`) |
|
|
459
|
-
|
|
460
|
-
### ID Type Polymorphism
|
|
238
|
+
// Issuer
|
|
239
|
+
{ mode: JWKSModes.ISSUER, /* ... */ applicationSecret: process.env.APP_ENV_APPLICATION_SECRET }
|
|
461
240
|
|
|
462
|
-
|
|
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' })
|
|
241
|
+
// Verifier - must match
|
|
242
|
+
{ mode: JWKSModes.VERIFIER, jwksUrl: '...', applicationSecret: process.env.APP_ENV_APPLICATION_SECRET }
|
|
472
243
|
```
|
|
473
244
|
|
|
474
|
-
##
|
|
475
|
-
|
|
476
|
-
The authentication module uses status classes from `@/common/statuses`. These extend `CommonStatuses` and provide lifecycle state management for auth entities.
|
|
477
|
-
|
|
478
|
-
### UserStatuses
|
|
245
|
+
## Auth flows
|
|
479
246
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
247
|
+
- **JWS:** extract Bearer token -> `jose.jwtVerify()` with the shared secret -> decrypt payload (if AES configured) -> set `CURRENT_USER`.
|
|
248
|
+
- **JWKS Issuer:** extract Bearer token -> `ensureInitialized()` (lazy-loads keys once) -> `jwtVerify()` with the public key -> decrypt payload -> set `CURRENT_USER`.
|
|
249
|
+
- **JWKS Verifier:** extract Bearer token -> `ensureInitialized()` (creates the remote JWKS verifier once) -> `jwtVerify()` with the remote JWKS -> decrypt payload -> set `CURRENT_USER`.
|
|
250
|
+
- **Basic:** decode `Authorization: Basic <base64>` -> call your `verifyCredentials` callback -> on `null`, throw `401`; on a user, set `CURRENT_USER`.
|
|
512
251
|
|
|
513
252
|
```mermaid
|
|
514
253
|
sequenceDiagram
|
|
515
254
|
participant C as Client
|
|
516
255
|
participant MW as Auth Middleware
|
|
517
|
-
participant S as
|
|
518
|
-
participant SVC as
|
|
519
|
-
participant JOSE as jose library
|
|
256
|
+
participant S as Strategy
|
|
257
|
+
participant SVC as TokenService
|
|
520
258
|
|
|
521
|
-
C->>MW: Request + Authorization
|
|
259
|
+
C->>MW: Request + Authorization header
|
|
522
260
|
MW->>S: authenticate(context)
|
|
523
261
|
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
|
|
262
|
+
SVC-->>S: { type, token } | { username, password }
|
|
263
|
+
S->>SVC: verify(...)
|
|
264
|
+
SVC-->>S: IAuthUser
|
|
530
265
|
S-->>MW: IAuthUser
|
|
531
266
|
MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
|
|
532
267
|
MW->>C: Continue to handler
|
|
533
268
|
```
|
|
534
269
|
|
|
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)`
|
|
270
|
+
## Multi-strategy authentication
|
|
541
271
|
|
|
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'] }`:
|
|
272
|
+
| Mode | Behavior | Use case |
|
|
273
|
+
|------|----------|----------|
|
|
274
|
+
| `'any'` (default) | Tried in order.<br>First success wins.<br>Failures are logged at debug, not returned.<br>All fail -> `401` listing the tried strategies. | Fallback auth (JWT primary, Basic for legacy clients) |
|
|
275
|
+
| `'all'` | Every strategy must pass.<br>First failure rejects immediately.<br>The **first** strategy's user payload is the identity source. | Multi-factor authentication |
|
|
652
276
|
|
|
653
277
|
```mermaid
|
|
654
278
|
flowchart TD
|
|
655
279
|
REQ["Request arrives"] --> MODE{"mode?"}
|
|
656
|
-
|
|
657
|
-
MODE -->|"any (default)"| ANY["Try strategies in order"]
|
|
658
|
-
ANY --> S1{"Strategy 1"}
|
|
280
|
+
MODE -->|"any"| S1{"Strategy 1"}
|
|
659
281
|
S1 -->|"Success"| WIN["Set user, continue"]
|
|
660
282
|
S1 -->|"Fail"| S2{"Strategy 2"}
|
|
661
283
|
S2 -->|"Success"| WIN
|
|
662
|
-
S2 -->|"Fail"| FAIL_ANY["401: Tried strategies
|
|
663
|
-
|
|
664
|
-
MODE -->|"all"| ALL["Run all strategies"]
|
|
665
|
-
ALL --> A1{"Strategy 1"}
|
|
284
|
+
S2 -->|"Fail"| FAIL_ANY["401: Tried strategies"]
|
|
285
|
+
MODE -->|"all"| A1{"Strategy 1"}
|
|
666
286
|
A1 -->|"Fail"| FAIL_ALL["Exception propagates"]
|
|
667
287
|
A1 -->|"Pass"| A2{"Strategy 2"}
|
|
668
288
|
A2 -->|"Fail"| FAIL_ALL
|
|
669
289
|
A2 -->|"Pass"| CHECK{"userId?"}
|
|
670
290
|
CHECK -->|"Yes"| WIN2["Set user, continue"]
|
|
671
291
|
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
292
|
```
|
|
679
293
|
|
|
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)
|
|
294
|
+
## Token encryption (optional AES)
|
|
686
295
|
|
|
687
|
-
|
|
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)
|
|
296
|
+
AES payload encryption is off by default. It only activates when you set `applicationSecret` on the JWS/JWKS options.
|
|
692
297
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
direction TB
|
|
702
|
-
P["Payload: { userId, roles, email }"]
|
|
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
|
|
298
|
+
| Aspect | Behavior |
|
|
299
|
+
|---|---|
|
|
300
|
+
| Default | Off. Without `applicationSecret`, payloads stay standard plaintext JWT. |
|
|
301
|
+
| Standard fields | `iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat` are never encrypted, on either side. |
|
|
302
|
+
| Other fields | Both the key and the value are AES-encrypted. `null` and `undefined` values are skipped. |
|
|
303
|
+
| Serialization | `JSON.stringify` by default. Opt in to `AuthenticationFieldCodecs.ROLES_CODEC` for `roles` - it serializes as pipe-separated `id\|identifier\|priority` strings instead. |
|
|
304
|
+
| Secret | Must stay constant. Changing `applicationSecret` invalidates every existing token. |
|
|
305
|
+
| Issuer/verifier match | Both sides need the identical secret, and identical `fieldCodecs` if you use them. |
|
|
709
306
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
style GENERATE fill:#e8f4fd,stroke:#0d6efd
|
|
721
|
-
style VERIFY fill:#d4edda,stroke:#28a745
|
|
307
|
+
```typescript
|
|
308
|
+
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
309
|
+
standard: JOSEStandards.JWS,
|
|
310
|
+
options: {
|
|
311
|
+
jwtSecret: process.env.APP_ENV_JWT_SECRET!,
|
|
312
|
+
getTokenExpiresFn: () => 86400,
|
|
313
|
+
applicationSecret: process.env.APP_ENV_APPLICATION_SECRET, // enables AES encryption
|
|
314
|
+
fieldCodecs: [AuthenticationFieldCodecs.ROLES_CODEC], // optional, opt-in
|
|
315
|
+
},
|
|
316
|
+
});
|
|
722
317
|
```
|
|
723
318
|
|
|
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
|
|
734
|
-
|
|
735
|
-
**Decryption process:**
|
|
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
|
|
319
|
+
## Hono context extension
|
|
745
320
|
|
|
746
|
-
The
|
|
321
|
+
The module augments Hono's `ContextVariableMap` (a plain interface, not generic) so `c.get()` is type-safe:
|
|
747
322
|
|
|
748
323
|
```typescript
|
|
749
324
|
declare module 'hono' {
|
|
@@ -754,435 +329,136 @@ declare module 'hono' {
|
|
|
754
329
|
}
|
|
755
330
|
```
|
|
756
331
|
|
|
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.
|
|
332
|
+
| Constant | Key string | Type | Description |
|
|
333
|
+
|----------|-----------|------|-------------|
|
|
334
|
+
| `Authentication.CURRENT_USER` | `auth.current.user` | `IAuthUser` | Authenticated user payload |
|
|
335
|
+
| `Authentication.AUDIT_USER_ID` | `audit.user.id` | `IdType` | Authenticated user's ID |
|
|
336
|
+
| `Authentication.SKIP_AUTHENTICATION` | `authentication.skip` | `boolean` | Set `true` to bypass authentication |
|
|
841
337
|
|
|
842
|
-
## API
|
|
338
|
+
## API endpoints
|
|
843
339
|
|
|
844
|
-
The built-in auth controller
|
|
340
|
+
The built-in auth controller exists only when `REST_OPTIONS.useAuthController: true` is set - see [Setup](./#common-tasks).
|
|
845
341
|
|
|
846
|
-
| Method | Path | Auth
|
|
342
|
+
| Method | Path | Auth required | Description |
|
|
847
343
|
|--------|------|---------------|-------------|
|
|
848
|
-
| `POST` | `/auth/sign-in` | No | Authenticate
|
|
849
|
-
| `POST` | `/auth/sign-up` | Configurable | Create a
|
|
344
|
+
| `POST` | `/auth/sign-in` | No | Authenticate, receive a JWT |
|
|
345
|
+
| `POST` | `/auth/sign-up` | Configurable (`requireAuthenticatedSignUp`) | Create a user account |
|
|
850
346
|
| `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 (
|
|
347
|
+
| `POST` | `/auth/token/refresh` | JWT | Re-issue a token from the caller's valid JWT |
|
|
348
|
+
| `GET` | `/auth/who-am-i` | JWT | Return the JWT payload, optionally merged with `getUserInformation` |
|
|
349
|
+
| `GET` | `/auth/me` | JWT | Return `getUserInformation` result directly |
|
|
350
|
+
| `GET` | `/certs` | No | JWKS endpoint (Issuer mode only) |
|
|
855
351
|
|
|
856
352
|
> [!NOTE]
|
|
857
|
-
>
|
|
858
|
-
|
|
859
|
-
### POST /auth/sign-in
|
|
860
|
-
|
|
861
|
-
**Authentication:** None
|
|
862
|
-
|
|
863
|
-
**Request Body:**
|
|
353
|
+
> `/auth` is configurable via `controllerOpts.restPath`. `/certs` is configurable via `rest.path` in `IJWKSIssuerOptions`, and is intentionally unauthenticated.
|
|
864
354
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
**Response 200:**
|
|
868
|
-
|
|
869
|
-
Uses `payload.signIn.response.schema` if provided, otherwise `AnyObjectSchema`.
|
|
355
|
+
**`POST /auth/sign-in`** - body defaults to `SignInRequestSchema` (nested `identifier`/`credential`), overridable via `payload.signIn`.
|
|
870
356
|
|
|
871
357
|
```json
|
|
872
|
-
{
|
|
873
|
-
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..."
|
|
874
|
-
}
|
|
358
|
+
{ "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..." }
|
|
875
359
|
```
|
|
876
360
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
**Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
|
|
880
|
-
|
|
881
|
-
When `requireAuthenticatedSignUp: true`, requires JWT authentication. When `false`, the endpoint is public.
|
|
361
|
+
**`POST /auth/sign-up`** - public unless `requireAuthenticatedSignUp: true`. Body defaults to a flat `SignUpRequestSchema` (`username`, `credential`) - unlike sign-in, the shape isn't nested.
|
|
882
362
|
|
|
883
|
-
|
|
363
|
+
**`POST /auth/change-password`** - always requires JWT. Body defaults to `ChangePasswordRequestSchema` (`scheme`, `oldCredential`, `newCredential`, `userId`).
|
|
884
364
|
|
|
885
|
-
|
|
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.
|
|
365
|
+
**`POST /auth/token/refresh`** - always requires JWT, no request body. Returns `501` if `IAuthService.refreshToken` isn't implemented.
|
|
922
366
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
926
|
-
|
|
927
|
-
Returns the current user's decrypted JWT payload directly from context.
|
|
367
|
+
**`GET /auth/who-am-i`** - always requires JWT. The `withUserInformation` query param (`true`, `false`, `1`, `0`; default `false`) attaches a `userInformation` field from `getUserInformation`. Returns `501` if you request that field without implementing `getUserInformation`.
|
|
928
368
|
|
|
929
369
|
```json
|
|
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
|
-
}
|
|
370
|
+
{ "userId": "123", "roles": [{ "id": "1", "identifier": "admin", "priority": 0 }] }
|
|
939
371
|
```
|
|
940
372
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
| Name | Type | Description |
|
|
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:
|
|
948
|
-
|
|
949
|
-
```json
|
|
950
|
-
{
|
|
951
|
-
"userId": "123",
|
|
952
|
-
"roles": [{ "id": "1", "identifier": "admin", "priority": 0 }],
|
|
953
|
-
"userInformation": { "fullName": "Ada Lovelace", "department": "R&D" }
|
|
954
|
-
}
|
|
955
|
-
```
|
|
956
|
-
|
|
957
|
-
**Response 501 (Not Implemented):**
|
|
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
|
-
```
|
|
373
|
+
**`GET /auth/me`** - always requires JWT, delegating entirely to `getUserInformation(context, {})`. The response is not merged with the JWT payload. Returns `501` if `getUserInformation` isn't implemented.
|
|
988
374
|
|
|
989
375
|
> [!TIP]
|
|
990
|
-
> One `getUserInformation` implementation backs both routes. Use `GET /me`
|
|
376
|
+
> One `getUserInformation` implementation backs both routes. Use `GET /me` for the raw profile. Use `GET /who-am-i?withUserInformation=true` to get it merged with the principal in one round-trip.
|
|
991
377
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
**Authentication:** None (intentionally public)
|
|
995
|
-
|
|
996
|
-
Returns the JSON Web Key Set for external verifiers.
|
|
378
|
+
**`GET /certs`** (Issuer mode only) - public, returns the JSON Web Key Set with `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`.
|
|
997
379
|
|
|
998
380
|
```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
|
-
}
|
|
381
|
+
{ "keys": [{ "kty": "EC", "kid": "my-key-id-1", "use": "sig", "alg": "ES256", "crv": "P-256", "x": "...", "y": "..." }] }
|
|
1012
382
|
```
|
|
1013
383
|
|
|
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 |
|
|
384
|
+
## Entity column helpers
|
|
1050
385
|
|
|
1051
|
-
|
|
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).
|
|
1075
|
-
|
|
1076
|
-
| Column | DB Column | Type | Nullable | Default | Description |
|
|
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
|
|
386
|
+
Column helper functions return pre-configured Drizzle columns for auth-related tables - spread them into `pgTable()` alongside your own columns.
|
|
1088
387
|
|
|
1089
388
|
```typescript
|
|
1090
|
-
import { pgTable,
|
|
389
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
1091
390
|
import {
|
|
1092
|
-
extraUserColumns,
|
|
1093
|
-
|
|
1094
|
-
extraPermissionColumns,
|
|
1095
|
-
extraPolicyDefinitionColumns,
|
|
391
|
+
extraUserColumns, extraRoleColumns, extraPermissionColumns, extraPolicyDefinitionColumns,
|
|
392
|
+
generateIdColumnDefs, generateTzColumnDefs,
|
|
1096
393
|
} from '@venizia/ignis';
|
|
1097
|
-
import { withSerialId, withTimestamps } from '@venizia/ignis';
|
|
1098
394
|
|
|
1099
|
-
// User table
|
|
1100
395
|
export const users = pgTable('users', {
|
|
1101
|
-
...
|
|
1102
|
-
...
|
|
396
|
+
...generateIdColumnDefs(),
|
|
397
|
+
...generateTzColumnDefs(),
|
|
1103
398
|
...extraUserColumns(),
|
|
1104
399
|
username: text('username').unique().notNull(),
|
|
1105
400
|
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
401
|
});
|
|
1115
402
|
|
|
1116
|
-
|
|
1117
|
-
export const permissions = pgTable('permissions', {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
})
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
})
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
|
1150
|
-
|
|
1151
|
-
| `
|
|
1152
|
-
| `
|
|
1153
|
-
| `
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
c.set(Authorization.SKIP_AUTHORIZATION, true);
|
|
1179
|
-
}
|
|
1180
|
-
return next();
|
|
1181
|
-
});
|
|
1182
|
-
```
|
|
1183
|
-
|
|
1184
|
-
## See Also
|
|
1185
|
-
|
|
1186
|
-
- [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
|
|
1187
|
-
- [API Reference](./api) -- Architecture, service internals, and strategy registry
|
|
1188
|
-
- [Error Reference](./errors) -- Error messages and troubleshooting
|
|
403
|
+
export const roles = pgTable('roles', { ...generateIdColumnDefs(), ...generateTzColumnDefs(), ...extraRoleColumns() });
|
|
404
|
+
export const permissions = pgTable('permissions', { ...generateIdColumnDefs(), ...generateTzColumnDefs(), ...extraPermissionColumns() });
|
|
405
|
+
export const policyDefinitions = pgTable('policy_definitions', { ...generateIdColumnDefs(), ...generateTzColumnDefs(), ...extraPolicyDefinitionColumns() });
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**`extraUserColumns(opts?: { idType })`**
|
|
409
|
+
|
|
410
|
+
| Column | DB column | Type | Default | Description |
|
|
411
|
+
|--------|-----------|------|---------|-------------|
|
|
412
|
+
| `realm` | `realm` | `text` | `''` | Multi-tenancy realm identifier |
|
|
413
|
+
| `status` | `status` | `text` | `UserStatuses.UNKNOWN` | User lifecycle status |
|
|
414
|
+
| `type` | `type` | `text` | `UserTypes.SYSTEM` | `SYSTEM` or `LINKED` |
|
|
415
|
+
| `activatedAt` | `activated_at` | `timestamp (tz)` | `null` | Activation timestamp |
|
|
416
|
+
| `lastLoginAt` | `last_login_at` | `timestamp (tz)` | `null` | Last login timestamp |
|
|
417
|
+
| `parentId` | `parent_id` | `text` or `integer` | `null` | Depends on `idType` |
|
|
418
|
+
|
|
419
|
+
**`extraRoleColumns()`** - no options.
|
|
420
|
+
|
|
421
|
+
| Column | DB column | Type | Default | Description |
|
|
422
|
+
|--------|-----------|------|---------|-------------|
|
|
423
|
+
| `identifier` | `identifier` | `text` (unique) | -- | For example, `'admin'`, `'editor'` |
|
|
424
|
+
| `name` | `name` | `text` | -- | Human-readable name |
|
|
425
|
+
| `description` | `description` | `text` | `null` | Optional |
|
|
426
|
+
| `priority` | `priority` | `integer` | -- | Lower = higher priority |
|
|
427
|
+
| `status` | `status` | `text` | `RoleStatuses.ACTIVATED` | Role lifecycle status |
|
|
428
|
+
|
|
429
|
+
**`extraPermissionColumns(opts?: { idType })`**
|
|
430
|
+
|
|
431
|
+
| Column | DB column | Type | Default | Description |
|
|
432
|
+
|--------|-----------|------|---------|-------------|
|
|
433
|
+
| `code` | `code` | `text` (unique) | -- | Unique permission code |
|
|
434
|
+
| `name` | `name` | `text` | -- | Display name |
|
|
435
|
+
| `subject` | `subject` | `text` | -- | For example, `'User'`, `'Order'` |
|
|
436
|
+
| `method` | `method` | `text` | -- | For example, `'GET'`, `'POST'` |
|
|
437
|
+
| `action` | `action` | `text` | -- | For example, `'read'`, `'write'` |
|
|
438
|
+
| `scope` | `scope` | `text` | -- | Permission scope |
|
|
439
|
+
| `description` | `description` | `text` | `null` | Optional |
|
|
440
|
+
| `parentId` | `parent_id` | `text` or `integer` | `null` | Depends on `idType` |
|
|
441
|
+
|
|
442
|
+
**`extraPolicyDefinitionColumns(opts?: { idType })`** - Casbin-style policies mapping subjects to targets.
|
|
443
|
+
|
|
444
|
+
| Column | DB column | Type | Nullable | Description |
|
|
445
|
+
|--------|-----------|------|----------|-------------|
|
|
446
|
+
| `variant` | `variant` | `text` | No | One of the seven `AuthorizationPolicyVariants` edge kinds: `grant`, `assign_role`, `role_inherits`, `join_domain`, `domain_inherits`, `resource_inherits`, `action_inherits` |
|
|
447
|
+
| `subjectType` | `subject_type` | `text` | No | For example, `'user'`, `'Role'` |
|
|
448
|
+
| `targetType` | `target_type` | `text` | No | For example, `'Permission'`, `'Role'` |
|
|
449
|
+
| `action` | `action` | `text` | Yes | Policy action |
|
|
450
|
+
| `effect` | `effect` | `text` | Yes | `'allow'` / `'deny'` |
|
|
451
|
+
| `domain` | `domain` | `text` | Yes | Multi-tenancy domain |
|
|
452
|
+
| `subjectId` | `subject_id` | `text` or `integer` | No | Depends on `idType` |
|
|
453
|
+
| `targetId` | `target_id` | `text` or `integer` | No | Depends on `idType` |
|
|
454
|
+
| `metadata` | `metadata` | `jsonb` | Yes | Free-form metadata. Only some grants populate it |
|
|
455
|
+
|
|
456
|
+
All `idType` options default to `'number'` (`integer` columns). Pass `'string'` for `text` columns - UUID primary keys, for example.
|
|
457
|
+
|
|
458
|
+
`'p'` and `'g'` are Casbin rule prefixes, not `variant` values - each `variant` maps to one of them internally. See the [Authorization component](../authorization/) to build these tables end to end. Its [Usage guide](../authorization/usage) covers policy definitions, domain scoping, and the adapter that reads this table.
|
|
459
|
+
|
|
460
|
+
## See also
|
|
461
|
+
|
|
462
|
+
- [Overview](./) - initial setup and binding keys
|
|
463
|
+
- [API Reference](./api) - full option tables, service class hierarchy, strategy registry
|
|
464
|
+
- [Error Reference](./errors) - every error message and how to fix it
|