@_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.
Files changed (208) hide show
  1. package/dist/esm/adapters/d1/index.js +18 -9
  2. package/dist/esm/adapters/d1/migrations.js +7 -2
  3. package/dist/esm/adapters/d1/session-store.js +39 -0
  4. package/dist/esm/adapters/d1/session.js +2 -2
  5. package/dist/esm/adapters/durable-object/index.js +4 -0
  6. package/dist/esm/adapters/durable-object/session-store.js +42 -0
  7. package/dist/esm/adapters/dynamo/index.js +24 -7
  8. package/dist/esm/adapters/dynamo/session-store.js +49 -0
  9. package/dist/esm/adapters/kms/index.js +6 -2
  10. package/dist/esm/adapters/kv/index.js +6 -2
  11. package/dist/esm/adapters/memory/index.js +24 -7
  12. package/dist/esm/adapters/memory/session-store.js +23 -0
  13. package/dist/esm/adapters/postgres/executor.js +2 -2
  14. package/dist/esm/adapters/postgres/index.js +33 -10
  15. package/dist/esm/adapters/postgres/migrations.js +8 -2
  16. package/dist/esm/adapters/postgres/session-store.js +42 -0
  17. package/dist/esm/client.js +27 -27
  18. package/dist/esm/domain/authorize.js +2 -2
  19. package/dist/esm/domain/callback.js +121 -7
  20. package/dist/esm/domain/client-auth.js +2 -2
  21. package/dist/esm/domain/crypto.js +14 -14
  22. package/dist/esm/domain/discovery.js +2 -2
  23. package/dist/esm/domain/dpop.js +4 -4
  24. package/dist/esm/domain/id-token.js +5 -5
  25. package/dist/esm/domain/jwt.js +4 -4
  26. package/dist/esm/domain/method-dispatch.js +27 -1
  27. package/dist/esm/domain/method-route.js +80 -1
  28. package/dist/esm/domain/par.js +2 -2
  29. package/dist/esm/domain/pkce.js +2 -2
  30. package/dist/esm/domain/revoke.js +2 -2
  31. package/dist/esm/domain/scim/discovery.js +206 -0
  32. package/dist/esm/domain/scim/filter.js +177 -0
  33. package/dist/esm/domain/scim/handle.js +338 -0
  34. package/dist/esm/domain/scim/patch.js +568 -0
  35. package/dist/esm/domain/scim/resource.js +256 -0
  36. package/dist/esm/domain/state-envelope.js +2 -2
  37. package/dist/esm/domain/token.js +5 -5
  38. package/dist/esm/error.js +8 -8
  39. package/dist/esm/http/cookies.js +19 -2
  40. package/dist/esm/http/errors.js +4 -4
  41. package/dist/esm/http/handlers/authorize.js +3 -19
  42. package/dist/esm/http/handlers/callback.js +7 -1
  43. package/dist/esm/http/handlers/metadata.js +2 -2
  44. package/dist/esm/http/handlers/method-route.js +47 -2
  45. package/dist/esm/http/handlers/revocation.js +2 -2
  46. package/dist/esm/http/handlers/scim.js +59 -0
  47. package/dist/esm/http/middleware/tenant.js +31 -6
  48. package/dist/esm/http/router.js +3 -0
  49. package/dist/esm/http/schemas/authorize.js +2 -2
  50. package/dist/esm/http/schemas/revocation.js +2 -2
  51. package/dist/esm/http/schemas/token.js +7 -7
  52. package/dist/esm/index.js +34 -32
  53. package/dist/esm/methods/oauth2-factory.js +2 -2
  54. package/dist/esm/methods/password-hash.js +2 -2
  55. package/dist/esm/methods/providers/index.js +30 -14
  56. package/dist/esm/methods/saml-sp/acs.js +232 -0
  57. package/dist/esm/methods/saml-sp/attributes.js +77 -0
  58. package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
  59. package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
  60. package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
  61. package/dist/esm/methods/saml-sp/factory.js +81 -0
  62. package/dist/esm/methods/saml-sp/index.js +7 -0
  63. package/dist/esm/methods/saml-sp/metadata.js +71 -0
  64. package/dist/esm/methods/saml-sp/method.js +29 -0
  65. package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
  66. package/dist/esm/methods/saml-sp/saml-instance.js +59 -0
  67. package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
  68. package/dist/esm/methods/saml-sp/sls.js +154 -0
  69. package/dist/esm/methods/saml-sp/types.js +0 -0
  70. package/dist/esm/ports/scim-directory.js +0 -0
  71. package/dist/esm/types/error.js +5 -0
  72. package/dist/esm/types/result.js +3 -3
  73. package/dist/esm/types/scim.js +0 -0
  74. package/dist/esm/ui/forms.js +3 -3
  75. package/dist/esm/ui/index.js +8 -5
  76. package/dist/esm/ui/picker.js +2 -2
  77. package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
  78. package/dist/types/adapters/d1/session-store.d.ts +3 -0
  79. package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
  80. package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
  81. package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
  82. package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
  83. package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
  84. package/dist/types/adapters/memory/session-store.d.ts +3 -0
  85. package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
  86. package/dist/types/adapters/postgres/migrations.d.ts +1 -1
  87. package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
  88. package/dist/types/adapters/postgres/session-store.d.ts +3 -0
  89. package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
  90. package/dist/types/domain/callback.d.ts +16 -1
  91. package/dist/types/domain/callback.d.ts.map +1 -1
  92. package/dist/types/domain/method-dispatch.d.ts.map +1 -1
  93. package/dist/types/domain/method-route.d.ts +57 -1
  94. package/dist/types/domain/method-route.d.ts.map +1 -1
  95. package/dist/types/domain/scim/discovery.d.ts +11 -0
  96. package/dist/types/domain/scim/discovery.d.ts.map +1 -0
  97. package/dist/types/domain/scim/filter.d.ts +51 -0
  98. package/dist/types/domain/scim/filter.d.ts.map +1 -0
  99. package/dist/types/domain/scim/handle.d.ts +35 -0
  100. package/dist/types/domain/scim/handle.d.ts.map +1 -0
  101. package/dist/types/domain/scim/patch.d.ts +73 -0
  102. package/dist/types/domain/scim/patch.d.ts.map +1 -0
  103. package/dist/types/domain/scim/resource.d.ts +56 -0
  104. package/dist/types/domain/scim/resource.d.ts.map +1 -0
  105. package/dist/types/http/context.d.ts +6 -1
  106. package/dist/types/http/context.d.ts.map +1 -1
  107. package/dist/types/http/cookies.d.ts +9 -1
  108. package/dist/types/http/cookies.d.ts.map +1 -1
  109. package/dist/types/http/handlers/callback.d.ts.map +1 -1
  110. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  111. package/dist/types/http/handlers/scim.d.ts +3 -0
  112. package/dist/types/http/handlers/scim.d.ts.map +1 -0
  113. package/dist/types/http/middleware/tenant.d.ts.map +1 -1
  114. package/dist/types/http/router.d.ts.map +1 -1
  115. package/dist/types/index.d.ts +3 -1
  116. package/dist/types/index.d.ts.map +1 -1
  117. package/dist/types/methods/saml-sp/acs.d.ts +4 -0
  118. package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
  119. package/dist/types/methods/saml-sp/attributes.d.ts +45 -0
  120. package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
  121. package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
  122. package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
  123. package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
  124. package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
  125. package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
  126. package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
  127. package/dist/types/methods/saml-sp/factory.d.ts +920 -0
  128. package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
  129. package/dist/types/methods/saml-sp/index.d.ts +24 -0
  130. package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
  131. package/dist/types/methods/saml-sp/metadata.d.ts +50 -0
  132. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
  133. package/dist/types/methods/saml-sp/method.d.ts +17 -0
  134. package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
  135. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
  136. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
  137. package/dist/types/methods/saml-sp/saml-instance.d.ts +93 -0
  138. package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
  139. package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
  140. package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
  141. package/dist/types/methods/saml-sp/sls.d.ts +4 -0
  142. package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
  143. package/dist/types/methods/saml-sp/types.d.ts +299 -0
  144. package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
  145. package/dist/types/ports/audit-log.d.ts +18 -5
  146. package/dist/types/ports/audit-log.d.ts.map +1 -1
  147. package/dist/types/ports/scim-directory.d.ts +104 -0
  148. package/dist/types/ports/scim-directory.d.ts.map +1 -0
  149. package/dist/types/ports/session-store.d.ts +23 -0
  150. package/dist/types/ports/session-store.d.ts.map +1 -1
  151. package/dist/types/types/error.d.ts +5 -0
  152. package/dist/types/types/error.d.ts.map +1 -1
  153. package/dist/types/types/idp.d.ts +83 -0
  154. package/dist/types/types/idp.d.ts.map +1 -1
  155. package/dist/types/types/method.d.ts +109 -0
  156. package/dist/types/types/method.d.ts.map +1 -1
  157. package/dist/types/types/scim.d.ts +269 -0
  158. package/dist/types/types/scim.d.ts.map +1 -0
  159. package/dist/types/types/tenant.d.ts +10 -0
  160. package/dist/types/types/tenant.d.ts.map +1 -1
  161. package/package.json +9 -1
  162. package/src/adapters/d1/migrations.ts +5 -0
  163. package/src/adapters/d1/session-store.ts +68 -0
  164. package/src/adapters/durable-object/session-store.ts +61 -0
  165. package/src/adapters/dynamo/session-store.ts +63 -0
  166. package/src/adapters/memory/session-store.ts +39 -0
  167. package/src/adapters/postgres/migrations.ts +6 -0
  168. package/src/adapters/postgres/session-store.ts +69 -0
  169. package/src/domain/callback.ts +215 -7
  170. package/src/domain/method-dispatch.ts +53 -1
  171. package/src/domain/method-route.ts +190 -1
  172. package/src/domain/scim/discovery.ts +262 -0
  173. package/src/domain/scim/filter.ts +295 -0
  174. package/src/domain/scim/handle.ts +597 -0
  175. package/src/domain/scim/patch.ts +807 -0
  176. package/src/domain/scim/resource.ts +358 -0
  177. package/src/http/context.ts +9 -0
  178. package/src/http/cookies.ts +19 -1
  179. package/src/http/handlers/authorize.ts +2 -22
  180. package/src/http/handlers/callback.ts +9 -1
  181. package/src/http/handlers/method-route.ts +72 -2
  182. package/src/http/handlers/scim.ts +107 -0
  183. package/src/http/middleware/tenant.ts +47 -1
  184. package/src/http/router.ts +9 -0
  185. package/src/index.ts +23 -0
  186. package/src/methods/saml-sp/acs.ts +449 -0
  187. package/src/methods/saml-sp/attributes.ts +146 -0
  188. package/src/methods/saml-sp/authnrequest.ts +129 -0
  189. package/src/methods/saml-sp/cache-provider.ts +76 -0
  190. package/src/methods/saml-sp/cert-rotation.ts +34 -0
  191. package/src/methods/saml-sp/factory.ts +139 -0
  192. package/src/methods/saml-sp/index.ts +34 -0
  193. package/src/methods/saml-sp/metadata.ts +215 -0
  194. package/src/methods/saml-sp/method.ts +65 -0
  195. package/src/methods/saml-sp/parse-idp-metadata.ts +158 -0
  196. package/src/methods/saml-sp/saml-instance.ts +210 -0
  197. package/src/methods/saml-sp/slo-initiate.ts +180 -0
  198. package/src/methods/saml-sp/sls.ts +307 -0
  199. package/src/methods/saml-sp/types.ts +298 -0
  200. package/src/ports/CONSISTENCY.md +52 -24
  201. package/src/ports/audit-log.ts +18 -5
  202. package/src/ports/scim-directory.ts +166 -0
  203. package/src/ports/session-store.ts +24 -0
  204. package/src/types/error.ts +11 -0
  205. package/src/types/idp.ts +86 -0
  206. package/src/types/method.ts +110 -0
  207. package/src/types/scim.ts +280 -0
  208. package/src/types/tenant.ts +10 -0
