@_mustachio/openauth 0.11.0 → 0.13.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 +23 -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 +27 -27
- package/dist/esm/domain/authorize.js +2 -2
- package/dist/esm/domain/callback.js +121 -7
- package/dist/esm/domain/client-auth.js +2 -2
- package/dist/esm/domain/crypto.js +14 -14
- package/dist/esm/domain/discovery.js +2 -2
- package/dist/esm/domain/dpop.js +4 -4
- package/dist/esm/domain/id-token.js +5 -5
- package/dist/esm/domain/jwt.js +4 -4
- package/dist/esm/domain/method-dispatch.js +27 -1
- package/dist/esm/domain/method-route.js +80 -1
- package/dist/esm/domain/par.js +2 -2
- package/dist/esm/domain/pkce.js +2 -2
- package/dist/esm/domain/revoke.js +2 -2
- package/dist/esm/domain/scim/discovery.js +206 -0
- package/dist/esm/domain/scim/filter.js +177 -0
- package/dist/esm/domain/scim/handle.js +338 -0
- package/dist/esm/domain/scim/patch.js +568 -0
- package/dist/esm/domain/scim/resource.js +256 -0
- package/dist/esm/domain/state-envelope.js +2 -2
- package/dist/esm/domain/token.js +5 -5
- 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 +3 -19
- package/dist/esm/http/handlers/callback.js +7 -1
- package/dist/esm/http/handlers/metadata.js +2 -2
- package/dist/esm/http/handlers/method-route.js +47 -2
- package/dist/esm/http/handlers/revocation.js +2 -2
- package/dist/esm/http/handlers/scim.js +59 -0
- package/dist/esm/http/middleware/tenant.js +31 -6
- package/dist/esm/http/router.js +3 -0
- package/dist/esm/http/schemas/authorize.js +2 -2
- package/dist/esm/http/schemas/revocation.js +2 -2
- package/dist/esm/http/schemas/token.js +7 -7
- package/dist/esm/index.js +34 -32
- 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 +232 -0
- package/dist/esm/methods/saml-sp/attributes.js +77 -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 +81 -0
- package/dist/esm/methods/saml-sp/index.js +7 -0
- package/dist/esm/methods/saml-sp/metadata.js +71 -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 +59 -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/ports/scim-directory.js +0 -0
- package/dist/esm/types/error.js +5 -0
- package/dist/esm/types/result.js +3 -3
- package/dist/esm/types/scim.js +0 -0
- 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 +3 -0
- package/dist/types/adapters/memory/session-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/domain/callback.d.ts +16 -1
- package/dist/types/domain/callback.d.ts.map +1 -1
- 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/scim/discovery.d.ts +11 -0
- package/dist/types/domain/scim/discovery.d.ts.map +1 -0
- package/dist/types/domain/scim/filter.d.ts +51 -0
- package/dist/types/domain/scim/filter.d.ts.map +1 -0
- package/dist/types/domain/scim/handle.d.ts +35 -0
- package/dist/types/domain/scim/handle.d.ts.map +1 -0
- package/dist/types/domain/scim/patch.d.ts +73 -0
- package/dist/types/domain/scim/patch.d.ts.map +1 -0
- package/dist/types/domain/scim/resource.d.ts +56 -0
- package/dist/types/domain/scim/resource.d.ts.map +1 -0
- 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/callback.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/http/handlers/scim.d.ts +3 -0
- package/dist/types/http/handlers/scim.d.ts.map +1 -0
- package/dist/types/http/middleware/tenant.d.ts.map +1 -1
- package/dist/types/http/router.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -1
- 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 +45 -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 +920 -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 +50 -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 +93 -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 +299 -0
- package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
- package/dist/types/ports/audit-log.d.ts +18 -5
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/scim-directory.d.ts +104 -0
- package/dist/types/ports/scim-directory.d.ts.map +1 -0
- package/dist/types/ports/session-store.d.ts +23 -0
- package/dist/types/ports/session-store.d.ts.map +1 -1
- package/dist/types/types/error.d.ts +5 -0
- package/dist/types/types/error.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +83 -0
- 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/scim.d.ts +269 -0
- package/dist/types/types/scim.d.ts.map +1 -0
- package/dist/types/types/tenant.d.ts +10 -0
- package/dist/types/types/tenant.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 +39 -0
- package/src/adapters/postgres/migrations.ts +6 -0
- package/src/adapters/postgres/session-store.ts +69 -0
- package/src/domain/callback.ts +215 -7
- package/src/domain/method-dispatch.ts +53 -1
- package/src/domain/method-route.ts +190 -1
- package/src/domain/scim/discovery.ts +262 -0
- package/src/domain/scim/filter.ts +295 -0
- package/src/domain/scim/handle.ts +597 -0
- package/src/domain/scim/patch.ts +807 -0
- package/src/domain/scim/resource.ts +358 -0
- package/src/http/context.ts +9 -0
- package/src/http/cookies.ts +19 -1
- package/src/http/handlers/authorize.ts +2 -22
- package/src/http/handlers/callback.ts +9 -1
- package/src/http/handlers/method-route.ts +72 -2
- package/src/http/handlers/scim.ts +107 -0
- package/src/http/middleware/tenant.ts +47 -1
- package/src/http/router.ts +9 -0
- package/src/index.ts +23 -0
- package/src/methods/saml-sp/acs.ts +449 -0
- package/src/methods/saml-sp/attributes.ts +146 -0
- package/src/methods/saml-sp/authnrequest.ts +129 -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 +139 -0
- package/src/methods/saml-sp/index.ts +34 -0
- package/src/methods/saml-sp/metadata.ts +215 -0
- package/src/methods/saml-sp/method.ts +65 -0
- package/src/methods/saml-sp/parse-idp-metadata.ts +158 -0
- package/src/methods/saml-sp/saml-instance.ts +210 -0
- package/src/methods/saml-sp/slo-initiate.ts +180 -0
- package/src/methods/saml-sp/sls.ts +307 -0
- package/src/methods/saml-sp/types.ts +298 -0
- package/src/ports/CONSISTENCY.md +52 -24
- package/src/ports/audit-log.ts +18 -5
- package/src/ports/scim-directory.ts +166 -0
- package/src/ports/session-store.ts +24 -0
- package/src/types/error.ts +11 -0
- package/src/types/idp.ts +86 -0
- package/src/types/method.ts +110 -0
- package/src/types/scim.ts +280 -0
- package/src/types/tenant.ts +10 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAML SP — public types.
|
|
3
|
+
*
|
|
4
|
+
* These types are the locked contract for the SAML SP method family and
|
|
5
|
+
* are intentionally introduced ahead of the runtime implementation so
|
|
6
|
+
* downstream callers can import them today, and so the public-API
|
|
7
|
+
* leak guard
|
|
8
|
+
* (`test/types/saml-sp-no-thirdparty-leaks.test.ts`) can verify
|
|
9
|
+
* the shapes before any `@node-saml/*` or `xml-crypto` code lands.
|
|
10
|
+
*
|
|
11
|
+
* No third-party types appear here. `@node-saml/node-saml`'s `Profile`
|
|
12
|
+
* is mapped onto `SamlSpProperties` at the wrapper boundary; consumers
|
|
13
|
+
* never see node-saml's surface.
|
|
14
|
+
*
|
|
15
|
+
* See `docs/plans/claude/saml-sp-plan.md` for the architectural
|
|
16
|
+
* decisions (SAML-AD1–AD7) backing this shape.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Standard SAML 2.0 NameID formats we support.
|
|
21
|
+
*
|
|
22
|
+
* - `persistent` — opaque, stable per user/SP pair; preferred.
|
|
23
|
+
* - `transient` — opaque, single-session.
|
|
24
|
+
* - `emailAddress` — RFC 822 email; common with legacy IdPs.
|
|
25
|
+
* - `unspecified` — IdP picks; pass through verbatim.
|
|
26
|
+
*/
|
|
27
|
+
export type SamlNameIdFormat =
|
|
28
|
+
| "persistent"
|
|
29
|
+
| "transient"
|
|
30
|
+
| "emailAddress"
|
|
31
|
+
| "unspecified"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One field's source. Either the assertion's NameID or a named
|
|
35
|
+
* attribute. `format` narrows attribute lookup when an IdP issues
|
|
36
|
+
* multiple attributes with the same `Name` but different `NameFormat`.
|
|
37
|
+
*/
|
|
38
|
+
export type SamlAttributeRef =
|
|
39
|
+
| { source: "nameId" }
|
|
40
|
+
| { source: "attribute"; name: string; format?: string }
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How the SAML method translates a verified assertion into the
|
|
44
|
+
* `providerSubject` + per-claim fields that flow into the host's
|
|
45
|
+
* `success` callback. Mirrors the OIDC method's claim-mapping
|
|
46
|
+
* configuration — the host owns the final `SubjectClaim`; this map
|
|
47
|
+
* just normalises the SAML side.
|
|
48
|
+
*/
|
|
49
|
+
export type SamlAttributeMapping = {
|
|
50
|
+
/** Which attribute becomes `providerSubject`. Defaults to NameID. */
|
|
51
|
+
subject?: SamlAttributeRef
|
|
52
|
+
email?: SamlAttributeRef
|
|
53
|
+
/** SAML assertions are typically issued only after IdP-side verification. */
|
|
54
|
+
emailVerified?: { source: "literal"; value: boolean }
|
|
55
|
+
name?: SamlAttributeRef
|
|
56
|
+
/** Multi-valued — the mapper preserves array shape. */
|
|
57
|
+
groups?: SamlAttributeRef
|
|
58
|
+
/** Pass-through map for additional claims hosts want surfaced. */
|
|
59
|
+
custom?: Record<string, SamlAttributeRef>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A single PEM-encoded IdP signing certificate, optionally bounded by a
|
|
64
|
+
* validity window. The runtime verifier accepts any cert whose window
|
|
65
|
+
* covers `now` — supports overlapping hot rotation without re-deploys.
|
|
66
|
+
*/
|
|
67
|
+
export type SamlIdpSigningCert = {
|
|
68
|
+
pem: string
|
|
69
|
+
/** Inclusive lower bound. Unix ms. Omit for no lower bound. */
|
|
70
|
+
notBefore?: number
|
|
71
|
+
/** Exclusive upper bound. Unix ms. Omit for no upper bound. */
|
|
72
|
+
notAfter?: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Identity-Provider-side configuration the host paste/imports from the
|
|
77
|
+
* IdP's metadata XML (or uploads a metadata URL we fetch once). Stable
|
|
78
|
+
* across deploys — keyed by `(tenantId, methodId)` in `MethodStore`.
|
|
79
|
+
*/
|
|
80
|
+
export type SamlIdpConfig = {
|
|
81
|
+
entityId: string
|
|
82
|
+
/** SSO endpoint URL. HTTP-Redirect binding is the default. */
|
|
83
|
+
ssoUrl: string
|
|
84
|
+
/** Single Logout endpoint URL. Optional; Phase 3 deliverable. */
|
|
85
|
+
sloUrl?: string
|
|
86
|
+
/** Preferred NameID format requested in `AuthnRequest`. */
|
|
87
|
+
nameIdFormat?: SamlNameIdFormat
|
|
88
|
+
/** One or more IdP signing certs. ≥1 required. Hot-rotatable. */
|
|
89
|
+
signingCerts: ReadonlyArray<SamlIdpSigningCert>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* IdP-initiated SSO binding. When set, the ACS endpoint accepts
|
|
94
|
+
* unsolicited SAML Responses (no `InResponseTo`) and synthesizes a
|
|
95
|
+
* flow record using these defaults so the existing
|
|
96
|
+
* `MethodResult.success` path can run end-to-end.
|
|
97
|
+
*
|
|
98
|
+
* Documented in plan SAML-AD7 — this is the one architectural carve-out
|
|
99
|
+
* SAML imposes on the framework. Omit to reject unsolicited Responses
|
|
100
|
+
* with `invalid_request` (the conservative default).
|
|
101
|
+
*/
|
|
102
|
+
export type SamlIdpInitiatedConfig = {
|
|
103
|
+
defaultClientId: string
|
|
104
|
+
defaultRedirectUri: string
|
|
105
|
+
defaultScopes?: string[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Tenant-supplied configuration for a SAML SP method instance.
|
|
110
|
+
* Validated by `samlSpFactory.configSchema` (Standard Schema v1).
|
|
111
|
+
*/
|
|
112
|
+
export type SamlSpConfig = {
|
|
113
|
+
idp: SamlIdpConfig
|
|
114
|
+
attributeMapping: SamlAttributeMapping
|
|
115
|
+
/** Whether to sign outbound `AuthnRequest`. Default `false`. */
|
|
116
|
+
signAuthnRequest?: boolean
|
|
117
|
+
/**
|
|
118
|
+
* Per-connection SP signing keypair (SAML-AD: O3). **Decoupled from
|
|
119
|
+
* the OIDC `KeyStore` on purpose** — the SP signing cert is pinned at
|
|
120
|
+
* the IdP and rotated as an IdP-coordination event, not on the OIDC
|
|
121
|
+
* token-key schedule; this also keeps the design KMS-agnostic.
|
|
122
|
+
* `privateKeyPem` signs the `AuthnRequest`; `certPem` is what the IdP
|
|
123
|
+
* pins and what SP metadata advertises. Required iff
|
|
124
|
+
* `signAuthnRequest` is `true`. Treat `privateKeyPem` as a secret:
|
|
125
|
+
* the host should encrypt the `MethodStore` at rest (or supply it via
|
|
126
|
+
* its own resolver) — same handling as any per-tenant credential.
|
|
127
|
+
*/
|
|
128
|
+
signingKey?: { privateKeyPem: string; certPem: string }
|
|
129
|
+
/**
|
|
130
|
+
* Accept `<saml:EncryptedAssertion>` responses. **Off by default** —
|
|
131
|
+
* a connection only opts in when its IdP is configured to encrypt.
|
|
132
|
+
* When `false`, an encrypted assertion is rejected (the SP advertises
|
|
133
|
+
* no decryption cert and node-saml has no `decryptionPvk`). Requires
|
|
134
|
+
* `decryptionKey` when `true` (enforced by `configSchema`).
|
|
135
|
+
*/
|
|
136
|
+
allowEncryptedAssertions?: boolean
|
|
137
|
+
/**
|
|
138
|
+
* Per-connection SP **decryption** keypair — the private half the IdP
|
|
139
|
+
* encrypts assertions to. Same SAML-AD O3 rationale as `signingKey`:
|
|
140
|
+
* decoupled from the OIDC `KeyStore` (the IdP pins the matching cert
|
|
141
|
+
* via SP metadata; rotation is an IdP-coordination event;
|
|
142
|
+
* KMS-agnostic). Required iff `allowEncryptedAssertions` is `true`.
|
|
143
|
+
* Treat `privateKeyPem` as a secret: the host should encrypt the
|
|
144
|
+
* `MethodStore` at rest (or supply it via its own resolver) — same
|
|
145
|
+
* handling as `signingKey.privateKeyPem` and any per-tenant
|
|
146
|
+
* credential. `certPem` is the matching X.509 cert the IdP encrypts
|
|
147
|
+
* to; SP metadata advertises it as a `use="encryption"`
|
|
148
|
+
* `KeyDescriptor` (same advertise-only-what-we-serve invariant as
|
|
149
|
+
* `signingKey`).
|
|
150
|
+
*/
|
|
151
|
+
decryptionKey?: { privateKeyPem: string; certPem: string }
|
|
152
|
+
idpInitiated?: SamlIdpInitiatedConfig
|
|
153
|
+
/** Clock skew allowance for `NotBefore` / `NotOnOrAfter`. Seconds. */
|
|
154
|
+
clockSkewSeconds?: number
|
|
155
|
+
/**
|
|
156
|
+
* Override the derived SP entityID.
|
|
157
|
+
*
|
|
158
|
+
* By default the entityID is derived as
|
|
159
|
+
* `<issuerUrl>/<tenantId>/<methodId>` (SAML-AD5) — stable, no config
|
|
160
|
+
* required, and guaranteed to match what SP metadata publishes. Set
|
|
161
|
+
* this **only** to adopt an entityID that already exists at the IdP,
|
|
162
|
+
* so an existing SAML app can be migrated without the customer
|
|
163
|
+
* editing their production SSO config.
|
|
164
|
+
*
|
|
165
|
+
* The override flows through every consumer at once — `AuthnRequest`
|
|
166
|
+
* issuer, `AudienceRestriction` validation, SP metadata, and logout
|
|
167
|
+
* messages — so the anti-drift invariant holds either way. Changing
|
|
168
|
+
* it on a live connection invalidates the IdP-side trust config;
|
|
169
|
+
* treat it as an IdP-coordination event.
|
|
170
|
+
*/
|
|
171
|
+
spEntityId?: string
|
|
172
|
+
/**
|
|
173
|
+
* Set `ForceAuthn="true"` on the outbound `AuthnRequest`, asking the
|
|
174
|
+
* IdP to re-authenticate the user even if it has a live session.
|
|
175
|
+
* Default `false`.
|
|
176
|
+
*
|
|
177
|
+
* Note that this is a *request*: SAML gives the IdP no obligation to
|
|
178
|
+
* honour it, and there is no way to verify from the Response that it
|
|
179
|
+
* did. Do not treat a successful assertion as proof of fresh
|
|
180
|
+
* authentication.
|
|
181
|
+
*/
|
|
182
|
+
forceAuthn?: boolean
|
|
183
|
+
/**
|
|
184
|
+
* Request specific authentication context classes (e.g. MFA) from
|
|
185
|
+
* the IdP via `<RequestedAuthnContext>`.
|
|
186
|
+
*
|
|
187
|
+
* **Omitted ⇒ no `RequestedAuthnContext` element is sent at all**,
|
|
188
|
+
* which lets the IdP apply its own sign-on policy. That is the right
|
|
189
|
+
* default for nearly every deployment: a `RequestedAuthnContext` the
|
|
190
|
+
* IdP cannot satisfy exactly is answered with `NoAuthnContext`
|
|
191
|
+
* instead of a login, and an MFA policy at the IdP is a common way
|
|
192
|
+
* to *not* satisfy `PasswordProtectedTransport` under
|
|
193
|
+
* `Comparison="exact"`.
|
|
194
|
+
*
|
|
195
|
+
* Set it only when the IdP has told you which class refs it honours.
|
|
196
|
+
* `comparison` maps to the `Comparison` attribute and defaults to
|
|
197
|
+
* `"exact"`; `"minimum"` is usually the safer choice when requesting
|
|
198
|
+
* MFA.
|
|
199
|
+
*
|
|
200
|
+
* Requesting a context does **not** verify one was used — read
|
|
201
|
+
* `SamlSpProperties.authnContextClassRef` for what the IdP actually
|
|
202
|
+
* asserted.
|
|
203
|
+
*/
|
|
204
|
+
requestedAuthnContext?: {
|
|
205
|
+
/** Full URNs, e.g. `urn:oasis:names:tc:SAML:2.0:ac:classes:MultiFactorAuthn`. */
|
|
206
|
+
classRefs: ReadonlyArray<string>
|
|
207
|
+
comparison?: "exact" | "minimum" | "maximum" | "better"
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Require the `<saml:Assertion>` itself to carry a valid XML-DSig.
|
|
211
|
+
* **Defaults to `true` and should stay that way** — the identity,
|
|
212
|
+
* conditions, and audience all live inside the assertion, so signing
|
|
213
|
+
* it is what actually binds them.
|
|
214
|
+
*
|
|
215
|
+
* Set `false` only for an IdP that signs the outer `<Response>` and
|
|
216
|
+
* nothing else, and only together with `requireSignedResponse: true`.
|
|
217
|
+
* The schema refuses to let both be off.
|
|
218
|
+
*/
|
|
219
|
+
requireSignedAssertion?: boolean
|
|
220
|
+
/**
|
|
221
|
+
* Require the outer `<samlp:Response>` to carry a valid XML-DSig.
|
|
222
|
+
* Default `false` — requiring it is stricter than the Okta / Entra
|
|
223
|
+
* default and would reject the majority of real IdPs. Enable it for
|
|
224
|
+
* an IdP that signs the Response, either as defence in depth
|
|
225
|
+
* alongside a signed assertion or (with
|
|
226
|
+
* `requireSignedAssertion: false`) as the only signature on offer.
|
|
227
|
+
*/
|
|
228
|
+
requireSignedResponse?: boolean
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Method-private state stashed in `FlowRecord.methodState` for the
|
|
233
|
+
* duration of an SP-initiated flow.
|
|
234
|
+
*
|
|
235
|
+
* `InResponseTo` correlation is handled out-of-band by node-saml's
|
|
236
|
+
* `CacheProvider` (backed by `methodScratch`), so the outstanding
|
|
237
|
+
* request id does **not** live here — only the framework state
|
|
238
|
+
* envelope echoed as RelayState and the issuance timestamp.
|
|
239
|
+
*/
|
|
240
|
+
export type SamlSpState = {
|
|
241
|
+
relayState: string
|
|
242
|
+
issuedAt: number
|
|
243
|
+
/**
|
|
244
|
+
* SP entityID + ACS URL computed at AuthnRequest time, where the
|
|
245
|
+
* framework's dispatch context is available. The ACS dispatch has
|
|
246
|
+
* no dispatch context, so these are read back from here to validate
|
|
247
|
+
* the assertion's `AudienceRestriction` / `Recipient` against the
|
|
248
|
+
* exact values the IdP saw in the request.
|
|
249
|
+
*/
|
|
250
|
+
spEntityId: string
|
|
251
|
+
acsUrl: string
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Properties handed to the host's `IdPOptions.success` callback on a
|
|
256
|
+
* successful SAML authentication. The host translates these into the
|
|
257
|
+
* final `SubjectClaim` it owns.
|
|
258
|
+
*
|
|
259
|
+
* `raw.responseXml` is provided as an escape hatch for hosts that want
|
|
260
|
+
* to inspect the verified Response themselves; the wrapper has already
|
|
261
|
+
* run the full signature gauntlet by the time these properties leave
|
|
262
|
+
* the method.
|
|
263
|
+
*/
|
|
264
|
+
export type SamlSpProperties = {
|
|
265
|
+
nameId: {
|
|
266
|
+
value: string
|
|
267
|
+
format: SamlNameIdFormat
|
|
268
|
+
}
|
|
269
|
+
attributes: Record<string, string | string[]>
|
|
270
|
+
/** Used for Single Logout correlation (Phase 3). */
|
|
271
|
+
sessionIndex?: string
|
|
272
|
+
/** Unix ms — the assertion's `AuthnInstant`. */
|
|
273
|
+
authnInstant: number
|
|
274
|
+
/**
|
|
275
|
+
* Unix ms — the `AuthnStatement/@SessionNotOnOrAfter` the IdP
|
|
276
|
+
* asserted, when it supplied one. This is the IdP's own view of when
|
|
277
|
+
* its session for this user expires.
|
|
278
|
+
*
|
|
279
|
+
* The library does **not** act on it: token and session lifetimes are
|
|
280
|
+
* host policy, and this library owns no session. Hosts that want
|
|
281
|
+
* "when their IdP session ends, ours ends" should clamp their own
|
|
282
|
+
* session/token TTL to this value in the `success` callback.
|
|
283
|
+
*/
|
|
284
|
+
sessionNotOnOrAfter?: number
|
|
285
|
+
/**
|
|
286
|
+
* The `AuthnContext/AuthnContextClassRef` the IdP actually asserted —
|
|
287
|
+
* i.e. how it says it authenticated the user. Absent when the
|
|
288
|
+
* assertion carries none.
|
|
289
|
+
*
|
|
290
|
+
* This is the value to check for step-up decisions ("was this really
|
|
291
|
+
* MFA?"). `SamlSpConfig.requestedAuthnContext` only *asks*; this is
|
|
292
|
+
* the answer, and the two can differ.
|
|
293
|
+
*/
|
|
294
|
+
authnContextClassRef?: string
|
|
295
|
+
raw: {
|
|
296
|
+
responseXml: string
|
|
297
|
+
}
|
|
298
|
+
}
|
package/src/ports/CONSISTENCY.md
CHANGED
|
@@ -15,30 +15,34 @@ overspecifies adapters that don't need that on the read-eventual paths
|
|
|
15
15
|
|
|
16
16
|
## Per-method consistency contracts
|
|
17
17
|
|
|
18
|
-
| Port
|
|
19
|
-
|
|
|
20
|
-
| `TokenStore`
|
|
21
|
-
| `TokenStore`
|
|
22
|
-
| `TokenStore`
|
|
23
|
-
| `TokenStore`
|
|
24
|
-
| `TokenStore`
|
|
25
|
-
| `TokenStore`
|
|
26
|
-
| `TokenStore`
|
|
27
|
-
| `SessionStore`
|
|
28
|
-
| `SessionStore`
|
|
29
|
-
| `SessionStore`
|
|
30
|
-
| `SessionStore`
|
|
31
|
-
| `SessionStore`
|
|
32
|
-
| `SessionStore`
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `KeyStore`
|
|
36
|
-
| `KeyStore`
|
|
37
|
-
| `
|
|
38
|
-
| `ConfigStore`
|
|
39
|
-
| `
|
|
40
|
-
| `MethodStore`
|
|
41
|
-
| `
|
|
18
|
+
| Port | Method | Required | Why |
|
|
19
|
+
| --------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `TokenStore` | `saveCode(code, payload, ttl)` | **Strong, atomic.** Payload **encrypted at rest** with a key from `KeyStore`; `ttl ≤ 60 s` (framework refuses larger). | Code must be created exactly once and visible to the next `consumeCode` on any node. Payload may carry upstream tokens — see _Code payload confidentiality_ in plan. |
|
|
21
|
+
| `TokenStore` | `consumeCode(code)` | **Strong, CAS.** Returns the **decrypted** payload on success. | Single-use; second consumption must fail deterministically. Race with itself must resolve to one winner. |
|
|
22
|
+
| `TokenStore` | `saveRefresh(refresh, payload)` | **Strong, atomic.** | New refresh tokens issued during rotation must be immediately retrievable on the next request. |
|
|
23
|
+
| `TokenStore` | `consumeRefresh(refresh, options)` | **Strong, CAS with reuse-detection window** (default 60 s). | Core of refresh-token rotation security. Concurrent presentations resolve to one winner; reuse within the window triggers the revoke chain. |
|
|
24
|
+
| `TokenStore` | `peekRefresh(refresh)` | **Eventual acceptable.** Non-destructive read; must never mark the token consumed. | Used by `/revoke` for token-to-client binding (RFC 7009 §2.2) and by the refresh-grant for client auth + audit fields. Race losers fall through to `consumeRefresh`, which is the strong gate, so a stale peek can only delay — not bypass — single-use enforcement. |
|
|
25
|
+
| `TokenStore` | `revokeFamily(family)` | Strong preferred; eventual acceptable with documented SLA. | Reuse-detection must invalidate the whole chain. Brief lag is tolerable. |
|
|
26
|
+
| `TokenStore` | `revokeBySubject(tenant, subject)` | Strong preferred; eventual acceptable with documented SLA. | Revocation must propagate quickly. A few seconds of lag is tolerable. |
|
|
27
|
+
| `SessionStore` | `saveFlow(flowId, payload, ttl)` | **Strong, atomic.** `ttl == expiresAt - createdAt`. | Flow record must be visible on the callback request to `consumeFlow`. |
|
|
28
|
+
| `SessionStore` | `updateFlowMethodState(flowId, state)` | **Strong, atomic.** Resolves **before** the user-agent redirect is sent. | Upstream PKCE verifier / nonce must be durably persisted before the redirect, otherwise the callback cannot validate. |
|
|
29
|
+
| `SessionStore` | `consumeFlow(flowId)` | **Strong, atomic delete-on-read** that **returns the full `FlowRecord`** (`CAS` or `DELETE … RETURNING`). | Single-use; concurrent consumption resolves to one winner. The record is returned so the framework can snapshot fields into the auth-code payload before disposal. |
|
|
30
|
+
| `SessionStore` | `savePar(uri, payload, ttl)` (optional) | **Strong, atomic.** `ttl` default 60 s. | RFC 9126 §3 — the PAR record must be visible to the next `consumePar` on any node. One-shot read. |
|
|
31
|
+
| `SessionStore` | `consumePar(uri)` (optional) | **Strong, atomic delete-on-read.** | RFC 9126 §4 — `request_uri` is single-use; concurrent presentations of the same uri resolve to one winner. Same semantics as `consumeFlow`. |
|
|
32
|
+
| `SessionStore` | `createSession / readSession / revokeSession` (optional long-lived) | **Strong.** | Session creation must be immediately readable on the next request. |
|
|
33
|
+
| `SessionStore` | `saveScratch / readScratch / deleteScratch` (optional) | **Strong, TTL-respecting.** Implement the trio together; partial implementations are not supported. | Backs `MethodContext.methodScratch` for methods that need cross-flow per-instance state (e.g. SAML SP assertion-ID replay protection). Keys arrive already namespaced by `(tenantId, methodId)` — the adapter stores opaque key/value pairs. |
|
|
34
|
+
| `TokenStore` | `recordDpopJti(jti, ttlMs)` (optional) | **Strong, atomic record-or-fail.** | RFC 9449 §11.1 — replay protection requires single-use enforcement on the jti within the TTL window. A re-presentation must return `invalid_grant` deterministically. Adapters without this method cannot satisfy DPoP and the verifier surfaces `invalid_dpop_proof`. |
|
|
35
|
+
| `KeyStore` | `currentSigningKey()` / `currentEncryptionKey()` | Strong. | Active key must be unambiguous. |
|
|
36
|
+
| `KeyStore` | `signingKeys()` (JWKS) | Eventual OK (with TTL). | Verifiers tolerate brief JWKS lag during rotation. |
|
|
37
|
+
| `KeyStore` | `getEncryptionKey(kid)` | Strong. | Required to decrypt code payloads encrypted under non-current keys during the overlap window. |
|
|
38
|
+
| `ConfigStore` | `getTenantConfig(id)` | **Eventual + bounded staleness (TTL ≤ 60 s).** Invalidation hook fires on update. | Read-heavy, write-rare. Aggressive caching is desirable. |
|
|
39
|
+
| `ConfigStore` | `putTenantConfig(config)` | **Strong write + immediate invalidation** of cached entries for the affected `TenantId`. | Subsequent `getTenantConfig` must reflect the new value once the write resolves. |
|
|
40
|
+
| `MethodStore` | `getMethodConfig` / `listMethods` | Same as `ConfigStore.getTenantConfig`. | Subset of tenant config. |
|
|
41
|
+
| `MethodStore` | `putMethodConfig` / `deleteMethodConfig` | Strong write + immediate invalidation. | Console-driven mutations must take effect on the next request. |
|
|
42
|
+
| `AuditLog` | `log(event)` | Append-only, durable. Cross-instance ordering **not** required (consumers sort by `timestamp` + `actor`). | Loss = audit gap; ordering is a UI / SIEM concern. |
|
|
43
|
+
| `ScimDirectory` | `createUser` / `replaceUser` / `patchUser` / `deleteUser` | **Strong, read-your-writes.** | A SCIM client issues `GET /Users?filter=userName eq "…"` immediately after a create to confirm it; an eventually-consistent read there makes the client create the user a second time. Duplicate users are the classic SCIM failure and are painful to unpick. |
|
|
44
|
+
| `ScimDirectory` | `getUser` / `findUsers` | **Read-your-writes for the tenant's own recent writes**; otherwise eventual is acceptable. | Same reason. Cross-tenant staleness is harmless — SCIM reads are always tenant-scoped. |
|
|
45
|
+
| `ScimDirectory` | uniqueness of `userName` | **Host-enforced.** Return `authError.conflict(...)` on collision. | The library stores no user rows, so it cannot enforce uniqueness. A host that checks-then-writes without a constraint will race under Okta's initial import, which is heavily concurrent. |
|
|
42
46
|
|
|
43
47
|
## Implications for adapter choice
|
|
44
48
|
|
|
@@ -64,6 +68,12 @@ demonstrate, under simulated replication lag:
|
|
|
64
68
|
2. `consumeRefresh` CAS resolves to exactly one winner under concurrent
|
|
65
69
|
attempts on the same token.
|
|
66
70
|
3. `revokeBySubject` propagates within the documented SLA.
|
|
71
|
+
4. `readScratch` immediately after the matching `saveScratch` returns the
|
|
72
|
+
value. Scratch backs SAML SP InResponseTo replay protection: the
|
|
73
|
+
AuthnRequest is correlated at the ACS by a scratch read that must see
|
|
74
|
+
the write made when the request was issued. The D1 adapter pins all
|
|
75
|
+
three scratch operations to the primary via the Sessions API
|
|
76
|
+
(`primarySession`), the same path `consumeFlow` uses.
|
|
67
77
|
|
|
68
78
|
Read-eventual paths (`ConfigStore.getTenantConfig`, JWKS) may use replicas
|
|
69
79
|
freely.
|
|
@@ -93,6 +103,11 @@ The fixture set covers, at minimum:
|
|
|
93
103
|
one-shot consume atomicity, expiry enforcement, ttl-0 rejection.
|
|
94
104
|
- **DPoP jti (`recordDpopJti`)** — first record succeeds, replay within
|
|
95
105
|
TTL fails with `invalid_grant`, post-TTL the slot is freed for reuse.
|
|
106
|
+
- **Method scratch (`supportsScratch: true`)** — `saveScratch` /
|
|
107
|
+
`readScratch` / `deleteScratch` round-trip, overwrite semantics on
|
|
108
|
+
same key, expiry enforcement, idempotent `deleteScratch`, ttl-0
|
|
109
|
+
rejection. Keys are opaque to the adapter (the framework scopes them
|
|
110
|
+
upstream); a single test confirms that distinct keys are isolated.
|
|
96
111
|
|
|
97
112
|
## Optional methods and graceful degradation
|
|
98
113
|
|
|
@@ -110,6 +125,19 @@ caught up yet:
|
|
|
110
125
|
does not support DPoP replay protection"`. Memory adapter
|
|
111
126
|
implements; production adapters add it the same way `consumeRefresh`
|
|
112
127
|
uses an atomic write-once primitive.
|
|
128
|
+
- `SessionStore.saveScratch / readScratch / deleteScratch` — required
|
|
129
|
+
for methods that hold cross-flow per-instance state (SAML SP replay
|
|
130
|
+
protection is the first user). Without them,
|
|
131
|
+
`MethodContext.methodScratch.put/get/delete` returns an
|
|
132
|
+
`internal_error` whose description names the missing operation.
|
|
133
|
+
Memory **and all four production adapters** (Postgres, D1, DynamoDB,
|
|
134
|
+
Durable Object) implement the trio as a TTL-respecting key/value
|
|
135
|
+
store keyed by an opaque string; each is opted into the
|
|
136
|
+
`supportsScratch` conformance cases. Semantics are upsert /
|
|
137
|
+
TTL-filtered read / idempotent delete — there is no atomic
|
|
138
|
+
delete-on-read (unlike `consumeFlow`). On DynamoDB the row also
|
|
139
|
+
carries a native `ttl` attribute for backstop eviction, but reads
|
|
140
|
+
still filter on the adapter clock because native TTL is best-effort.
|
|
113
141
|
|
|
114
142
|
The framework never advertises a feature in discovery that the wired
|
|
115
143
|
adapters cannot actually serve: `pushed_authorization_request_endpoint`
|
package/src/ports/audit-log.ts
CHANGED
|
@@ -93,16 +93,29 @@ export type AuditEvent =
|
|
|
93
93
|
}
|
|
94
94
|
| {
|
|
95
95
|
/**
|
|
96
|
-
* Emitted
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
96
|
+
* Emitted after a logout is processed. Two channels, discriminated
|
|
97
|
+
* by `via`:
|
|
98
|
+
*
|
|
99
|
+
* - `rp_initiated` (or absent — the default): `/end_session`
|
|
100
|
+
* (OIDC RP-Initiated Logout 1.0 §2), regardless of whether a
|
|
101
|
+
* `post_logout_redirect_uri` was supplied. `subjectId` present
|
|
102
|
+
* when an `id_token_hint` verified; absent otherwise.
|
|
103
|
+
* - `upstream_slo`: an upstream IdP notified us a federated
|
|
104
|
+
* session ended (SAML front-channel Single Logout). `methodId`
|
|
105
|
+
* / `methodKind` identify the federation connection;
|
|
106
|
+
* `subjectId` present only when the host's `onLogout` returned
|
|
107
|
+
* a subject to revoke.
|
|
108
|
+
*
|
|
109
|
+
* `via` is general (OIDC back-channel logout would reuse
|
|
110
|
+
* `upstream_slo`), not SAML-specific surface.
|
|
101
111
|
*/
|
|
102
112
|
kind: "session_logout"
|
|
103
113
|
tenantId: TenantId
|
|
104
114
|
clientId?: string
|
|
105
115
|
subjectId?: string
|
|
116
|
+
via?: "rp_initiated" | "upstream_slo"
|
|
117
|
+
methodId?: string
|
|
118
|
+
methodKind?: string
|
|
106
119
|
}
|
|
107
120
|
| {
|
|
108
121
|
/**
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ScimDirectory` — the host's user directory, as SCIM needs to see it.
|
|
3
|
+
*
|
|
4
|
+
* This is the boundary from `SCIM-AD2`: the library owns the SCIM
|
|
5
|
+
* protocol (routing, bearer auth, schema validation, PATCH
|
|
6
|
+
* normalization, error envelope, pagination, discovery docs) and the
|
|
7
|
+
* host owns every byte of persistence. The library stores no user data.
|
|
8
|
+
*
|
|
9
|
+
* That split is not a compromise — it is the same protocol-over-
|
|
10
|
+
* host-owned-state shape as `ConfigStore`, `MethodStore` and the
|
|
11
|
+
* `success` callback. The host's Users table is the host's.
|
|
12
|
+
*
|
|
13
|
+
* **Consistency:** read-your-writes is required. A SCIM client
|
|
14
|
+
* (notably Okta) will `GET /Users?filter=userName eq "…"` immediately
|
|
15
|
+
* after a `POST /Users` to confirm the create; an eventually-consistent
|
|
16
|
+
* read there produces duplicate users. See `ports/CONSISTENCY.md`.
|
|
17
|
+
*
|
|
18
|
+
* **Errors.** SCIM clients retry `5xx` and give up on `4xx`, so which
|
|
19
|
+
* error you return decides whether a failure is surfaced to an admin or
|
|
20
|
+
* retried forever:
|
|
21
|
+
*
|
|
22
|
+
* - `authError.conflict(...)` → `409 uniqueness`. A collision only you
|
|
23
|
+
* can detect, because only you store the rows.
|
|
24
|
+
* - `authError.invalidRequest(...)` → `400 invalidValue`. A
|
|
25
|
+
* **permanent** rejection — you understood the request and will
|
|
26
|
+
* never accept it. Use it when `addMembers` names a user you do not
|
|
27
|
+
* have (an IdP's group push can reference a member its user push
|
|
28
|
+
* filtered out, or one deleted between operations), or when the data
|
|
29
|
+
* violates a constraint you cannot satisfy.
|
|
30
|
+
* - anything else → `500`, which the client retries. Correct for a
|
|
31
|
+
* transient fault, and far better than reporting a success you did
|
|
32
|
+
* not perform — but returning it for a permanent problem means the
|
|
33
|
+
* IdP retries the same doomed request indefinitely.
|
|
34
|
+
*
|
|
35
|
+
* Your message is passed through on both `4xx` paths; it is what an IdP
|
|
36
|
+
* admin reads in the provisioning log, so make it specific.
|
|
37
|
+
*/
|
|
38
|
+
import type { Result } from "../types/result"
|
|
39
|
+
import type {
|
|
40
|
+
ScimGroupPatch,
|
|
41
|
+
ScimGroupQuery,
|
|
42
|
+
ScimGroupRecord,
|
|
43
|
+
ScimGroupWrite,
|
|
44
|
+
ScimPage,
|
|
45
|
+
ScimUserPatch,
|
|
46
|
+
ScimUserQuery,
|
|
47
|
+
ScimUserRecord,
|
|
48
|
+
ScimUserWrite,
|
|
49
|
+
} from "../types/scim"
|
|
50
|
+
import type { TenantId } from "../types/tenant"
|
|
51
|
+
|
|
52
|
+
export type ScimDirectory = {
|
|
53
|
+
/** `null` (not an error) when no user has that id in this tenant. */
|
|
54
|
+
getUser(
|
|
55
|
+
tenantId: TenantId,
|
|
56
|
+
id: string,
|
|
57
|
+
): Promise<Result<ScimUserRecord | null>>
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Query users. `query.startIndex` is 1-based and `query.count` is
|
|
61
|
+
* already clamped; `query.totalResults` in the response must be the
|
|
62
|
+
* full match count, not the page length.
|
|
63
|
+
*/
|
|
64
|
+
findUsers(
|
|
65
|
+
tenantId: TenantId,
|
|
66
|
+
query: ScimUserQuery,
|
|
67
|
+
): Promise<Result<ScimPage<ScimUserRecord>>>
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a user. Return `conflict` if `userName` (or another
|
|
71
|
+
* uniqueness constraint of yours) is already taken — the library
|
|
72
|
+
* cannot check that for you.
|
|
73
|
+
*/
|
|
74
|
+
createUser(
|
|
75
|
+
tenantId: TenantId,
|
|
76
|
+
user: ScimUserWrite,
|
|
77
|
+
): Promise<Result<ScimUserRecord>>
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Full replace (`PUT`). Attributes absent from `user` are cleared, per
|
|
81
|
+
* RFC 7644 §3.5.1 — this is a replace, not a merge. Use `patchUser`
|
|
82
|
+
* for partial updates.
|
|
83
|
+
*/
|
|
84
|
+
replaceUser(
|
|
85
|
+
tenantId: TenantId,
|
|
86
|
+
id: string,
|
|
87
|
+
user: ScimUserWrite,
|
|
88
|
+
): Promise<Result<ScimUserRecord>>
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Apply a normalized delta. Present key ⇒ set; absent ⇒ leave alone;
|
|
92
|
+
* `null` ⇒ clear. The library has already resolved SCIM path
|
|
93
|
+
* expressions, so no parsing is needed here.
|
|
94
|
+
*
|
|
95
|
+
* `{ active: false }` is the ordinary deprovisioning signal and is
|
|
96
|
+
* usually the single most important operation to implement correctly.
|
|
97
|
+
*/
|
|
98
|
+
patchUser(
|
|
99
|
+
tenantId: TenantId,
|
|
100
|
+
id: string,
|
|
101
|
+
patch: ScimUserPatch,
|
|
102
|
+
): Promise<Result<ScimUserRecord>>
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Hard delete (`DELETE`). Distinct from deactivation on purpose
|
|
106
|
+
* (`SCIM-AD8`): the library will not quietly turn a destructive
|
|
107
|
+
* request into a soft one. Hosts that do not want cascading deletes
|
|
108
|
+
* should implement this as a tombstone and say so in their runbook —
|
|
109
|
+
* but that decision is theirs to make explicitly, not the library's to
|
|
110
|
+
* make silently.
|
|
111
|
+
*/
|
|
112
|
+
deleteUser(tenantId: TenantId, id: string): Promise<Result<void>>
|
|
113
|
+
|
|
114
|
+
// ─── Groups ───
|
|
115
|
+
//
|
|
116
|
+
// Optional as a set: a host that only needs user provisioning can omit
|
|
117
|
+
// all six, and `/scim/v2/Groups` then answers 501 instead of failing
|
|
118
|
+
// at runtime. Implement all of them or none — a half-implemented
|
|
119
|
+
// Groups surface fails an IdP's group push in confusing ways.
|
|
120
|
+
|
|
121
|
+
getGroup?(
|
|
122
|
+
tenantId: TenantId,
|
|
123
|
+
id: string,
|
|
124
|
+
): Promise<Result<ScimGroupRecord | null>>
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `query.excludeMembers` is set when the client asked for
|
|
128
|
+
* `excludedAttributes=members`. Honour it by not loading membership —
|
|
129
|
+
* Okta sets it while enumerating groups, and ignoring it turns a cheap
|
|
130
|
+
* listing into a fan-out read per group.
|
|
131
|
+
*/
|
|
132
|
+
findGroups?(
|
|
133
|
+
tenantId: TenantId,
|
|
134
|
+
query: ScimGroupQuery,
|
|
135
|
+
): Promise<Result<ScimPage<ScimGroupRecord>>>
|
|
136
|
+
|
|
137
|
+
createGroup?(
|
|
138
|
+
tenantId: TenantId,
|
|
139
|
+
group: ScimGroupWrite,
|
|
140
|
+
): Promise<Result<ScimGroupRecord>>
|
|
141
|
+
|
|
142
|
+
/** Full replace, including membership. */
|
|
143
|
+
replaceGroup?(
|
|
144
|
+
tenantId: TenantId,
|
|
145
|
+
id: string,
|
|
146
|
+
group: ScimGroupWrite,
|
|
147
|
+
): Promise<Result<ScimGroupRecord>>
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Apply a normalized group delta.
|
|
151
|
+
*
|
|
152
|
+
* Exactly one membership shape arrives at a time: `members` (replace
|
|
153
|
+
* everything) or `addMembers` / `removeMembers` (incremental). The
|
|
154
|
+
* incremental form exists so a 20,000-member group does not have to be
|
|
155
|
+
* read and rewritten to add one person — see `SCIM-AD9`. Adding an
|
|
156
|
+
* existing member or removing an absent one must succeed quietly;
|
|
157
|
+
* IdPs retry and expect idempotence.
|
|
158
|
+
*/
|
|
159
|
+
patchGroup?(
|
|
160
|
+
tenantId: TenantId,
|
|
161
|
+
id: string,
|
|
162
|
+
patch: ScimGroupPatch,
|
|
163
|
+
): Promise<Result<ScimGroupRecord>>
|
|
164
|
+
|
|
165
|
+
deleteGroup?(tenantId: TenantId, id: string): Promise<Result<void>>
|
|
166
|
+
}
|