@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.
|
|
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.
|
|
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.
|
|
45
|
+
"@t4h.framework/core": "^0.8.0"
|
|
45
46
|
},
|
|
46
47
|
"packageManager": "yarn@4.12.0",
|
|
47
48
|
"engines": {
|