@owlmeans/server-oidc-rp 0.1.18-rc.3 → 0.1.18-rc.30

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.
Files changed (47) hide show
  1. package/README.md +182 -69
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-oidc-rp/SKILL.md +182 -50
  4. package/build/actions/init.d.ts.map +1 -1
  5. package/build/actions/init.js +10 -2
  6. package/build/actions/init.js.map +1 -1
  7. package/build/actions/process.js +1 -1
  8. package/build/actions/process.js.map +1 -1
  9. package/build/auth/plugins/google-client.js +2 -2
  10. package/build/auth/plugins/google-client.js.map +1 -1
  11. package/build/auth/plugins/oidc-client.js +3 -3
  12. package/build/auth/plugins/oidc-client.js.map +1 -1
  13. package/build/entrypoints.d.ts +3 -0
  14. package/build/entrypoints.d.ts.map +1 -0
  15. package/build/entrypoints.js +10 -0
  16. package/build/entrypoints.js.map +1 -0
  17. package/build/guard.d.ts +6 -2
  18. package/build/guard.d.ts.map +1 -1
  19. package/build/guard.js +7 -7
  20. package/build/guard.js.map +1 -1
  21. package/build/index.d.ts +1 -1
  22. package/build/index.d.ts.map +1 -1
  23. package/build/index.js +1 -1
  24. package/build/index.js.map +1 -1
  25. package/build/model/gate.js +6 -6
  26. package/build/model/gate.js.map +1 -1
  27. package/build/utils/auth.js +1 -1
  28. package/build/wrapper.d.ts.map +1 -1
  29. package/build/wrapper.js +20 -5
  30. package/build/wrapper.js.map +1 -1
  31. package/package.json +18 -18
  32. package/src/actions/init.ts +11 -2
  33. package/src/actions/process.ts +1 -1
  34. package/src/auth/plugins/google-client.ts +2 -2
  35. package/src/auth/plugins/oidc-client.ts +3 -3
  36. package/src/entrypoints.ts +24 -0
  37. package/src/guard.ts +8 -11
  38. package/src/index.ts +1 -1
  39. package/src/model/gate.ts +6 -6
  40. package/src/utils/auth.ts +1 -1
  41. package/src/wrapper.ts +21 -5
  42. package/tests/context.ts +1 -2
  43. package/build/modules.d.ts +0 -3
  44. package/build/modules.d.ts.map +0 -1
  45. package/build/modules.js +0 -12
  46. package/build/modules.js.map +0 -1
  47. package/src/modules.ts +0 -30
package/README.md CHANGED
@@ -1,108 +1,221 @@
1
1
  # @owlmeans/server-oidc-rp
2
2
 
3
- Server-side OIDC relying party — registers the OIDC guard, wrapping service, gate, and IdP-side client wiring.
4
-
5
- ## Overview
6
-
7
- - `appendOidcGuard(context)` — registers the OIDC guard service
8
- - `makeOidcWrappingService()` / `makeOidcGate()` — token wrapping service and gate factory
9
- - `setupOidcGuard(modules, coguards?, extras?)` — attaches the OIDC guard to module declarations
10
- - `setupAuthServiceModules(...)` — wires external auth modules
11
- - `makeOidcClientService()` / `OidcClientService` — IdP-side client (e.g., Keycloak admin)
12
- - Subpath `./auth` — admin-client constants like `OIDC_ADMIN_CLIENT`
3
+ The server-side OIDC relying party. An application uses it when users sign in through an OIDC or
4
+ OAuth provider (an OIDC IAM, Google): it reads provider descriptors from `cfg.oidc.providers`, runs
5
+ discovery and the code exchange, registers the OIDC and Google plugins in the auth manager, and keeps
6
+ OIDC-wrapped tokens fresh. It also supplies the OIDC guard and UMA2 gate for a product whose
7
+ authorization is the provider's own grants. A product that uses the provider only to log in and keeps
8
+ authorization in local records uses `@owlmeans/server-auth-identity` plus its own `GateService`
9
+ instead of the guard and gate. Being a provider is `@owlmeans/server-oidc-provider`, the browser side
10
+ is `@owlmeans/web-oidc-rp`, and `appendIam()` from `@owlmeans/server-iam` performs the standard
11
+ registrations with the IAM gate.
13
12
 
14
13
  ## Installation
15
14
 
16
15
  ```bash
17
- bun add @owlmeans/server-oidc-rp
18
- ```
19
-
20
- ## Usage
21
-
22
- Register services in `context.ts`:
23
-
24
- ```typescript
25
- import {
26
- appendOidcGuard, makeOidcWrappingService, makeOidcGate, makeOidcClientService
27
- } from '@owlmeans/server-oidc-rp'
28
-
29
- context.registerService(makeOidcWrappingService())
30
- context.registerService(makeOidcGate())
31
- context.registerService(makeOidcClientService())
32
- appendOidcGuard<C, T>(context)
16
+ bun add @owlmeans/server-oidc-rp@^0.1.18-rc.29
33
17
  ```
34
18
 
35
- Wire onto modules in `modules.ts`:
19
+ ## Concepts
20
+
21
+ - **Provider descriptor** — an entry of `cfg.oidc.providers`: `clientId`, `secret`, either
22
+ `discoveryUrl` or `service` + `basePath`, optional `extraScopes`, `entityId`, `internal`, `def`.
23
+ - **Client service** — `OidcClientService` (`oidc-client`), the only way to reach a descriptor and
24
+ the `OidcClientAdapter` that performs grants, refresh and introspection.
25
+ - **Account linking** — the optional `AccountLinkingService` named by
26
+ `cfg.oidc.accountLinkingService`, which maps a provider profile onto a local `AuthPayload`.
27
+ - **Wrapped token** — a bearer that wraps a provider token set; the `WRAPPED_OIDC` service
28
+ re-validates it (refresh or introspection) once `OIDC_WRAP_FRESHNESS` has passed.
29
+ - **UMA2 gate** — `makeOidcGate()` under `OIDC_GATE`, admitting a request only when the token's
30
+ permission claim grants what the gate params ask for.
31
+ - **Token cache** — records in `AUTH_CACHE` whose ids are prefixed with `OIDC_TOKEN_STORE`; there is
32
+ no separate resource.
36
33
 
