@t4h.framework/jwks 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai/skills/framework-jwks/SKILL.md +275 -0
- package/CHANGELOG.md +21 -0
- package/package.json +5 -4
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framework-jwks
|
|
3
|
+
description: >-
|
|
4
|
+
JWKS-backed authorization for T4H Framework workflows and apps. Use when
|
|
5
|
+
importing @t4h.framework/jwks, wiring JWKSAuthorization on Workflow or App,
|
|
6
|
+
validating RS256 JWTs against a JWKS endpoint, verifying detached signatures
|
|
7
|
+
with kid/algorithm/signatureFormat, configuring issuer/audience checks, or
|
|
8
|
+
implementing the @t4h.framework/jwks/authorize runtime handler. Requires
|
|
9
|
+
@t4h.framework/core. Package path: framework/packages/jwks.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# @t4h.framework/jwks
|
|
13
|
+
|
|
14
|
+
JWKS-backed **Authorization** for workflows and apps. Exposes `JWKSAuthorization` (core manifest) and a default runtime handler at `@t4h.framework/jwks/authorize` that fetches signing keys from a JWKS URL and verifies credentials.
|
|
15
|
+
|
|
16
|
+
**Peer dependency:** `@t4h.framework/core` (same major line).
|
|
17
|
+
|
|
18
|
+
**Node:** `>=22`.
|
|
19
|
+
|
|
20
|
+
Package path: `framework/packages/jwks`. See [README.md](../../../README.md) for overview (field names below match **source**, not older README examples that mention `value`).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Imports
|
|
25
|
+
|
|
26
|
+
| Entry | Purpose |
|
|
27
|
+
|-------|---------|
|
|
28
|
+
| `@t4h.framework/jwks` | `JWKSAuthorization`, `JWKSAuthorizationContext`, `JWKSAuthorizationManifest`, `JWKSAlgorithm`, re-exports authorize module |
|
|
29
|
+
| `@t4h.framework/jwks/authorize` | Default async handler: `(context) => Promise<boolean>` |
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
JWKSAuthorization,
|
|
34
|
+
type JWKSAuthorizationContext,
|
|
35
|
+
JWKSAlgorithm,
|
|
36
|
+
} from '@t4h.framework/jwks'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Do **not** assume OIDC discovery, env-variable mapping, or `credentialSource` helpers—this package does not read HTTP headers or env. Your app or hosting layer supplies `signature`, `rawPayload`, and `url`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Wiring authorization (core)
|
|
44
|
+
|
|
45
|
+
`JWKSAuthorization` extends core `Authorization`. Attach to a **Workflow** or **App**; `toManifest()` registers the handler module for the platform.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { App, Workflow } from '@t4h.framework/core'
|
|
49
|
+
import { JWKSAuthorization } from '@t4h.framework/jwks'
|
|
50
|
+
|
|
51
|
+
const authorization = new JWKSAuthorization({
|
|
52
|
+
url: 'https://auth.example.com/.well-known/jwks.json',
|
|
53
|
+
signature: '<credential-from-runtime>',
|
|
54
|
+
rawPayload: '<payload-or-same-as-jwt>',
|
|
55
|
+
audience: 'https://api.example.com',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const workflow = new Workflow(
|
|
59
|
+
{ id: 'protected/v1', authorization },
|
|
60
|
+
async () => ({ ok: true }),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const app = new App({
|
|
64
|
+
id: 'my-app',
|
|
65
|
+
workflows: [workflow],
|
|
66
|
+
authorization, // optional app-level default
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Manifest shape (from tests):
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
{
|
|
74
|
+
pathToModule: '@t4h.framework/jwks/authorize',
|
|
75
|
+
context: {
|
|
76
|
+
url: string
|
|
77
|
+
signature: string
|
|
78
|
+
rawPayload: string
|
|
79
|
+
issuer?: string
|
|
80
|
+
audience?: string
|
|
81
|
+
kid?: string
|
|
82
|
+
algorithm?: string
|
|
83
|
+
signatureFormat?: string
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Optional manifest fields are still present as `undefined` when omitted (`toManifest` copies all keys).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Context: `JWKSAuthorizationContext`
|
|
93
|
+
|
|
94
|
+
| Field | Required | Description |
|
|
95
|
+
|-------|----------|-------------|
|
|
96
|
+
| `url` | Yes | JWKS document URL (`jwks-rsa` `jwksUri`). |
|
|
97
|
+
| `signature` | Yes | Credential to verify: JWT string, or detached signature for non-JWT path. Empty string → handler returns `false`. |
|
|
98
|
+
| `rawPayload` | Yes | Payload bytes as string for non-JWT verification; for JWT path, tests pass the same string as `signature` (handler only uses `signature` on JWT path). |
|
|
99
|
+
| `issuer` | No | When set, passed to `jwt.verify` as expected `iss`. Mismatch **rejects** (throws). |
|
|
100
|
+
| `audience` | No | When set, passed to `jwt.verify` as expected `aud`. |
|
|
101
|
+
| `kid` | Conditional | Required when `signature` is **not** a decodable JWT (non-JWT path). |
|
|
102
|
+
| `algorithm` | Conditional | Non-JWT path: **enum key** on `JWKSAlgorithm` (e.g. `RS256`), not the Node crypto string. |
|
|
103
|
+
| `signatureFormat` | Conditional | Non-JWT path: encoding for the signature buffer (`hex`, `base64`, etc.—`BinaryToTextEncoding`). |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Runtime handler (`@t4h.framework/jwks/authorize`)
|
|
108
|
+
|
|
109
|
+
Default export:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
(options: JWKSAuthorizationContext) => Promise<boolean>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Behavior (from `jwks-authorize.ts` and tests):
|
|
116
|
+
|
|
117
|
+
1. If `signature` is falsy → **`false`** (no throw).
|
|
118
|
+
2. Build `JwksClient({ jwksUri: options.url })`.
|
|
119
|
+
3. `jwt.decode(signature, { complete: true })`:
|
|
120
|
+
- **Decoded (JWT path)** → `validateJwt(signature, client, { issuer, audience })`.
|
|
121
|
+
- **Not decoded (non-JWT path)** → require `kid`, `algorithm`, `signatureFormat`; else **throw** with fixed messages (see below).
|
|
122
|
+
|
|
123
|
+
### JWT path (`validateJwt`)
|
|
124
|
+
|
|
125
|
+
- Algorithm must be **RS256** (`decoded.header.alg === 'RS256'`); otherwise **`false`** (e.g. HS256 tokens return `false`, not throw).
|
|
126
|
+
- Header must include **`kid`**; used to `getSigningKey(kid)` from JWKS.
|
|
127
|
+
- **`exp` enforced** (`ignoreExpiration: false`). Expired or wrong signing key → **`jwt.verify` rejects** (promise rejection / throw from handler).
|
|
128
|
+
- Optional `issuer` / `audience` from context applied when present.
|
|
129
|
+
|
|
130
|
+
### Non-JWT path
|
|
131
|
+
|
|
132
|
+
Uses Node `crypto.createVerify` with public key from JWKS:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
verifier.update(options.rawPayload)
|
|
136
|
+
verifier.verify(signingKey, options.signature, signatureFormat)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Throws if any required field is missing:
|
|
140
|
+
|
|
141
|
+
| Missing | Error message |
|
|
142
|
+
|---------|----------------|
|
|
143
|
+
| `kid` | `KID is required for payload verification` |
|
|
144
|
+
| `algorithm` | `Algorithm is required for payload verification` |
|
|
145
|
+
| `signatureFormat` | `Signature format is required for payload verification` |
|
|
146
|
+
|
|
147
|
+
`algorithm` must be a **key** of `JWKSAlgorithm`:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
export enum JWKSAlgorithm {
|
|
151
|
+
RS256 = 'RSA-SHA256',
|
|
152
|
+
RS384 = 'RSA-SHA384',
|
|
153
|
+
RS512 = 'RSA-SHA512',
|
|
154
|
+
PS256 = 'RSA-PSS-SHA256',
|
|
155
|
+
PS384 = 'RSA-PSS-SHA384',
|
|
156
|
+
PS512 = 'RSA-PSS-SHA512',
|
|
157
|
+
ES256 = 'ECDSA-SHA256',
|
|
158
|
+
ES384 = 'ECDSA-SHA384',
|
|
159
|
+
ES512 = 'ECDSA-SHA512',
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Handler uses `JWKSAlgorithm[algorithm]` as the `createVerify` algorithm name. Pass `algorithm: 'RS256'`, not `'RSA-SHA256'`.
|
|
164
|
+
|
|
165
|
+
Returns **`true`** on successful verification, **`false`** when verification fails (non-JWT invalid signature).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Examples
|
|
170
|
+
|
|
171
|
+
### RS256 JWT (typical API Bearer token)
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { JWKSAuthorization } from '@t4h.framework/jwks'
|
|
175
|
+
|
|
176
|
+
const authorization = new JWKSAuthorization({
|
|
177
|
+
url: 'https://id.example.com/.well-known/jwks.json',
|
|
178
|
+
signature: bearerToken, // without "Bearer " prefix if your layer strips it
|
|
179
|
+
rawPayload: bearerToken,
|
|
180
|
+
issuer: 'https://issuer.example.com',
|
|
181
|
+
audience: 'my-api',
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Token must be signed RS256 with `kid` in header. Platform invokes `@t4h.framework/jwks/authorize` with manifest `context` when the protected workflow runs.
|
|
186
|
+
|
|
187
|
+
### Detached signature (non-JWT)
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
new JWKSAuthorization({
|
|
191
|
+
url: 'https://id.example.com/jwks',
|
|
192
|
+
signature: detachedSignatureHex,
|
|
193
|
+
rawPayload: canonicalPayloadString,
|
|
194
|
+
kid: 'key-1',
|
|
195
|
+
algorithm: 'RS256',
|
|
196
|
+
signatureFormat: 'hex',
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
If `signature` accidentally looks like a JWT to `jwt.decode`, the JWT path runs instead—structure payloads accordingly.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Return values vs errors
|
|
205
|
+
|
|
206
|
+
| Situation | Result |
|
|
207
|
+
|-----------|--------|
|
|
208
|
+
| Empty `signature` | `false` |
|
|
209
|
+
| Valid RS256 JWT, matching JWKS key | `true` |
|
|
210
|
+
| JWT wrong alg (e.g. HS256), no `kid`, undecodable token | `false` |
|
|
211
|
+
| Expired JWT, issuer mismatch, signature mismatch | **Rejected** (throw from `jwt.verify`) |
|
|
212
|
+
| Non-JWT missing `kid` / `algorithm` / `signatureFormat` | **Throw** (configuration error) |
|
|
213
|
+
| Non-JWT verify fails | `false` |
|
|
214
|
+
|
|
215
|
+
Design callers so manifest context is complete **before** the handler runs; missing non-JWT fields are programmer errors, not silent denials.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Testing the handler locally
|
|
220
|
+
|
|
221
|
+
Unit tests mock `jwks-rsa` and use real RSA key pairs + `jsonwebtoken`:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import jwks from '@t4h.framework/jwks/authorize'
|
|
225
|
+
import jwt from 'jsonwebtoken'
|
|
226
|
+
|
|
227
|
+
// Valid JWT
|
|
228
|
+
const token = jwt.sign({ sub: 'user-1' }, privateKeyPem, {
|
|
229
|
+
algorithm: 'RS256',
|
|
230
|
+
keyid: 'kid-1',
|
|
231
|
+
expiresIn: '1h',
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
jwks({
|
|
236
|
+
url: 'https://example.com/jwks',
|
|
237
|
+
signature: token,
|
|
238
|
+
rawPayload: token,
|
|
239
|
+
}),
|
|
240
|
+
).resolves.toBe(true)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Test files: `src/__tests__/jwks-authorize.spec.ts`, `src/helpers/__tests__/validateJwt.spec.ts`, `src/models/__tests__/JWKSAuthorization.spec.ts`.
|
|
244
|
+
|
|
245
|
+
`JWKSAuthorization` tests only assert `toManifest()` shape—handler tests cover verification behavior.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Dependencies (implementation)
|
|
250
|
+
|
|
251
|
+
- **`jwks-rsa`** — `JwksClient`, `getSigningKey(kid)`, `getPublicKey()`.
|
|
252
|
+
- **`jsonwebtoken`** — decode/verify on JWT path.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## What this package does not do
|
|
257
|
+
|
|
258
|
+
- No built-in OIDC discovery or `JWKS_ISSUER` env mapping.
|
|
259
|
+
- No HTTP header / query / env extraction for credentials.
|
|
260
|
+
- JWT path supports **RS256 only** (not RS384/ES256 via JWT path—those enum values apply to non-JWT `createVerify` only).
|
|
261
|
+
- Does not define **Claims** or **Activities**—only **Authorization** manifests for core `Workflow` / `App`.
|
|
262
|
+
|
|
263
|
+
For workflow orchestration and claims use **framework-core**; for the `TestWorkflowEnvironment` testing harness use **framework-workflow-testing**.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Quick checklist for agents
|
|
268
|
+
|
|
269
|
+
1. Add `@t4h.framework/jwks` and peer `@t4h.framework/core`.
|
|
270
|
+
2. Build `JWKSAuthorizationContext` with `url`, `signature`, `rawPayload` at minimum.
|
|
271
|
+
3. Attach `new JWKSAuthorization(context)` to `Workflow` and/or `App` `authorization` option.
|
|
272
|
+
4. JWT: ensure RS256 + `kid`; optional `issuer` / `audience`.
|
|
273
|
+
5. Non-JWT: set `kid`, `algorithm` (`JWKSAlgorithm` key), `signatureFormat`.
|
|
274
|
+
6. Expect `false` for deny, throws for misconfiguration or JWT verify failures (expired, wrong iss).
|
|
275
|
+
7. Do not reference APIs absent from `src/` and tests.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @t4h.framework/jwks
|
|
2
|
+
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`8098041`](https://github.com/tech4humans-brasil/framework/commit/80980419d59a987378b7c94372ee2f7b7dcc00f5)]:
|
|
8
|
+
- @t4h.framework/core@0.10.0
|
|
9
|
+
|
|
10
|
+
## 0.4.0
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Update skills ([`6d7e7bc`](https://github.com/tech4humans-brasil/framework/commit/6d7e7bca0c780a9c53a0c9e5aa0a865c5af67959))
|
|
15
|
+
|
|
16
|
+
## 0.3.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- Implements jwks package ([#52](https://github.com/tech4humans-brasil/framework/pull/52))
|
|
21
|
+
- Adds .ai/skills in packages ([#88](https://github.com/tech4humans-brasil/framework/pull/88))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t4h.framework/jwks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "JWKS module for the T4H Framework authorization",
|
|
5
5
|
"homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/jwks",
|
|
6
6
|
"bugs": "https://github.com/tech4humans-brasil/framework/issues",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"types": "./dist/jwks.d.ts",
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
|
-
"LICENSE"
|
|
29
|
+
"LICENSE",
|
|
30
|
+
".ai"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
|
32
33
|
"build": "tsc --project tsconfig.build.json",
|
|
@@ -36,13 +37,13 @@
|
|
|
36
37
|
"test:watch": "vitest"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@t4h.framework/core": "^0.
|
|
40
|
+
"@t4h.framework/core": "^0.10.0",
|
|
40
41
|
"@types/jsonwebtoken": "^9",
|
|
41
42
|
"typescript": "^5.9.3",
|
|
42
43
|
"vitest": "^4.0.18"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
|
-
"@t4h.framework/core": "^0.
|
|
46
|
+
"@t4h.framework/core": "^0.10.0"
|
|
46
47
|
},
|
|
47
48
|
"packageManager": "yarn@4.12.0",
|
|
48
49
|
"engines": {
|