@t4h.framework/jwks 0.7.7 → 0.7.8

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.
@@ -1,267 +1,108 @@
1
1
  ---
2
2
  name: framework-jwks
3
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.
4
+ Verifies RS256 JWTs and detached public-key signatures against a JWKS
5
+ endpoint with @t4h.framework/jwks. Use when a T4H Framework workflow or App
6
+ needs JWKSAuthorization, when an async activity such as webhook() declares
7
+ auth { type: 'jwt' } or auth { type: 'jwks' } for its callbacks, or when the
8
+ user mentions a JWKS URL, .well-known/jwks.json, kid, issuer, audience,
9
+ JWKSAlgorithm, a bearer JWT (Google Pub/Sub push, Cloud Tasks) or a signature
10
+ verified with a key published in a JWKS.
10
11
  ---
11
12
 
12
13
  # @t4h.framework/jwks
13
14
 
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
+ The package has two uses. Pick the one that matches the task:
15
16
 
16
- **Peer dependency:** `@t4h.framework/core` (same major line).
17
+ - **Protect a workflow or an `App`**: `new JWKSAuthorization(context)`.
18
+ - **Verify the callbacks of an async activity**: declare
19
+ `auth: { type: 'jwt', … }` or `auth: { type: 'jwks', … }` on the activity
20
+ (for `webhook()`, see the **framework-webhook** skill). The host verifies
21
+ each callback with `@t4h.framework/jwks/callback`. You do not import it.
17
22
 
18
- **Node:** `>=22`.
23
+ ## Protect a workflow or an App
19
24
 
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
- | `@t4h.framework/jwks/callback` | Default export `jwksCallback`: `(auth, delivery) => Promise<boolean>`, the authorizer hosts pass to the core `AsyncActivityCallback.authorize` for `jwt` (bearer JWT) and `jwks` (detached signature over the body, key resolved by `kid` from a JWKS URL) |
31
-
32
- ```typescript
33
- import {
34
- JWKSAuthorization,
35
- type JWKSAuthorizationContext,
36
- JWKSAlgorithm,
37
- } from '@t4h.framework/jwks'
38
- ```
39
-
40
- 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`.
41
-
42
- ---
43
-
44
- ## Wiring authorization (core)
45
-
46
- `JWKSAuthorization` extends core `Authorization`. Attach to a **Workflow** or **App**; `toManifest()` registers the handler module for the platform.
25
+ The handler picks the path from `signature`: a value that decodes as a JWT
26
+ takes the JWT path, any other value takes the raw-payload path.
47
27
 
48
28
  ```typescript
49
- import { App, Workflow } from '@t4h.framework/core'
50
29
  import { JWKSAuthorization } from '@t4h.framework/jwks'
51
30
 
