@t4h.framework/oidc 0.6.11 → 0.6.12

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,272 +1,88 @@
1
1
  ---
2
2
  name: framework-oidc
3
3
  description: >-
4
- Wire OpenID Connect authorization in T4H Framework apps and workflows using
5
- @t4h.framework/oidc: OIDCAuthorization manifest, OIDCAuthorizationContext,
6
- RFC 7662 bearer token introspection with client_secret_basic, and
7
- discoveryUrl. Use when securing App/Workflow with OIDC, token
8
- validation, OpenID Provider discovery, introspection_endpoint, attaching
9
- authorization to manifests, or testing @t4h.framework/oidc/authorize.
4
+ Protects a T4H Framework App or Workflow with OpenID Connect using
5
+ @t4h.framework/oidc: `OIDCAuthorization` with `discoveryUrl`, `clientId`,
6
+ `clientSecret` and a `token` placeholder such as
7
+ '<<headers.authorization>>', checked by RFC 7662 token introspection with
8
+ client_secret_basic. Use when an app or workflow must run only for callers
9
+ with an active access token from an OpenID Provider (Keycloak, Auth0,
10
+ Zitadel), when setting the `authorization` option, or when testing
11
+ `@t4h.framework/oidc/authorize`.
10
12
  ---
11
13
 
12
14
  # @t4h.framework/oidc
13
15
 
