@_mustachio/openauth 0.10.0 → 0.12.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.
- package/dist/esm/adapters/d1/index.js +18 -9
- package/dist/esm/adapters/d1/migrations.js +7 -2
- package/dist/esm/adapters/d1/session-store.js +39 -0
- package/dist/esm/adapters/d1/session.js +2 -2
- package/dist/esm/adapters/durable-object/index.js +4 -0
- package/dist/esm/adapters/durable-object/session-store.js +42 -0
- package/dist/esm/adapters/dynamo/index.js +24 -7
- package/dist/esm/adapters/dynamo/session-store.js +49 -0
- package/dist/esm/adapters/kms/index.js +6 -2
- package/dist/esm/adapters/kv/index.js +6 -2
- package/dist/esm/adapters/memory/index.js +24 -7
- package/dist/esm/adapters/memory/session-store.js +45 -0
- package/dist/esm/adapters/memory/token-store.js +13 -0
- package/dist/esm/adapters/postgres/executor.js +2 -2
- package/dist/esm/adapters/postgres/index.js +33 -10
- package/dist/esm/adapters/postgres/migrations.js +8 -2
- package/dist/esm/adapters/postgres/session-store.js +42 -0
- package/dist/esm/client.js +35 -28
- package/dist/esm/domain/authorize.js +7 -2
- package/dist/esm/domain/callback.js +124 -7
- package/dist/esm/domain/client-auth.js +2 -2
- package/dist/esm/domain/crypto.js +14 -14
- package/dist/esm/domain/discovery.js +46 -4
- package/dist/esm/domain/dpop.js +122 -0
- package/dist/esm/domain/id-token.js +94 -0
- package/dist/esm/domain/introspect.js +5 -1
- package/dist/esm/domain/jwt.js +30 -2
- package/dist/esm/domain/logout.js +75 -0
- package/dist/esm/domain/method-dispatch.js +27 -1
- package/dist/esm/domain/method-route.js +83 -1
- package/dist/esm/domain/par.js +51 -0
- package/dist/esm/domain/pkce.js +2 -2
- package/dist/esm/domain/refresh.js +12 -1
- package/dist/esm/domain/register.js +74 -0
- package/dist/esm/domain/revoke.js +2 -2
- package/dist/esm/domain/state-envelope.js +2 -2
- package/dist/esm/domain/token-exchange.js +1 -0
- package/dist/esm/domain/token.js +53 -12
- package/dist/esm/domain/userinfo.js +38 -3
- package/dist/esm/error.js +8 -8
- package/dist/esm/http/cookies.js +19 -2
- package/dist/esm/http/errors.js +4 -4
- package/dist/esm/http/handlers/authorize.js +92 -21
- package/dist/esm/http/handlers/callback.js +7 -1
- package/dist/esm/http/handlers/end-session.js +64 -0
- package/dist/esm/http/handlers/metadata.js +7 -4
- package/dist/esm/http/handlers/method-route.js +47 -2
- package/dist/esm/http/handlers/par.js +53 -0
- package/dist/esm/http/handlers/register.js +46 -0
- package/dist/esm/http/handlers/revocation.js +2 -2
- package/dist/esm/http/handlers/token.js +36 -6
- package/dist/esm/http/handlers/userinfo.js +35 -29
- package/dist/esm/http/middleware/tenant.js +6 -6
- package/dist/esm/http/router.js +10 -0
- package/dist/esm/http/schemas/authorize.js +8 -2
- package/dist/esm/http/schemas/end-session.js +13 -0
- package/dist/esm/http/schemas/par.js +9 -0
- package/dist/esm/http/schemas/register.js +16 -0
- package/dist/esm/http/schemas/revocation.js +2 -2
- package/dist/esm/http/schemas/token.js +7 -7
- package/dist/esm/index.js +40 -34
- package/dist/esm/methods/oauth2-factory.js +2 -2
- package/dist/esm/methods/password-hash.js +2 -2
- package/dist/esm/methods/providers/index.js +30 -14
- package/dist/esm/methods/saml-sp/acs.js +208 -0
- package/dist/esm/methods/saml-sp/attributes.js +75 -0
- package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
- package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
- package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
- package/dist/esm/methods/saml-sp/factory.js +70 -0
- package/dist/esm/methods/saml-sp/index.js +7 -0
- package/dist/esm/methods/saml-sp/metadata.js +69 -0
- package/dist/esm/methods/saml-sp/method.js +29 -0
- package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
- package/dist/esm/methods/saml-sp/saml-instance.js +49 -0
- package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
- package/dist/esm/methods/saml-sp/sls.js +154 -0
- package/dist/esm/methods/saml-sp/types.js +0 -0
- package/dist/esm/types/error.js +5 -0
- package/dist/esm/types/result.js +3 -3
- package/dist/esm/ui/forms.js +3 -3
- package/dist/esm/ui/index.js +8 -5
- package/dist/esm/ui/picker.js +2 -2
- package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
- package/dist/types/adapters/d1/session-store.d.ts +3 -0
- package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
- package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
- package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
- package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
- package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/session-store.d.ts +6 -1
- package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/token-store.d.ts +1 -0
- package/dist/types/adapters/memory/token-store.d.ts.map +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
- package/dist/types/adapters/postgres/session-store.d.ts +3 -0
- package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
- package/dist/types/client.d.ts +19 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/domain/authorize.d.ts.map +1 -1
- package/dist/types/domain/callback.d.ts +16 -1
- package/dist/types/domain/callback.d.ts.map +1 -1
- package/dist/types/domain/client-credentials.d.ts +2 -0
- package/dist/types/domain/client-credentials.d.ts.map +1 -1
- package/dist/types/domain/discovery.d.ts +37 -0
- package/dist/types/domain/discovery.d.ts.map +1 -1
- package/dist/types/domain/dpop.d.ts +61 -0
- package/dist/types/domain/dpop.d.ts.map +1 -0
- package/dist/types/domain/id-token.d.ts +83 -0
- package/dist/types/domain/id-token.d.ts.map +1 -0
- package/dist/types/domain/introspect.d.ts +8 -0
- package/dist/types/domain/introspect.d.ts.map +1 -1
- package/dist/types/domain/jwt.d.ts +27 -1
- package/dist/types/domain/jwt.d.ts.map +1 -1
- package/dist/types/domain/logout.d.ts +69 -0
- package/dist/types/domain/logout.d.ts.map +1 -0
- package/dist/types/domain/method-dispatch.d.ts.map +1 -1
- package/dist/types/domain/method-route.d.ts +57 -1
- package/dist/types/domain/method-route.d.ts.map +1 -1
- package/dist/types/domain/par.d.ts +62 -0
- package/dist/types/domain/par.d.ts.map +1 -0
- package/dist/types/domain/refresh.d.ts +9 -0
- package/dist/types/domain/refresh.d.ts.map +1 -1
- package/dist/types/domain/register.d.ts +33 -0
- package/dist/types/domain/register.d.ts.map +1 -0
- package/dist/types/domain/token-exchange.d.ts +2 -0
- package/dist/types/domain/token-exchange.d.ts.map +1 -1
- package/dist/types/domain/token.d.ts +41 -1
- package/dist/types/domain/token.d.ts.map +1 -1
- package/dist/types/domain/userinfo.d.ts +44 -5
- package/dist/types/domain/userinfo.d.ts.map +1 -1
- package/dist/types/http/context.d.ts +6 -1
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/cookies.d.ts +9 -1
- package/dist/types/http/cookies.d.ts.map +1 -1
- package/dist/types/http/handlers/authorize.d.ts.map +1 -1
- package/dist/types/http/handlers/callback.d.ts.map +1 -1
- package/dist/types/http/handlers/end-session.d.ts +3 -0
- package/dist/types/http/handlers/end-session.d.ts.map +1 -0
- package/dist/types/http/handlers/metadata.d.ts +1 -1
- package/dist/types/http/handlers/metadata.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/http/handlers/par.d.ts +3 -0
- package/dist/types/http/handlers/par.d.ts.map +1 -0
- package/dist/types/http/handlers/register.d.ts +3 -0
- package/dist/types/http/handlers/register.d.ts.map +1 -0
- package/dist/types/http/handlers/token.d.ts.map +1 -1
- package/dist/types/http/handlers/userinfo.d.ts.map +1 -1
- package/dist/types/http/router.d.ts.map +1 -1
- package/dist/types/http/schemas/authorize.d.ts +40 -0
- package/dist/types/http/schemas/authorize.d.ts.map +1 -1
- package/dist/types/http/schemas/end-session.d.ts +31 -0
- package/dist/types/http/schemas/end-session.d.ts.map +1 -0
- package/dist/types/http/schemas/par.d.ts +20 -0
- package/dist/types/http/schemas/par.d.ts.map +1 -0
- package/dist/types/http/schemas/register.d.ts +42 -0
- package/dist/types/http/schemas/register.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/saml-sp/acs.d.ts +4 -0
- package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/attributes.d.ts +40 -0
- package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/factory.d.ts +704 -0
- package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/index.d.ts +24 -0
- package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/metadata.d.ts +42 -0
- package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/method.d.ts +17 -0
- package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts +81 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/sls.d.ts +4 -0
- package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/types.d.ts +204 -0
- package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
- package/dist/types/ports/audit-log.d.ts +40 -0
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/session-store.d.ts +50 -0
- package/dist/types/ports/session-store.d.ts.map +1 -1
- package/dist/types/ports/token-store.d.ts +12 -0
- package/dist/types/ports/token-store.d.ts.map +1 -1
- package/dist/types/types/authorization.d.ts +23 -0
- package/dist/types/types/authorization.d.ts.map +1 -1
- package/dist/types/types/error.d.ts +9 -0
- package/dist/types/types/error.d.ts.map +1 -1
- package/dist/types/types/flow.d.ts +10 -0
- package/dist/types/types/flow.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +162 -1
- package/dist/types/types/idp.d.ts.map +1 -1
- package/dist/types/types/method.d.ts +109 -0
- package/dist/types/types/method.d.ts.map +1 -1
- package/dist/types/types/tenant.d.ts +37 -0
- package/dist/types/types/tenant.d.ts.map +1 -1
- package/dist/types/types/token.d.ts +128 -0
- package/dist/types/types/token.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/adapters/d1/migrations.ts +5 -0
- package/src/adapters/d1/session-store.ts +68 -0
- package/src/adapters/durable-object/session-store.ts +61 -0
- package/src/adapters/dynamo/session-store.ts +63 -0
- package/src/adapters/memory/session-store.ts +83 -1
- package/src/adapters/memory/token-store.ts +16 -0
- package/src/adapters/postgres/migrations.ts +6 -0
- package/src/adapters/postgres/session-store.ts +69 -0
- package/src/client.ts +32 -0
- package/src/domain/authorize.ts +9 -0
- package/src/domain/callback.ts +220 -7
- package/src/domain/client-credentials.ts +2 -0
- package/src/domain/discovery.ts +80 -2
- package/src/domain/dpop.ts +293 -0
- package/src/domain/id-token.ts +213 -0
- package/src/domain/introspect.ts +10 -0
- package/src/domain/jwt.ts +80 -1
- package/src/domain/logout.ts +180 -0
- package/src/domain/method-dispatch.ts +53 -1
- package/src/domain/method-route.ts +195 -1
- package/src/domain/par.ts +141 -0
- package/src/domain/refresh.ts +41 -0
- package/src/domain/register.ts +168 -0
- package/src/domain/token-exchange.ts +11 -0
- package/src/domain/token.ts +134 -8
- package/src/domain/userinfo.ts +121 -6
- package/src/http/context.ts +10 -0
- package/src/http/cookies.ts +19 -1
- package/src/http/handlers/authorize.ts +146 -24
- package/src/http/handlers/callback.ts +9 -1
- package/src/http/handlers/end-session.ts +106 -0
- package/src/http/handlers/metadata.ts +7 -2
- package/src/http/handlers/method-route.ts +75 -2
- package/src/http/handlers/par.ts +84 -0
- package/src/http/handlers/register.ts +70 -0
- package/src/http/handlers/token.ts +56 -0
- package/src/http/handlers/userinfo.ts +62 -38
- package/src/http/router.ts +19 -0
- package/src/http/schemas/authorize.ts +27 -0
- package/src/http/schemas/end-session.ts +23 -0
- package/src/http/schemas/par.ts +23 -0
- package/src/http/schemas/register.ts +29 -0
- package/src/index.ts +21 -1
- package/src/methods/saml-sp/acs.ts +399 -0
- package/src/methods/saml-sp/attributes.ts +137 -0
- package/src/methods/saml-sp/authnrequest.ts +128 -0
- package/src/methods/saml-sp/cache-provider.ts +76 -0
- package/src/methods/saml-sp/cert-rotation.ts +34 -0
- package/src/methods/saml-sp/factory.ts +118 -0
- package/src/methods/saml-sp/index.ts +34 -0
- package/src/methods/saml-sp/metadata.ts +204 -0
- package/src/methods/saml-sp/method.ts +65 -0
- package/src/methods/saml-sp/parse-idp-metadata.ts +164 -0
- package/src/methods/saml-sp/saml-instance.ts +173 -0
- package/src/methods/saml-sp/slo-initiate.ts +177 -0
- package/src/methods/saml-sp/sls.ts +306 -0
- package/src/methods/saml-sp/types.ts +203 -0
- package/src/ports/CONSISTENCY.md +77 -21
- package/src/ports/audit-log.ts +42 -0
- package/src/ports/session-store.ts +61 -0
- package/src/ports/token-store.ts +13 -0
- package/src/types/authorization.ts +25 -0
- package/src/types/error.ts +23 -0
- package/src/types/flow.ts +10 -0
- package/src/types/idp.ts +170 -0
- package/src/types/method.ts +110 -0
- package/src/types/tenant.ts +37 -0
- package/src/types/token.ts +135 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@node-saml/node-saml` `CacheProvider` backed by `methodScratch`.
|
|
3
|
+
*
|
|
4
|
+
* node-saml tracks outstanding `AuthnRequest` IDs through a
|
|
5
|
+
* `CacheProvider`: `saveAsync(requestId, marker)` when the request is
|
|
6
|
+
* generated, then `getAsync` + `removeAsync` during response
|
|
7
|
+
* validation to enforce `InResponseTo` (defeats unsolicited /
|
|
8
|
+
* replayed Responses on the SP-initiated path).
|
|
9
|
+
*
|
|
10
|
+
* The default in-memory provider is single-process only. We back it
|
|
11
|
+
* with `MethodContext.methodScratch`, which is already scoped per
|
|
12
|
+
* `(tenantId, methodId)` and shared across instances via the
|
|
13
|
+
* `SessionStore` adapter — so an AuthnRequest minted on one node
|
|
14
|
+
* validates on another.
|
|
15
|
+
*
|
|
16
|
+
* `methodScratch.get` returning `unknown_state` (missing / expired)
|
|
17
|
+
* maps to `null`, which is exactly node-saml's "unknown request id"
|
|
18
|
+
* signal.
|
|
19
|
+
*/
|
|
20
|
+
import type { MethodScratch } from "../../types/method"
|
|
21
|
+
|
|
22
|
+
import { isOk } from "../../types/result"
|
|
23
|
+
|
|
24
|
+
type CacheItem = { value: string; createdAt: number }
|
|
25
|
+
type NodeSamlCacheProvider = {
|
|
26
|
+
saveAsync(key: string, value: string): Promise<CacheItem | null>
|
|
27
|
+
getAsync(key: string): Promise<string | null>
|
|
28
|
+
removeAsync(key: string | null): Promise<string | null>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Key namespace inside the (already tenant/method-scoped) scratch. */
|
|
32
|
+
const KEY_PREFIX = "saml-inresponseto:"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param scratch Per-request `MethodContext.methodScratch`.
|
|
36
|
+
* @param ttlMs How long an outstanding request id stays valid. Should
|
|
37
|
+
* comfortably exceed the slowest realistic IdP login
|
|
38
|
+
* (the flow record itself expires independently).
|
|
39
|
+
*/
|
|
40
|
+
export function methodScratchCacheProvider(
|
|
41
|
+
scratch: MethodScratch,
|
|
42
|
+
ttlMs: number,
|
|
43
|
+
): NodeSamlCacheProvider {
|
|
44
|
+
return {
|
|
45
|
+
async saveAsync(key, value) {
|
|
46
|
+
const createdAt = Date.now()
|
|
47
|
+
const item: CacheItem = { value, createdAt }
|
|
48
|
+
const r = await scratch.put(
|
|
49
|
+
`${KEY_PREFIX}${key}`,
|
|
50
|
+
JSON.stringify(item),
|
|
51
|
+
ttlMs,
|
|
52
|
+
)
|
|
53
|
+
// node-saml treats a null return as "could not cache" and fails
|
|
54
|
+
// the request generation loudly — which is what we want if the
|
|
55
|
+
// backing store is unavailable.
|
|
56
|
+
return isOk(r) ? item : null
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async getAsync(key) {
|
|
60
|
+
const r = await scratch.get(`${KEY_PREFIX}${key}`)
|
|
61
|
+
if (!isOk(r)) return null
|
|
62
|
+
try {
|
|
63
|
+
const item = JSON.parse(r.value) as CacheItem
|
|
64
|
+
return item.value
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async removeAsync(key) {
|
|
71
|
+
if (key === null) return null
|
|
72
|
+
const r = await scratch.delete(`${KEY_PREFIX}${key}`)
|
|
73
|
+
return isOk(r) ? key : null
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cert-rotation shim.
|
|
3
|
+
*
|
|
4
|
+
* `@node-saml/node-saml`'s `idpCert` accepts a single PEM, an array of
|
|
5
|
+
* PEMs, or a callback. Our config models hot rotation as an array of
|
|
6
|
+
* `{ pem, notBefore?, notAfter? }`. This pure function filters that
|
|
7
|
+
* array to the certs whose validity window covers `now`, producing the
|
|
8
|
+
* PEM list node-saml verifies against.
|
|
9
|
+
*
|
|
10
|
+
* A cert with no `notBefore` has no lower bound; no `notAfter` has no
|
|
11
|
+
* upper bound. `notBefore` is inclusive, `notAfter` is exclusive — a
|
|
12
|
+
* cert is active while `notBefore <= now < notAfter`. Overlapping
|
|
13
|
+
* windows are the whole point: during rotation both the outgoing and
|
|
14
|
+
* incoming cert are active so in-flight assertions from either key
|
|
15
|
+
* still verify.
|
|
16
|
+
*
|
|
17
|
+
* Returns `[]` when nothing is in window — the caller surfaces a
|
|
18
|
+
* configuration error rather than handing node-saml an empty cert set
|
|
19
|
+
* (which it would treat as "accept nothing").
|
|
20
|
+
*/
|
|
21
|
+
import type { SamlIdpSigningCert } from "./types"
|
|
22
|
+
|
|
23
|
+
export function selectActiveCertPems(
|
|
24
|
+
certs: ReadonlyArray<SamlIdpSigningCert>,
|
|
25
|
+
nowMs: number,
|
|
26
|
+
): string[] {
|
|
27
|
+
return certs
|
|
28
|
+
.filter((c) => {
|
|
29
|
+
if (c.notBefore !== undefined && nowMs < c.notBefore) return false
|
|
30
|
+
if (c.notAfter !== undefined && nowMs >= c.notAfter) return false
|
|
31
|
+
return true
|
|
32
|
+
})
|
|
33
|
+
.map((c) => c.pem)
|
|
34
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAML SP factory — `kind: "saml-sp"`.
|
|
3
|
+
*
|
|
4
|
+
* Parallels `oauth2Factory` / `oidcFactory`: a Zod `configSchema`
|
|
5
|
+
* validates the tenant-supplied `SamlSpConfig` blob (Zod 3.24+ is
|
|
6
|
+
* Standard Schema v1 conformant, so it satisfies
|
|
7
|
+
* `AuthMethodFactory.configSchema` directly), and `build` delegates to
|
|
8
|
+
* `buildSamlSpMethod`.
|
|
9
|
+
*
|
|
10
|
+
* No `@node-saml/*` import appears here — node-saml is reached only at
|
|
11
|
+
* request time inside the route handlers, so importing the factory
|
|
12
|
+
* type surface stays cheap and the public-API leak guard stays green.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod"
|
|
15
|
+
|
|
16
|
+
import type { AuthMethod, AuthMethodFactory } from "../../types/method"
|
|
17
|
+
|
|
18
|
+
import { buildSamlSpMethod } from "./method"
|
|
19
|
+
import type { SamlSpConfig, SamlSpProperties, SamlSpState } from "./types"
|
|
20
|
+
|
|
21
|
+
const attributeRefSchema = z.union([
|
|
22
|
+
z.object({ source: z.literal("nameId") }),
|
|
23
|
+
z.object({
|
|
24
|
+
source: z.literal("attribute"),
|
|
25
|
+
name: z.string().min(1),
|
|
26
|
+
format: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
const attributeMappingSchema = z.object({
|
|
31
|
+
subject: attributeRefSchema.optional(),
|
|
32
|
+
email: attributeRefSchema.optional(),
|
|
33
|
+
emailVerified: z
|
|
34
|
+
.object({ source: z.literal("literal"), value: z.boolean() })
|
|
35
|
+
.optional(),
|
|
36
|
+
name: attributeRefSchema.optional(),
|
|
37
|
+
groups: attributeRefSchema.optional(),
|
|
38
|
+
custom: z.record(attributeRefSchema).optional(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const signingCertSchema = z.object({
|
|
42
|
+
pem: z.string().min(1),
|
|
43
|
+
notBefore: z.number().optional(),
|
|
44
|
+
notAfter: z.number().optional(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const idpSchema = z.object({
|
|
48
|
+
entityId: z.string().min(1),
|
|
49
|
+
ssoUrl: z.string().url(),
|
|
50
|
+
sloUrl: z.string().url().optional(),
|
|
51
|
+
nameIdFormat: z
|
|
52
|
+
.enum(["persistent", "transient", "emailAddress", "unspecified"])
|
|
53
|
+
.optional(),
|
|
54
|
+
signingCerts: z.array(signingCertSchema).min(1),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const idpInitiatedSchema = z.object({
|
|
58
|
+
defaultClientId: z.string().min(1),
|
|
59
|
+
defaultRedirectUri: z.string().url(),
|
|
60
|
+
defaultScopes: z.array(z.string()).optional(),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const samlSpConfigSchema = z
|
|
64
|
+
.object({
|
|
65
|
+
idp: idpSchema,
|
|
66
|
+
attributeMapping: attributeMappingSchema,
|
|
67
|
+
signAuthnRequest: z.boolean().optional(),
|
|
68
|
+
signingKey: z
|
|
69
|
+
.object({
|
|
70
|
+
privateKeyPem: z.string().min(1),
|
|
71
|
+
certPem: z.string().min(1),
|
|
72
|
+
})
|
|
73
|
+
.optional(),
|
|
74
|
+
allowEncryptedAssertions: z.boolean().optional(),
|
|
75
|
+
decryptionKey: z
|
|
76
|
+
.object({
|
|
77
|
+
privateKeyPem: z.string().min(1),
|
|
78
|
+
certPem: z.string().min(1),
|
|
79
|
+
})
|
|
80
|
+
.optional(),
|
|
81
|
+
idpInitiated: idpInitiatedSchema.optional(),
|
|
82
|
+
clockSkewSeconds: z.number().int().nonnegative().optional(),
|
|
83
|
+
})
|
|
84
|
+
.refine((c) => !c.signAuthnRequest || c.signingKey !== undefined, {
|
|
85
|
+
message: "signingKey is required when signAuthnRequest is true",
|
|
86
|
+
path: ["signingKey"],
|
|
87
|
+
})
|
|
88
|
+
.refine(
|
|
89
|
+
(c) => !c.allowEncryptedAssertions || c.decryptionKey !== undefined,
|
|
90
|
+
{
|
|
91
|
+
message:
|
|
92
|
+
"decryptionKey is required when allowEncryptedAssertions is true",
|
|
93
|
+
path: ["decryptionKey"],
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
export type SamlSpFactoryConfig = z.infer<typeof samlSpConfigSchema>
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generic SAML 2.0 Service Provider factory.
|
|
101
|
+
*
|
|
102
|
+
* `kind: "saml-sp"`. Use a distinct `MethodConfig.id` per upstream IdP
|
|
103
|
+
* to register multiple SAML connections against a single tenant.
|
|
104
|
+
*/
|
|
105
|
+
export const samlSpFactory: AuthMethodFactory<
|
|
106
|
+
SamlSpProperties,
|
|
107
|
+
SamlSpState,
|
|
108
|
+
SamlSpConfig
|
|
109
|
+
> = {
|
|
110
|
+
kind: "saml-sp",
|
|
111
|
+
configSchema: samlSpConfigSchema,
|
|
112
|
+
build: async ({
|
|
113
|
+
id,
|
|
114
|
+
kind,
|
|
115
|
+
config,
|
|
116
|
+
}): Promise<AuthMethod<SamlSpProperties, SamlSpState>> =>
|
|
117
|
+
buildSamlSpMethod(id, kind, config),
|
|
118
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@_mustachio/openauth/methods/saml-sp` — SAML 2.0 Service Provider
|
|
3
|
+
* method family.
|
|
4
|
+
*
|
|
5
|
+
* **Node-only.** This entry depends transitively on
|
|
6
|
+
* `@node-saml/node-saml` and `xml-crypto`, which require `node:crypto`
|
|
7
|
+
* and the `@xmldom/xmldom` DOM. Cloudflare Workers, browsers, and
|
|
8
|
+
* other edge runtimes cannot load this module; use the root
|
|
9
|
+
* `@_mustachio/openauth` entry (OIDC / OAuth methods) on those
|
|
10
|
+
* platforms.
|
|
11
|
+
*
|
|
12
|
+
* The library's root entry is verified edge-clean by
|
|
13
|
+
* `test/types/public-api-no-thirdparty-leaks.test.ts` and the
|
|
14
|
+
* complementary `saml-sp-no-thirdparty-leaks.test.ts` — neither this
|
|
15
|
+
* subpath's third-party deps nor its public types are reachable from
|
|
16
|
+
* `@_mustachio/openauth`.
|
|
17
|
+
*
|
|
18
|
+
* See `docs/plans/claude/saml-sp-plan.md` for the architectural
|
|
19
|
+
* decisions backing this surface (SAML-AD1–AD7) and the phase plan.
|
|
20
|
+
*/
|
|
21
|
+
export { samlSpFactory } from "./factory"
|
|
22
|
+
export { parseSamlIdpMetadata } from "./parse-idp-metadata"
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
SamlAttributeMapping,
|
|
26
|
+
SamlAttributeRef,
|
|
27
|
+
SamlIdpConfig,
|
|
28
|
+
SamlIdpInitiatedConfig,
|
|
29
|
+
SamlIdpSigningCert,
|
|
30
|
+
SamlNameIdFormat,
|
|
31
|
+
SamlSpConfig,
|
|
32
|
+
SamlSpProperties,
|
|
33
|
+
SamlSpState,
|
|
34
|
+
} from "./types"
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP metadata XML — the outbound half of metadata exchange
|
|
3
|
+
* (`parseSamlIdpMetadata` is the inbound half).
|
|
4
|
+
*
|
|
5
|
+
* Served anonymously at `GET /m/<methodId>/metadata` via the method's
|
|
6
|
+
* `publicRoutes` allowlist (no flow cookie). An enterprise IdP admin
|
|
7
|
+
* imports this document to register us as an SP.
|
|
8
|
+
*
|
|
9
|
+
* **Conformance invariant:** the `entityID` and ACS `Location` are
|
|
10
|
+
* derived from the *same* inputs the live AuthnRequest / ACS path uses
|
|
11
|
+
* (`deriveSpEntityId(issuerUrl, tenantId, methodId)` and
|
|
12
|
+
* `ctx.dispatch.callbackUrl`). The metadata therefore describes exactly
|
|
13
|
+
* what the runtime accepts — never an independently re-specified value
|
|
14
|
+
* that could drift. `metadata.test.ts` asserts this equality against
|
|
15
|
+
* `buildAuthnRequestRedirect` so drift fails CI.
|
|
16
|
+
*
|
|
17
|
+
* `KeyDescriptor` is emitted only when we actually sign AuthnRequests
|
|
18
|
+
* (advertising a cert we cannot use would be the bug);
|
|
19
|
+
* `SingleLogoutService` only when an IdP SLO endpoint is configured
|
|
20
|
+
* and the `/sls` route is therefore served (advertising an endpoint we
|
|
21
|
+
* do not serve would break interop). `AuthnRequestsSigned` /
|
|
22
|
+
* `WantAssertionsSigned="true"` truthfully state actual behaviour.
|
|
23
|
+
*/
|
|
24
|
+
import { authError } from "../../types/error"
|
|
25
|
+
import type { MethodContext, MethodResult } from "../../types/method"
|
|
26
|
+
|
|
27
|
+
import { deriveSpEntityId } from "./saml-instance"
|
|
28
|
+
import type {
|
|
29
|
+
SamlNameIdFormat,
|
|
30
|
+
SamlSpConfig,
|
|
31
|
+
SamlSpProperties,
|
|
32
|
+
SamlSpState,
|
|
33
|
+
} from "./types"
|
|
34
|
+
|
|
35
|
+
const NAME_ID_FORMAT_URN: Record<SamlNameIdFormat, string> = {
|
|
36
|
+
persistent: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
|
|
37
|
+
transient: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
|
38
|
+
emailAddress: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
39
|
+
unspecified: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
|
43
|
+
const HTTP_REDIRECT =
|
|
44
|
+
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
45
|
+
|
|
46
|
+
/** Minimal XML attribute/text escaping for URL-shaped values. */
|
|
47
|
+
function xmlEscape(s: string): string {
|
|
48
|
+
return s
|
|
49
|
+
.replace(/&/g, "&")
|
|
50
|
+
.replace(/</g, "<")
|
|
51
|
+
.replace(/>/g, ">")
|
|
52
|
+
.replace(/"/g, """)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type SpMetadataInput = {
|
|
56
|
+
spEntityId: string
|
|
57
|
+
acsUrl: string
|
|
58
|
+
/**
|
|
59
|
+
* The SP's own Single Logout Service URL (`/m/<methodId>/sls`).
|
|
60
|
+
* Present ⇒ front-channel SLO is served, so the metadata advertises
|
|
61
|
+
* `SingleLogoutService` for both bindings we accept. Same
|
|
62
|
+
* advertise-only-what-we-serve invariant as the ACS / signing cert.
|
|
63
|
+
*/
|
|
64
|
+
slsUrl?: string
|
|
65
|
+
nameIdFormat?: SamlNameIdFormat
|
|
66
|
+
/**
|
|
67
|
+
* SP signing cert (PEM). Present ⇒ we sign AuthnRequests, so the
|
|
68
|
+
* metadata advertises `AuthnRequestsSigned="true"` and a
|
|
69
|
+
* `KeyDescriptor use="signing"`. Keeping this in lockstep with the
|
|
70
|
+
* runtime (the same `config.signingKey.certPem` that actually signs)
|
|
71
|
+
* is the same anti-drift invariant as the entityID/ACS.
|
|
72
|
+
*/
|
|
73
|
+
signingCertPem?: string
|
|
74
|
+
/**
|
|
75
|
+
* SP encryption cert (PEM). Present ⇒ the connection accepts
|
|
76
|
+
* encrypted assertions, so the metadata advertises a
|
|
77
|
+
* `KeyDescriptor use="encryption"` (the cert the IdP encrypts to —
|
|
78
|
+
* `config.decryptionKey.certPem`). Same anti-drift / advertise-only-
|
|
79
|
+
* what-we-serve invariant as the signing cert.
|
|
80
|
+
*/
|
|
81
|
+
encryptionCertPem?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** PEM cert body → bare base64 (SAML metadata X509Certificate form). */
|
|
85
|
+
function certBody(pem: string): string {
|
|
86
|
+
return pem
|
|
87
|
+
.replace(/-----BEGIN CERTIFICATE-----/g, "")
|
|
88
|
+
.replace(/-----END CERTIFICATE-----/g, "")
|
|
89
|
+
.replace(/\s+/g, "")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Pure SP metadata builder. Mirrors `parseSamlIdpMetadata`'s purity so
|
|
94
|
+
* it is trivially unit-testable and reusable.
|
|
95
|
+
*/
|
|
96
|
+
export function buildSpMetadataXml(input: SpMetadataInput): string {
|
|
97
|
+
const entityId = xmlEscape(input.spEntityId)
|
|
98
|
+
const acs = xmlEscape(input.acsUrl)
|
|
99
|
+
const signed = input.signingCertPem !== undefined
|
|
100
|
+
const nameIdLine =
|
|
101
|
+
input.nameIdFormat !== undefined
|
|
102
|
+
? `\n <md:NameIDFormat>${NAME_ID_FORMAT_URN[input.nameIdFormat]}</md:NameIDFormat>`
|
|
103
|
+
: ""
|
|
104
|
+
const x509 = (cert: string) =>
|
|
105
|
+
`<ds:KeyInfo><ds:X509Data><ds:X509Certificate>` +
|
|
106
|
+
certBody(cert) +
|
|
107
|
+
`</ds:X509Certificate></ds:X509Data></ds:KeyInfo>`
|
|
108
|
+
const keyDescriptor =
|
|
109
|
+
(signed
|
|
110
|
+
? `\n <md:KeyDescriptor use="signing">` +
|
|
111
|
+
x509(input.signingCertPem as string) +
|
|
112
|
+
`</md:KeyDescriptor>`
|
|
113
|
+
: "") +
|
|
114
|
+
(input.encryptionCertPem !== undefined
|
|
115
|
+
? `\n <md:KeyDescriptor use="encryption">` +
|
|
116
|
+
x509(input.encryptionCertPem) +
|
|
117
|
+
`</md:KeyDescriptor>`
|
|
118
|
+
: "")
|
|
119
|
+
// Schema order: KeyDescriptor → SingleLogoutService → NameIDFormat →
|
|
120
|
+
// AssertionConsumerService. Advertise both bindings we accept at /sls.
|
|
121
|
+
const sls =
|
|
122
|
+
input.slsUrl !== undefined
|
|
123
|
+
? `\n <md:SingleLogoutService ` +
|
|
124
|
+
`Binding="${HTTP_REDIRECT}" Location="${xmlEscape(input.slsUrl)}"/>` +
|
|
125
|
+
`\n <md:SingleLogoutService ` +
|
|
126
|
+
`Binding="${HTTP_POST}" Location="${xmlEscape(input.slsUrl)}"/>`
|
|
127
|
+
: ""
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
131
|
+
`<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ` +
|
|
132
|
+
`xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ` +
|
|
133
|
+
`entityID="${entityId}">\n` +
|
|
134
|
+
` <md:SPSSODescriptor AuthnRequestsSigned="${signed}" ` +
|
|
135
|
+
`WantAssertionsSigned="true" ` +
|
|
136
|
+
`protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">` +
|
|
137
|
+
keyDescriptor +
|
|
138
|
+
sls +
|
|
139
|
+
nameIdLine +
|
|
140
|
+
`\n <md:AssertionConsumerService ` +
|
|
141
|
+
`Binding="${HTTP_POST}" Location="${acs}" index="0" isDefault="true"/>` +
|
|
142
|
+
`\n </md:SPSSODescriptor>\n` +
|
|
143
|
+
`</md:EntityDescriptor>\n`
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* `GET /metadata` route handler. Anonymous (declared in the method's
|
|
149
|
+
* `publicRoutes`): `ctx.flow` / `ctx.methodState` are null; everything
|
|
150
|
+
* comes from `ctx.tenant` + `ctx.dispatch` + captured config.
|
|
151
|
+
*/
|
|
152
|
+
export async function buildSpMetadata(
|
|
153
|
+
ctx: MethodContext<SamlSpState>,
|
|
154
|
+
methodId: string,
|
|
155
|
+
config: SamlSpConfig,
|
|
156
|
+
): Promise<MethodResult<SamlSpProperties, SamlSpState>> {
|
|
157
|
+
if (!ctx.dispatch) {
|
|
158
|
+
return {
|
|
159
|
+
kind: "error",
|
|
160
|
+
error: authError.internalError(
|
|
161
|
+
"saml-sp: metadata route dispatched without issuer context " +
|
|
162
|
+
"(ctx.dispatch is null)",
|
|
163
|
+
),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const spEntityId = deriveSpEntityId(
|
|
168
|
+
ctx.dispatch.issuerUrl,
|
|
169
|
+
ctx.tenant.id,
|
|
170
|
+
methodId,
|
|
171
|
+
)
|
|
172
|
+
// SP SLS URL — same host as the ACS callback, at the public method
|
|
173
|
+
// mount `/m/<methodId>/sls`. Only advertised when SLO is actually
|
|
174
|
+
// served (idp.sloUrl set ⇒ /sls is in publicRoutes). Derived from the
|
|
175
|
+
// same dispatch input as the ACS so it cannot drift.
|
|
176
|
+
const cb = new URL(ctx.dispatch.callbackUrl)
|
|
177
|
+
const slsUrl = `${cb.protocol}//${cb.host}/m/${methodId}/sls`
|
|
178
|
+
const xml = buildSpMetadataXml({
|
|
179
|
+
spEntityId,
|
|
180
|
+
acsUrl: ctx.dispatch.callbackUrl,
|
|
181
|
+
...(config.idp.sloUrl ? { slsUrl } : {}),
|
|
182
|
+
...(config.idp.nameIdFormat !== undefined
|
|
183
|
+
? { nameIdFormat: config.idp.nameIdFormat }
|
|
184
|
+
: {}),
|
|
185
|
+
// Truthful: advertise signing iff we actually sign AuthnRequests.
|
|
186
|
+
...(config.signAuthnRequest && config.signingKey
|
|
187
|
+
? { signingCertPem: config.signingKey.certPem }
|
|
188
|
+
: {}),
|
|
189
|
+
// Truthful: advertise an encryption cert iff we accept (and can
|
|
190
|
+
// decrypt) encrypted assertions.
|
|
191
|
+
...(config.allowEncryptedAssertions && config.decryptionKey
|
|
192
|
+
? { encryptionCertPem: config.decryptionKey.certPem }
|
|
193
|
+
: {}),
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
kind: "challenge",
|
|
198
|
+
response: new Response(xml, {
|
|
199
|
+
status: 200,
|
|
200
|
+
headers: { "content-type": "application/samlmetadata+xml" },
|
|
201
|
+
}),
|
|
202
|
+
cache: { sMaxAge: 300 },
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildSamlSpMethod` — assembles the `AuthMethod` for a SAML SP
|
|
3
|
+
* instance. Mirrors `buildOauth2Method`: data + handler functions, no
|
|
4
|
+
* framework imports beyond `types/`.
|
|
5
|
+
*
|
|
6
|
+
* Routes:
|
|
7
|
+
* - `GET /authorize` → SP-initiated AuthnRequest redirect (implemented).
|
|
8
|
+
* - `GET /callback` → ACS verification gauntlet (next increment).
|
|
9
|
+
*
|
|
10
|
+
* The framework's universal callback (`/cb/<methodId>`, both GET and
|
|
11
|
+
* POST) always dispatches the `"GET /callback"` route key, so the ACS
|
|
12
|
+
* lives there rather than at a bespoke `/acs` sub-path.
|
|
13
|
+
*/
|
|
14
|
+
import type { AuthMethod, MethodContext } from "../../types/method"
|
|
15
|
+
|
|
16
|
+
import { consumeAssertion } from "./acs"
|
|
17
|
+
import { buildAuthnRequestRedirect } from "./authnrequest"
|
|
18
|
+
import { buildSpMetadata } from "./metadata"
|
|
19
|
+
import { consumeSls } from "./sls"
|
|
20
|
+
import { initiateSpLogout } from "./slo-initiate"
|
|
21
|
+
import type { SamlSpConfig, SamlSpProperties, SamlSpState } from "./types"
|
|
22
|
+
|
|
23
|
+
export function buildSamlSpMethod(
|
|
24
|
+
id: string,
|
|
25
|
+
kind: string,
|
|
26
|
+
config: SamlSpConfig,
|
|
27
|
+
): AuthMethod<SamlSpProperties, SamlSpState> {
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
kind,
|
|
31
|
+
type: "custom",
|
|
32
|
+
routes: {
|
|
33
|
+
"GET /authorize": (ctx: MethodContext<SamlSpState>) =>
|
|
34
|
+
buildAuthnRequestRedirect(ctx, id, config),
|
|
35
|
+
"GET /callback": (ctx: MethodContext<SamlSpState>) =>
|
|
36
|
+
consumeAssertion(ctx, id, config),
|
|
37
|
+
"GET /metadata": (ctx: MethodContext<SamlSpState>) =>
|
|
38
|
+
buildSpMetadata(ctx, id, config),
|
|
39
|
+
// Front-channel Single Logout. `/sls` receives IdP LogoutRequest
|
|
40
|
+
// / LogoutResponse (both bindings; verified by XML-DSig, not a
|
|
41
|
+
// flow cookie — see sls.ts). `POST /logout` is the host-driven
|
|
42
|
+
// SP-initiated send (see slo-initiate.ts).
|
|
43
|
+
"GET /sls": (ctx: MethodContext<SamlSpState>) =>
|
|
44
|
+
consumeSls(ctx, id, config),
|
|
45
|
+
"POST /sls": (ctx: MethodContext<SamlSpState>) =>
|
|
46
|
+
consumeSls(ctx, id, config),
|
|
47
|
+
"POST /logout": (ctx: MethodContext<SamlSpState>) =>
|
|
48
|
+
initiateSpLogout(ctx, id, config),
|
|
49
|
+
},
|
|
50
|
+
// Anonymous, no flow cookie. `/metadata` always; the SLO routes
|
|
51
|
+
// (`/sls` receive + `/logout` send) only when an IdP SLO endpoint
|
|
52
|
+
// is configured — advertising/serving an SLO surface we cannot
|
|
53
|
+
// complete a round-trip on would break interop (same conservative
|
|
54
|
+
// gating as `unsolicitedCallback` ⇐ `idpInitiated`).
|
|
55
|
+
publicRoutes: [
|
|
56
|
+
"GET /metadata",
|
|
57
|
+
...(config.idp.sloUrl
|
|
58
|
+
? (["GET /sls", "POST /sls", "POST /logout"] as const)
|
|
59
|
+
: ([] as const)),
|
|
60
|
+
],
|
|
61
|
+
// IdP-initiated SSO: only when this instance is configured for it.
|
|
62
|
+
// Absent ⇒ unsolicited Responses stay invalid_request.
|
|
63
|
+
unsolicitedCallback: config.idpInitiated !== undefined,
|
|
64
|
+
}
|
|
65
|
+
}
|