52
- const authorization = new JWKSAuthorization({
31
+ export const authorization = new JWKSAuthorization({
53
32
  url: 'https://auth.example.com/.well-known/jwks.json',
54
- signature: '<credential-from-runtime>',
55
- rawPayload: '<payload-or-same-as-jwt>',
33
+ signature: '<jwt-from-the-request>',
34
+ rawPayload: '',
35
+ issuer: 'https://auth.example.com/',
56
36
  audience: 'https://api.example.com',
57
37
  })
58
-
59
- const workflow = new Workflow(
60
- { id: 'protected/v1', authorization },
61
- async () => ({ ok: true }),
62
- )
63
-
64
- const app = new App({
65
- id: 'my-app',
66
- workflows: [workflow],
67
- authorization, // optional app-level default
68
- })
69
- ```
70
-
71
- Manifest shape (from tests):
72
-
73
- ```typescript
74
- {
75
- pathToModule: '@t4h.framework/jwks/authorize',
76
- context: {
77
- url: string
78
- signature: string
79
- rawPayload: string
80
- issuer?: string
81
- audience?: string
82
- kid?: string
83
- algorithm?: string
84
- signatureFormat?: string
85
- },
86
- }
87
- ```
88
-
89
- Optional manifest fields are still present as `undefined` when omitted (`toManifest` copies all keys).
90
-
91
- ---
92
-
93
- ## Context: `JWKSAuthorizationContext`
94
-
95
- | Field | Required | Description |
96
- |-------|----------|-------------|
97
- | `url` | Yes | JWKS document URL (`jwks-rsa` `jwksUri`). |
98
- | `signature` | Yes | Credential to verify: JWT string, or detached signature for non-JWT path. Empty string → handler returns `false`. |
99
- | `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). |
100
- | `issuer` | No | When set, passed to `jwt.verify` as expected `iss`. Mismatch **rejects** (throws). |
101
- | `audience` | No | When set, passed to `jwt.verify` as expected `aud`. |
102
- | `kid` | Non-JWT path | Key id to look up in the JWKS. Used when `signature` is **not** a decodable JWT. |
103
- | `algorithm` | Non-JWT path | **Enum key** on `JWKSAlgorithm` (e.g. `RS256`), not the Node crypto string. |
104
- | `signatureFormat` | Non-JWT path | Encoding for the signature buffer (`hex`, `base64`, etc.—`BinaryToTextEncoding`). |
105
-
106
- ---
107
-
108
- ## Runtime handler (`@t4h.framework/jwks/authorize`)
109
-
110
- Default export:
111
-
112
- ```typescript
113
- (options: JWKSAuthorizationContext) => Promise<boolean>
114
- ```
115
-
116
- Behavior (from `jwks-authorize.ts` and tests):
117
-
118
- 1. If `signature` is falsy → **`false`** (no throw).
119
- 2. Build `JwksClient({ jwksUri: options.url })`.
120
- 3. `jwt.decode(signature, { complete: true })`:
121
- - **Decoded (JWT path)** → `validateJwt(signature, client, { issuer, audience })`.
122
- - **Not decoded (non-JWT path)** → verify the detached signature with `kid`, `algorithm` and `signatureFormat`.
123
-
124
- ### JWT path (`validateJwt`)
125
-
126
- - Algorithm must be **RS256** (`decoded.header.alg === 'RS256'`); otherwise **`false`** (e.g. HS256 tokens return `false`, not throw).
127
- - Header must include **`kid`**; used to `getSigningKey(kid)` from JWKS.
128
- - **`exp` enforced** (`ignoreExpiration: false`). Expired or wrong signing key → **`jwt.verify` rejects** (promise rejection / throw from handler).
129
- - Optional `issuer` / `audience` from context applied when present.
130
-
131
- ### Non-JWT path
132
-
133
- Uses Node `crypto.createVerify` with public key from JWKS:
134
-
135
- ```typescript
136
- authorizer.update(options.rawPayload)
137
- authorizer.verify(signingKey, options.signature, signatureFormat)
138
38
  ```
139
39
 
140
- A missing `kid`, `algorithm` or `signatureFormat` throws (`jwks-authorize.ts`).
40
+ Attach it to the `authorization` option of a `Workflow` or an `App`. For a
41
+ detached signature, give the signed bytes in `rawPayload` and set `kid`,
42
+ `algorithm` and `signatureFormat`.
141
43
 
142
- `algorithm` must be a **key** of `JWKSAlgorithm`:
44
+ ## Verify async activity callbacks
143
45
 
144
46
  ```typescript
145
- export enum JWKSAlgorithm {
146
- RS256 = 'RSA-SHA256',
147
- RS384 = 'RSA-SHA384',
148
- RS512 = 'RSA-SHA512',
149
- PS256 = 'RSA-PSS-SHA256',
150
- PS384 = 'RSA-PSS-SHA384',
151
- PS512 = 'RSA-PSS-SHA512',
152
- ES256 = 'ECDSA-SHA256',
153
- ES384 = 'ECDSA-SHA384',
154
- ES512 = 'ECDSA-SHA512',
47
+ auth: {
48
+ type: ('jwt', jwksUri, issuer, audience)
155
49
  }
156
50
  ```
157
51
 
158
- Handler uses `JWKSAlgorithm[algorithm]` as the `createVerify` algorithm name. Pass `algorithm: 'RS256'`, not `'RSA-SHA256'`.
159
-
160
- Returns **`true`** on successful verification, **`false`** when verification fails (non-JWT invalid signature).
161
-
162
- ---
163
-
164
- ## Examples
165
-
166
- ### RS256 JWT (typical API Bearer token)
167
-
168
- ```typescript
169
- import { JWKSAuthorization } from '@t4h.framework/jwks'
170
-
171
- const authorization = new JWKSAuthorization({
172
- url: 'https://id.example.com/.well-known/jwks.json',
173
- signature: bearerToken, // without "Bearer " prefix if your layer strips it
174
- rawPayload: bearerToken,
175
- issuer: 'https://issuer.example.com',
176
- audience: 'my-api',
177
- })
178
- ```
179
-
180
- Token must be signed RS256 with `kid` in header. Platform invokes `@t4h.framework/jwks/authorize` with manifest `context` when the protected workflow runs.
181
-
182
- ### Detached signature (non-JWT)
183
-
184
- ```typescript
185
- new JWKSAuthorization({
186
- url: 'https://id.example.com/jwks',
187
- signature: detachedSignatureHex,
188
- rawPayload: canonicalPayloadString,
189
- kid: 'key-1',
190
- algorithm: 'RS256',
191
- signatureFormat: 'hex',
192
- })
193
- ```
194
-
195
- If `signature` accidentally looks like a JWT to `jwt.decode`, the JWT path runs instead—structure payloads accordingly.
196
-
197
- ---
198
-
199
- ## Return values vs errors
200
-
201
- | Situation | Result |
202
- |-----------|--------|
203
- | Empty `signature` | `false` |
204
- | Valid RS256 JWT, matching JWKS key | `true` |
205
- | JWT wrong alg (e.g. HS256), no `kid`, undecodable token | `false` |
206
- | Expired JWT, issuer mismatch, signature mismatch | **Rejected** (throw from `jwt.verify`) |
207
- | Non-JWT verify fails | `false` |
208
-
209
- Design callers so manifest context is complete **before** the handler runs; missing non-JWT fields are programmer errors, not silent denials.
210
-
211
- ---
212
-
213
- ## Testing the handler locally
214
-
215
- Unit tests mock `jwks-rsa` and use real RSA key pairs + `jsonwebtoken`:
216
-
217
52
  ```typescript
218
- import jwks from '@t4h.framework/jwks/authorize'
219
- import jwt from 'jsonwebtoken'
220
-
221
- // Valid JWT
222
- const token = jwt.sign({ sub: 'user-1' }, privateKeyPem, {
53
+ auth: {
54
+ type: 'jwks',
55
+ jwksUri: 'https://partner.example.com/.well-known/jwks.json',
56
+ header: 'x-signature',
57
+ kidHeader: 'x-key-id',
223
58
  algorithm: 'RS256',
224
- keyid: 'kid-1',
225
- expiresIn: '1h',
226
- })
227
-
228
- await expect(
229
- jwks({
230
- url: 'https://example.com/jwks',
231
- signature: token,
232
- rawPayload: token,
233
- }),
234
- ).resolves.toBe(true)
59
+ signatureFormat: 'base64',
60
+ timestampHeader: 'x-timestamp',
61
+ toleranceInSeconds: 300,
62
+ signedPayload: '{timestamp}.{body}',
63
+ }
235
64
  ```
236
65
 
237
- Test files: `src/__tests__/jwks-authorize.spec.ts`, `src/helpers/__tests__/validateJwt.spec.ts`, `src/models/__tests__/JWKSAuthorization.spec.ts`.
238
-
239
- `JWKSAuthorization` tests only assert `toManifest()` shape—handler tests cover verification behavior.
240
-
241
- ---
242
-
243
- ## Dependencies (implementation)
244
-
245
- - **`jwks-rsa`** — `JwksClient`, `getSigningKey(kid)`, `getPublicKey()`.
246
- - **`jsonwebtoken`** — decode/verify on JWT path.
247
-
248
- ---
249
-
250
- ## What this package does not do
251
-
252
- - No built-in OIDC discovery or `JWKS_ISSUER` env mapping.
253
- - No HTTP header / query / env extraction for credentials.
254
- - JWT path supports **RS256 only** (not RS384/ES256 via JWT path—those enum values apply to non-JWT `createVerify` only).
255
- - Does not define **Claims** or **Activities**—only **Authorization** manifests for core `Workflow` / `App`.
256
-
257
- For workflow orchestration and claims use **framework-core**; for the `TestWorkflowEnvironment` testing harness use **framework-workflow-testing**.
258
-
259
- ---
260
-
261
- ## Quick checklist for agents
262
-
263
- 1. Add `@t4h.framework/jwks` and peer `@t4h.framework/core`.
264
- 2. Build `JWKSAuthorizationContext` with `url`, `signature`, `rawPayload` at minimum.
265
- 3. Attach `new JWKSAuthorization(context)` to `Workflow` and/or `App` `authorization` option.
266
- 4. JWT: ensure RS256 + `kid`; optional `issuer` / `audience`.
267
- 5. Expect `false` for deny, throws for misconfiguration or JWT verify failures (expired, wrong iss).
66
+ On `jwks`, each of `kid`, `algorithm` and `signatureFormat` is a fixed value or
67
+ a `*Header` name. A fixed value wins. `signatureFormat` defaults to `'hex'`.
68
+
69
+ ## Traps
70
+
71
+ - The field is `signature`. There is no `value` field.
72
+ - The JWT path accepts only RS256 tokens with a `kid` in the header. HS256 or
73
+ ES256 tokens give `false`.
74
+ - On the JWT path `rawPayload` is not used. Pass `''`.
75
+ - `algorithm` is a `JWKSAlgorithm` KEY such as `'RS256'`, not the Node name
76
+ `'RSA-SHA256'`. Today only `RS256`, `RS384` and `RS512` verify; the `PS*` and
77
+ `ES*` keys throw `Invalid digest`.
78
+ - A detached signature that happens to decode as a JWT takes the JWT path.
79
+ - `JWKSAuthorization`: a denied raw-payload signature gives `false`, but an
80
+ expired JWT, an `issuer` or `audience` mismatch, a bad JWT signature or a
81
+ missing `kid`/`algorithm`/`signatureFormat` THROWS. On a callback each error
82
+ becomes a refusal.
83
+ - Always set `issuer` and `audience` for a JWT. Without them a token minted for
84
+ another service passes.
85
+ - Callback timestamps: all digits with 13 or more digits are milliseconds,
86
+ fewer are seconds. Other strings go through `Date.parse`.
87
+ - The package does not read HTTP headers or env, and has no OIDC discovery. For
88
+ a token from an OIDC provider, see **framework-oidc**.
89
+
90
+ ## Test
91
+
92
+ The handler fetches keys over HTTP from `url`. Serve a JWKS from a local HTTP
93
+ server in the test (export the public key with
94
+ `publicKey.export({ format: 'jwk' })` and add `kid`), sign a token with
95
+ `jsonwebtoken` (`algorithm: 'RS256'`, `keyid`), and call the default export of
96
+ `@t4h.framework/jwks/authorize`.
97
+
98
+ ## Related skills
99
+
100
+ - **framework-hmac**: shared-secret signatures.
101
+ - **framework-webhook**: wait for a partner callback that uses this `auth`.
102
+
103
+ ## Reference
104
+
105
+ - Guide: <https://tech4humans-brasil.github.io/framework/jwks/>
106
+ - Types: `node_modules/@t4h.framework/jwks/dist/jwks.d.ts`
107
+ - Callback `auth` types: `AsyncActivityCallbackAuthJwt` and
108
+ `AsyncActivityCallbackAuthJwks` in `node_modules/@t4h.framework/core/dist/`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @t4h.framework/jwks
2
2
 
3
+ ## 0.7.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [#257](https://github.com/tech4humans-brasil/framework/pull/257) [`586bfb4`](https://github.com/tech4humans-brasil/framework/commit/586bfb4663f4a478527ee0c8db360603f034d615) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The jwks README is now short and links to the site. The site page now describes the `jwt` and `jwks` callback `auth`, the results and errors, and says that only the `RS*` keys verify a detached signature.
8
+
3
9
  ## 0.7.7
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -1,76 +1,31 @@
1
1
  # @t4h.framework/jwks
2
2
 
3
- JWKS-backed authorization for the T4H Framework. It exposes a core `Authorization` manifest and a runtime handler that validates credentials against keys from a JWKS endpoint.
3
+ JWKS authorization for the T4H Framework. It verifies an RS256 JWT, or a
4
+ detached signature over a raw payload, against the public keys of a JWKS
5
+ endpoint, for a workflow, an app or the callbacks of an async activity.
4
6
 
5
- ## Peer dependency
7
+ ## Install
6
8
 
7
- Requires `@t4h.framework/core` (same major line as this package).
8
-
9
- ## Exports
10
-
11
- | Entry | Purpose |
12
- |------------------|---------|
13
- | `@t4h.framework/jwks` | `JWKSAuthorization`, `JWKSAuthorizationContext`, and related types |
14
- | `@t4h.framework/jwks/authorize` | Default async handler invoked by the runtime with the manifest context |
15
- | `@t4h.framework/jwks/callback` | Default export `jwksCallback`: the `jwt` (bearer JWT) and `jwks` (detached signature verified with a JWKS key) callback authorizer for the core `AsyncActivityCallback.authorize` |
16
-
17
- ## `JWKSAuthorization`
18
-
19
- Subclass of [`Authorization`](https://github.com/tech4humans-brasil/framework/tree/main/packages/core) from core. `toManifest()` points the runtime at `@t4h.framework/jwks/authorize` and passes a `JWKSAuthorizationContext`.
20
-
21
- Construct it with a full `JWKSAuthorizationContext`. `url`, `signature` and `rawPayload` are required. Your app or deployment pipeline supplies `signature` and `rawPayload`, for example from a request header or by runtime injection. This package does not read HTTP or env.
22
-
23
- ## Context: `JWKSAuthorizationContext`
24
-
25
- | Field | Required | Description |
26
- |-------|----------|-------------|
27
- | `url` | Yes | JWKS document URL (`jwks-rsa` `jwksUri`). |
28
- | `signature` | Yes | Credential to verify: a JWT, or a detached signature for the non-JWT path. An empty string gives `false`. |
29
- | `rawPayload` | Yes | The signed payload for the non-JWT path. The JWT path does not use it; pass an empty string. |
30
- | `issuer` | No | JWT path: the expected `iss`. |
31
- | `audience` | No | JWT path: the expected `aud`. |
32
- | `kid` | Non-JWT path | Key id to find in the JWKS. Required when `signature` is **not** a JWT that decodes. |
33
- | `algorithm` | Non-JWT path | A key of `JWKSAlgorithm` (`RS256`, `PS256`, `ES256`, …). |
34
- | `signatureFormat` | Non-JWT path | Encoding of `signature` (`hex`, `base64`, …). |
35
-
36
- There is no OIDC discovery (`JWKS_ISSUER`), no environment-variable mapping in this package, and no `credentialSource` helper. Your application or hosting layer gives `signature` and `rawPayload`.
37
-
38
- ## Runtime handler (`jwks/authorize`)
39
-
40
- The default export is an async function:
41
-
42
- ```ts
43
- (options: JWKSAuthorizationContext) => Promise<boolean>
9
+ ```bash
10
+ npm install @t4h.framework/jwks @t4h.framework/core
44
11
  ```
45
12
 
46
- - Returns `false` if `signature` is empty.
47
- - Returns `true` if verification succeeds.
48
-
49
- ### JWT path
50
-
51
- If `signature` decodes as a JWT (`jsonwebtoken` `decode` with `complete: true` succeeds), the handler verifies it with `validateJwt` ([source](https://github.com/tech4humans-brasil/framework/blob/main/packages/jwks/src/helpers/validateJwt.ts)):
52
-
53
- - The algorithm must be **RS256**. A different `alg` in the header gives `false`.
54
- - The header must have a `kid`, which selects the signing key in the JWKS. No `kid` gives `false`.
55
- - `exp` is enforced.
56
- - `issuer` and `audience` from the context apply when you set them.
57
- - An expired token, an `issuer` or `audience` mismatch, or a bad signature makes `jwt.verify` throw, so the handler throws.
58
-
59
- ### Non-JWT path
60
-
61
- If `signature` does not decode as a JWT, the handler needs `kid`, `algorithm` and `signatureFormat` on the context. If one of them is missing, it **throws**. It gets the public key from the JWKS by `kid`, and verifies `signature` over `rawPayload` with Node `crypto.createVerify`. A signature that does not match gives `false`.
62
-
63
- ## Example
13
+ ## Usage
64
14
 
65
15
  ```typescript
66
16
  import { JWKSAuthorization } from '@t4h.framework/jwks'
67
17
 
68
18
  const authorization = new JWKSAuthorization({
69
19
  url: 'https://auth.example.com/.well-known/jwks.json',
70
- signature: '<credential-from-runtime>',
20
+ signature: '<jwt-from-the-request>',
71
21
  rawPayload: '',
22
+ issuer: 'https://auth.example.com/',
72
23
  audience: 'https://api.example.com',
73
24
  })
74
25
  ```
75
26
 
76
- Wire `authorization` on an `App` or `Workflow` per the core package. The platform loads `@t4h.framework/jwks/authorize` with the manifest context when a protected workflow runs.
27
+ Attach `authorization` to a `Workflow` or an `App`.
28
+
29
+ ## Documentation
30
+
31
+ <https://tech4humans-brasil.github.io/framework/jwks/>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/jwks",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
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",
@@ -43,7 +43,7 @@
43
43
  "test:watch": "vitest"
44
44
  },
45
45
  "devDependencies": {
46
- "@t4h.framework/core": "^0.20.0",
46
+ "@t4h.framework/core": "^0.21.0",
47
47
  "@types/jsonwebtoken": "^9",
48
48
  "typescript": "^5.9.3",
49
49
  "vitest": "^4.0.18"