14
- OpenID Connect–oriented **authorization** for the T4H Framework. This package does **not** define Activities or Claims; it extends core’s `Authorization` and supplies a runtime handler that validates an incoming credential via **token introspection** (**RFC 7662**), authenticating that call with **`client_secret_basic`**.
15
-
16
- Package path: `framework/packages/oidc`. Peer dependency: `@t4h.framework/core` (same major line). Node `>=22`.
17
-
18
- For workflow Activities, Claims, and `History.reconciler`, use the **framework-core** skill. OIDC only gates access at the App/Workflow authorization layer.
19
-
20
- ---
21
-
22
- ## Imports
23
-
24
- | Entry | Import | Purpose |
25
- |--------|--------|---------|
26
- | `@t4h.framework/oidc` | `OIDCAuthorization`, `OIDCAuthorizationContext`, `OIDCAuthorizationManifest` | Author-time manifest |
27
- | `@t4h.framework/oidc/authorize` | default `oidc` handler | Runtime/tests only — `(context) => Promise<boolean>` |
28
-
29
- ```typescript
30
- import { OIDCAuthorization } from '@t4h.framework/oidc'
31
- import type { OIDCAuthorizationContext } from '@t4h.framework/oidc'
32
-
33
- // Direct handler import (unit tests, custom runtime wiring):
34
- import oidc from '@t4h.framework/oidc/authorize'
35
- ```
36
-
37
- ---
38
-
39
- ## Where it fits (core `Authorization`)
40
-
41
- Core exposes an abstract `Authorization` with `toManifest(): AuthorizationManifest`:
42
-
43
- ```typescript
44
- type AuthorizationManifest<TContext extends object = any> = {
45
- context?: TContext
46
- pathToModule: string
47
- }
48
- ```
49
-
50
- Attach `OIDCAuthorization` on **`App`** or **`Workflow`** options; `toManifest()` embeds `authorization` in the published manifest. The deployed runtime resolves dynamic fields (especially `token`), then loads `@t4h.framework/oidc/authorize` with the manifest `context`.
51
-
52
- Declare the values in the `EnvDesign` in `src/constants/environments.ts`. The context types `clientSecret` as `string`, so declare the entry with `type: String`, not `type: Secret`. This is an exception to the `type: Secret` rule of **framework-core**.
53
-
54
- ```typescript
55
- // src/constants/environments.ts
56
- import { EnvDesign } from '@t4h.framework/core'
57
-
58
- export const environments = EnvDesign({
59
- OIDC_DISCOVERY_URL: { type: String },
60
- OIDC_CLIENT: { type: String },
61
- OIDC_SECRET: { type: String },
62
- })
63
- ```
64
-
65
- ```typescript
66
- import { App } from '@t4h.framework/core'
67
- import { OIDCAuthorization } from '@t4h.framework/oidc'
68
-
69
- import { environments } from './constants/environments.js'
70
-
71
- export default new App({
72
- id: 'my-app',
73
- authorization: new OIDCAuthorization({
74
- discoveryUrl: environments.OIDC_DISCOVERY_URL,
75
- clientId: environments.OIDC_CLIENT,
76
- clientSecret: environments.OIDC_SECRET,
77
- token: '<<headers.authorization>>',
78
- }),
79
- workflows: [/* ... */],
80
- })
81
- ```
82
-
83
- Per-workflow override:
84
-
85
- ```typescript
86
- import { Workflow } from '@t4h.framework/core'
87
- import { OIDCAuthorization } from '@t4h.framework/oidc'
88
-
89
- const secured = new Workflow(
90
- {
91
- id: 'orders/v1',
92
- authorization: new OIDCAuthorization({ /* same context shape */ }),
93
- },
94
- async input => ({ ok: true }),
95
- )
96
- ```
97
-
98
- **App-level vs workflow-level:** set on whichever manifest should enforce OIDC. Workflow `authorization` in `toManifest()` is `authorization?.toManifest() ?? null`; App forwards `authorization?.toManifest()` at app scope.
99
-
100
- ---
101
-
102
- ## `OIDCAuthorization`
103
-
104
- Subclass of `Authorization<OIDCAuthorizationContext>`. Constructor stores context; `toManifest()` always sets:
105
-
106
- - `pathToModule: '@t4h.framework/oidc/authorize'`
107
- - `context`: full context object (`discoveryUrl`, `clientId`, `clientSecret`, `token`)
108
-
109
- ```typescript
110
- new OIDCAuthorization({
111
- discoveryUrl: environments.OIDC_DISCOVERY_URL,
112
- clientId: environments.OIDC_CLIENT,
113
- clientSecret: environments.OIDC_SECRET,
114
- token: '<<headers.authorization>>',
115
- })
116
- ```
117
-
118
- The package does not read env. `@t4h.framework/core/no-literal-secret-in-authorization` rejects a literal `clientSecret`. Pass `discoveryUrl`, `clientSecret` and the other values from the `EnvDesign` when you construct the instance.
119
-
120
- ### Runtime token placeholder
121
-
122
- `token` is authored as a manifest placeholder the runtime replaces:
123
-
124
- ```typescript
125
- token: '<<headers.authorization>>',
126
- ```
127
-
128
- Author with whatever placeholder or value your deployment pipeline documents; the handler always receives the resolved string at runtime.
129
-
130
- ---
131
-
132
- ## `OIDCAuthorizationContext`
133
-
134
- | Field | Required | Description |
135
- |-------|----------|-------------|
136
- | `discoveryUrl` | Yes | OpenID Provider configuration document URL (JSON with `introspection_endpoint`). |
137
- | `clientId` | Yes | OAuth 2.0 client id used as the Basic username on introspection. |
138
- | `clientSecret` | Yes | OAuth 2.0 client secret used as the Basic password on introspection. |
139
- | `token` | Yes | Credential to verify. Must use a `Bearer ` prefix for the token value to be sent to introspection (see below). |
140
-
141
- Types exported: `OIDCAuthorizationContext`, `OIDCAuthorizationManifest` (`AuthorizationManifest<OIDCAuthorizationContext>`).
142
-
143
- ---
144
-
145
- ## Runtime handler (`@t4h.framework/oidc/authorize`)
146
-
147
- Default export: async function `(context: OIDCAuthorizationContext) => Promise<boolean>`.
148
-
149
- ### Flow
150
-
151
- ```mermaid
152
- sequenceDiagram
153
- participant R as Runtime handler
154
- participant D as discoveryUrl
155
- participant I as introspection_endpoint
156
-
157
- R->>D: GET discovery JSON
158
- R->>I: POST form { token, token_type_hint } + Basic client credentials
159
- I-->>R: { active }
160
- R-->>R: true if active else false
161
- ```
162
-
163
- 1. **Discovery** — `GET` `discoveryUrl`; parse JSON as `{ introspection_endpoint }`. Non-OK HTTP → throw `Discovery request failed: <status> <statusText>`.
164
- 2. **Introspection** — `POST` `introspection_endpoint` per **RFC 7662** with:
165
- - `Content-Type: application/x-www-form-urlencoded`
166
- - `Accept: application/json`
167
- - `Authorization: Basic base64(<form-urlencoded clientId>:<form-urlencoded clientSecret>)` (`client_secret_basic`, RFC 6749 section 2.3.1)
168
- - body `URLSearchParams` with `token=<stripped incoming token>` and `token_type_hint=access_token`
169
- Non-OK HTTP → throw `Introspection request failed: ...`.
170
- 3. **Result** — parse the RFC 7662 response (`active` plus optional `scope`, `client_id`, `exp`, `sub`, `aud`, …):
171
- - `active: true` → return `true`
172
- - anything else, including a missing `active` → return `false`
173
-
174
- Network failures from `fetch` propagate as rejected promises (not converted to `false`).
175
-
176
- ### Bearer prefix rules
177
-
178
- Incoming `token` is parsed as an RFC 6750 bearer credential (scheme matched case-insensitively):
179
-
180
- - `Bearer <token>` → introspection sends the token value (e.g. `Bearer abc.def` → `token=abc.def`).
181
- - Anything else → the handler returns `false` immediately, with **no** discovery, token or introspection request (`token` is a required RFC 7662 parameter).
182
-
183
- Always author/runtime-resolve credentials as `Bearer <access-token>` when you expect introspection of the caller’s token.
184
-
185
- ---
186
-
187
- ## Authoring checklist
188
-
189
- 1. Add `@t4h.framework/oidc` dependency alongside `@t4h.framework/core`.
190
- 2. Construct `new OIDCAuthorization({ discoveryUrl, clientId, clientSecret, token, ... })`.
191
- 3. Attach to `App` and/or `Workflow` `authorization` option.
192
- 4. Register a confidential OAuth client allowed to call **introspection** with **`client_secret_basic`**.
193
- 5. Pass incoming caller token with `Bearer ` prefix (or use a runtime placeholder like `<<headers.authorization>>`).
194
-
195
- ---
196
-
197
- ## Testing
198
-
199
- Package tests use **Vitest** with `vi.stubGlobal('fetch', ...)`.
200
-
201
- ### `OIDCAuthorization.toManifest()`
202
-
203
- Assert manifest shape:
204
-
205
- ```typescript
206
- expect(authorization.toManifest()).toEqual({
207
- pathToModule: '@t4h.framework/oidc/authorize',
208
- context: {
209
- discoveryUrl: '...',
210
- clientId: 'svc',
211
- clientSecret: 'secret',
212
- token: 'Bearer t',
213
- },
214
- })
215
- ```
216
-
217
- ### Handler (`import oidc from '@t4h.framework/oidc/authorize'`)
218
-
219
- Mock two `fetch` calls in order: discovery → introspection.
220
-
221
- ```typescript
222
- const baseContext = {
223
- discoveryUrl: 'https://auth.example.com/.well-known/openid-configuration',
224
- clientId: 'client-id',
225
- clientSecret: 'client-secret',
226
- token: 'Bearer incoming-token',
227
- }
228
-
229
- // Happy path: true
230
- fetchMock
231
- .mockResolvedValueOnce(jsonResponse({
232
- introspection_endpoint: 'https://auth.example.com/oauth/introspect',
233
- }))
234
- .mockResolvedValueOnce(jsonResponse({ active: true }))
235
-
236
- await expect(oidc(baseContext)).resolves.toBe(true)
237
- ```
238
-
239
- Verify wire format from tests:
240
-
241
- - Introspection: `Authorization: Basic <base64 client-id:client-secret>`, form body `token=incoming-token&token_type_hint=access_token` after stripping `Bearer `.
242
- - Missing Bearer prefix: resolves `false` with `fetch` never called.
243
- - Inactive token: second response `{ active: false }` → `false`; a response without `active` also yields `false`.
244
- - HTTP errors on discovery/introspection: thrown `Error` messages as documented above.
245
-
246
- Run package tests: `yarn test` in `packages/oidc`.
247
-
248
- Workflow-level authorization behavior in process is owned by the **runtime** (outside this package); this package only defines manifest + handler contract.
249
-
250
- ---
251
-
252
- ## Common mistakes
253
-
254
- | Mistake | Consequence |
255
- |---------|-------------|
256
- | Raw JWT without `Bearer ` prefix | Denied up front; no provider request is made |
257
- | Expecting env auto-wiring | Secrets/URLs must be set when constructing `OIDCAuthorization` |
258
- | Missing `introspection_endpoint` on IdP | The handler throws `Discovery response has no introspection_endpoint` (`oidc-authorize.ts`) |
259
- | Treating `false` as HTTP error | `false` means inactive token; errors are thrown separately |
260
-
261
- ---
262
-
263
- ## API summary
264
-
265
- | Symbol | Kind | Role |
266
- |--------|------|------|
267
- | `OIDCAuthorization` | class | `Authorization` subclass; `toManifest()` → oidc authorize module |
268
- | `OIDCAuthorizationContext` | type | Manifest/runtime context |
269
- | `OIDCAuthorizationManifest` | type | `AuthorizationManifest<OIDCAuthorizationContext>` |
270
- | default `oidc` | function | `@t4h.framework/oidc/authorize` — discovery + RFC 7662 introspection |
271
-
272
- See [README.md](../../../README.md) in this package for the same behavior described for humans.
16
+ `OIDCAuthorization` gates the incoming call of an `App` or a `Workflow`. Before
17
+ the run starts, the check reads the discovery document, sends the caller's
18
+ token to the provider's `introspection_endpoint`, and allows the run only when
19
+ the response has `"active": true`.
20
+
21
+ This package does not authenticate outgoing calls. To call an API with an
22
+ OAuth2 token, use the **framework-http** skill. To check a JWT locally against
23
+ the provider's keys, with no introspection call, use the **framework-jwks**
24
+ skill.
25
+
26
+ ## Protect an app or a workflow
27
+
28
+ 1. Declare the values in the `EnvDesign` in `src/constants/environments.ts`.
29
+ Use `type: String` for the secret: `clientSecret` is a `string`, not a
30
+ `Secret`.
31
+
32
+ ```typescript
33
+ export const environments = EnvDesign({
34
+ OIDC_DISCOVERY_URL: { type: String },
35
+ OIDC_CLIENT: { type: String },
36
+ OIDC_SECRET: { type: String },
37
+ })
38
+ ```
39
+
40
+ 2. Set `authorization` on the `App` (all its workflows) or on one `Workflow`:
41
+
42
+ ```typescript
43
+ import { OIDCAuthorization } from '@t4h.framework/oidc'
44
+
45
+ const authorization = new OIDCAuthorization({
46
+ discoveryUrl: environments.OIDC_DISCOVERY_URL,
47
+ clientId: environments.OIDC_CLIENT,
48
+ clientSecret: environments.OIDC_SECRET,
49
+ token: '<<headers.authorization>>',
50
+ })
51
+
52
+ export default new App({
53
+ id: 'my-app',
54
+ workflows: [cashback],
55
+ authorization,
56
+ })
57
+ ```
58
+
59
+ ## Traps
60
+
61
+ - `token` is a placeholder, not a value. The runtime replaces
62
+ `<<headers.authorization>>` with the `Authorization` header of the incoming
63
+ request before the check runs.
64
+ - The token must be `Bearer <token>` (any letter case, no spaces in the
65
+ token). Any other form is denied without a call to the provider.
66
+ - `@t4h.framework/core/no-literal-secret-in-authorization` rejects a literal
67
+ `clientSecret`. The package reads no env by itself: pass each value from the
68
+ `EnvDesign`.
69
+ - `discoveryUrl` is the well-known document
70
+ (`.../.well-known/openid-configuration`), not the introspection endpoint.
71
+ - The provider must let the client call introspection with
72
+ `client_secret_basic`.
73
+ - `false` means "denied". An HTTP error from the provider, a missing
74
+ `introspection_endpoint` or a network failure throws instead.
75
+
76
+ ## Test
77
+
78
+ - `OIDCAuthorization`: compare `toManifest()` with
79
+ `{ pathToModule: '@t4h.framework/oidc/authorize', context: { ... } }`.
80
+ - The check: `import oidc from '@t4h.framework/oidc/authorize'`, stub the
81
+ global `fetch` with `vi.stubGlobal`, and resolve it two times in order:
82
+ the discovery document, then the introspection response.
83
+
84
+ ## Reference
85
+
86
+ - Guide, errors and testing: https://tech4humans-brasil.github.io/framework/oidc/
87
+ - Types: `node_modules/@t4h.framework/oidc/dist/oidc.d.ts` and
88
+ `node_modules/@t4h.framework/oidc/dist/oidc-authorize.d.ts`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @t4h.framework/oidc
2
2
 