@@ -7,21 +7,42 @@ import { dispatchMethod } from "./method-dispatch";
7
7
  import { saveEncryptedCode } from "./token";
8
8
  import { verifyStateEnvelope } from "./state-envelope";
9
9
  import { AUTH_CODE_TTL_MS } from "./authorize";
10
+ async function extractCallbackState(req) {
11
+ const fromQuery = new URL(req.url).searchParams.get("state");
12
+ if (fromQuery)
13
+ return fromQuery;
14
+ if (req.method !== "POST")
15
+ return null;
16
+ const ct = req.headers.get("content-type") ?? "";
17
+ if (!ct.includes("application/x-www-form-urlencoded"))
18
+ return null;
19
+ try {
20
+ const body = await req.clone().text();
21
+ const form = new URLSearchParams(body);
22
+ const v = form.get("state") ?? form.get("RelayState");
23
+ return v && v.length > 0 ? v : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
10
28
  async function handleCallback(input, deps) {
11
29
  const url = new URL(input.rawRequest.url);
12
- const state = url.searchParams.get("state");
13
- if (!state) {
14
- return err(authError.invalidRequest("missing state parameter", "state"));
15
- }
16
- const envelopeRes = await verifyStateEnvelope(state, deps.stateKeys);
17
- if (isErr(envelopeRes)) {
30
+ const state = await extractCallbackState(input.rawRequest);
31
+ const envelopeRes = state ? await verifyStateEnvelope(state, deps.stateKeys) : null;
32
+ if (envelopeRes === null || isErr(envelopeRes)) {
33
+ const idp = await tryIdpInitiated(input, deps, url);
34
+ if (idp)
35
+ return idp;
36
+ if (!state) {
37
+ return err(authError.invalidRequest("missing state parameter", "state"));
38
+ }
18
39
  await safeAudit(deps, {
19
40
  kind: "flow_replay_attempt",
20
41
  tenantId: null,
21
42
  flowId: "unknown",
22
43
  timestamp: deps.clock()
23
44
  });
24
- return err(authError.invalidRequest(envelopeRes.error.description, "state"));
45
+ return err(authError.invalidRequest(envelopeRes && isErr(envelopeRes) ? envelopeRes.error.description : "invalid state", "state"));
25
46
  }
26
47
  const envelope = envelopeRes.value;
27
48
  const flowRes = await deps.sessionStore.consumeFlow(envelope.flowId);
@@ -168,6 +189,99 @@ async function translate(result, flow, deps) {
168
189
  return err(result.error);
169
190
  }
170
191
  }
192
+ async function tryIdpInitiated(input, deps, url) {
193
+ if (input.rawRequest.method !== "POST")
194
+ return null;
195
+ if (!input.tenant || !input.issuerUrl)
196
+ return null;
197
+ const segments = url.pathname.split("/").filter(Boolean);
198
+ if (segments.length < 2 || segments[0] !== "cb")
199
+ return null;
200
+ const methodId = segments[1];
201
+ const methodRes = await deps.methodCache.resolve(input.tenant.config, methodId);
202
+ if (isErr(methodRes))
203
+ return null;
204
+ const method = methodRes.value;
205
+ if (method.unsolicitedCallback !== true)
206
+ return null;
207
+ const callbackUrl = `${url.protocol}//${url.host}/cb/${methodId}`;
208
+ const dispatched = await dispatchMethod({
209
+ method,
210
+ route: "GET /callback",
211
+ tenant: input.tenant,
212
+ request: input.rawRequest,
213
+ subPath: "/callback",
214
+ flow: null,
215
+ cookies: input.cookies,
216
+ sessionStore: deps.sessionStore,
217
+ dispatch: { state: "", callbackUrl, issuerUrl: input.issuerUrl }
218
+ });
219
+ if (isErr(dispatched))
220
+ return err(dispatched.error);
221
+ const result = dispatched.value;
222
+ if (result.kind === "challenge") {
223
+ return ok({
224
+ kind: "challenge",
225
+ response: result.response,
226
+ setCookies: result.setCookies ?? [],
227
+ ...result.cache !== undefined ? { cache: result.cache } : {}
228
+ });
229
+ }
230
+ if (result.kind === "denied") {
231
+ await safeAudit(deps, {
232
+ kind: "authorize_failed",
233
+ tenantId: input.tenant.id,
234
+ clientId: "idp-initiated",
235
+ methodId,
236
+ methodKind: method.kind,
237
+ flowId: "idp-initiated",
238
+ reason: result.reason,
239
+ timestamp: deps.clock()
240
+ });
241
+ return ok({
242
+ kind: "denied",
243
+ reason: result.reason,
244
+ setCookies: result.setCookies ?? []
245
+ });
246
+ }
247
+ if (result.kind === "error")
248
+ return err(result.error);
249
+ const binding = result.unsolicitedBinding;
250
+ if (!binding) {
251
+ return err(authError.internalError(`method "${methodId}" handled an unsolicited callback but returned ` + `no unsolicitedBinding`));
252
+ }
253
+ const client = input.tenant.config.clients.find((c) => c.id === binding.clientId);
254
+ if (!client) {
255
+ return err(authError.invalidRequest(`IdP-initiated binding references unknown client "${binding.clientId}"`));
256
+ }
257
+ if (!client.redirectUris.includes(binding.redirectUri)) {
258
+ return err(authError.invalidRequest(`IdP-initiated redirect_uri "${binding.redirectUri}" is not ` + `registered for client "${binding.clientId}"`));
259
+ }
260
+ const code = (deps.newCodeId ?? randomToken)();
261
+ const now = deps.clock();
262
+ const saved = await saveEncryptedCode(code, {
263
+ tenantId: input.tenant.id,
264
+ clientId: binding.clientId,
265
+ appRedirectUri: binding.redirectUri,
266
+ appState: null,
267
+ scopes: binding.scopes,
268
+ methodId,
269
+ methodKind: method.kind,
270
+ context: input.tenant.request.custom ?? null,
271
+ providerSubject: result.providerSubject,
272
+ properties: result.properties,
273
+ authTime: Math.floor(now / 1000),
274
+ expiresAt: now + AUTH_CODE_TTL_MS
275
+ }, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
276
+ if (isErr(saved))
277
+ return err(saved.error);
278
+ return ok({
279
+ kind: "issue-code",
280
+ code,
281
+ appRedirectUri: binding.redirectUri,
282
+ appState: null
283
+ });
284
+ }
171
285
  function normalizePath(p) {
172
286
  return p.replace(/\/+$/, "");
173
287
  }
@@ -55,7 +55,7 @@ function resolveClientCredentials(input) {
55
55
  };
56
56
  }
57
57
  export {
58
- verifyClientCredentials,
58
+ parseBasicAuth,
59
59
  resolveClientCredentials,
60
- parseBasicAuth
60
+ verifyClientCredentials
61
61
  };
@@ -79,19 +79,19 @@ function generateSymmetricKey() {
79
79
  return buf;
80
80
  }
81
81
  export {
82
- utf8,
83
- timingSafeEqualStr,
84
- timingSafeEqual,
85
- sha256,
86
- randomToken,
87
- randomId,
88
- randomHex,
89
- randomBytes,
90
- importHmacKey,
91
- hmacVerify,
92
- hmacSign,
93
- generateSymmetricKey,
94
- encryptPayload,
82
+ base64url,
95
83
  decryptPayload,
96
- base64url
84
+ encryptPayload,
85
+ generateSymmetricKey,
86
+ hmacSign,
87
+ hmacVerify,
88
+ importHmacKey,
89
+ randomBytes,
90
+ randomHex,
91
+ randomId,
92
+ randomToken,
93
+ sha256,
94
+ timingSafeEqual,
95
+ timingSafeEqualStr,
96
+ utf8
97
97
  };
@@ -78,6 +78,6 @@ async function buildJwks(keyStore) {
78
78
  return ok(buildJwksDocument(res.value));
79
79
  }
80
80
  export {
81
- buildJwks,
82
- buildDiscoveryDocument
81
+ buildDiscoveryDocument,
82
+ buildJwks
83
83
  };
@@ -114,9 +114,9 @@ function stringifyError(e) {
114
114
  return e instanceof Error ? e.message : String(e);
115
115
  }
116
116
  export {
117
- verifyDpopProof,
118
- computeAth,
119
- canonicalHtu,
117
+ DEFAULT_DPOP_IAT_TOLERANCE_SEC,
120
118
  DEFAULT_DPOP_JTI_TTL_MS,
121
- DEFAULT_DPOP_IAT_TOLERANCE_SEC
119
+ canonicalHtu,
120
+ computeAth,
121
+ verifyDpopProof
122
122
  };
@@ -85,10 +85,10 @@ function shouldIssueIdToken(scopes) {
85
85
  return scopes.includes("openid");
86
86
  }
87
87
  export {
88
- shouldIssueIdToken,
89
- pickScopedClaims,
90
- deriveAmr,
91
- computeAtHash,
88
+ DEFAULT_ID_TOKEN_TTL_MS,
92
89
  buildIdTokenClaims,
93
- DEFAULT_ID_TOKEN_TTL_MS
90
+ computeAtHash,
91
+ deriveAmr,
92
+ pickScopedClaims,
93
+ shouldIssueIdToken
94
94
  };
@@ -63,9 +63,9 @@ function buildJwksDocument(keys) {
63
63
  };
64
64
  }
65
65
  export {
66
- verifyIdToken,
67
- verifyAccessToken,
68
- signIdToken,
66
+ buildJwksDocument,
69
67
  signAccessToken,
70
- buildJwksDocument
68
+ signIdToken,
69
+ verifyAccessToken,
70
+ verifyIdToken
71
71
  };
@@ -13,7 +13,8 @@ async function dispatchMethod(input) {
13
13
  flow: input.flow,
14
14
  methodState: input.flow?.methodState ?? null,
15
15
  cookies: input.cookies,
16
- dispatch: input.dispatch
16
+ dispatch: input.dispatch,
17
+ methodScratch: buildMethodScratch(input.sessionStore, input.tenant.id, input.method.id)
17
18
  };
18
19
  let result;
19
20
  try {
@@ -31,6 +32,31 @@ async function dispatchMethod(input) {
31
32
  }
32
33
  return ok(result);
33
34
  }
35
+ function buildMethodScratch(store, tenantId, methodId) {
36
+ const prefix = `scratch:${tenantId}:${methodId}:`;
37
+ const scope = (key) => `${prefix}${key}`;
38
+ const unsupported = (op) => authError.internalError(`SessionStore does not implement ${op}; methodScratch is unavailable on this adapter`);
39
+ return {
40
+ put: async (key, value, ttlMs) => {
41
+ if (!store.saveScratch)
42
+ return err(unsupported("saveScratch"));
43
+ if (ttlMs <= 0) {
44
+ return err(authError.internalError(`methodScratch.put: ttlMs must be positive, got ${ttlMs}`));
45
+ }
46
+ return store.saveScratch(scope(key), value, ttlMs);
47
+ },
48
+ get: async (key) => {
49
+ if (!store.readScratch)
50
+ return err(unsupported("readScratch"));
51
+ return store.readScratch(scope(key));
52
+ },
53
+ delete: async (key) => {
54
+ if (!store.deleteScratch)
55
+ return err(unsupported("deleteScratch"));
56
+ return store.deleteScratch(scope(key));
57
+ }
58
+ };
59
+ }
34
60
  export {
35
61
  dispatchMethod
36
62
  };
@@ -5,6 +5,7 @@ import { safeAudit } from "./audit";
5
5
  import { AUTH_CODE_TTL_MS } from "./authorize";
6
6
  import { randomToken } from "./crypto";
7
7
  import { dispatchMethod } from "./method-dispatch";
8
+ import { revokeAllForSubject } from "./revoke";
8
9
  import { saveEncryptedCode } from "./token";
9
10
  async function handleMethodRoute(input, deps) {
10
11
  const flowRes = await deps.sessionStore.readFlow(input.flowId);
@@ -106,6 +107,84 @@ async function translate(result, flow, deps) {
106
107
  return err(result.error);
107
108
  }
108
109
  }
110
+ async function handlePublicMethodRoute(input, deps) {
111
+ if (!input.method.publicRoutes?.includes(input.route)) {
112
+ return err(authError.invalidRequest(`route "${input.route}" is not public on method "${input.method.id}"`, "path"));
113
+ }
114
+ const dispatched = await dispatchMethod({
115
+ method: input.method,
116
+ route: input.route,
117
+ tenant: input.tenant,
118
+ request: input.rawRequest,
119
+ subPath: input.subPath,
120
+ flow: null,
121
+ cookies: new Map,
122
+ sessionStore: deps.sessionStore,
123
+ dispatch: input.dispatch
124
+ });
125
+ if (isErr(dispatched))
126
+ return err(dispatched.error);
127
+ const r = dispatched.value;
128
+ switch (r.kind) {
129
+ case "challenge": {
130
+ if (r.logout) {
131
+ const sideEffect = await runUpstreamLogout(input, deps, r.logout);
132
+ if (isErr(sideEffect))
133
+ return err(sideEffect.error);
134
+ }
135
+ return ok({
136
+ kind: "challenge",
137
+ response: r.response,
138
+ ...r.cache !== undefined ? { cache: r.cache } : {}
139
+ });
140
+ }
141
+ case "denied":
142
+ return ok({ kind: "denied", reason: r.reason });
143
+ case "success":
144
+ return err(authError.internalError(`public route "${input.route}" on method "${input.method.id}" ` + `returned success — a flowless route cannot authenticate`));
145
+ case "error":
146
+ return err(r.error);
147
+ }
148
+ }
149
+ async function runUpstreamLogout(input, deps, logout) {
150
+ let hookResult = undefined;
151
+ if (deps.onLogout) {
152
+ const evt = {
153
+ tenant: input.tenant,
154
+ methodId: input.method.id,
155
+ methodKind: input.method.kind,
156
+ reason: "upstream_slo",
157
+ ...logout.nameId !== undefined ? { nameId: logout.nameId } : {},
158
+ ...logout.sessionIndex !== undefined ? { sessionIndex: logout.sessionIndex } : {}
159
+ };
160
+ try {
161
+ hookResult = await deps.onLogout(evt);
162
+ } catch (e) {
163
+ return err(authError.internalError(`onLogout hook threw during upstream Single Logout for method ` + `"${input.method.id}"`, e));
164
+ }
165
+ }
166
+ const revokeSubject = hookResult && typeof hookResult === "object" ? hookResult.revokeSubject : undefined;
167
+ if (revokeSubject) {
168
+ const revoked = await revokeAllForSubject(input.tenant.id, revokeSubject, {
169
+ tokenStore: deps.tokenStore,
170
+ ...deps.auditLog ? { auditLog: deps.auditLog } : {},
171
+ clock: deps.clock
172
+ });
173
+ if (isErr(revoked))
174
+ return err(revoked.error);
175
+ }
176
+ await safeAudit(deps, {
177
+ kind: "session_logout",
178
+ tenantId: input.tenant.id,
179
+ via: "upstream_slo",
180
+ methodId: input.method.id,
181
+ methodKind: input.method.kind,
182
+ ...revokeSubject ? { subjectId: revokeSubject } : {},
183
+ timestamp: deps.clock()
184
+ });
185
+ return ok(undefined);
186
+ }
109
187
  export {
110
- handleMethodRoute
188
+ handleMethodRoute,
189
+ handlePublicMethodRoute
111
190
  };
@@ -45,7 +45,7 @@ async function pushAuthorizationRequest(req, tenant, deps) {
45
45
  });
46
46
  }
