@t4h.framework/oidc 0.2.0 → 0.4.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.
@@ -0,0 +1,282 @@
1
+ ---
2
+ name: framework-oidc
3
+ description: >-
4
+ Wire OpenID Connect authorization in T4H Framework apps and workflows using
5
+ @t4h.framework/oidc: OIDCAuthorization manifest, OIDCAuthorizationContext,
6
+ Bearer token introspection via OAuth client credentials, discoveryUrl,
7
+ scope, and resource. 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.
10
+ ---
11
+
12
+ # @t4h.framework/oidc
13
+
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** after obtaining a service token with the **OAuth 2.0 client credentials** grant.
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
+ ```typescript
53
+ import { App } from '@t4h.framework/core'
54
+ import { OIDCAuthorization } from '@t4h.framework/oidc'
55
+
56
+ export default new App({
57
+ id: 'my-app',
58
+ authorization: new OIDCAuthorization({
59
+ discoveryUrl:
60
+ 'https://auth.example.com/.well-known/openid-configuration',
61
+ clientId: 'my-service',
62
+ clientSecret: process.env.OIDC_CLIENT_SECRET!,
63
+ token: 'Bearer <incoming-access-token-from-runtime>',
64
+ scope: ['introspection'],
65
+ }),
66
+ workflows: [/* ... */],
67
+ })
68
+ ```
69
+
70
+ Per-workflow override:
71
+
72
+ ```typescript
73
+ import { Workflow } from '@t4h.framework/core'
74
+ import { OIDCAuthorization } from '@t4h.framework/oidc'
75
+
76
+ const secured = new Workflow(
77
+ {
78
+ id: 'orders/v1',
79
+ authorization: new OIDCAuthorization({ /* same context shape */ }),
80
+ },
81
+ async input => ({ ok: true }),
82
+ )
83
+ ```
84
+
85
+ **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.
86
+
87
+ ---
88
+
89
+ ## `OIDCAuthorization`
90
+
91
+ Subclass of `Authorization<OIDCAuthorizationContext>`. Constructor stores context; `toManifest()` always sets:
92
+
93
+ - `pathToModule: '@t4h.framework/oidc/authorize'`
94
+ - `context`: full context object (including `scope` / `resource` as `undefined` when omitted)
95
+
96
+ ```typescript
97
+ new OIDCAuthorization({
98
+ discoveryUrl: 'https://auth.example.com/.well-known/openid-configuration',
99
+ clientId: 'svc',
100
+ clientSecret: 'secret',
101
+ token: 'Bearer t',
102
+ scope: ['a', 'b'], // optional
103
+ resource: 'https://api.example.com', // optional
104
+ })
105
+ ```
106
+
107
+ There is **no** built-in mapping from environment variables — supply `clientSecret`, `discoveryUrl`, etc. from your app/hosting layer when constructing the instance.
108
+
109
+ ### Runtime token placeholder
110
+
111
+ `token` is authored as a manifest placeholder the runtime replaces:
112
+
113
+ ```typescript
114
+ token: '<<headers.authorization>>',
115
+ ```
116
+
117
+ Author with whatever placeholder or value your deployment pipeline documents; the handler always receives the resolved string at runtime.
118
+
119
+ ---
120
+
121
+ ## `OIDCAuthorizationContext`
122
+
123
+ | Field | Required | Description |
124
+ |-------|----------|-------------|
125
+ | `discoveryUrl` | Yes | OpenID Provider configuration document URL (JSON with `token_endpoint` and `introspection_endpoint`). |
126
+ | `clientId` | Yes | OAuth 2.0 client id for client credentials grant. |
127
+ | `clientSecret` | Yes | OAuth 2.0 client secret for client credentials grant. |
128
+ | `token` | Yes | Credential to verify. Must use a `Bearer ` prefix for the token value to be sent to introspection (see below). |
129
+ | `scope` | No | Scopes for the client-credentials token (`string[]`, joined with spaces on the wire). |
130
+ | `resource` | No | If set, sent as `resource` on the client credentials request (RFC 8707 style). |
131
+
132
+ Types exported: `OIDCAuthorizationContext`, `OIDCAuthorizationManifest` (`AuthorizationManifest<OIDCAuthorizationContext>`).
133
+
134
+ ---
135
+
136
+ ## Runtime handler (`@t4h.framework/oidc/authorize`)
137
+
138
+ Default export: async function `(context: OIDCAuthorizationContext) => Promise<boolean>`.
139
+
140
+ ### Flow
141
+
142
+ ```mermaid
143
+ sequenceDiagram
144
+ participant R as Runtime handler
145
+ participant D as discoveryUrl
146
+ participant T as token_endpoint
147
+ participant I as introspection_endpoint
148
+
149
+ R->>D: GET discovery JSON
150
+ R->>T: POST client_credentials
151
+ T-->>R: access_token
152
+ R->>I: POST JSON { token } + Bearer access_token
153
+ I-->>R: { active }
154
+ R-->>R: true if active else false
155
+ ```
156
+
157
+ 1. **Discovery** — `GET` `discoveryUrl`; parse JSON as `{ token_endpoint, introspection_endpoint }`. Non-OK HTTP → throw `Discovery request failed: <status> <statusText>`.
158
+ 2. **Client credentials** — `POST` `token_endpoint` with `application/x-www-form-urlencoded` body:
159
+ - `grant_type=client_credentials`
160
+ - `client_id`, `client_secret`
161
+ - optional `scope` (space-joined from array)
162
+ - optional `resource`
163
+ Non-OK HTTP → throw `Token request failed: ...`.
164
+ 3. **Introspection** — `POST` `introspection_endpoint` with:
165
+ - `Content-Type: application/json`
166
+ - `Authorization: Bearer <access_token>` (from step 2)
167
+ - body `JSON.stringify({ token: <stripped incoming token or null> })`
168
+ Non-OK HTTP → throw `Introspection request failed: ...`.
169
+ 4. **Result** — parse `{ active: boolean }`:
170
+ - `active: true` → return `true`
171
+ - `active: false` → return `false`
172
+
173
+ Network failures from `fetch` propagate as rejected promises (not converted to `false`).
174
+
175
+ ### Bearer prefix rules
176
+
177
+ Incoming `token` is parsed with a simple prefix check:
178
+
179
+ - Starts with `Bearer ` → introspection body uses the substring after the first `Bearer ` (e.g. `Bearer abc.def` → `"abc.def"`).
180
+ - Does **not** start with `Bearer ` → introspection body sends `{ "token": null }` (typically yields `active: false`).
181
+
182
+ Always author/runtime-resolve credentials as `Bearer <access-token>` when you expect introspection of the caller’s token.
183
+
184
+ ---
185
+
186
+ ## Authoring checklist
187
+
188
+ 1. Add `@t4h.framework/oidc` dependency alongside `@t4h.framework/core`.
189
+ 2. Construct `new OIDCAuthorization({ discoveryUrl, clientId, clientSecret, token, ... })`.
190
+ 3. Attach to `App` and/or `Workflow` `authorization` option.
191
+ 4. Ensure IdP discovery document exposes `token_endpoint` and `introspection_endpoint`.
192
+ 5. Register a confidential OAuth client allowed to use **client credentials** and call **introspection**.
193
+ 6. Pass incoming caller token with `Bearer ` prefix (or use a runtime placeholder like `<<headers.authorization>>`).
194
+ 7. Optionally set `scope` (e.g. scopes required for introspection on the service token) and `resource` (audience/resource indicator for the CC token).
195
+
196
+ Do **not** look for OIDC Activities or Claim classes in this package — they do not exist here.
197
+
198
+ ---
199
+
200
+ ## Testing
201
+
202
+ Package tests use **Vitest** with `vi.stubGlobal('fetch', ...)`.
203
+
204
+ ### `OIDCAuthorization.toManifest()`
205
+
206
+ Assert manifest shape:
207
+
208
+ ```typescript
209
+ expect(authorization.toManifest()).toEqual({
210
+ pathToModule: '@t4h.framework/oidc/authorize',
211
+ context: {
212
+ discoveryUrl: '...',
213
+ clientId: 'svc',
214
+ clientSecret: 'secret',
215
+ scope: undefined, // when omitted
216
+ resource: undefined,
217
+ token: 'Bearer t',
218
+ },
219
+ })
220
+ ```
221
+
222
+ ### Handler (`import oidc from '@t4h.framework/oidc/authorize'`)
223
+
224
+ Mock three `fetch` calls in order: discovery → token → introspection.
225
+
226
+ ```typescript
227
+ const baseContext = {
228
+ discoveryUrl: 'https://auth.example.com/.well-known/openid-configuration',
229
+ clientId: 'client-id',
230
+ clientSecret: 'client-secret',
231
+ token: 'Bearer incoming-token',
232
+ }
233
+
234
+ // Happy path: true
235
+ fetchMock
236
+ .mockResolvedValueOnce(jsonResponse({
237
+ token_endpoint: 'https://auth.example.com/oauth/token',
238
+ introspection_endpoint: 'https://auth.example.com/oauth/introspect',
239
+ }))
240
+ .mockResolvedValueOnce(jsonResponse({ access_token: 'cc-access-token' }))
241
+ .mockResolvedValueOnce(jsonResponse({ active: true }))
242
+
243
+ await expect(oidc(baseContext)).resolves.toBe(true)
244
+ ```
245
+
246
+ Verify wire format from tests:
247
+
248
+ - Token request: `URLSearchParams` with `grant_type`, `client_id`, `client_secret`; optional `scope` (`openid profile` from `['openid','profile']`); optional `resource`.
249
+ - Introspection: `Authorization: Bearer cc-access-token`, body `{ token: 'incoming-token' }` after stripping `Bearer `.
250
+ - Missing Bearer prefix: body `{ token: null }`.
251
+ - Inactive token: third response `{ active: false }` → `false`.
252
+ - HTTP errors on discovery/token/introspection: thrown `Error` messages as documented above.
253
+
254
+ Run package tests: `yarn test` in `packages/oidc`.
255
+
256
+ Workflow-level authorization behavior in process is owned by the **runtime** (outside this package); this package only defines manifest + handler contract.
257
+
258
+ ---
259
+
260
+ ## Common mistakes
261
+
262
+ | Mistake | Consequence |
263
+ |---------|-------------|
264
+ | Raw JWT without `Bearer ` prefix | Introspection gets `token: null`; usually denied |
265
+ | Expecting env auto-wiring | Secrets/URLs must be set when constructing `OIDCAuthorization` |
266
+ | Missing `introspection_endpoint` on IdP | Discovery JSON incomplete; runtime fails at fetch/parse |
267
+ | Treating `false` as HTTP error | `false` means inactive token; errors are thrown separately |
268
+ | Adding OIDC as a Claim provider | Wrong abstraction — use `Authorization` on App/Workflow |
269
+ | Importing handler from main entry | Use `@t4h.framework/oidc/authorize` (default export) |
270
+
271
+ ---
272
+
273
+ ## API summary
274
+
275
+ | Symbol | Kind | Role |
276
+ |--------|------|------|
277
+ | `OIDCAuthorization` | class | `Authorization` subclass; `toManifest()` → oidc authorize module |
278
+ | `OIDCAuthorizationContext` | type | Manifest/runtime context |
279
+ | `OIDCAuthorizationManifest` | type | `AuthorizationManifest<OIDCAuthorizationContext>` |
280
+ | default `oidc` | function | `@t4h.framework/oidc/authorize` — discovery + CC + introspection |
281
+
282
+ See [README.md](../../../README.md) in this package for the same behavior described for humans.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/oidc",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
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",
@@ -26,7 +26,8 @@
26
26
  "types": "./dist/oidc.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,12 +37,12 @@
36
37
  "test:watch": "vitest"
37
38
  },
38
39
  "devDependencies": {
39
- "@t4h.framework/core": "^0.2.0",
40
+ "@t4h.framework/core": "^0.8.0",
40
41
  "typescript": "^5.9.3",
41
42
  "vitest": "^4.0.18"
42
43
  },
43
44
  "peerDependencies": {
44
- "@t4h.framework/core": "^0.2.0"
45
+ "@t4h.framework/core": "^0.8.0"
45
46
  },
46
47
  "packageManager": "yarn@4.12.0",
47
48
  "engines": {