@owlmeans/server-oidc-provider 0.1.18-rc.2 → 0.1.18-rc.20
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.
- package/README.md +3 -3
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-oidc-provider/SKILL.md +97 -40
- package/build/service.d.ts.map +1 -1
- package/build/service.js +31 -17
- package/build/service.js.map +1 -1
- package/package.json +10 -10
- package/src/service.ts +35 -18
- package/tests/config.spec.ts +1 -2
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Embedded OIDC identity provider — wraps the `oidc-provider` library as an OwlM
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
bun add @owlmeans/server-oidc-provider
|
|
17
|
+
bun add @owlmeans/server-oidc-provider@^0.1.18-rc.17
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Usage
|
|
@@ -59,7 +59,7 @@ Returns middleware that mounts the OIDC authorize/token/userinfo/jwks endpoints
|
|
|
59
59
|
|
|
60
60
|
## Related Packages
|
|
61
61
|
|
|
62
|
-
- [`@owlmeans/oidc`](../oidc) — shared protocol constants and
|
|
62
|
+
- [`@owlmeans/oidc`](../oidc) — shared protocol constants and entrypoint declarations
|
|
63
63
|
- [`@owlmeans/server-oidc-rp`](../server-oidc-rp) — relying-party counterpart for consuming external IdPs
|
|
64
64
|
- [`@owlmeans/server-api`](../server-api) — the underlying server API the middleware mounts onto
|
|
65
65
|
- [`@owlmeans/web-oidc-provider`](../web-oidc-provider) — browser UI for the provider's interaction screens
|
|
@@ -72,7 +72,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
72
72
|
your project's skill store (`.agents/skills/`):
|
|
73
73
|
|
|
74
74
|
```sh
|
|
75
|
-
npx @owlmeans/agent-skills
|
|
75
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.14
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-oidc-provider",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.20",
|
|
5
|
+
"generatedAt": "2026-09-10T23:12:07.515Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: server-oidc-provider
|
|
3
|
-
description: How to use @owlmeans/server-oidc-provider — embedded OIDC identity provider
|
|
3
|
+
description: How to use @owlmeans/server-oidc-provider — the embedded OIDC identity provider on top of the oidc-provider library — service wiring, the account and adapter seams, the interaction URL, how scopes are derived from claims, and the response headers an authorization endpoint has to correct. Auto-invoked when serving OIDC endpoints from your own service.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,66 +8,123 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/server-oidc-provider
|
|
9
9
|
|
|
10
10
|
**Layer:** Server
|
|
11
|
-
**Install:** `"@owlmeans/server-oidc-provider": "^0.1.18-rc.
|
|
12
|
-
**Runtime deps:** `oidc-provider@9.
|
|
11
|
+
**Install:** `"@owlmeans/server-oidc-provider": "^0.1.18-rc.20"` in `dependencies`
|
|
12
|
+
**Runtime deps:** `oidc-provider@9.11.1` (exact), `jose@6.2.5` (exact), `@types/oidc-provider@9.5.0`
|
|
13
|
+
|
|
14
|
+
Use this only when your service **is** the identity provider. For consuming someone else's issuer,
|
|
15
|
+
use `@owlmeans/server-oidc-rp`.
|
|
13
16
|
|
|
14
17
|
## Key Exports
|
|
15
18
|
|
|
16
19
|
| Export | Description |
|
|
17
20
|
|--------|-------------|
|
|
18
|
-
| `createOidcProviderService(alias?)` |
|
|
19
|
-
| `appendOidcProviderService<C,T>(ctx, alias?)` |
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
| `createOidcProviderService(alias?)` | The provider service factory |
|
|
22
|
+
| `appendOidcProviderService<C,T>(ctx, alias?)` | Registers it on a context |
|
|
23
|
+
| `createOidcProviderMiddleware(webAlias?, oidcAlias?)` | Loading-stage middleware that mounts the provider on the API server exactly once. Register it, or nothing is ever mounted |
|
|
24
|
+
| `OidcProviderService` | `instance()`, `update(api)`, `getInteraction(id)` and the live `oidc` provider |
|
|
25
|
+
| `OidcAccountService` | The account seam: `loadById(ctx, id, { clientId? })` — what `findAccount` delegates to |
|
|
26
|
+
| `OidcAdapterService` | The storage seam: `instance(name)` returns an `oidc-provider` `Adapter` |
|
|
27
|
+
| `OIDC_ACCOUNT_SERVICE`, `DEFAULT_ALIAS` | The default account-service alias and the default provider alias |
|
|
28
|
+
| `OidcConfig`, `OidcConfigAppend` | The `cfg.oidc` shape this package reads |
|
|
29
|
+
| `OidcAccountParams` | `{ clientId? }` — lets the account service scope its claims per client |
|
|
30
|
+
| `toClientMetadata(client)` | Maps a stored `OidcRegisteredClient` to `oidc-provider` `ClientMetadata` |
|
|
31
|
+
| `OidcRegisteredClient`, `OidcClientMetadata` | The stored-client shape `toClientMetadata` takes, and the `oidc-provider` metadata shape it returns, carrying `owlEntityId` |
|
|
32
|
+
| `Config`, `Context` | `ServerConfig`/`ServerContext` with `cfg.oidc` and the `debug.oidc` / `debug.oidcServer` / `debug.oidcData` switches |
|
|
33
|
+
|
|
34
|
+
## Wiring
|
|
26
35
|
|
|
27
36
|
```typescript
|
|
28
|
-
import { appendOidcProviderService } from '@owlmeans/server-oidc-provider'
|
|
37
|
+
import { appendOidcProviderService, createOidcProviderMiddleware } from '@owlmeans/server-oidc-provider'
|
|
38
|
+
|
|
29
39
|
appendOidcProviderService<C, T>(context)
|
|
40
|
+
context.registerMiddleware(createOidcProviderMiddleware())
|
|
30
41
|
```
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
`cfg.oidc` supplies `clients` (`ClientMetadata[]`), `defaultKeys.RS256.pk` (a PKCS#8 PEM), and
|
|
44
|
+
optionally `basePath` (default `oidc`), `authService` (whose registered route the issuer is built
|
|
45
|
+
from — defaults to this service), `behindProxy`, `accountService`, `adapterService` and
|
|
46
|
+
`customConfiguration` (merged over the defaults below). The shape also declares `frontBase`, which
|
|
47
|
+
this package reads nowhere — setting it configures nothing.
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
A client's `redirect_uris` and `post_logout_redirect_uris` may be written as `{{service-alias}}/path`
|
|
50
|
+
and are expanded against that registered service's host. A client with no `client_secret` is refused
|
|
51
|
+
unless a debug flag is on, in which case one is generated and printed.
|
|
52
|
+
|
|
53
|
+
## Building the interaction URL
|
|
35
54
|
|
|
36
55
|
`interactions.url` must return the fully-qualified address of the `INTERACTION` screen, which is a
|
|
37
|
-
**frontend** route resolved from a **server** context. `
|
|
38
|
-
only by `@owlmeans/client-entrypoint` — so the URL is assembled the way
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
**frontend** route resolved from a **server** context. `url()` does not exist there — that helper is
|
|
57
|
+
attached only by `@owlmeans/client-entrypoint` — so the URL is assembled the way the browser's own
|
|
58
|
+
`url()` does it: take the entrypoint's `path()`, substitute the `INTERACTION_UID` path param, then
|
|
59
|
+
`makeSecurityHelper().makeUrl(entry.address(), path)`. `address()` is the address the entrypoint
|
|
60
|
+
answers on — the frontend service's, not this one's — so the base comes from it and must not be
|
|
61
|
+
prepended a second time.
|
|
42
62
|
|
|
43
63
|
## Scopes are derived from `claims`
|
|
44
64
|
|
|
45
|
-
`oidc-provider` adds every key of the `claims` configuration to the supported-scope set
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
- **
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`
|
|
65
|
+
`oidc-provider` adds every key of the `claims` configuration to the supported-scope set. Declaring
|
|
66
|
+
`claims.email` therefore makes `email` a **static** scope, and static scopes are checked against each
|
|
67
|
+
client's own `scope` allowlist: a client that omits one fails the whole authorization request with
|
|
68
|
+
`invalid_scope`. Whatever provisions clients for this provider must allow every scope the RP requests
|
|
69
|
+
(`OIDC_RP_BASE_SCOPES`), and the account service must emit the matching claims — a granted scope with
|
|
70
|
+
no claim behind it is silently empty.
|
|
71
|
+
|
|
72
|
+
The defaults this package sets: `claims` covers `email` (`email`, `email_verified`), `profile` (the
|
|
73
|
+
standard set) and `PERMISSIONS_SCOPE` → `PERMISSIONS_CLAIM`, which stays inert unless the account
|
|
74
|
+
service actually emits it; `scopes` is `openid profile offline_access permissions`;
|
|
75
|
+
`features.devInteractions` is off. Anything under `cfg.oidc.customConfiguration` merges over these.
|
|
76
|
+
|
|
77
|
+
## oidc-provider v9 configuration
|
|
78
|
+
|
|
79
|
+
- **The provider is a Koa app**, and `Provider#use(fn)` splices `fn` **ahead of** the provider's own
|
|
80
|
+
router, so a middleware registered that way runs before routing and resumes after `await next()`.
|
|
81
|
+
The header corrections below are still mounted on the API server ahead of `oidc.callback()`, for
|
|
82
|
+
the reason given there.
|
|
83
|
+
- **`enableHttpPostMethods` is `false`.** Set it only together with `cookies.long.sameSite: 'none'` —
|
|
84
|
+
the configuration check throws `TypeError: HTTP POST Method support requires that
|
|
85
|
+
cookies.long.sameSite is set to none` otherwise, at construction time.
|
|
86
|
+
- **DPoP is enabled.** Pass `features: { dPoP: { enabled: false } }` when the clients send no DPoP
|
|
87
|
+
proofs.
|
|
88
|
+
- **Cookie `sameSite` is `lax`** for both `cookies.long` and `cookies.short`. Revisit it when the
|
|
89
|
+
interaction UI lives on another subdomain.
|
|
90
|
+
- **Every key in `jwks.keys` needs a distinct `kid`.**
|
|
91
|
+
- There is no `provider.Account` getter — accounts come from the `findAccount` callback, which this
|
|
92
|
+
package points at `OidcAccountService`.
|
|
93
|
+
- Outbound HTTP goes through the `fetch` configuration option, which mirrors the fetch API.
|
|
94
|
+
|
|
95
|
+
## jose v6 key import
|
|
96
|
+
|
|
97
|
+
`importPKCS8` returns a **non-extractable** `CryptoKey` for a private key. Pass `{ extractable: true }`
|
|
98
|
+
so the JWKS export succeeds:
|
|
64
99
|
|
|
65
100
|
```typescript
|
|
66
101
|
const key = await jose.importPKCS8(pkcs8Pem, 'RS256', { extractable: true })
|
|
67
102
|
const jwk = await jose.exportJWK(key)
|
|
68
103
|
```
|
|
69
104
|
|
|
105
|
+
## Response headers the hardened defaults get wrong
|
|
106
|
+
|
|
107
|
+
`@fastify/helmet` runs with its defaults on every OwlMeans server, and two of them break an
|
|
108
|
+
authorization endpoint. Both are corrected by middleware this package mounts ahead of the provider.
|
|
109
|
+
|
|
110
|
+
- **`Cross-Origin-Opener-Policy: same-origin`** puts the authorization document in a fresh browsing
|
|
111
|
+
context group, severing `window.opener` **permanently** — navigating back to the relying party
|
|
112
|
+
afterwards does not restore it. That kills popup-based login, which is how an app embedded in an
|
|
113
|
+
iframe must authenticate (see the `web-oidc-rp` skill). The provider sends `unsafe-none`.
|
|
114
|
+
- **`form-action 'self'`** blocks the provider's own interaction form posting across origins, so it
|
|
115
|
+
is rewritten to `form-action *`.
|
|
116
|
+
|
|
117
|
+
**Set these as middleware, never as a Fastify `onSend` hook.** `oidc.callback()` is a Koa handler
|
|
118
|
+
mounted through Middie: it ends the response itself, outside Fastify's reply lifecycle, so `onSend`
|
|
119
|
+
never fires for any route the provider answers and anything set there silently never reaches the
|
|
120
|
+
wire — the header looks configured in code and is absent in production. Middleware registered before
|
|
121
|
+
the provider writes to the raw response, where helmet has already put its defaults, so the override
|
|
122
|
+
is what ships. Verify with `curl -D -` against the provider's
|
|
123
|
+
`.well-known/openid-configuration` rather than by reading the code.
|
|
124
|
+
|
|
70
125
|
## Depends On
|
|
71
126
|
|
|
72
|
-
- `@owlmeans/oidc`, `@owlmeans/server-api`, `@owlmeans/server-context
|
|
73
|
-
|
|
127
|
+
- `@owlmeans/oidc`, `@owlmeans/server-api`, `@owlmeans/server-context`, `@owlmeans/config`,
|
|
128
|
+
`@owlmeans/context`, `@owlmeans/entrypoint`, `@owlmeans/route`, `@noble/hashes`, `@scure/base`
|
|
129
|
+
- `fastify` (peer) — the API server this package mounts the provider onto
|
|
130
|
+
- `oidc-provider` v9, `jose` v6 — see [[oidc-versions]] for the pins and the upgrade checklist
|
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAA0C,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAS9G,eAAO,MAAM,yBAAyB,WAAW,MAAM,KAAmB,mBAoHzE,CAAA;AAED,eAAO,MAAM,yBAAyB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OACzE,CAAC,UAAS,MAAM,KACpB,CAOF,CAAA"}
|
package/build/service.js
CHANGED
|
@@ -29,34 +29,48 @@ export const createOidcProviderService = (alias = DEFAULT_ALIAS) => {
|
|
|
29
29
|
interactions: {
|
|
30
30
|
url: async (_, interaction) => {
|
|
31
31
|
// The interaction screen is a FRONTEND route, and this is a server context — the
|
|
32
|
-
// entrypoint registered here has no `
|
|
33
|
-
// `@owlmeans/client-entrypoint` only). So the URL is assembled the way `
|
|
34
|
-
// it in the browser:
|
|
35
|
-
// with the
|
|
32
|
+
// entrypoint registered here has no `url()` (that helper is attached by
|
|
33
|
+
// `@owlmeans/client-entrypoint` only). So the URL is assembled the way `entrypointUrl`
|
|
34
|
+
// does it in the browser: take the entrypoint's path, substitute the path params, then
|
|
35
|
+
// qualify it with the address the entrypoint answers on — the FRONTEND service's, not
|
|
36
|
+
// this one's.
|
|
36
37
|
const entry = context.entrypoint(INTERACTION);
|
|
37
|
-
const
|
|
38
|
-
const path = entry.getPath().split(SEP)
|
|
38
|
+
const path = entry.path().split(SEP)
|
|
39
39
|
.map(part => part === `${PARAM}${INTERACTION_UID}` ? interaction.uid : part)
|
|
40
40
|
.join(SEP);
|
|
41
|
-
return helper.makeUrl(
|
|
41
|
+
return helper.makeUrl(entry.address(), path);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
oidc.proxy = cfg.behindProxy ?? unsecure;
|
|
46
46
|
const base = SEP + (cfg.basePath ?? DEFAULT_PATH);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Response headers the hardened defaults get wrong for an authorization endpoint.
|
|
49
|
+
*
|
|
50
|
+
* This runs as middleware, ahead of the provider in the same chain, and writes to the raw
|
|
51
|
+
* response — deliberately, not as a Fastify `onSend` hook. `oidc.callback()` is a Koa
|
|
52
|
+
* handler mounted through Middie: it ends the response itself, outside Fastify's reply
|
|
53
|
+
* lifecycle, so `onSend` never fires for any route the provider answers and anything set
|
|
54
|
+
* there silently never reaches the wire. The security defaults are already on the raw
|
|
55
|
+
* response by this point (`@fastify/helmet` applies them in `onRequest`), so overriding
|
|
56
|
+
* them here is what actually takes effect.
|
|
57
|
+
*/
|
|
58
|
+
api.server.use(base, (_req, res, next) => {
|
|
59
|
+
// `Cross-Origin-Opener-Policy: same-origin` puts this document in a fresh browsing
|
|
60
|
+
// context group, which severs `window.opener` **permanently** — navigating back to the
|
|
61
|
+
// relying party afterwards does not restore it. Popup-based login is the standard way an
|
|
62
|
+
// embedded application authenticates and the opener is its only channel home, so an
|
|
63
|
+
// authorization endpoint must not be the thing that cuts it.
|
|
64
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'unsafe-none');
|
|
65
|
+
// The provider posts its own interaction forms across origins; `form-action 'self'`
|
|
66
|
+
// would block the submission.
|
|
67
|
+
const csp = res.getHeader('Content-Security-Policy');
|
|
55
68
|
if (typeof csp === 'string' && csp.includes("form-action 'self'")) {
|
|
56
|
-
|
|
69
|
+
res.setHeader('Content-Security-Policy', csp.replace(/form-action 'self'/, 'form-action *'));
|
|
57
70
|
}
|
|
58
|
-
|
|
71
|
+
next();
|
|
59
72
|
});
|
|
73
|
+
api.server.use(base, oidc.callback());
|
|
60
74
|
if (context.cfg.debug?.all || context.cfg.debug?.oidc) {
|
|
61
75
|
oidc.on('grant.error', (_, error) => {
|
|
62
76
|
console.warn('GRANT ERROR .......: ');
|
package/build/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACjE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-oidc-provider",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@owlmeans/dep-config": "workspace:*",
|
|
26
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
27
|
-
"@types/bun": "^1.
|
|
26
|
+
"@owlmeans/context": "^0.1.18-rc.10",
|
|
27
|
+
"@types/bun": "^1.4.0",
|
|
28
28
|
"@types/node": "^26.1.0",
|
|
29
29
|
"@types/oidc-provider": "9.5.0",
|
|
30
30
|
"nodemon": "^3.1.14",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@noble/hashes": "^1.5.0",
|
|
38
|
-
"@owlmeans/config": "^0.1.18-rc.
|
|
39
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
40
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
41
|
-
"@owlmeans/oidc": "^0.1.18-rc.
|
|
42
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
43
|
-
"@owlmeans/server-api": "^0.1.18-rc.
|
|
44
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
38
|
+
"@owlmeans/config": "^0.1.18-rc.14",
|
|
39
|
+
"@owlmeans/context": "^0.1.18-rc.10",
|
|
40
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.13",
|
|
41
|
+
"@owlmeans/oidc": "^0.1.18-rc.15",
|
|
42
|
+
"@owlmeans/route": "^0.1.18-rc.11",
|
|
43
|
+
"@owlmeans/server-api": "^0.1.18-rc.19",
|
|
44
|
+
"@owlmeans/server-context": "^0.1.18-rc.14",
|
|
45
45
|
"@scure/base": "^2.3.0",
|
|
46
46
|
"jose": "6.2.5",
|
|
47
47
|
"oidc-provider": "9.11.1"
|
package/src/service.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertContext, createService } from '@owlmeans/context'
|
|
2
2
|
import { DEFAULT_ALIAS, OIDC_ACCOUNT_SERVICE } from './consts.js'
|
|
3
3
|
import { DEFAULT_PATH, INTERACTION, INTERACTION_UID } from '@owlmeans/oidc'
|
|
4
|
+
import type { ServerResponse } from 'node:http'
|
|
4
5
|
import type { Config, Context, OidcAccountService, OidcAdapterService, OidcProviderService } from './types.js'
|
|
5
6
|
import Provider from 'oidc-provider'
|
|
6
7
|
import type { BasicRoute } from '@owlmeans/route'
|
|
@@ -42,17 +43,17 @@ export const createOidcProviderService = (alias: string = DEFAULT_ALIAS): OidcPr
|
|
|
42
43
|
interactions: {
|
|
43
44
|
url: async (_, interaction) => {
|
|
44
45
|
// The interaction screen is a FRONTEND route, and this is a server context — the
|
|
45
|
-
// entrypoint registered here has no `
|
|
46
|
-
// `@owlmeans/client-entrypoint` only). So the URL is assembled the way `
|
|
47
|
-
// it in the browser:
|
|
48
|
-
// with the
|
|
46
|
+
// entrypoint registered here has no `url()` (that helper is attached by
|
|
47
|
+
// `@owlmeans/client-entrypoint` only). So the URL is assembled the way `entrypointUrl`
|
|
48
|
+
// does it in the browser: take the entrypoint's path, substitute the path params, then
|
|
49
|
+
// qualify it with the address the entrypoint answers on — the FRONTEND service's, not
|
|
50
|
+
// this one's.
|
|
49
51
|
const entry = context.entrypoint<CommonEntrypoint>(INTERACTION)
|
|
50
|
-
const
|
|
51
|
-
const path = entry.getPath().split(SEP)
|
|
52
|
+
const path = entry.path().split(SEP)
|
|
52
53
|
.map(part => part === `${PARAM}${INTERACTION_UID}` ? interaction.uid : part)
|
|
53
54
|
.join(SEP)
|
|
54
55
|
|
|
55
|
-
return helper.makeUrl(
|
|
56
|
+
return helper.makeUrl(entry.address(), path)
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
})
|
|
@@ -60,21 +61,37 @@ export const createOidcProviderService = (alias: string = DEFAULT_ALIAS): OidcPr
|
|
|
60
61
|
oidc.proxy = cfg.behindProxy ?? unsecure
|
|
61
62
|
const base = SEP + (cfg.basePath ?? DEFAULT_PATH)
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Response headers the hardened defaults get wrong for an authorization endpoint.
|
|
66
|
+
*
|
|
67
|
+
* This runs as middleware, ahead of the provider in the same chain, and writes to the raw
|
|
68
|
+
* response — deliberately, not as a Fastify `onSend` hook. `oidc.callback()` is a Koa
|
|
69
|
+
* handler mounted through Middie: it ends the response itself, outside Fastify's reply
|
|
70
|
+
* lifecycle, so `onSend` never fires for any route the provider answers and anything set
|
|
71
|
+
* there silently never reaches the wire. The security defaults are already on the raw
|
|
72
|
+
* response by this point (`@fastify/helmet` applies them in `onRequest`), so overriding
|
|
73
|
+
* them here is what actually takes effect.
|
|
74
|
+
*/
|
|
75
|
+
api.server.use(base, (_req: unknown, res: ServerResponse, next: () => void) => {
|
|
76
|
+
// `Cross-Origin-Opener-Policy: same-origin` puts this document in a fresh browsing
|
|
77
|
+
// context group, which severs `window.opener` **permanently** — navigating back to the
|
|
78
|
+
// relying party afterwards does not restore it. Popup-based login is the standard way an
|
|
79
|
+
// embedded application authenticates and the opener is its only channel home, so an
|
|
80
|
+
// authorization endpoint must not be the thing that cuts it.
|
|
81
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'unsafe-none')
|
|
82
|
+
|
|
83
|
+
// The provider posts its own interaction forms across origins; `form-action 'self'`
|
|
84
|
+
// would block the submission.
|
|
85
|
+
const csp = res.getHeader('Content-Security-Policy')
|
|
72
86
|
if (typeof csp === 'string' && csp.includes("form-action 'self'")) {
|
|
73
|
-
|
|
87
|
+
res.setHeader('Content-Security-Policy', csp.replace(/form-action 'self'/, 'form-action *'))
|
|
74
88
|
}
|
|
75
|
-
|
|
89
|
+
|
|
90
|
+
next()
|
|
76
91
|
})
|
|
77
92
|
|
|
93
|
+
api.server.use(base, oidc.callback())
|
|
94
|
+
|
|
78
95
|
if (context.cfg.debug?.all || context.cfg.debug?.oidc) {
|
|
79
96
|
|
|
80
97
|
oidc.on('grant.error', (_, error) => {
|
package/tests/config.spec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { generateKeyPairSync } from 'node:crypto'
|
|
3
|
-
import { AppType,
|
|
3
|
+
import { AppType, makeBasicContext } from '@owlmeans/context'
|
|
4
4
|
import type { BasicContext } from '@owlmeans/context'
|
|
5
5
|
import { combineConfig } from '../src/utils/config.js'
|
|
6
6
|
import type { Config } from '../src/types.js'
|
|
@@ -12,7 +12,6 @@ const makeTestContext = () => {
|
|
|
12
12
|
const cfg: Config = {
|
|
13
13
|
ready: false,
|
|
14
14
|
service: 'server-oidc-provider-tests',
|
|
15
|
-
layer: Layer.Service,
|
|
16
15
|
type: AppType.Backend,
|
|
17
16
|
services: {},
|
|
18
17
|
debug: { all: false },
|
package/build/.gitkeep
DELETED
|
File without changes
|