47
47
  export {
48
- pushAuthorizationRequest,
48
+ DEFAULT_PAR_TTL_MS,
49
49
  PAR_URI_PREFIX,
50
- DEFAULT_PAR_TTL_MS
50
+ pushAuthorizationRequest
51
51
  };
@@ -11,6 +11,6 @@ async function validatePkce(verifier, challenge) {
11
11
  return timingSafeEqualStr(computed, challenge);
12
12
  }
13
13
  export {
14
- validatePkce,
15
- s256Challenge
14
+ s256Challenge,
15
+ validatePkce
16
16
  };
@@ -76,6 +76,6 @@ async function revokeAllForSubject(tenantId, subjectId, deps) {
76
76
  return ok(undefined);
77
77
  }
78
78
  export {
79
- revokeToken,
80
- revokeAllForSubject
79
+ revokeAllForSubject,
80
+ revokeToken
81
81
  };
@@ -0,0 +1,206 @@
1
+ // src/domain/scim/discovery.ts
2
+ import {
3
+ SCIM_ENTERPRISE_SCHEMA,
4
+ SCIM_GROUP_SCHEMA,
5
+ SCIM_LIST_SCHEMA,
6
+ SCIM_USER_SCHEMA
7
+ } from "./resource";
8
+ function serviceProviderConfig(baseUrl, maxResults) {
9
+ return {
10
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
11
+ documentationUri: "https://datatracker.ietf.org/doc/html/rfc7644",
12
+ patch: { supported: true },
13
+ bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
14
+ filter: { supported: true, maxResults },
15
+ changePassword: { supported: false },
16
+ sort: { supported: false },
17
+ etag: { supported: false },
18
+ authenticationSchemes: [
19
+ {
20
+ type: "oauthbearertoken",
21
+ name: "OAuth Bearer Token",
22
+ description: "Authentication via the bearer token issued for this SCIM connection",
23
+ specUri: "https://datatracker.ietf.org/doc/html/rfc6750",
24
+ primary: true
25
+ }
26
+ ],
27
+ meta: {
28
+ resourceType: "ServiceProviderConfig",
29
+ location: `${baseUrl}/ServiceProviderConfig`
30
+ }
31
+ };
32
+ }
33
+ function resourceTypes(baseUrl, groups) {
34
+ const user = {
35
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
36
+ id: "User",
37
+ name: "User",
38
+ endpoint: "/Users",
39
+ description: "User Account",
40
+ schema: SCIM_USER_SCHEMA,
41
+ schemaExtensions: [{ schema: SCIM_ENTERPRISE_SCHEMA, required: false }],
42
+ meta: {
43
+ resourceType: "ResourceType",
44
+ location: `${baseUrl}/ResourceTypes/User`
45
+ }
46
+ };
47
+ const group = {
48
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
49
+ id: "Group",
50
+ name: "Group",
51
+ endpoint: "/Groups",
52
+ description: "Group",
53
+ schema: SCIM_GROUP_SCHEMA,
54
+ meta: {
55
+ resourceType: "ResourceType",
56
+ location: `${baseUrl}/ResourceTypes/Group`
57
+ }
58
+ };
59
+ const resources = groups ? [user, group] : [user];
60
+ return {
61
+ schemas: [SCIM_LIST_SCHEMA],
62
+ totalResults: resources.length,
63
+ startIndex: 1,
64
+ itemsPerPage: resources.length,
65
+ Resources: resources
66
+ };
67
+ }
68
+ var str = (name, extra = {}) => ({
69
+ name,
70
+ type: "string",
71
+ multiValued: false,
72
+ required: false,
73
+ caseExact: false,
74
+ mutability: "readWrite",
75
+ returned: "default",
76
+ uniqueness: "none",
77
+ ...extra
78
+ });
79
+ var multiValueSub = [
80
+ str("value"),
81
+ str("type"),
82
+ {
83
+ name: "primary",
84
+ type: "boolean",
85
+ multiValued: false,
86
+ required: false,
87
+ mutability: "readWrite",
88
+ returned: "default"
89
+ }
90
+ ];
91
+ function schemas(baseUrl, groups) {
92
+ const userSchema = {
93
+ id: SCIM_USER_SCHEMA,
94
+ name: "User",
95
+ description: "User Account",
96
+ attributes: [
97
+ str("userName", { required: true, uniqueness: "server" }),
98
+ {
99
+ name: "name",
100
+ type: "complex",
101
+ multiValued: false,
102
+ required: false,
103
+ mutability: "readWrite",
104
+ returned: "default",
105
+ subAttributes: [
106
+ str("formatted"),
107
+ str("familyName"),
108
+ str("givenName"),
109
+ str("middleName"),
110
+ str("honorificPrefix"),
111
+ str("honorificSuffix")
112
+ ]
113
+ },
114
+ str("displayName"),
115
+ {
116
+ name: "active",
117
+ type: "boolean",
118
+ multiValued: false,
119
+ required: false,
120
+ mutability: "readWrite",
121
+ returned: "default"
122
+ },
123
+ {
124
+ name: "emails",
125
+ type: "complex",
126
+ multiValued: true,
127
+ required: false,
128
+ mutability: "readWrite",
129
+ returned: "default",
130
+ subAttributes: multiValueSub
131
+ },
132
+ {
133
+ name: "phoneNumbers",
134
+ type: "complex",
135
+ multiValued: true,
136
+ required: false,
137
+ mutability: "readWrite",
138
+ returned: "default",
139
+ subAttributes: multiValueSub
140
+ }
141
+ ],
142
+ meta: {
143
+ resourceType: "Schema",
144
+ location: `${baseUrl}/Schemas/${SCIM_USER_SCHEMA}`
145
+ }
146
+ };
147
+ const enterpriseSchema = {
148
+ id: SCIM_ENTERPRISE_SCHEMA,
149
+ name: "EnterpriseUser",
150
+ description: "Enterprise User Extension",
151
+ attributes: [
152
+ str("employeeNumber"),
153
+ str("costCenter"),
154
+ str("organization"),
155
+ str("division"),
156
+ str("department"),
157
+ {
158
+ name: "manager",
159
+ type: "complex",
160
+ multiValued: false,
161
+ required: false,
162
+ mutability: "readWrite",
163
+ returned: "default",
164
+ subAttributes: [str("value"), str("displayName")]
165
+ }
166
+ ],
167
+ meta: {
168
+ resourceType: "Schema",
169
+ location: `${baseUrl}/Schemas/${SCIM_ENTERPRISE_SCHEMA}`
170
+ }
171
+ };
172
+ const groupSchema = {
173
+ id: SCIM_GROUP_SCHEMA,
174
+ name: "Group",
175
+ description: "Group",
176
+ attributes: [
177
+ str("displayName", { required: true }),
178
+ {
179
+ name: "members",
180
+ type: "complex",
181
+ multiValued: true,
182
+ required: false,
183
+ mutability: "readWrite",
184
+ returned: "default",
185
+ subAttributes: [str("value"), str("display"), str("type")]
186
+ }
187
+ ],
188
+ meta: {
189
+ resourceType: "Schema",
190
+ location: `${baseUrl}/Schemas/${SCIM_GROUP_SCHEMA}`
191
+ }
192
+ };
193
+ const resources = groups ? [userSchema, enterpriseSchema, groupSchema] : [userSchema, enterpriseSchema];
194
+ return {
195
+ schemas: [SCIM_LIST_SCHEMA],
196
+ totalResults: resources.length,
197
+ startIndex: 1,
198
+ itemsPerPage: resources.length,
199
+ Resources: resources
200
+ };
201
+ }
202
+ export {
203
+ resourceTypes,
204
+ schemas,
205
+ serviceProviderConfig
206
+ };