3
+ ## 0.6.12
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 oidc docs now describe the errors of the check and how to test it, and no longer say that it uses the client credentials grant. The README is short and the skill keeps only the traps and links to the site.
8
+
3
9
  ## 0.6.11
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -1,81 +1,38 @@
1
1
  # @t4h.framework/oidc
2
2
 
3
- OpenID Connect–oriented authorization for the T4H Framework. It exposes a core `Authorization` manifest and a runtime handler that validates an incoming credential through the **token introspection** endpoint ([RFC 7662](https://www.rfc-editor.org/rfc/rfc7662)) from the issuer’s OpenID configuration.
3
+ OpenID Connect authorization for T4H Framework apps and workflows. It lets a
4
+ run start only when the OpenID Provider reports the caller's token as active,
5
+ through token introspection ([RFC 7662](https://www.rfc-editor.org/rfc/rfc7662)).
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/oidc` | `OIDCAuthorization`, `OIDCAuthorizationContext`, and related types |
14
- | `@t4h.framework/oidc/authorize` | Default async handler invoked by the runtime with the manifest context |
15
-
16
- ## `OIDCAuthorization`
17
-
18
- Subclass of [`Authorization`](https://github.com/tech4humans-brasil/framework/tree/main/packages/core) from core. `toManifest()` points the runtime at `@t4h.framework/oidc/authorize` and passes an `OIDCAuthorizationContext`.
19
-
20
- You construct it with a context object; your app or hosting layer is responsible for supplying the credential string (`token`) when the workflow runs.
21
-
22
- ## Context: `OIDCAuthorizationContext`
23
-
24
- | Field | Required | Description |
25
- |-------|----------|-------------|
26
- | `discoveryUrl` | Yes | OpenID Provider configuration document URL (must return JSON with at least `introspection_endpoint`). |
27
- | `clientId` | Yes | OAuth 2.0 client identifier used to authenticate the introspection call. |
28
- | `clientSecret` | Yes | OAuth 2.0 client secret used to authenticate the introspection call. |
29
- | `token` | Yes | Credential to verify. Must use a `Bearer ` prefix (see below). |
30
-
31
- ## Runtime handler (`oidc/authorize`)
32
-
33
- The default export is an async function:
34
-
35
- ```ts
36
- (context: OIDCAuthorizationContext) => Promise<boolean>
9
+ ```bash
10
+ npm install @t4h.framework/oidc
37
11
  ```
38
12
 
39
- Behavior:
40
-
41
- 1. **Discovery** — `GET` `discoveryUrl` and read JSON with `introspection_endpoint`.
42
- 2. **Introspection** — `POST` the introspection endpoint following [RFC 7662](https://www.rfc-editor.org/rfc/rfc7662), with `token` and `token_type_hint=access_token` as `application/x-www-form-urlencoded`, where `token` is taken from the context `token` after stripping the `Bearer ` prefix (the scheme is matched case-insensitively).
43
- 3. **Client authentication** — RFC 7662 requires the caller to authorize itself; the handler uses `client_secret_basic` (RFC 6749 section 2.3.1), sending `Authorization: Basic base64(clientId:clientSecret)` with both values form-urlencoded. Your client must be allowed to call introspection with this method.
13
+ Peer dependency: `@t4h.framework/core`.
44
14
 
45
- Result:
46
-
47
- - Returns `true` only if the introspection response includes `"active": true`.
48
- - Returns `false` for any other response, including a missing `active` member.
49
- - If the incoming credential does not carry a `Bearer ` prefix, the handler returns `false` without contacting the provider — `token` is a required introspection parameter.
50
-
51
- There is no built-in mapping from environment variables; all configuration comes from the manifest context.
52
-
53
- ## Example
54
-
55
- Declare the values in the `EnvDesign` in `src/constants/environments.ts`. The context types `clientSecret` as `string`, so declare the entry with `type: String`, not `type: Secret`.
56
-
57
- ```typescript
58
- // src/constants/environments.ts
59
- import { EnvDesign } from '@t4h.framework/core'
60
-
61
- export const environments = EnvDesign({
62
- OIDC_DISCOVERY_URL: { type: String },
63
- OIDC_CLIENT: { type: String },
64
- OIDC_SECRET: { type: String },
65
- })
66
- ```
15
+ ## Usage
67
16
 
68
17
  ```typescript
18
+ import { App } from '@t4h.framework/core'
69
19
  import { OIDCAuthorization } from '@t4h.framework/oidc'
70
20
 
71
21
  import { environments } from './constants/environments.js'
72
22
 
73
- const authorization = new OIDCAuthorization({
74
- discoveryUrl: environments.OIDC_DISCOVERY_URL,
75
- clientId: environments.OIDC_CLIENT,
76
- clientSecret: environments.OIDC_SECRET,
77
- token: '<<headers.authorization>>',
23
+ export default new App({
24
+ id: 'my-app',
25
+ workflows: [cashback],
26
+ authorization: new OIDCAuthorization({
27
+ discoveryUrl: environments.OIDC_DISCOVERY_URL,
28
+ clientId: environments.OIDC_CLIENT,
29
+ clientSecret: environments.OIDC_SECRET,
30
+ token: '<<headers.authorization>>',
31
+ }),
78
32
  })
79
33
  ```
80
34
 
81
- Wire `authorization` on an `App` or `Workflow` per the core package; the deployed runtime fills `token` (and any other fields you resolve dynamically) and invokes `@t4h.framework/oidc/authorize`.
35
+ ## Documentation
36
+
37
+ Configuration, the validation steps, errors and testing:
38
+ https://tech4humans-brasil.github.io/framework/oidc/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/oidc",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
4
4
  "description": "OIDC module for the T4H Framework authorization",
5
5
  "homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/oidc",
6
6
  "bugs": "https://github.com/tech4humans-brasil/framework/issues",
@@ -39,7 +39,7 @@
39
39
  "test:watch": "vitest"
40
40
  },
41
41
  "devDependencies": {
42
- "@t4h.framework/core": "^0.20.0",
42
+ "@t4h.framework/core": "^0.21.0",
43
43
  "typescript": "^5.9.3",
44
44
  "vitest": "^4.0.18"
45
45
  },