37
- ```typescript
38
- import { setupOidcGuard, setupAuthServiceModules } from '@owlmeans/server-oidc-rp'
39
-
40
- setupOidcGuard(appModules)
41
- setupAuthServiceModules(appModules)
42
- ```
34
+ ## Usage
43
35
 
44
- Configure providers in `config.ts`:
36
+ ### Configure providers
45
37
 
46
- ```typescript
38
+ ```ts
39
+ import { GOOGLE_SERVICE } from '@owlmeans/oidc'
47
40
  import { OIDC_ADMIN_CLIENT } from '@owlmeans/server-oidc-rp/auth'
41
+ import { AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity'
48
42
 
49
43
  cfg.oidc ??= {}
50
44
  cfg.oidc.providers ??= []
45
+ cfg.oidc.accountLinkingService = AUTH_IDENTITY_LINKING
46
+
47
+ // an internal admin client of the IAM; the issuer is reassembled from the `my-app-iam` service host
51
48
  cfg.oidc.providers.push({
52
49
  clientId: OIDC_ADMIN_CLIENT,
53
50
  basePath: 'realms/master',
54
- service: OIDC_PRODUCT,
55
- secret: '/etc/master-secret/oidc-admin-secret',
56
- internal: true
51
+ service: 'my-app-iam',
52
+ secret: process.env.IAM_ADMIN_SECRET!,
53
+ internal: true,
54
+ })
55
+
56
+ // Google login; on a Google descriptor extraScopes is the whole scope
57
+ cfg.oidc.providers.push({
58
+ clientId: process.env.GOOGLE_CLIENT_ID!,
59
+ secret: process.env.GOOGLE_CLIENT_SECRET!,
60
+ service: GOOGLE_SERVICE,
61
+ extraScopes: 'openid profile email',
57
62
  })
58
63
  ```
59
64
 
60
- Use the IdP-side client in a service:
65
+ ### Login-only: provider sign-in, local authorization
61
66
 
62
- ```typescript
63
- import { DEFAULT_ALIAS as OIDC_SERVICE } from '@owlmeans/server-oidc-rp'
64
- import type { OidcClientService } from '@owlmeans/server-oidc-rp'
67
+ The API context registers the client service; the process that serves the auth manager imports the
68
+ plugin module once.
65
69
 
66
- const oidc = context.service<OidcClientService>(OIDC_SERVICE)
70
+ ```ts
71
+ // context.ts
72
+ import { makeOidcClientService } from '@owlmeans/server-oidc-rp'
73
+
74
+ context.registerService(makeOidcClientService())
75
+ appendAuthIdentityResources(context, AUTH_IDENTITY_DB_ALIAS)
76
+ context.registerService(makeMyAppGate()) // a GateService over IdentityProfile scopes
67
77
  ```
68
78
 
69
- ## API
79
+ ```ts
80
+ // entrypoints.ts of the service that hosts the auth manager
81
+ import { entrypoints as authManagerEntrypoints } from '@owlmeans/server-auth/manager'
82
+ import '@owlmeans/server-oidc-rp/auth/plugins' // registers OIDC_CLIENT_AUTH and GOOGLE_CLIENT_AUTH
83
+ ```
70
84
 
71
- ### Service factories
85
+ ### Provider-authorized: guard, gate and bindings
72
86
 
73
- - `appendOidcGuard<C, T>(context): T` — register the OIDC guard
74
- - `makeOidcWrappingService(alias?)` — token-wrapping service
75
- - `makeOidcGate(alias?)` — gate factory
76
- - `makeOidcClientService(alias?)` — IdP-side client (used to call OIDC admin API)
87
+ ```ts
88
+ // context.ts
89
+ import {
90
+ appendOidcGuard, makeOidcClientService, makeOidcGate, makeOidcWrappingService,
91
+ } from '@owlmeans/server-oidc-rp'
77
92
 
78
- ### Module wiring
93
+ context.registerService(makeOidcClientService())
94
+ context.registerService(makeOidcWrappingService())
95
+ context.registerService(makeOidcGate())
96
+ appendOidcGuard<C, T>(context)
97
+ ```
79
98
 
80
- - `setupOidcGuard(modules, coguards?, extras?)` — attach guard to module declarations
81
- - `setupAuthServiceModules(modules, ...)` — register external auth modules
99
+ ```ts
100
+ // entrypoints.ts
101
+ import { withOidcGuard } from '@owlmeans/oidc'
102
+ import { makeAuthServiceEntrypoints, oidcEntrypoints } from '@owlmeans/server-oidc-rp'
103
+ import { bindAll } from '@owlmeans/server-entrypoint'
104
+ import { protocols } from '@owlmeans/entrypoint'
105
+
106
+ // decorate first, then make handlers from and bind the decorated objects
107
+ export const configuredProtocols = withOidcGuard(appProtocols)
108
+
109
+ export const serverBindings = [
110
+ ...bindAll(protocols(configuredProtocols.api), [invoice.list, invoice.pay]),
111
+ ...oidcEntrypoints,
112
+ ...bindAll(makeAuthServiceEntrypoints('my-app-auth-api')),
113
+ ]
114
+ ```
115
+
116
+ ### Call a provider with the client service
117
+
118
+ ```ts
119
+ import { GOOGLE_SERVICE } from '@owlmeans/oidc'
120
+ import { DEFAULT_ALIAS as OIDC_CLIENT, requestedScope } from '@owlmeans/server-oidc-rp'
121
+ import type { OidcClientService } from '@owlmeans/server-oidc-rp'
122
+ import { OIDC_ADMIN_CLIENT } from '@owlmeans/server-oidc-rp/auth'
82
123
 
83
- ### Constants
124
+ const oidc = context.service<OidcClientService>(OIDC_CLIENT)
84
125
 
85
- - `DEFAULT_ALIAS` — `'oidc-client'`
86
- - `OIDC_TOKEN_STORE`, `PROVIDER_CACHE_TTL`, `OIDC_AUTH_LIFTETIME`, `OIDC_WRAP_FRESHNESS`
87
- - `./auth` subpath: `OIDC_ADMIN_CLIENT` and related admin-client constants
126
+ const admin = oidc.findProvider(provider => provider.clientId === OIDC_ADMIN_CLIENT && provider.internal === true)
127
+ if (admin != null) {
128
+ const client = await oidc.getClient(admin.clientId)
129
+ const { access_token } = await client.grantWithCredentials()
130
+ // call the provider's admin API with access_token
131
+ }
88
132
 
89
- ### Types
133
+ const google = oidc.findProvider(provider => provider.service === GOOGLE_SERVICE)
134
+ const scope = requestedScope(google?.extraScopes) // base scopes + extras, deduplicated
135
+ ```
136
+
137
+ ### Read the provider token set behind a request
90
138
 
91
- `OidcClientService`, RP config types — exported from the root entry.
139
+ ```ts
140
+ import { AUTH_CACHE } from '@owlmeans/server-auth'
141
+ import { OIDC_TOKEN_STORE } from '@owlmeans/server-oidc-rp'
142
+ import type { OidcTokenSetParameters } from '@owlmeans/server-oidc-rp'
143
+ import type { Resource, ResourceRecord } from '@owlmeans/resource'
144
+ import { decodeJwt } from 'jose'
92
145
 
93
- ## Product-Viable Integration Notes
146
+ interface TokenRecord extends ResourceRecord { payload?: OidcTokenSetParameters }
94
147
 
95
- - `makeOidcClientService()` reads provider descriptors from `cfg.oidc.providers`, including Google and internal admin providers.
96
- - Use `findProvider(predicate)`, `hasProvider(params)`, and `entityToClientId(params)` for provider lookup rather than ad hoc config scans.
97
- - `setupAuthServiceModules(managerModules, AUTH_API)` wires provider-list and token-update endpoints protected by `GUARD_ED25519`.
98
- - If OIDC/Google is only the login provider and local identity records hold authorization, do not re-add `appendOidcGuard()`, `makeOidcGate()`, or `setupOidcGuard()` as product authorization. Use a product-specific `GateService` over local profile scopes.
148
+ const record = await context.resource<Resource<TokenRecord>>(AUTH_CACHE)
149
+ .load(`${OIDC_TOKEN_STORE}:token:${request.auth!.token}`)
99
150
 
100
- ## Related Packages
151
+ // only the id_token is a JWT; an access token is opaque
152
+ const claims = record?.payload?.id_token != null ? decodeJwt(record.payload.id_token) : undefined
153
+ ```
154
+
155
+ ## API
101
156
 
102
- - [`@owlmeans/oidc`](../oidc) — shared `OIDC_GATE`, `OIDC_GUARD`, types
103
- - [`@owlmeans/server-auth`](../server-auth) — works alongside the OIDC guard for token verification
104
- - [`@owlmeans/server-context`](../server-context) — base context where services are registered
105
- - [`@owlmeans/auth-common`](../auth-common) — `DEFAULT_GUARD` co-attached via `setupOidcGuard`
157
+ ### Root export
158
+
159
+ | Symbol | Kind | Purpose |
160
+ |---|---|---|
161
+ | `makeOidcClientService(alias = DEFAULT_ALIAS)` | function | The relying-party service over `cfg.oidc.providers` |
162
+ | `makeOidcWrappingService()` | function | Registers `WRAPPED_OIDC`, which refreshes and re-issues wrapped tokens |
163
+ | `makeOidcGate(alias = OIDC_GATE)` | function | UMA2 gate; throws `AuthForbidden` without a matching permission |
164
+ | `appendOidcGuard<C, T>(context, opts?)` | function | Register the OIDC guard; `opts` is `OidcGuardOptions` from `@owlmeans/oidc` |
165
+ | `oidcEntrypoints` | const | Server bindings of `oidcProtocols.init` and `oidcProtocols.authenticate` |
166
+ | `makeAuthServiceEntrypoints(serviceAlias, prefix = 'oidc-api')` | function | Provider-list and token-update protocol declarations guarded by `GUARD_ED25519` |
167
+ | `requestedScope(extraScopes?)` | function | `OIDC_RP_BASE_SCOPES` plus a descriptor's extras, deduplicated |
168
+ | `createGateModel(ctx)` | function | Permission model: `loadPermissions(auth, params)` |
169
+ | `extractPermissionSets(claim)` | function | Validate a permissions claim into `PermissionSet[]` or `undefined` |
170
+ | `authService` | const | Aliases `authService.provider.list`, `authService.auth.update` |
171
+ | `DEFAULT_ALIAS` | const | `'oidc-client'` |
172
+ | `DEF_OIDC_ACCOUNT_LINKING`, `DEF_OIDC_PROVIDER_API` | const | Default aliases of the linking and provider-API seams |
173
+ | `OIDC_TOKEN_STORE` | const | Record-id prefix inside `AUTH_CACHE` — not a resource alias |
174
+ | `OIDC_AUTH_LIFTETIME` | const | 24 h TTL of a stored token record |
175
+ | `OIDC_WRAP_FRESHNESS` | const | Window inside which a validated record is returned unchanged |
176
+ | `PROVIDER_CACHE_TTL` | const | Exported but unused |
177
+ | `OidcClientService`, `OidcClientAdapter` | type | Client service and per-provider adapter |
178
+ | `OidcTokenSet`, `OidcTokenSetParameters`, `OidcGrantChecks`, `OidcServerMetadata`, `OidcIntrospectionResponse`, `OidcClientDescriptor` | type | Owned replacements for `openid-client` types |
179
+ | `AccountLinkingService`, `AccountMeta`, `ProviderApiService` | type | Optional seams |
180
+ | `OidcRpConfig`, `Config`, `Context` | type | `cfg.oidc` with `accountLinkingService?` / `providerApiService?`, and the server shapes |
181
+
182
+ ### Subpaths
183
+
184
+ | Subpath | Symbol | Purpose |
185
+ |---|---|---|
186
+ | `./auth` | `OIDC_ADMIN_CLIENT` | `'admin-cli'` — the IAM admin client id |
187
+ | `./auth/plugins` | side-effect import, `plugins` | Registers `OIDC_CLIENT_AUTH` and `GOOGLE_CLIENT_AUTH` in the `@owlmeans/server-auth` manager registry |
188
+
189
+ ## Common pitfalls
190
+
191
+ - A product that only logs in through the provider and authorizes against local identity records
192
+ must not adopt `appendOidcGuard()` or `makeOidcGate()` as its authorization.
193
+ - Register the wrapping service whenever the guard is registered, or wrapped tokens expire
194
+ mid-session.
195
+ - Resolve descriptors through `OidcClientService` (`findProvider`, `hasProvider`, `getConfig`,
196
+ `getDefault`, `entityToClientId`); never scan `cfg.oidc.providers` in application code.
197
+ - `registerTemporaryProvider` is reference-counted — pair every call with
198
+ `unregisterTemporaryProvider`.
199
+ - Decode only `id_token`. An access token's format is provider-private; use `introspect` when its
200
+ contents are needed.
201
+ - PKCE verifiers are consume-once: a repeated exchange for one authorization code fails even though
202
+ the first one succeeded.
203
+ - The token cache is `AUTH_CACHE` at `${OIDC_TOKEN_STORE}:…` ids; `context.resource(OIDC_TOKEN_STORE)`
204
+ resolves nothing.
205
+ - The provider client's allowlist must include every scope `requestedScope` yields, or the provider
206
+ rejects the whole request with `invalid_scope`.
207
+ - A server context has no `.url()` on a frontend entrypoint; compose absolute URLs with
208
+ `makeSecurityHelper(ctx).makeUrl(entry.address(), entry.path())` from `@owlmeans/config`.
209
+
210
+ ## Related packages
211
+
212
+ - [`@owlmeans/oidc`](../oidc) — shared OIDC types, `withOidcGuard`, `OIDC_GATE`, `oidcProtocols`
213
+ - [`@owlmeans/server-auth`](../server-auth) — `AUTH_CACHE` and the manager plugin registry
214
+ - [`@owlmeans/server-auth-identity`](../server-auth-identity) — local identity behind account linking
215
+ - [`@owlmeans/server-iam`](../server-iam) — `appendIam()` with the IAM gate
216
+ - [`@owlmeans/server-oidc-provider`](../server-oidc-provider) — the provider side
217
+ - [`@owlmeans/web-oidc-rp`](../web-oidc-rp) — the browser side
218
+ - [`@owlmeans/auth-common`](../auth-common) — `DEFAULT_GUARD`, which `withOidcGuard()` decorates
106
219
 
107
220
  <!-- owlmeans:agent-guidance:start -->
108
221
  ## Agent guidance
@@ -112,7 +225,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
112
225
  your project's skill store (`.agents/skills/`):
113
226
 
114
227
  ```sh
115
- npx @owlmeans/agent-skills
228
+ npx @owlmeans/agent-skills@^0.1.18-rc.22
116
229
  ```
117
230
 
118
231
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-oidc-rp",
4
- "version": "0.1.18-rc.0",
5
- "generatedAt": "2026-08-16T22:20:50.509Z",
4
+ "version": "0.1.18-rc.30",
5
+ "generatedAt": "2026-09-15T12:38:15.193Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: server-oidc-rp
3
- description: How to use @owlmeans/server-oidc-rp — OIDC relying party. appendOidcGuard(), makeOidcWrappingService(), makeOidcGate() to wire OIDC into a server context; setupOidcGuard() to attach the guard to module declarations. Auto-invoked when importing server-oidc-rp helpers.
3
+ description: How to use @owlmeans/server-oidc-rp — the server-side OIDC relying party — appendOidcGuard, oidcEntrypoints and makeAuthServiceEntrypoints, the OidcClientService and its adapter, the requested-scope contract, the UMA2 gate, the wrapped-token service, and the owned public types that keep openid-client out of the public surface. Auto-invoked when importing server-oidc-rp helpers or configuring identity providers on a server.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,93 +8,225 @@ user-invocable: false
8
8
  # @owlmeans/server-oidc-rp
9
9
 
10
10
  **Layer:** Server
11
- **Install:** `"@owlmeans/server-oidc-rp": "^0.1.18-rc.0"` in `dependencies`
11
+ **Install:** `"@owlmeans/server-oidc-rp": "^0.1.18-rc.30"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
15
15
  | Export | Description |
16
16
  |--------|-------------|
17
- | `appendOidcGuard<C, T>(context)` | Add OIDC auth guard to context |
18
- | `makeOidcWrappingService()` | OIDC token service factory |
19
- | `makeOidcGate()` | OIDC gate factory (used by guards) |
20
- | `setupOidcGuard(modules, options?)` | Wire OIDC guard onto module declarations |
21
- | `requestedScope(extraScopes?)` | The `scope` of an authorization request — base scopes + provider extras |
22
- | Constants | OIDC service / guard aliases |
23
- | `OidcRp` types | RP-side config and token shapes |
17
+ | `makeOidcClientService(alias?)` | The relying-party service: reads `cfg.oidc.providers`, runs discovery, hands back client adapters |
18
+ | `makeOidcWrappingService()` | Registers `WRAPPED_OIDC` — refreshes and re-issues an OIDC-wrapped token before it goes stale |
19
+ | `makeOidcGate(alias?)` | The UMA2 gate, registered under `OIDC_GATE` |
20
+ | `appendOidcGuard<C, T>(context, opts?)` | Registers the OIDC guard on a server context. `opts` is `OidcGuardOptions` (`@owlmeans/oidc`) and is forwarded to the base guard unchanged |
21
+ | `oidcEntrypoints` | Server-local bindings for the shared `oidcProtocols.init` and `.authenticate` declarations |
22
+ | `makeAuthServiceEntrypoints(serviceAlias, prefix?)` | Returns provider-list and token-update protocol declarations, guarded by `GUARD_ED25519`. `prefix` defaults to `oidc-api` |
23
+ | `requestedScope(extraScopes?)` | The `scope` of an authorization request — base scopes plus the provider's extras, deduplicated |
24
+ | `createGateModel(ctx)` | The UMA2 permission model — `loadPermissions(auth, params)` |
25
+ | `extractPermissionSets(claim)` | Shape-validates a `permissions` claim into `PermissionSet[]`, or `undefined` |
26
+ | `authService` | The service entrypoint aliases: `authService.provider.list`, `authService.auth.update` |
27
+ | `DEFAULT_ALIAS` | `'oidc-client'` — the relying-party service alias |
28
+ | `DEF_OIDC_ACCOUNT_LINKING`, `DEF_OIDC_PROVIDER_API` | Default aliases of the two optional seams below |
29
+ | `OIDC_TOKEN_STORE` | The **record-id prefix** this package composes cache ids from — not a resource alias. See "Where the tokens are cached" |
30
+ | `OIDC_AUTH_LIFTETIME` | 24 h — the TTL a stored token record is saved with |
31
+ | `OIDC_WRAP_FRESHNESS` | How long a validated record stays fresh: inside this window of its last validation the wrapping service returns the token unchanged, past it the token is re-validated and re-issued |
32
+ | `PROVIDER_CACHE_TTL` | Exported, but its only use in the package is commented out — it configures nothing |
33
+ | `AccountLinkingService` | Optional seam: turn a provider profile into a local `AuthPayload` (`getLinkedProfile`, `linkProfile`, `linkCredentials`, `getOwnerProfiles`, `getOwnerCredentials`) |
34
+ | `ProviderApiService` | Optional seam onto the provider's own admin API (`getUserDetails`, `getSettings`) |
35
+ | `OidcRpConfig` | `cfg.oidc` plus `accountLinkingService?` and `providerApiService?` |
36
+
37
+ ### Subpath exports
38
+
39
+ - `./auth` — `OIDC_ADMIN_CLIENT` (`'admin-cli'`)
40
+ - `./auth/plugins` — importing it registers the `OIDC_CLIENT_AUTH` and `GOOGLE_CLIENT_AUTH` plugins
41
+ into the `@owlmeans/server-auth` manager registry
42
+
43
+ ## Wiring
24
44
 
25
- ## Requested scope
26
-
27
- Every authorization request this package builds gets its `scope` from `requestedScope(cfg.extraScopes)`
28
- — `OIDC_RP_BASE_SCOPES` (`@owlmeans/oidc`) plus the provider descriptor's `extraScopes`, deduplicated.
29
- Both request sites (`actions/init.ts` for the browser-starts-server-finishes flow, and the
30
- `oidc-client` auth plugin) call it; never write a scope literal at a call site, and never let the two
31
- drift apart.
32
-
33
- The provider's client registration must allow every scope this yields. A provider supports `email`
34
- as soon as it declares `claims.email` — and then rejects the whole request with
35
- `invalid_scope: requested scope is not allowed` if the client's own allowlist omits it, rather than
36
- dropping the scope. A client provisioned by an older revision therefore stays broken until its
37
- allowlist is backfilled.
38
-
39
- ## Subpath Exports
40
-
41
- - `./auth` — auth integration helpers
42
- - `./auth/plugins` — pluggable token verifiers
43
-
44
- ## Usage
45
-
46
- ### In `context.ts`
47
45
  ```typescript
48
- import { appendOidcGuard, makeOidcWrappingService, makeOidcGate } from '@owlmeans/server-oidc-rp'
46
+ import { appendOidcGuard, makeOidcWrappingService, makeOidcGate, makeOidcClientService } from '@owlmeans/server-oidc-rp'
49
47
 
48
+ context.registerService(makeOidcClientService())
50
49
  context.registerService(makeOidcWrappingService())
51
50
  context.registerService(makeOidcGate())
52
51
  appendOidcGuard<C, T>(context)
53
52
  ```
54
53
 
55
- ### In `modules.ts`
56
54
  ```typescript
57
- import { setupOidcGuard } from '@owlmeans/server-oidc-rp'
58
- setupOidcGuard(appModules)
55
+ import { withOidcGuard } from '@owlmeans/oidc'
56
+ import { oidcEntrypoints, makeAuthServiceEntrypoints } from '@owlmeans/server-oidc-rp'
57
+ import { bindAll } from '@owlmeans/server-entrypoint'
58
+
59
+ export const configuredProtocols = withOidcGuard(protocols)
60
+ export const serverBindings = [
61
+ ...bindAll(configuredProtocols.api),
62
+ ...oidcEntrypoints,
63
+ ...bindAll(makeAuthServiceEntrypoints('my-auth-api')),
64
+ ]
59
65
  ```
60
66
 
61
- ### Configure OIDC providers in `config.ts`
67
+ Decorate the application tree before binding it; do not mutate declarations or append protocol
68
+ objects to a server entrypoint list. `oidcEntrypoints` are bindings and are spread once. The
69
+ auth-service helper returns declarations, so bind those declarations in the serving app.
70
+
62
71
  ```typescript
72
+ // config
63
73
  cfg.oidc ??= {}
64
74
  cfg.oidc.providers ??= []
65
75
  cfg.oidc.providers.push({
66
76
  clientId: OIDC_ADMIN_CLIENT,
67
77
  basePath: 'realms/master',
68
- service: OIDC_PRODUCT,
78
+ // the alias of a registered service whose host the issuer is reassembled from;
79
+ // a provider reachable by its own URL carries `discoveryUrl` instead
80
+ service: 'my-iam',
69
81
  secret: '/etc/master-secret/oidc-admin-secret',
70
82
  internal: true
71
83
  })
72
84
  ```
73
85
 
74
- ## Product-Viable Usage Notes
86
+ A consumer that only needs the IAM defaults calls `appendIam()` from `@owlmeans/server-iam`, which
87
+ performs the four registrations above with the IAM gate in place of `makeOidcGate()`.
75
88
 
76
- - Viable uses `makeOidcClientService()` to read provider descriptors from `cfg.oidc.providers`, including Google and internal admin providers.
77
- - `findProvider(predicate)`, `hasProvider(params)`, and `entityToClientId(params)` are the preferred provider lookup helpers; avoid scanning config ad hoc in downstream code.
78
- - `setupAuthServiceModules(managerModules, AUTH_API)` exposes provider-list and token-update service modules protected by `GUARD_ED25519`.
79
- - When a downstream app uses OIDC/Google only for login and maps users into `@owlmeans/server-auth-identity`, do not reintroduce `appendOidcGuard()`, `makeOidcGate()`, or `setupOidcGuard()` as product authorization. Use a product-specific `GateService` over local identity data.
89
+ ## Resolving a provider
90
+
91
+ `OidcClientService` is the only way to reach a provider descriptor. Use `findProvider(predicate)`,
92
+ `hasProvider(params)`, `getConfig(clientId | partial)`, `getDefault()` and
93
+ `entityToClientId(params)` rather than scanning `cfg.oidc.providers` in downstream code.
94
+ `registerTemporaryProvider` / `unregisterTemporaryProvider` add a descriptor learned at runtime and
95
+ are reference-counted, so every registration needs its matching removal.
96
+
97
+ `getConfiguration` prefers `discoveryUrl` verbatim; it falls back to `service` + `basePath` and fails
98
+ with `AuthManagerError('oidc.client.basepath')` or `('oidc.client.service')` when neither is usable.
99
+ `getClient` returns an `OidcClientAdapter`: `getMetadata`, `getClientId`, `getConfig`, `makeAuthUrl`,
100
+ `grantWithCredentials`, `grantWithCode`, `refresh` and `introspect`. That adapter is the whole
101
+ supported surface — no `openid-client` object is ever handed out.
102
+
103
+ ## Requested scope
104
+
105
+ `requestedScope(extraScopes)` is `OIDC_RP_BASE_SCOPES` (`@owlmeans/oidc`) plus the provider
106
+ descriptor's `extraScopes`, deduplicated and trimmed. The two generic request sites — the
107
+ browser-starts-server-finishes init handler and the `oidc-client` auth plugin — both build their
108
+ `scope` from it, and neither may grow a scope literal.
109
+
110
+ The `GOOGLE_CLIENT_AUTH` plugin is the exception, and it behaves differently on purpose: it sends
111
+ the descriptor's `extraScopes` as the **whole** scope, falling back to `'openid profile email'` when
112
+ the descriptor names none. So on a Google descriptor `extraScopes` replaces the base scopes instead
113
+ of extending them, and adding a scope to `OIDC_RP_BASE_SCOPES` does not reach it.
114
+
115
+ The provider's client registration must allow every scope this yields. A provider supports `email`
116
+ as soon as it declares `claims.email` — and then rejects the whole request with
117
+ `invalid_scope: requested scope is not allowed` if the client's own allowlist omits it, rather than
118
+ dropping the scope. A client provisioned by an older revision stays broken until its allowlist is
119
+ backfilled.
120
+
121
+ ## Only the `id_token` is a JWT
122
+
123
+ `id_token` is a JWT by specification; an **access token's format is provider-private**. `oidc-provider`
124
+ issues opaque access tokens by default, so `decodeJwt(tokenSet.access_token)` (`jose`) throws
125
+ `JWTInvalid: Invalid JWT`. Inside the exchange handler that failure surfaces as a 500 on the
126
+ `DISPATCHER_OIDC` endpoint *after* the code exchange already succeeded, which reads as a broken login
127
+ rather than as the log line it came from. Decode only `tokenSet.id_token` — every claim this package
128
+ needs (`sub`, the `PERMISSIONS_CLAIM` grant) lives there. This applies to debug logging too: a
129
+ `console.log` argument is evaluated before the call, so a throwing decode in a log statement fails the
130
+ request just as hard as one in real logic. Never introspect an access token locally; use the
131
+ provider's introspection endpoint (`introspect`) when its contents are genuinely needed.
132
+
133
+ ## Where the tokens are cached
134
+
135
+ There is no dedicated resource. The cache is `AUTH_CACHE` from `@owlmeans/server-auth`, and
136
+ `OIDC_TOKEN_STORE` is only the prefix of the ids written into it:
137
+
138
+ | Record id | Holds |
139
+ |---|---|
140
+ | `${OIDC_TOKEN_STORE}:verifier:<challenge or state>` | The PKCE verifier and the client it belongs to, until the exchange takes it |
141
+ | `${OIDC_TOKEN_STORE}:exchange:<exchange token>` | The token set from a completed code exchange, short-lived, deleted when the process step consumes it |
142
+ | `${OIDC_TOKEN_STORE}:token:<bearer token>` | The live token set for an issued bearer token, saved with `OIDC_AUTH_LIFTETIME` |
143
+
144
+ A consumer that needs the provider token set behind the request it is serving reads
145
+ `context.resource(AUTH_CACHE)` at `${OIDC_TOKEN_STORE}:token:<token>` — never
146
+ `context.resource(OIDC_TOKEN_STORE)`, which resolves nothing.
147
+
148
+ ## PKCE verifiers are consume-once
149
+
150
+ The verifier cached at init is read back with the resource's `take` — a delete-and-return. The
151
+ exchange therefore succeeds exactly once per authorization code: a repeated exchange (a React effect
152
+ that runs twice for one `code`, a retried request) fails with `resource:unknown-record` even though
153
+ the first attempt worked. Callers guard against re-entry rather than expecting the read to be
154
+ idempotent.
155
+
156
+ ## `entityId` in the browser-starts-server-finishes flow
157
+
158
+ The init handler resolves the OIDC client two ways: via `getDefault()` (a provider flagged `def: true`,
159
+ matched without looking at `entityId` at all) or, only when no default exists, via a remote provider
160
+ lookup keyed by the caller-supplied `entityId`. The cached verifier record carries `entityId` **only**
161
+ when that second path actually used it to resolve the client. The exchange step later calls
162
+ `getConfig({ clientId, ...(entityId != null ? { entityId } : {}) })`, which requires an exact match on
163
+ every field it is given; a stored `entityId` that was never validated against the registered provider
164
+ — a caller-side default or placeholder, say — makes the exchange fail with a bare `AuthenFailed()`
165
+ even though the same default provider is trivially available again at exchange time.
166
+
167
+ ## Building the redirect URI: the init handler has no `url()`
168
+
169
+ `init.ts` runs on the SERVER, but `DISPATCHER` (`@owlmeans/auth`) is a FRONTEND route
170
+ (`frontend({ service: DISPATCHER })` in `@owlmeans/auth-common`'s entrypoints) — the browser page
171
+ the identity provider redirects back to. `.url()` is attached to an entrypoint reference only by
172
+ `@owlmeans/client-entrypoint`, for a browser context that can resolve its own address; a server
173
+ context's `context.entrypoint(DISPATCHER)` is a `CommonEntrypoint` and has no such method. Typing
174
+ the call as `ClientEntrypoint<string>` (which does declare `.url()`) type-checks and builds clean
175
+ but throws at request time: `context.entrypoint(DISPATCHER).url is not a function`. This shipped
176
+ unnoticed for a long time because it is unreachable until a caller's own OIDC login FLOW actually
177
+ reaches the step that calls `authenticate()` — a consumer whose flow config never reaches that step
178
+ never exercises this line, so the fault surfaces only in a fully wired application, and only on the
179
+ very first sign-in attempt.
180
+
181
+ The fix — and the pattern for anywhere else in this package (or a consumer) that needs an absolute
182
+ URL for a route it does not own — is the one `@owlmeans/server-oidc-provider`'s interaction `url`
183
+ callback already uses: type the reference as `CommonEntrypoint`, then compose the address from
184
+ `.path()` and `.address()` through `makeSecurityHelper(ctx).makeUrl(entry.address(), entry.path())`
185
+ (`@owlmeans/config`). A route with path params (this one has none) needs its own substitution
186
+ first, the way the interaction URL substitutes `:uid` before qualifying it. Typing the reference as
187
+ `CommonEntrypoint` rather than `ClientEntrypoint` is itself the regression guard: calling `.url()`
188
+ on it is now a compile error, not a runtime one.
80
189
 
81
190
  ## Public type contract (isolation principle)
82
191
 
83
- `openid-client` types **never appear in this package's public exports**. All public types are OwlMeans-owned:
192
+ `openid-client` types **never appear in this package's public exports**. All public types are
193
+ OwlMeans-owned:
84
194
 
85
195
  | Owned type | Replaces upstream | Description |
86
196
  |---|---|---|
87
- | `OidcTokenSet` | `TokenEndpointResponse & TokenEndpointResponseHelpers` | `access_token`, `refresh_token`, `id_token`, `token_type`, `expires_in`, `scope` |
88
- | `OidcTokenSetParameters` | `TokenEndpointResponse` | Subset without helper methods |
197
+ | `OidcTokenSet` | `TokenEndpointResponse & TokenEndpointResponseHelpers` | `access_token`, `refresh_token`, `id_token`, `token_type`, `expires_in`, `scope`, `claims()` |
198
+ | `OidcTokenSetParameters` | `TokenEndpointResponse` | The same without the helper method |
89
199
  | `OidcGrantChecks` | `AuthorizationCodeGrantChecks` | `{ pkceCodeVerifier?: string; idTokenExpected?: boolean }` |
90
200
  | `OidcServerMetadata` | `ServerMetadata` | Issuer and endpoint metadata |
91
- | `OidcIntrospectionResponse` | `IntrospectionResponse` | Active, scope, sub, client_id |
201
+ | `OidcIntrospectionResponse` | `IntrospectionResponse` | `active`, `scope`, `sub`, `client_id`, … |
92
202
  | `OidcClientDescriptor` | `Configuration` (opaque) | Pass-through; consumers must never read its internals |
93
203
 
94
- Upstream `openid-client` types are imported **only** in `src/service.ts`; mappings happen at the method boundaries. This keeps future library swaps confined to that one file.
204
+ What the owned names guarantee is the **exported** surface, not the internal imports. The type module
205
+ imports `Configuration`, `ServerMetadata`, `TokenEndpointResponse` and `TokenEndpointResponseHelpers`
206
+ from `openid-client` in order to alias them under those names; the service module imports the
207
+ functional `openid-client` API; and `jose`'s `decodeJwt` is read directly by the token wrapper, the
208
+ `oidc-client` auth plugin and the exchange handler. A library swap touches every one of those
209
+ modules — what the owned names buy is that it stops there and never reaches a consumer. See
210
+ [[oidc-versions]].
211
+
212
+ ## Rules
213
+
214
+ - An application that uses an identity provider **only to log in**, and then maps the subject onto a
215
+ local identity through `@owlmeans/server-auth-identity`, must not adopt `appendOidcGuard()` or
216
+ `makeOidcGate()` as its authorization mechanism. Those decide against the
217
+ provider's grants; a product that owns its own identity records declares its own `GateService` over
218
+ them.
219
+ - The browser starts the flow and the server finishes it: the exchange, the account linking and the
220
+ bearer token an application actually carries are all produced here.
221
+ - Register the wrapping service whenever the guard is registered — an OIDC-wrapped token that nothing
222
+ refreshes expires mid-session.
95
223
 
96
224
  ## Depends On
97
225
 
98
- - `@owlmeans/oidc`, `@owlmeans/server-auth`, `@owlmeans/server-context`, `@owlmeans/server-entrypoint`
99
- - `@owlmeans/auth-common`, `@owlmeans/basic-keys`
100
- - `openid-client@6.8.4` (exact), `jose@6.2.3` (exact) — see [[oidc-versions]]
226
+ - `@owlmeans/oidc`, `@owlmeans/server-auth`, `@owlmeans/server-context`, `@owlmeans/server-entrypoint`,
227
+ `@owlmeans/server-api`, `@owlmeans/auth`, `@owlmeans/auth-common`, `@owlmeans/basic-envelope`,
228
+ `@owlmeans/client-entrypoint`, `@owlmeans/config`, `@owlmeans/context`, `@owlmeans/did`,
229
+ `@owlmeans/entrypoint`, `@owlmeans/resource`, `@owlmeans/route`, `@noble/hashes`, `@scure/base`,
230
+ `dayjs`
231
+ - `ajv` (peer)
232
+ - `openid-client@6.8.4` (exact), `jose@6.2.5` (exact) — see [[oidc-versions]]
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/actions/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAkBzE,eAAO,MAAM,IAAI,EAAE,sBA0FjB,CAAA"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/actions/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAoBzE,eAAO,MAAM,IAAI,EAAE,sBAiGjB,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { handleBody } from '@owlmeans/server-api';
2
2
  import { AuthenPayloadError, AuthUnknown, DISPATCHER } from '@owlmeans/auth';
3
3
  import { assertContext } from '@owlmeans/context';
4
+ import { makeSecurityHelper } from '@owlmeans/config';
4
5
  import { authService, DEFAULT_ALIAS } from '../consts.js';
5
6
  // import { PROVIDER_CACHE_TTL } from '../consts.js'
6
7
  // import type { Client } from 'openid-client'
@@ -35,7 +36,7 @@ export const init = handleBody(async (body, ctx) => {
35
36
  * @TODO We need to move it to some remote resource.
36
37
  * And make oidc service itself use such a resource to get required client.
37
38
  */
38
- const [providers] = await context.entrypoint(authService.provider.list).call({
39
+ const providers = await context.entrypoint(authService.provider.list).call({
39
40
  params: { service: context.cfg.alias ?? context.cfg.service },
40
41
  query: { entityId }
41
42
  });
@@ -72,7 +73,14 @@ export const init = handleBody(async (body, ctx) => {
72
73
  client: client.getClientId(),
73
74
  ...(entityIdUsedForResolution ? { entityId } : {}),
74
75
  }, { ttl: AUTHEN_TIMEFRAME / 1000 });
75
- const [dispatcherUrl] = await context.entrypoint(DISPATCHER).call();
76
+ // The dispatcher is a FRONTEND route, and this is a server context — the entrypoint registered
77
+ // here has no `url()` (that helper is attached by `@owlmeans/client-entrypoint` only, for a
78
+ // browser context that can resolve its own address). Assembled the way `server-oidc-provider`'s
79
+ // interaction URL is: the entrypoint's path, qualified with the address it answers on — the
80
+ // frontend service's, not this backend's.
81
+ const dispatcherEntry = context.entrypoint(DISPATCHER);
82
+ const dispatcherUrl = makeSecurityHelper(context)
83
+ .makeUrl(dispatcherEntry.address(), dispatcherEntry.path());
76
84
  const cfg = client.getConfig();
77
85
  const url = client.makeAuthUrl({
78
86
  scope: requestedScope(cfg?.extraScopes),