@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,91 +1,122 @@
|
|
|
1
1
|
# Authentication -- API Reference
|
|
2
2
|
|
|
3
|
-
> Architecture, service
|
|
3
|
+
> Architecture, service hierarchy, strategy registry, JWKS controller, and controller factory. See [Setup & Configuration](./) for initial setup.
|
|
4
4
|
|
|
5
5
|
## Architecture
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ Application │
|
|
10
|
+
│ │
|
|
11
|
+
│ preConfigure() │
|
|
12
|
+
│ ├── bind JWT_OPTIONS (TJWTTokenServiceOptions) │
|
|
13
|
+
│ │ └── standard: JWS | JWKS │
|
|
14
|
+
│ ├── bind BASIC_OPTIONS / REST_OPTIONS │
|
|
15
|
+
│ ├── this.component(AuthenticateComponent) │
|
|
16
|
+
│ └── AuthenticationStrategyRegistry.register() ← manual │
|
|
17
|
+
└────────────────────────────┬─────────────────────────────────────┘
|
|
18
|
+
│
|
|
19
|
+
▼
|
|
20
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
21
|
+
│ AuthenticateComponent.binding() │
|
|
22
|
+
│ │
|
|
23
|
+
│ 1. Read JWT_OPTIONS, BASIC_OPTIONS, REST_OPTIONS │
|
|
24
|
+
│ 2. Switch on jwtOptions.standard: │
|
|
25
|
+
│ ├── JWS → defineJWSAuth() → JWSTokenService │
|
|
26
|
+
│ └── JWKS → defineJWKSAuth() → switch on mode: │
|
|
27
|
+
│ ├── issuer → JWKSIssuerTokenService │
|
|
28
|
+
│ │ + JWKSController (/certs) │
|
|
29
|
+
│ └── verifier → JWKSVerifierTokenService │
|
|
30
|
+
│ 3. defineBasicAuth() → BasicTokenService │
|
|
31
|
+
│ 4. defineControllers() → AuthController (factory-built) │
|
|
32
|
+
│ 5. defineOAuth2() → stub (not yet implemented) │
|
|
33
|
+
└────────────────────────────┬─────────────────────────────────────┘
|
|
34
|
+
│
|
|
35
|
+
┌─────────────────────┼──────────────────────┐
|
|
36
|
+
▼ ▼ ▼
|
|
37
|
+
┌──────────────┐ ┌────────────────┐ ┌──────────────────┐
|
|
38
|
+
│ Bearer Token │ │ BasicToken │ │ AuthController │
|
|
39
|
+
│ Services │ │ Service │ │ (factory-built) │
|
|
40
|
+
│ (see below) │ └───────┬────────┘ └────────┬─────────┘
|
|
41
|
+
└──────┬───────┘ │ │
|
|
42
|
+
│ ▼ ▼
|
|
43
|
+
│ ┌──────────────┐ ┌──────────────────┐
|
|
44
|
+
│ │ Basic │ │ /sign-in │
|
|
45
|
+
│ │ Strategy │ │ /sign-up │
|
|
46
|
+
│ └──────────────┘ │ /change-password │
|
|
47
|
+
│ │ /who-am-i │
|
|
48
|
+
▼ └──────────────────┘
|
|
8
49
|
┌──────────────────────────────────────────────────────────┐
|
|
9
|
-
│
|
|
50
|
+
│ Bearer Token Service Hierarchy │
|
|
10
51
|
│ │
|
|
11
|
-
│
|
|
12
|
-
│ ├──
|
|
13
|
-
│ ├──
|
|
14
|
-
│
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
│
|
|
20
|
-
│
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
│
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
│
|
|
32
|
-
│
|
|
33
|
-
|
|
34
|
-
│ │ │
|
|
35
|
-
▼ ▼ ▼
|
|
36
|
-
┌────────────┐ ┌──────────────┐ ┌──────────────────┐
|
|
37
|
-
│ JWT │ │ Basic │ │ /sign-in │
|
|
38
|
-
│ Strategy │ │ Strategy │ │ /sign-up │
|
|
39
|
-
│ │ │ │ │ /change-password │
|
|
40
|
-
└────────────┘ └──────────────┘ │ /who-am-i │
|
|
41
|
-
└──────────────────┘
|
|
52
|
+
│ AbstractBearerTokenService (base) │
|
|
53
|
+
│ ├── extractCredentials() │
|
|
54
|
+
│ ├── verify() → doVerify() │
|
|
55
|
+
│ ├── generate() → getSigner() + getSigningKey() │
|
|
56
|
+
│ ├── encryptPayload() / decryptPayload() (optional) │
|
|
57
|
+
│ │ │
|
|
58
|
+
│ ├── JWSTokenService (symmetric HS256) │
|
|
59
|
+
│ │ ├── JWSAuthenticationStrategy │
|
|
60
|
+
│ │ └── sign with shared secret │
|
|
61
|
+
│ │ │
|
|
62
|
+
│ └── AbstractJWKSTokenService (lazy-init + retry) │
|
|
63
|
+
│ ├── ensureInitialized() / initialize() │
|
|
64
|
+
│ │ │
|
|
65
|
+
│ ├── JWKSIssuerTokenService (asymmetric) │
|
|
66
|
+
│ │ ├── JWKSIssuerAuthenticationStrategy │
|
|
67
|
+
│ │ ├── sign with private key │
|
|
68
|
+
│ │ ├── verify with public key │
|
|
69
|
+
│ │ └── getJWKS() / getJWKSAsync() │
|
|
70
|
+
│ │ │
|
|
71
|
+
│ └── JWKSVerifierTokenService (remote verify) │
|
|
72
|
+
│ ├── JWKSVerifierAuthenticationStrategy │
|
|
73
|
+
│ └── verify via createRemoteJWKSet() │
|
|
74
|
+
└──────────────────────────────────────────────────────────┘
|
|
42
75
|
```
|
|
43
76
|
|
|
44
77
|
### Tech Stack
|
|
45
78
|
|
|
46
79
|
| Technology | Purpose |
|
|
47
80
|
|------------|---------|
|
|
48
|
-
| **`jose`** | JWT signing (`SignJWT`), verification (`jwtVerify`),
|
|
49
|
-
| **`@venizia/ignis-helpers`** | `AES` utility for payload
|
|
81
|
+
| **`jose`** | JWT signing (`SignJWT`), verification (`jwtVerify`), JWKS (`createRemoteJWKSet`, `exportJWK`, `importPKCS8`, `importSPKI`, `importJWK`), and type definitions |
|
|
82
|
+
| **`@venizia/ignis-helpers`** | `AES` utility for payload encryption, `BaseHelper`/`BaseService` base classes, `getError` for error creation, `HTTP` result codes |
|
|
50
83
|
| **Hono middleware** | Route-level authentication integration via `createMiddleware` from `hono/factory` |
|
|
84
|
+
| **`node:fs/promises`** | Async file reading for JWKS key files |
|
|
51
85
|
| **Drizzle ORM** | Database access for user lookup (in your implementation) |
|
|
52
|
-
| **lodash/isEmpty** | Used in strategy registry for name validation |
|
|
53
86
|
|
|
54
|
-
## Component
|
|
87
|
+
## Component Methods
|
|
55
88
|
|
|
56
|
-
The `AuthenticateComponent` uses
|
|
89
|
+
The `AuthenticateComponent` uses five methods during its `binding()` lifecycle (four private, one public):
|
|
57
90
|
|
|
58
91
|
| Method | Purpose |
|
|
59
92
|
|--------|---------|
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
93
|
+
| `defineJWSAuth(opts)` | Validates JWS secrets (rejects falsy values and `'unknown_secret'`), validates `getTokenExpiresFn`, binds `IJWSTokenServiceOptions` to `JWT_OPTIONS`, registers `JWSTokenService`. |
|
|
94
|
+
| `defineJWKSAuth(opts)` | Switches on `mode`: **Issuer** — validates keys, format, kid, getTokenExpiresFn; binds to `JWKS_OPTIONS`; registers `JWKSIssuerTokenService` + `JWKSController`. **Verifier** — validates jwksUrl; binds to `JWKS_OPTIONS`; registers `JWKSVerifierTokenService`. |
|
|
62
95
|
| `defineBasicAuth(opts)` | Validates `verifyCredentials` callback presence, binds `BasicTokenService` as a service. Logs debug if skipped. |
|
|
63
96
|
| `defineControllers(opts)` | Requires `jwtOptions` when `useAuthController: true`. Calls `defineAuthController()` factory and registers the generated controller. |
|
|
64
|
-
| `defineOAuth2()` |
|
|
97
|
+
| `defineOAuth2()` | **Public** stub method -- not yet implemented. Called during `binding()` but performs no action. |
|
|
65
98
|
|
|
66
|
-
> [!
|
|
67
|
-
>
|
|
99
|
+
> [!NOTE]
|
|
100
|
+
> The component reads `TJWTTokenServiceOptions` (the discriminated union) from `AuthenticateBindingKeys.JWT_OPTIONS`, then re-binds just the inner options (`IJWSTokenServiceOptions` or `IJWKSIssuerOptions`/`IJWKSVerifierOptions`) to the appropriate binding key for the service to consume via `@inject`.
|
|
68
101
|
|
|
69
102
|
## Strategy Registry
|
|
70
103
|
|
|
71
|
-
|
|
104
|
+
`AuthenticationStrategyRegistry` is a **singleton** that manages all registered strategies. It extends `AbstractAuthRegistry<IAuthenticationStrategy>` (not generic on `Env`).
|
|
72
105
|
|
|
73
106
|
### API
|
|
74
107
|
|
|
75
108
|
| Method | Signature | Returns | Description |
|
|
76
109
|
|--------|-----------|---------|-------------|
|
|
77
110
|
| `getInstance()` | `static` | `AuthenticationStrategyRegistry` | Returns the singleton instance (creates if not exists) |
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `register` | <code v-pre>(opts: { container: Container; strategies: Array<{ name: string; strategy: TClass<IAuthenticationStrategy<E>> }> }) => this</code> | `this` | Registers strategies as singletons in the container. Returns `this` for chaining. |
|
|
81
|
-
| `authenticate` | `(opts: { strategies: string[]; mode?: TAuthMode }) => MiddlewareHandler` | `MiddlewareHandler` | Creates a Hono middleware that performs the auth check |
|
|
111
|
+
| `register` | <code v-pre>(opts: { container: Container; strategies: Array<{ name: string; strategy: TClass<IAuthenticationStrategy> }> }) => this</code> | `this` | Registers strategies as singletons in the container. Returns `this` for chaining. |
|
|
112
|
+
| `resolveStrategy` | `(opts: { name: string }) => IAuthenticationStrategy` | `IAuthenticationStrategy` | Resolves a strategy instance from the container by name |
|
|
82
113
|
|
|
83
114
|
**Registration:**
|
|
84
115
|
```typescript
|
|
85
116
|
AuthenticationStrategyRegistry.getInstance().register({
|
|
86
117
|
container: this,
|
|
87
118
|
strategies: [
|
|
88
|
-
{ name: Authentication.STRATEGY_JWT, strategy:
|
|
119
|
+
{ name: Authentication.STRATEGY_JWT, strategy: JWKSIssuerAuthenticationStrategy },
|
|
89
120
|
{ name: Authentication.STRATEGY_BASIC, strategy: BasicAuthenticationStrategy },
|
|
90
121
|
],
|
|
91
122
|
});
|
|
@@ -95,60 +126,181 @@ AuthenticationStrategyRegistry.getInstance().register({
|
|
|
95
126
|
> `register()` returns `this`, enabling method chaining if needed.
|
|
96
127
|
|
|
97
128
|
**How it works:**
|
|
98
|
-
- Strategies are stored in an internal
|
|
129
|
+
- Strategies are stored in an internal map and bound to the DI container as singletons
|
|
99
130
|
- Binding keys follow the pattern `authentication.strategy.{name}` (e.g., `authentication.strategy.jwt`, `authentication.strategy.basic`)
|
|
100
|
-
- The `authenticate()`
|
|
101
|
-
- The standalone `authenticate()` function is a convenience wrapper around the registry singleton
|
|
102
|
-
|
|
103
|
-
**Strategy binding:**
|
|
104
|
-
```typescript
|
|
105
|
-
// Internally, the registry binds strategies like this:
|
|
106
|
-
container.bind({ key: 'authentication.strategy.jwt' })
|
|
107
|
-
.toClass(JWTAuthenticationStrategy)
|
|
108
|
-
.setScope(BindingScopes.SINGLETON);
|
|
109
|
-
```
|
|
131
|
+
- The standalone `authenticate()` function creates middleware via `AuthenticationProvider`, which uses the registry to resolve strategies
|
|
110
132
|
|
|
111
133
|
**Middleware creation:**
|
|
112
134
|
|
|
113
135
|
The `authenticate()` function returns a Hono middleware that:
|
|
114
|
-
1. Checks if `Authentication.SKIP_AUTHENTICATION` is set on context
|
|
115
|
-
2. Checks if `Authentication.CURRENT_USER` is already set on context
|
|
136
|
+
1. Checks if `Authentication.SKIP_AUTHENTICATION` is set on context — if true, skips entirely (logs debug)
|
|
137
|
+
2. Checks if `Authentication.CURRENT_USER` is already set on context — if true, skips (already authenticated)
|
|
116
138
|
3. Reads `strategies` and `mode` from the provided options
|
|
117
139
|
4. Executes strategies based on mode (`any` or `all`)
|
|
118
140
|
5. On success, sets `Authentication.CURRENT_USER` and `Authentication.AUDIT_USER_ID` on context
|
|
119
141
|
6. On failure, throws 401 with list of tried strategies
|
|
120
142
|
|
|
143
|
+
```mermaid
|
|
144
|
+
sequenceDiagram
|
|
145
|
+
participant C as Client
|
|
146
|
+
participant MW as Auth Middleware
|
|
147
|
+
participant P as AuthenticationProvider
|
|
148
|
+
participant R as StrategyRegistry
|
|
149
|
+
participant S as Strategy
|
|
150
|
+
participant SVC as TokenService
|
|
151
|
+
|
|
152
|
+
C->>MW: Request with Authorization header
|
|
153
|
+
MW->>P: authenticateFn({ strategies, mode })
|
|
154
|
+
P->>P: Check SKIP_AUTHENTICATION
|
|
155
|
+
P->>P: Check CURRENT_USER already set
|
|
156
|
+
P->>R: resolveStrategy({ name })
|
|
157
|
+
R-->>P: strategy instance
|
|
158
|
+
P->>S: authenticate(context)
|
|
159
|
+
S->>SVC: extractCredentials(context)
|
|
160
|
+
SVC-->>S: { type, token }
|
|
161
|
+
S->>SVC: verify({ type, token })
|
|
162
|
+
SVC-->>S: IJWTTokenPayload
|
|
163
|
+
S-->>P: IAuthUser
|
|
164
|
+
P->>MW: Set CURRENT_USER + AUDIT_USER_ID
|
|
165
|
+
MW->>C: Continue to handler
|
|
166
|
+
```
|
|
167
|
+
|
|
121
168
|
### Standalone `authenticate()` Function
|
|
122
169
|
|
|
123
170
|
```typescript
|
|
171
|
+
const authenticationProvider = new AuthenticationProvider();
|
|
172
|
+
const authenticateFn = authenticationProvider.value();
|
|
173
|
+
|
|
124
174
|
export const authenticate = (opts: { strategies: string[]; mode?: TAuthMode }) => {
|
|
125
|
-
return
|
|
175
|
+
return authenticateFn(opts);
|
|
126
176
|
};
|
|
127
177
|
```
|
|
128
178
|
|
|
129
|
-
This is the primary export for creating auth middleware. It
|
|
179
|
+
This is the primary export for creating auth middleware. It creates an `AuthenticationProvider` instance and calls `.value()` to get the middleware factory. The provider uses `AuthenticationStrategyRegistry.getInstance()` internally to resolve strategies.
|
|
130
180
|
|
|
131
181
|
> [!NOTE]
|
|
132
|
-
> In `all` mode, if every strategy passes but the final user payload has no `userId`, the middleware throws a `401` with message `"Failed to identify authenticated user!"`. The `any` mode
|
|
182
|
+
> In `all` mode, if every strategy passes but the final user payload has no `userId`, the middleware throws a `401` with message `"Failed to identify authenticated user!"`. The `any` mode **discards errors** from each failing strategy (logs at debug level) and only throws after all strategies are exhausted.
|
|
183
|
+
|
|
184
|
+
## Service Class Hierarchy
|
|
185
|
+
|
|
186
|
+
```mermaid
|
|
187
|
+
classDiagram
|
|
188
|
+
class AbstractBearerTokenService {
|
|
189
|
+
<<abstract>>
|
|
190
|
+
#aes: AES | null
|
|
191
|
+
#applicationSecret: string | null
|
|
192
|
+
+extractCredentials(context) credentials
|
|
193
|
+
+verify(opts) IJWTTokenPayload
|
|
194
|
+
+generate(opts) string
|
|
195
|
+
+encryptPayload(payload) Record
|
|
196
|
+
+decryptPayload(opts) IJWTTokenPayload
|
|
197
|
+
#doVerify(token)* IJWTTokenPayload
|
|
198
|
+
+getSigner(opts)* SignJWT
|
|
199
|
+
#getSigningKey()* Uint8Array | CryptoKey
|
|
200
|
+
#getDefaultTokenExpiresFn()* TGetTokenExpiresFn
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class JWSTokenService {
|
|
204
|
+
#jwtSecret: Uint8Array
|
|
205
|
+
#options: IJWSTokenServiceOptions
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
class AbstractJWKSTokenService {
|
|
209
|
+
<<abstract>>
|
|
210
|
+
#initialized: boolean
|
|
211
|
+
#initPromise: Promise | null
|
|
212
|
+
+ensureInitialized()
|
|
213
|
+
#initialize()* void
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
class JWKSIssuerTokenService {
|
|
217
|
+
#privateKey: CryptoKey | null
|
|
218
|
+
#publicKey: CryptoKey | null
|
|
219
|
+
#jwks: object | null
|
|
220
|
+
+getJWKS() keys
|
|
221
|
+
+getJWKSAsync() keys
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class JWKSVerifierTokenService {
|
|
225
|
+
#jwksVerifier: Function | null
|
|
226
|
+
+getSigner() never
|
|
227
|
+
+getSigningKey() never
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
AbstractBearerTokenService <|-- JWSTokenService
|
|
231
|
+
AbstractBearerTokenService <|-- AbstractJWKSTokenService
|
|
232
|
+
AbstractJWKSTokenService <|-- JWKSIssuerTokenService
|
|
233
|
+
AbstractJWKSTokenService <|-- JWKSVerifierTokenService
|
|
234
|
+
```
|
|
133
235
|
|
|
134
|
-
##
|
|
236
|
+
## AbstractBearerTokenService
|
|
135
237
|
|
|
136
|
-
|
|
238
|
+
Base class for all Bearer token services. Extends `BaseService`. Generic on <code v-pre><E extends Env = Env></code>.
|
|
137
239
|
|
|
138
|
-
|
|
240
|
+
**File:** `packages/core/src/components/auth/authenticate/services/bearer/abstract.service.ts`
|
|
139
241
|
|
|
140
|
-
|
|
242
|
+
### Static Fields
|
|
243
|
+
|
|
244
|
+
| Field | Type | Description |
|
|
245
|
+
|-------|------|-------------|
|
|
246
|
+
| `JWT_COMMON_FIELDS` | <code v-pre>Set<'iss' \| 'sub' \| 'aud' \| 'jti' \| 'nbf' \| 'exp' \| 'iat'></code> | Standard JWT fields that are never encrypted |
|
|
247
|
+
|
|
248
|
+
### Protected Fields
|
|
249
|
+
|
|
250
|
+
| Field | Type | Default | Description |
|
|
251
|
+
|-------|------|---------|-------------|
|
|
252
|
+
| `aes` | `AES \| null` | `null` | AES utility instance, configured by `configurePayloadEncryption()` |
|
|
253
|
+
| `applicationSecret` | `string \| null` | `null` | AES secret, configured by `configurePayloadEncryption()` |
|
|
254
|
+
| `fieldCodecs` | <code v-pre>Map<string, IPayloadFieldCodec></code> | `new Map()` | Field codec map keyed by field name, configured by `configurePayloadEncryption()` |
|
|
255
|
+
|
|
256
|
+
### Methods
|
|
257
|
+
|
|
258
|
+
| Method | Signature | Description |
|
|
259
|
+
|--------|-----------|-------------|
|
|
260
|
+
| `configurePayloadEncryption` | <code v-pre>(opts: { aesAlgorithm?: AESAlgorithmType; applicationSecret?: string; fieldCodecs?: IPayloadFieldCodec[] }) => void</code> | Configures optional AES encryption and field codecs. Codecs are converted to a Map keyed by `codec.key` for O(1) lookup. |
|
|
261
|
+
| `extractCredentials` | <code v-pre>(context: TContext<E, string>) => { type: string; token: string }</code> | Extracts Bearer token from Authorization header |
|
|
262
|
+
| `verify` | <code v-pre>(opts: { type: string; token: string }) => Promise<IJWTTokenPayload></code> | Template method — calls `doVerify()` |
|
|
263
|
+
| `generate` | <code v-pre>(opts: { payload: IJWTTokenPayload; getTokenExpiresFn?: TGetTokenExpiresFn }) => Promise<string></code> | Template method — calls `getSigner()` + `getSigningKey()` |
|
|
264
|
+
| `serializeField` | <code v-pre>(opts: { key: string; value: any }) => string</code> | Serializes a single field: codec → `JSON.stringify` fallback |
|
|
265
|
+
| `deserializeField` | <code v-pre>(opts: { key: string; value: string }) => any</code> | Deserializes a single field: codec → `JSON.parse` fallback |
|
|
266
|
+
| `encryptPayload` | <code v-pre>(payload: IJWTTokenPayload) => Record<string, any></code> | AES-encrypts non-standard JWT fields using `serializeField`. Returns payload unchanged if AES not configured. |
|
|
267
|
+
| `decryptPayload` | <code v-pre>(opts: { result: JWTVerifyResult<IJWTTokenPayload> }) => IJWTTokenPayload</code> | Decrypts AES-encrypted fields using `deserializeField`. Returns payload unchanged if AES not configured. |
|
|
268
|
+
|
|
269
|
+
### Abstract Methods (implemented by subclasses)
|
|
270
|
+
|
|
271
|
+
| Method | Visibility | Signature | Description |
|
|
272
|
+
|--------|------------|-----------|-------------|
|
|
273
|
+
| `doVerify` | `protected` | `(token: string) => Promise<IJWTTokenPayload>` | Verify the raw JWT token and return the payload |
|
|
274
|
+
| `getSigner` | **`public`** | <code v-pre>(opts: { payload: IJWTTokenPayload; getTokenExpiresFn: TGetTokenExpiresFn }) => Promise<SignJWT></code> | Create a `jose.SignJWT` instance with the payload |
|
|
275
|
+
| `getSigningKey` | `protected` | `() => ValueOrPromise<Uint8Array \| CryptoKey>` | Return the signing key |
|
|
276
|
+
| `getDefaultTokenExpiresFn` | `protected` | `() => TGetTokenExpiresFn` | Return the default token expiry function |
|
|
277
|
+
|
|
278
|
+
## JWSTokenService
|
|
279
|
+
|
|
280
|
+
Symmetric JWT (HS256) token service with optional AES-encrypted payloads. Extends `AbstractBearerTokenService`.
|
|
281
|
+
|
|
282
|
+
**File:** `packages/core/src/components/auth/authenticate/services/bearer/jws.service.ts`
|
|
283
|
+
|
|
284
|
+
### JWSAuthenticationStrategy
|
|
285
|
+
|
|
286
|
+
Extends `BaseHelper` and implements <code v-pre>IAuthenticationStrategy<E></code>.
|
|
141
287
|
|
|
142
288
|
```typescript
|
|
143
|
-
class
|
|
289
|
+
class JWSAuthenticationStrategy<E extends Env = Env>
|
|
144
290
|
extends BaseHelper
|
|
145
291
|
implements IAuthenticationStrategy<E>
|
|
146
292
|
{
|
|
147
|
-
name = Authentication.STRATEGY_JWT;
|
|
293
|
+
name = Authentication.STRATEGY_JWT;
|
|
294
|
+
standard = JOSEStandards.JWS;
|
|
148
295
|
|
|
149
296
|
constructor(
|
|
150
|
-
@inject({
|
|
151
|
-
|
|
297
|
+
@inject({
|
|
298
|
+
key: BindingKeys.build({
|
|
299
|
+
namespace: BindingNamespaces.SERVICE,
|
|
300
|
+
key: JWSTokenService.name,
|
|
301
|
+
}),
|
|
302
|
+
})
|
|
303
|
+
private service: JWSTokenService<E>,
|
|
152
304
|
) { ... }
|
|
153
305
|
|
|
154
306
|
authenticate(context: TContext<E, string>): Promise<IAuthUser> {
|
|
@@ -158,48 +310,281 @@ class JWTAuthenticationStrategy<E extends Env = Env>
|
|
|
158
310
|
}
|
|
159
311
|
```
|
|
160
312
|
|
|
313
|
+
### Protected Fields
|
|
314
|
+
|
|
315
|
+
| Field | Type | Description |
|
|
316
|
+
|-------|------|-------------|
|
|
317
|
+
| `jwtSecret` | `Uint8Array` | Encoded JWT secret for `jose` signing/verification |
|
|
318
|
+
| `options` | `IJWSTokenServiceOptions` | Injected options |
|
|
319
|
+
|
|
320
|
+
### Constructor Behavior
|
|
321
|
+
|
|
322
|
+
The constructor validates required options and throws immediately (status 500) if any are missing:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
constructor(
|
|
326
|
+
@inject({ key: AuthenticateBindingKeys.JWT_OPTIONS })
|
|
327
|
+
protected options: IJWSTokenServiceOptions,
|
|
328
|
+
) {
|
|
329
|
+
// Throws '[JWSTokenService] Invalid jwtSecret' if !jwtSecret
|
|
330
|
+
// Throws '[JWSTokenService] Invalid getTokenExpiresFn' if !getTokenExpiresFn
|
|
331
|
+
// Calls configurePayloadEncryption({ aesAlgorithm, applicationSecret })
|
|
332
|
+
// Encodes jwtSecret to Uint8Array for jose
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
> [!NOTE]
|
|
337
|
+
> `applicationSecret` is no longer validated in the constructor. If not provided, AES encryption is simply not configured, and payloads pass through in plaintext.
|
|
338
|
+
|
|
339
|
+
### Overridden Methods
|
|
340
|
+
|
|
341
|
+
| Method | Behavior |
|
|
342
|
+
|--------|----------|
|
|
343
|
+
| `doVerify(token)` | Calls `jwtVerify(token, this.jwtSecret)`, then `this.decryptPayload()` |
|
|
344
|
+
| `getSigner(opts)` | Calls `this.encryptPayload()`, then creates `SignJWT` with HS256 header |
|
|
345
|
+
| `getSigningKey()` | Returns `this.jwtSecret` |
|
|
346
|
+
| `getDefaultTokenExpiresFn()` | Returns `this.options.getTokenExpiresFn` |
|
|
347
|
+
|
|
348
|
+
## AbstractJWKSTokenService
|
|
349
|
+
|
|
350
|
+
Base class for JWKS token services (Issuer + Verifier). Extends `AbstractBearerTokenService`.
|
|
351
|
+
|
|
352
|
+
**File:** `packages/core/src/components/auth/authenticate/services/bearer/jwks/abstract.service.ts`
|
|
353
|
+
|
|
354
|
+
Consolidates the lazy-initialization pattern with retry-on-failure semantics. If `initialize()` rejects, `initPromise` is reset so the next call retries instead of caching the failure permanently.
|
|
355
|
+
|
|
356
|
+
### Protected Fields
|
|
357
|
+
|
|
358
|
+
| Field | Type | Default | Description |
|
|
359
|
+
|-------|------|---------|-------------|
|
|
360
|
+
| `initialized` | `boolean` | `false` | Whether the service has been initialized |
|
|
361
|
+
| `initPromise` | `Promise<void> \| null` | `null` | Pending initialization promise (for concurrent callers) |
|
|
362
|
+
|
|
161
363
|
### Methods
|
|
162
364
|
|
|
163
365
|
| Method | Signature | Description |
|
|
164
366
|
|--------|-----------|-------------|
|
|
165
|
-
| `
|
|
166
|
-
| `verify` | <code v-pre>(opts: { type: string; token: string }) => Promise<IJWTTokenPayload></code> | Verifies JWT signature via `jose.jwtVerify()` and decrypts payload |
|
|
167
|
-
| `generate` | <code v-pre>(opts: { payload: IJWTTokenPayload; getTokenExpiresFn?: TGetTokenExpiresFn }) => Promise<string></code> | Encrypts payload, signs JWT with configurable expiration |
|
|
168
|
-
| `getSigner` | <code v-pre>(opts: { payload: IJWTTokenPayload; getTokenExpiresFn: TGetTokenExpiresFn }) => Promise<SignJWT></code> | Creates a `jose.SignJWT` instance with encrypted payload, iat, exp, nbf |
|
|
169
|
-
| `encryptPayload` | <code v-pre>(payload: IJWTTokenPayload) => Record<string, string></code> | AES-encrypts non-standard JWT fields (keys + values) |
|
|
170
|
-
| `decryptPayload` | <code v-pre>(opts: { result: JWTVerifyResult<IJWTTokenPayload> }) => IJWTTokenPayload</code> | Decrypts AES-encrypted fields back to IJWTTokenPayload |
|
|
367
|
+
| `ensureInitialized` | `() => Promise<void>` | Lazily initializes the service on first call. Concurrent callers share the same promise. On failure, resets `initPromise` so the next call retries. |
|
|
171
368
|
|
|
172
|
-
###
|
|
369
|
+
### Abstract Methods
|
|
370
|
+
|
|
371
|
+
| Method | Signature | Description |
|
|
372
|
+
|--------|-----------|-------------|
|
|
373
|
+
| `initialize` | `() => Promise<void>` | Perform one-time async initialization (load keys, create verifier, etc.) |
|
|
374
|
+
|
|
375
|
+
## JWKSIssuerTokenService
|
|
376
|
+
|
|
377
|
+
Asymmetric JWT token service (issuer mode). Extends `AbstractJWKSTokenService`.
|
|
378
|
+
|
|
379
|
+
**File:** `packages/core/src/components/auth/authenticate/services/bearer/jwks/issuer.service.ts`
|
|
380
|
+
|
|
381
|
+
### JWKSIssuerAuthenticationStrategy
|
|
173
382
|
|
|
174
|
-
|
|
383
|
+
```typescript
|
|
384
|
+
class JWKSIssuerAuthenticationStrategy<E extends Env = Env>
|
|
385
|
+
extends BaseHelper
|
|
386
|
+
implements IAuthenticationStrategy<E>
|
|
387
|
+
{
|
|
388
|
+
name = Authentication.STRATEGY_JWT;
|
|
389
|
+
standard = JOSEStandards.JWKS;
|
|
390
|
+
|
|
391
|
+
constructor(
|
|
392
|
+
@inject({
|
|
393
|
+
key: BindingKeys.build({
|
|
394
|
+
namespace: BindingNamespaces.SERVICE,
|
|
395
|
+
key: JWKSIssuerTokenService.name,
|
|
396
|
+
}),
|
|
397
|
+
})
|
|
398
|
+
private service: JWKSIssuerTokenService<E>,
|
|
399
|
+
) { ... }
|
|
400
|
+
|
|
401
|
+
authenticate(context: TContext<E, string>): Promise<IAuthUser> {
|
|
402
|
+
const token = this.service.extractCredentials(context);
|
|
403
|
+
return this.service.verify(token);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
175
407
|
|
|
176
408
|
### Protected Fields
|
|
177
409
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
410
|
+
| Field | Type | Default | Description |
|
|
411
|
+
|-------|------|---------|-------------|
|
|
412
|
+
| `privateKey` | `CryptoKey \| Uint8Array \| null` | `null` | Private key for signing (loaded during `initialize`) |
|
|
413
|
+
| `publicKey` | `CryptoKey \| Uint8Array \| null` | `null` | Public key for verification (loaded during `initialize`) |
|
|
414
|
+
| `jwks` | `{ keys: JWK[] } \| null` | `null` | Cached JWKS for the `/certs` endpoint |
|
|
181
415
|
|
|
182
416
|
### Constructor Behavior
|
|
183
417
|
|
|
184
|
-
|
|
418
|
+
```typescript
|
|
419
|
+
constructor(
|
|
420
|
+
@inject({ key: AuthenticateBindingKeys.JWKS_OPTIONS })
|
|
421
|
+
protected options: IJWKSIssuerOptions,
|
|
422
|
+
) {
|
|
423
|
+
// Calls configurePayloadEncryption({ aesAlgorithm, applicationSecret })
|
|
424
|
+
// Keys are NOT loaded here — loaded lazily via ensureInitialized()
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Initialization Flow
|
|
429
|
+
|
|
430
|
+
The `initialize()` method:
|
|
431
|
+
1. **Resolves key content** — reads from file (`readFile` from `node:fs/promises`) or uses inline text, based on `keys.driver`
|
|
432
|
+
2. **Parses key material** — imports keys using `importPKCS8`/`importSPKI` (PEM format) or `importJWK` (JWK format), based on `keys.format`
|
|
433
|
+
3. **Exports public JWK** — calls `exportJWK()` and adds `kid`, `alg`, `use: 'sig'` metadata
|
|
434
|
+
4. **Caches JWKS** — stores `{ keys: [publicJWK] }` for the `/certs` endpoint
|
|
435
|
+
5. **Sets `initialized = true`**
|
|
436
|
+
|
|
437
|
+
### Overridden Methods
|
|
438
|
+
|
|
439
|
+
| Method | Behavior |
|
|
440
|
+
|--------|----------|
|
|
441
|
+
| `doVerify(token)` | Calls `ensureInitialized()`, then `jwtVerify(token, this.publicKey!)`, then `this.decryptPayload()` |
|
|
442
|
+
| `getSigner(opts)` | Calls `ensureInitialized()`, then `this.encryptPayload()`, then creates `SignJWT` with algorithm + kid header |
|
|
443
|
+
| `getSigningKey()` | Returns `this.privateKey` |
|
|
444
|
+
| `getDefaultTokenExpiresFn()` | Returns `this.options.getTokenExpiresFn` |
|
|
445
|
+
|
|
446
|
+
### JWKS Methods
|
|
447
|
+
|
|
448
|
+
| Method | Signature | Description |
|
|
449
|
+
|--------|-----------|-------------|
|
|
450
|
+
| `getJWKS` | `() => { keys: JWK[] }` | Synchronous — returns cached JWKS. Throws if not yet initialized. |
|
|
451
|
+
| `getJWKSAsync` | `() => Promise<{ keys: JWK[] }>` | Async — calls `ensureInitialized()` first, then returns JWKS. |
|
|
452
|
+
|
|
453
|
+
### Internal Methods
|
|
454
|
+
|
|
455
|
+
| Method | Signature | Description |
|
|
456
|
+
|--------|-----------|-------------|
|
|
457
|
+
| `resolveKeyContent` | `(opts: { keys }) => Promise<{ priv: string; pub: string }>` | Reads key content from file or returns inline text |
|
|
458
|
+
| `parseKeyMaterial` | `(opts: { raw, algorithm, keys }) => Promise<{ priv, pub }>` | Imports keys using `jose` based on format (PEM or JWK) |
|
|
459
|
+
|
|
460
|
+
## JWKSVerifierTokenService
|
|
461
|
+
|
|
462
|
+
Asymmetric JWT token service (verifier mode). Extends `AbstractJWKSTokenService`.
|
|
463
|
+
|
|
464
|
+
**File:** `packages/core/src/components/auth/authenticate/services/bearer/jwks/verifier.service.ts`
|
|
465
|
+
|
|
466
|
+
### JWKSVerifierAuthenticationStrategy
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
class JWKSVerifierAuthenticationStrategy<E extends Env = Env>
|
|
470
|
+
extends BaseHelper
|
|
471
|
+
implements IAuthenticationStrategy<E>
|
|
472
|
+
{
|
|
473
|
+
name = Authentication.STRATEGY_JWT;
|
|
474
|
+
standard = JOSEStandards.JWKS;
|
|
475
|
+
|
|
476
|
+
constructor(
|
|
477
|
+
@inject({
|
|
478
|
+
key: BindingKeys.build({
|
|
479
|
+
namespace: BindingNamespaces.SERVICE,
|
|
480
|
+
key: JWKSVerifierTokenService.name,
|
|
481
|
+
}),
|
|
482
|
+
})
|
|
483
|
+
private service: JWKSVerifierTokenService<E>,
|
|
484
|
+
) { ... }
|
|
485
|
+
|
|
486
|
+
authenticate(context: TContext<E, string>): Promise<IAuthUser> {
|
|
487
|
+
const token = this.service.extractCredentials(context);
|
|
488
|
+
return this.service.verify(token);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Protected Fields
|
|
494
|
+
|
|
495
|
+
| Field | Type | Default | Description |
|
|
496
|
+
|-------|------|---------|-------------|
|
|
497
|
+
| `jwksVerifier` | `ReturnType<typeof createRemoteJWKSet> \| null` | `null` | Remote JWKS verifier function |
|
|
498
|
+
|
|
499
|
+
### Constructor Behavior
|
|
185
500
|
|
|
186
501
|
```typescript
|
|
187
502
|
constructor(
|
|
188
|
-
@inject({ key: AuthenticateBindingKeys.
|
|
189
|
-
protected options:
|
|
503
|
+
@inject({ key: AuthenticateBindingKeys.JWKS_OPTIONS })
|
|
504
|
+
protected options: IJWKSVerifierOptions,
|
|
190
505
|
) {
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
506
|
+
// Calls configurePayloadEncryption({ aesAlgorithm, applicationSecret })
|
|
507
|
+
// Remote JWKS is NOT fetched here — fetched lazily via ensureInitialized()
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Initialization Flow
|
|
512
|
+
|
|
513
|
+
The `initialize()` method:
|
|
514
|
+
1. Creates a `createRemoteJWKSet()` from the configured `jwksUrl`
|
|
515
|
+
2. Configures `cacheMaxAge` (default 12h) and `cooldownDuration` (default 30s)
|
|
516
|
+
3. Sets `initialized = true`
|
|
517
|
+
|
|
518
|
+
### Overridden Methods
|
|
519
|
+
|
|
520
|
+
| Method | Behavior |
|
|
521
|
+
|--------|----------|
|
|
522
|
+
| `doVerify(token)` | Calls `ensureInitialized()`, then `jwtVerify(token, this.jwksVerifier!)`, then `this.decryptPayload()` |
|
|
523
|
+
| `getSigner(opts)` | Throws — verifier mode cannot sign tokens |
|
|
524
|
+
| `getSigningKey()` | Throws — verifier mode cannot sign tokens |
|
|
525
|
+
| `getDefaultTokenExpiresFn()` | Throws — verifier mode has no token expiry |
|
|
526
|
+
|
|
527
|
+
## JWKSController
|
|
528
|
+
|
|
529
|
+
Serves the JWKS endpoint (default path `/certs`). This endpoint is **intentionally unauthenticated** — it serves the public keys needed by external verifiers.
|
|
530
|
+
|
|
531
|
+
**File:** `packages/core/src/components/auth/authenticate/controllers/jwks/controller.ts`
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
class JWKSController extends BaseController {
|
|
535
|
+
constructor(
|
|
536
|
+
@inject({
|
|
537
|
+
key: BindingKeys.build({
|
|
538
|
+
namespace: BindingNamespaces.SERVICE,
|
|
539
|
+
key: JWKSIssuerTokenService.name,
|
|
540
|
+
}),
|
|
541
|
+
})
|
|
542
|
+
private jwksService: JWKSIssuerTokenService,
|
|
543
|
+
) {
|
|
544
|
+
super({ scope: JWKSController.name, path: '/certs', isStrict: true });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
override binding(): ValueOrPromise<void> {
|
|
548
|
+
this.defineRoute({
|
|
549
|
+
configs: RouteConfigs.GET_JWKS_CERTS,
|
|
550
|
+
handler: async context => {
|
|
551
|
+
const jwks = await this.jwksService.getJWKSAsync();
|
|
552
|
+
context.header('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400');
|
|
553
|
+
return context.json(jwks, HTTP.ResultCodes.RS_2.Ok);
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Response format:**
|
|
561
|
+
```json
|
|
562
|
+
{
|
|
563
|
+
"keys": [
|
|
564
|
+
{
|
|
565
|
+
"kty": "EC",
|
|
566
|
+
"kid": "my-key-id-1",
|
|
567
|
+
"use": "sig",
|
|
568
|
+
"alg": "ES256",
|
|
569
|
+
"crv": "P-256",
|
|
570
|
+
"x": "...",
|
|
571
|
+
"y": "..."
|
|
572
|
+
}
|
|
573
|
+
]
|
|
196
574
|
}
|
|
197
575
|
```
|
|
198
576
|
|
|
577
|
+
**Cache headers:** `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`
|
|
578
|
+
|
|
579
|
+
> [!NOTE]
|
|
580
|
+
> The `/certs` path is configurable via `rest.path` in `IJWKSIssuerOptions`. The component applies the `@controller` decorator dynamically with the configured path.
|
|
581
|
+
|
|
199
582
|
## BasicTokenService
|
|
200
583
|
|
|
201
584
|
All methods are instance methods on <code v-pre>BasicTokenService<E extends Env = Env></code>, which extends `BaseService`.
|
|
202
585
|
|
|
586
|
+
**File:** `packages/core/src/components/auth/authenticate/services/basic/service.ts`
|
|
587
|
+
|
|
203
588
|
### BasicAuthenticationStrategy
|
|
204
589
|
|
|
205
590
|
Extends `BaseHelper` and implements <code v-pre>IAuthenticationStrategy<E></code>. Generic on <code v-pre><E extends Env = Env></code>.
|
|
@@ -209,10 +594,15 @@ class BasicAuthenticationStrategy<E extends Env = Env>
|
|
|
209
594
|
extends BaseHelper
|
|
210
595
|
implements IAuthenticationStrategy<E>
|
|
211
596
|
{
|
|
212
|
-
name = Authentication.STRATEGY_BASIC;
|
|
597
|
+
name = Authentication.STRATEGY_BASIC;
|
|
213
598
|
|
|
214
599
|
constructor(
|
|
215
|
-
@inject({
|
|
600
|
+
@inject({
|
|
601
|
+
key: BindingKeys.build({
|
|
602
|
+
namespace: BindingNamespaces.SERVICE,
|
|
603
|
+
key: BasicTokenService.name,
|
|
604
|
+
}),
|
|
605
|
+
})
|
|
216
606
|
private service: BasicTokenService<E>,
|
|
217
607
|
) { ... }
|
|
218
608
|
|
|
@@ -230,16 +620,12 @@ class BasicAuthenticationStrategy<E extends Env = Env>
|
|
|
230
620
|
| `extractCredentials` | <code v-pre>(context: TContext<E, string>) => { username: string; password: string }</code> | Decodes Base64 <code v-pre>Authorization: Basic <base64></code> header |
|
|
231
621
|
| `verify` | <code v-pre>(opts: { credentials: { username: string; password: string }; context: TContext<E, string> }) => Promise<IAuthUser></code> | Calls user-provided `verifyCredentials` callback |
|
|
232
622
|
|
|
233
|
-
### Private Fields
|
|
234
|
-
|
|
235
|
-
- `verifyCredentials` -- the callback function extracted from injected options
|
|
236
|
-
|
|
237
623
|
### Constructor Behavior
|
|
238
624
|
|
|
239
625
|
```typescript
|
|
240
626
|
constructor(
|
|
241
627
|
@inject({ key: AuthenticateBindingKeys.BASIC_OPTIONS })
|
|
242
|
-
protected options:
|
|
628
|
+
protected options: TBasicTokenServiceOptions<E>,
|
|
243
629
|
) {
|
|
244
630
|
// Throws '[BasicTokenService] Invalid verifyCredentials function' if !options?.verifyCredentials
|
|
245
631
|
}
|
|
@@ -299,7 +685,7 @@ The `defineAuthController()` function dynamically creates a controller class at
|
|
|
299
685
|
1. **Class creation:** A new class is created dynamically with `class AuthController extends BaseController {}` inside the factory closure
|
|
300
686
|
2. **Decorator application:** The `@controller({ path: restPath })` decorator is applied to set the base path. The controller is created with `isStrict: true`
|
|
301
687
|
3. **Service injection:** The auth service is injected via `inject({ key: serviceKey })(AuthController, undefined, 0)` after class definition -- this programmatically applies `@inject` to constructor parameter 0
|
|
302
|
-
-
|
|
688
|
+
- Service key is provided via `controllerOpts.serviceKey` (required)
|
|
303
689
|
- Service must implement `IAuthService` interface
|
|
304
690
|
4. **Route definition:** Routes are defined in the controller's `binding()` method using `this.defineRoute()`
|
|
305
691
|
5. **Schema customization:** Custom Zod schemas can be provided per endpoint via the `payload` option. Defaults to built-in schemas when not provided, with `AnyObjectSchema` as the response fallback.
|
|
@@ -313,111 +699,6 @@ function defineAuthController(opts: TDefineAuthControllerOpts): typeof AuthContr
|
|
|
313
699
|
> [!NOTE]
|
|
314
700
|
> The factory also exports `JWTTokenPayloadSchema`, a Zod schema used for the `/who-am-i` response validation.
|
|
315
701
|
|
|
316
|
-
**Internal route binding:**
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
// Inside the factory-generated controller
|
|
320
|
-
binding(): void {
|
|
321
|
-
// /sign-in -- no auth, delegates to service.signIn()
|
|
322
|
-
this.defineRoute({
|
|
323
|
-
configs: {
|
|
324
|
-
path: '/sign-in',
|
|
325
|
-
method: 'post',
|
|
326
|
-
request: {
|
|
327
|
-
body: jsonContent({
|
|
328
|
-
description: 'Sign-in request body',
|
|
329
|
-
required: true,
|
|
330
|
-
schema: payload?.signIn?.request?.schema ?? SignInRequestSchema,
|
|
331
|
-
}),
|
|
332
|
-
},
|
|
333
|
-
responses: jsonResponse({
|
|
334
|
-
schema: payload?.signIn?.request?.schema ?? AnyObjectSchema,
|
|
335
|
-
description: 'Success Response',
|
|
336
|
-
}),
|
|
337
|
-
},
|
|
338
|
-
handler: async (context) => {
|
|
339
|
-
const body = await context.req.json();
|
|
340
|
-
const rs = await this.service.signIn(context, body);
|
|
341
|
-
return context.json(rs, HTTP.ResultCodes.RS_2.Ok);
|
|
342
|
-
},
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// /sign-up -- conditionally requires JWT auth
|
|
346
|
-
this.defineRoute({
|
|
347
|
-
configs: {
|
|
348
|
-
path: '/sign-up',
|
|
349
|
-
method: 'post',
|
|
350
|
-
authenticate: {
|
|
351
|
-
strategies: !requireAuthenticatedSignUp ? [] : [Authentication.STRATEGY_JWT],
|
|
352
|
-
},
|
|
353
|
-
request: {
|
|
354
|
-
body: jsonContent({
|
|
355
|
-
description: 'Sign-up request body',
|
|
356
|
-
required: true,
|
|
357
|
-
schema: payload?.signUp?.request?.schema ?? SignUpRequestSchema,
|
|
358
|
-
}),
|
|
359
|
-
},
|
|
360
|
-
responses: jsonResponse({
|
|
361
|
-
schema: payload?.signUp?.response?.schema ?? AnyObjectSchema,
|
|
362
|
-
description: 'Success Response',
|
|
363
|
-
}),
|
|
364
|
-
},
|
|
365
|
-
handler: async (context) => {
|
|
366
|
-
const body = await context.req.json();
|
|
367
|
-
const rs = await this.service.signUp(context, body);
|
|
368
|
-
return context.json(rs, HTTP.ResultCodes.RS_2.Ok);
|
|
369
|
-
},
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// /change-password -- always requires JWT auth
|
|
373
|
-
this.defineRoute({
|
|
374
|
-
configs: {
|
|
375
|
-
path: '/change-password',
|
|
376
|
-
method: 'post',
|
|
377
|
-
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
378
|
-
request: {
|
|
379
|
-
body: jsonContent({
|
|
380
|
-
description: 'Change password request body',
|
|
381
|
-
required: true,
|
|
382
|
-
schema: payload?.changePassword?.request?.schema ?? ChangePasswordRequestSchema,
|
|
383
|
-
}),
|
|
384
|
-
},
|
|
385
|
-
responses: jsonResponse({
|
|
386
|
-
schema: payload?.changePassword?.response?.schema ?? AnyObjectSchema,
|
|
387
|
-
description: 'Success Response',
|
|
388
|
-
}),
|
|
389
|
-
},
|
|
390
|
-
handler: async (context) => {
|
|
391
|
-
const body = await context.req.json();
|
|
392
|
-
const rs = await this.service.changePassword(context, body);
|
|
393
|
-
return context.json(rs, HTTP.ResultCodes.RS_2.Ok);
|
|
394
|
-
},
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// /who-am-i -- always requires JWT, returns current user from context
|
|
398
|
-
this.defineRoute({
|
|
399
|
-
configs: {
|
|
400
|
-
path: '/who-am-i',
|
|
401
|
-
method: 'get',
|
|
402
|
-
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
403
|
-
responses: {
|
|
404
|
-
[HTTP.ResultCodes.RS_2.Ok]: jsonContent({
|
|
405
|
-
description: 'Success Response',
|
|
406
|
-
schema: JWTTokenPayloadSchema,
|
|
407
|
-
}),
|
|
408
|
-
},
|
|
409
|
-
},
|
|
410
|
-
handler: (context) => {
|
|
411
|
-
const currentUser = context.get(Authentication.CURRENT_USER as never) as IJWTTokenPayload;
|
|
412
|
-
return context.json(currentUser, HTTP.ResultCodes.RS_2.Ok);
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
> [!TIP]
|
|
419
|
-
> If the default request/response schemas do not fit your needs, provide custom Zod schemas through the `payload` option in `controllerOpts`. This allows full control over validation while keeping the built-in routing.
|
|
420
|
-
|
|
421
702
|
**Service resolution:**
|
|
422
703
|
|
|
423
704
|
The factory applies `@inject` programmatically to constructor parameter 0:
|
|
@@ -437,6 +718,45 @@ constructor(
|
|
|
437
718
|
|
|
438
719
|
If the service is not bound, the component will throw: `"[AuthController] Failed to init auth controller | Invalid injectable authentication service!"`
|
|
439
720
|
|
|
721
|
+
## File Structure
|
|
722
|
+
|
|
723
|
+
```
|
|
724
|
+
packages/core/src/components/auth/authenticate/
|
|
725
|
+
├── common/
|
|
726
|
+
│ ├── codecs.ts # AuthenticationFieldCodecs (ROLES_CODEC, build() factory)
|
|
727
|
+
│ ├── constants.ts # AuthenticateStrategy, JOSEStandards, JWKSModes, JWKSKeyDrivers, JWKSKeyFormats, Authentication, AuthenticationTokenTypes, AuthenticationModes
|
|
728
|
+
│ ├── keys.ts # AuthenticateBindingKeys (REST_OPTIONS, JWT_OPTIONS, JWKS_OPTIONS, BASIC_OPTIONS)
|
|
729
|
+
│ ├── types.ts # All option interfaces, discriminated unions, IAuthUser, IJWTTokenPayload, IPayloadFieldCodec, IAuthService
|
|
730
|
+
│ └── index.ts # Barrel export
|
|
731
|
+
├── controllers/
|
|
732
|
+
│ ├── factory.ts # defineAuthController() factory + JWTTokenPayloadSchema
|
|
733
|
+
│ └── jwks/
|
|
734
|
+
│ ├── controller.ts # JWKSController (serves /certs endpoint)
|
|
735
|
+
│ └── definitions.ts # Route config for GET /certs
|
|
736
|
+
├── middlewares/
|
|
737
|
+
│ └── authenticate.middleware.ts # Standalone authenticate() convenience function
|
|
738
|
+
├── providers/
|
|
739
|
+
│ └── authentication.provider.ts # AuthenticationProvider (IProvider pattern, creates middleware)
|
|
740
|
+
├── services/
|
|
741
|
+
│ ├── basic/
|
|
742
|
+
│ │ └── service.ts # BasicTokenService
|
|
743
|
+
│ ├── bearer/
|
|
744
|
+
│ │ ├── abstract.service.ts # AbstractBearerTokenService (shared logic)
|
|
745
|
+
│ │ ├── jws.service.ts # JWSTokenService (symmetric HS256)
|
|
746
|
+
│ │ └── jwks/
|
|
747
|
+
│ │ ├── abstract.service.ts # AbstractJWKSTokenService (lazy-init)
|
|
748
|
+
│ │ ├── issuer.service.ts # JWKSIssuerTokenService
|
|
749
|
+
│ │ └── verifier.service.ts # JWKSVerifierTokenService
|
|
750
|
+
│ └── index.ts # Barrel export
|
|
751
|
+
├── strategies/
|
|
752
|
+
│ ├── basic.strategy.ts # BasicAuthenticationStrategy
|
|
753
|
+
│ ├── jws.strategy.ts # JWSAuthenticationStrategy
|
|
754
|
+
│ ├── jwks.strategy.ts # JWKSIssuerAuthenticationStrategy + JWKSVerifierAuthenticationStrategy
|
|
755
|
+
│ ├── strategy-registry.ts # AuthenticationStrategyRegistry singleton
|
|
756
|
+
│ └── index.ts # Barrel export
|
|
757
|
+
└── component.ts # AuthenticateComponent
|
|
758
|
+
```
|
|
759
|
+
|
|
440
760
|
## See Also
|
|
441
761
|
|
|
442
762
|
- [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
|