@_mustachio/openauth 0.11.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.
Files changed (164) 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/state-envelope.js +2 -2
  32. package/dist/esm/domain/token.js +5 -5
  33. package/dist/esm/error.js +8 -8
  34. package/dist/esm/http/cookies.js +19 -2
  35. package/dist/esm/http/errors.js +4 -4
  36. package/dist/esm/http/handlers/authorize.js +3 -19
  37. package/dist/esm/http/handlers/callback.js +7 -1
  38. package/dist/esm/http/handlers/metadata.js +2 -2
  39. package/dist/esm/http/handlers/method-route.js +47 -2
  40. package/dist/esm/http/handlers/revocation.js +2 -2
  41. package/dist/esm/http/middleware/tenant.js +6 -6
  42. package/dist/esm/http/schemas/authorize.js +2 -2
  43. package/dist/esm/http/schemas/revocation.js +2 -2
  44. package/dist/esm/http/schemas/token.js +7 -7
  45. package/dist/esm/index.js +33 -32
  46. package/dist/esm/methods/oauth2-factory.js +2 -2
  47. package/dist/esm/methods/password-hash.js +2 -2
  48. package/dist/esm/methods/providers/index.js +30 -14
  49. package/dist/esm/methods/saml-sp/acs.js +208 -0
  50. package/dist/esm/methods/saml-sp/attributes.js +75 -0
  51. package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
  52. package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
  53. package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
  54. package/dist/esm/methods/saml-sp/factory.js +70 -0
  55. package/dist/esm/methods/saml-sp/index.js +7 -0
  56. package/dist/esm/methods/saml-sp/metadata.js +69 -0
  57. package/dist/esm/methods/saml-sp/method.js +29 -0
  58. package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
  59. package/dist/esm/methods/saml-sp/saml-instance.js +49 -0
  60. package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
  61. package/dist/esm/methods/saml-sp/sls.js +154 -0
  62. package/dist/esm/methods/saml-sp/types.js +0 -0
  63. package/dist/esm/types/result.js +3 -3
  64. package/dist/esm/ui/forms.js +3 -3
  65. package/dist/esm/ui/index.js +8 -5
  66. package/dist/esm/ui/picker.js +2 -2
  67. package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
  68. package/dist/types/adapters/d1/session-store.d.ts +3 -0
  69. package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
  70. package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
  71. package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
  72. package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
  73. package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
  74. package/dist/types/adapters/memory/session-store.d.ts +3 -0
  75. package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
  76. package/dist/types/adapters/postgres/migrations.d.ts +1 -1
  77. package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
  78. package/dist/types/adapters/postgres/session-store.d.ts +3 -0
  79. package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
  80. package/dist/types/domain/callback.d.ts +16 -1
  81. package/dist/types/domain/callback.d.ts.map +1 -1
  82. package/dist/types/domain/method-dispatch.d.ts.map +1 -1
  83. package/dist/types/domain/method-route.d.ts +57 -1
  84. package/dist/types/domain/method-route.d.ts.map +1 -1
  85. package/dist/types/http/context.d.ts +3 -1
  86. package/dist/types/http/context.d.ts.map +1 -1
  87. package/dist/types/http/cookies.d.ts +9 -1
  88. package/dist/types/http/cookies.d.ts.map +1 -1
  89. package/dist/types/http/handlers/callback.d.ts.map +1 -1
  90. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  91. package/dist/types/index.d.ts +1 -1
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/methods/saml-sp/acs.d.ts +4 -0
  94. package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
  95. package/dist/types/methods/saml-sp/attributes.d.ts +40 -0
  96. package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
  97. package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
  98. package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
  99. package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
  100. package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
  101. package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
  102. package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
  103. package/dist/types/methods/saml-sp/factory.d.ts +704 -0
  104. package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
  105. package/dist/types/methods/saml-sp/index.d.ts +24 -0
  106. package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
  107. package/dist/types/methods/saml-sp/metadata.d.ts +42 -0
  108. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
  109. package/dist/types/methods/saml-sp/method.d.ts +17 -0
  110. package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
  111. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
  112. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
  113. package/dist/types/methods/saml-sp/saml-instance.d.ts +81 -0
  114. package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
  115. package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
  116. package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
  117. package/dist/types/methods/saml-sp/sls.d.ts +4 -0
  118. package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
  119. package/dist/types/methods/saml-sp/types.d.ts +204 -0
  120. package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
  121. package/dist/types/ports/audit-log.d.ts +18 -5
  122. package/dist/types/ports/audit-log.d.ts.map +1 -1
  123. package/dist/types/ports/session-store.d.ts +23 -0
  124. package/dist/types/ports/session-store.d.ts.map +1 -1
  125. package/dist/types/types/idp.d.ts +71 -0
  126. package/dist/types/types/idp.d.ts.map +1 -1
  127. package/dist/types/types/method.d.ts +109 -0
  128. package/dist/types/types/method.d.ts.map +1 -1
  129. package/package.json +9 -1
  130. package/src/adapters/d1/migrations.ts +5 -0
  131. package/src/adapters/d1/session-store.ts +68 -0
  132. package/src/adapters/durable-object/session-store.ts +61 -0
  133. package/src/adapters/dynamo/session-store.ts +63 -0
  134. package/src/adapters/memory/session-store.ts +39 -0
  135. package/src/adapters/postgres/migrations.ts +6 -0
  136. package/src/adapters/postgres/session-store.ts +69 -0
  137. package/src/domain/callback.ts +215 -7
  138. package/src/domain/method-dispatch.ts +53 -1
  139. package/src/domain/method-route.ts +190 -1
  140. package/src/http/context.ts +6 -0
  141. package/src/http/cookies.ts +19 -1
  142. package/src/http/handlers/authorize.ts +2 -21
  143. package/src/http/handlers/callback.ts +9 -1
  144. package/src/http/handlers/method-route.ts +75 -2
  145. package/src/index.ts +3 -0
  146. package/src/methods/saml-sp/acs.ts +399 -0
  147. package/src/methods/saml-sp/attributes.ts +137 -0
  148. package/src/methods/saml-sp/authnrequest.ts +128 -0
  149. package/src/methods/saml-sp/cache-provider.ts +76 -0
  150. package/src/methods/saml-sp/cert-rotation.ts +34 -0
  151. package/src/methods/saml-sp/factory.ts +118 -0
  152. package/src/methods/saml-sp/index.ts +34 -0
  153. package/src/methods/saml-sp/metadata.ts +204 -0
  154. package/src/methods/saml-sp/method.ts +65 -0
  155. package/src/methods/saml-sp/parse-idp-metadata.ts +164 -0
  156. package/src/methods/saml-sp/saml-instance.ts +173 -0
  157. package/src/methods/saml-sp/slo-initiate.ts +177 -0
  158. package/src/methods/saml-sp/sls.ts +306 -0
  159. package/src/methods/saml-sp/types.ts +203 -0
  160. package/src/ports/CONSISTENCY.md +25 -0
  161. package/src/ports/audit-log.ts +18 -5
  162. package/src/ports/session-store.ts +28 -0
  163. package/src/types/idp.ts +74 -0
  164. package/src/types/method.ts +110 -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
  };
@@ -75,6 +75,6 @@ function isEnvelopeShape(value) {
75
75
  return typeof v.tenantId === "string" && typeof v.flowId === "string" && typeof v.nonce === "string" && typeof v.kid === "string";
76
76
  }
77
77
  export {
78
- verifyStateEnvelope,
79
- mintStateEnvelope
78
+ mintStateEnvelope,
79
+ verifyStateEnvelope
80
80
  };
@@ -237,10 +237,10 @@ async function hashTokenForAudit(token) {
237
237
  return base64url.encode(await sha256(utf8.encode(token))).slice(0, 16);
238
238
  }
239
239
  export {
240
- saveEncryptedCode,
241
- mintTokens,
242
- hashClientSecret,
243
- exchangeCode,
240
+ DEFAULT_ACCESS_TTL_MS,
244
241
  DEFAULT_REFRESH_TTL_MS,
245
- DEFAULT_ACCESS_TTL_MS
242
+ exchangeCode,
243
+ hashClientSecret,
244
+ mintTokens,
245
+ saveEncryptedCode
246
246
  };
package/dist/esm/error.js CHANGED
@@ -61,13 +61,13 @@ class InvalidAuthorizationCodeError extends Error {
61
61
  }
62
62
  }
63
63
  export {
64
- UnknownStateError,
65
- UnauthorizedClientError,
66
- OauthError,
67
- MissingProviderError,
68
- MissingParameterError,
69
- InvalidSubjectError,
70
- InvalidRefreshTokenError,
64
+ InvalidAccessTokenError,
71
65
  InvalidAuthorizationCodeError,
72
- InvalidAccessTokenError
66
+ InvalidRefreshTokenError,
67
+ InvalidSubjectError,
68
+ MissingParameterError,
69
+ MissingProviderError,
70
+ OauthError,
71
+ UnauthorizedClientError,
72
+ UnknownStateError
73
73
  };
@@ -67,6 +67,22 @@ var STRIPPED_HEADERS = [
67
67
  "permissions-policy",
68
68
  "cache-control"
69
69
  ];
70
+ function cacheControlHeader(cache) {
71
+ if (!cache)
72
+ return "no-store";
73
+ if ((cache.maxAge ?? -1) === 0)
74
+ return "no-store";
75
+ const parts = [];
76
+ if (cache.isPrivate)
77
+ parts.push("private");
78
+ if (cache.maxAge !== undefined)
79
+ parts.push(`max-age=${cache.maxAge}`);
80
+ if (cache.sMaxAge !== undefined)
81
+ parts.push(`s-maxage=${cache.sMaxAge}`);
82
+ if (cache.immutable)
83
+ parts.push("immutable");
84
+ return parts.length ? parts.join(", ") : "no-store";
85
+ }
70
86
  function applyResponsePolicy(response, opts = {}) {
71
87
  const headers = new Headers(response.headers);
72
88
  const stripped = [];
@@ -92,7 +108,8 @@ function applyResponsePolicy(response, opts = {}) {
92
108
  });
93
109
  }
94
110
  export {
95
- serializeSetCookie,
111
+ applyResponsePolicy,
112
+ cacheControlHeader,
96
113
  parseCookieHeader,
97
- applyResponsePolicy
114
+ serializeSetCookie
98
115
  };
@@ -63,9 +63,9 @@ function isNonRecoverable(error) {
63
63
  return false;
64
64
  }
65
65
  export {
66
- tokenEndpointErrorResponse,
67
- publicErrorCode,
68
- isNonRecoverable,
66
+ authorizeDirectErrorResponse,
69
67
  authorizeRedirectErrorResponse,
70
- authorizeDirectErrorResponse
68
+ isNonRecoverable,
69
+ publicErrorCode,
70
+ tokenEndpointErrorResponse
71
71
  };
@@ -4,7 +4,7 @@ import { startAuthorize } from "../../domain/authorize";
4
4
  import { asTenantId } from "../../types/tenant";
5
5
  import { authError } from "../../types/error";
6
6
  import { renderPicker as renderDefaultPicker } from "../../ui/picker";
7
- import { applyResponsePolicy } from "../cookies";
7
+ import { applyResponsePolicy, cacheControlHeader } from "../cookies";
8
8
  import {
9
9
  authorizeDirectErrorResponse,
10
10
  authorizeRedirectErrorResponse,
@@ -48,7 +48,7 @@ function makeAuthorizeHandler(deps) {
48
48
  }
49
49
  const q = parsed.data;
50
50
  if (raw.request_uri === undefined) {
51
- const client = tenant.config.clients.find((c2) => c2.id === q.client_id);
51
+ const client = tenant.config.clients.find((c) => c.id === q.client_id);
52
52
  if (client?.requirePushedAuthorizationRequests) {
53
53
  return authorizeDirectErrorResponse(authError.invalidRequest("this client requires Pushed Authorization Requests (RFC 9126)", "request_uri"));
54
54
  }
@@ -123,7 +123,7 @@ function makeAuthorizeHandler(deps) {
123
123
  return applyResponsePolicy(out.response, {
124
124
  setCookies: [flowCookie, ...out.setCookies],
125
125
  cookieDefaults: deps.cookieDefaults,
126
- cacheControl: cacheControlFor(out.cache)
126
+ cacheControl: cacheControlHeader(out.cache)
127
127
  });
128
128
  }
129
129
  case "issue-code": {
@@ -229,22 +229,6 @@ function clearFlowCookie() {
229
229
  maxAge: 0
230
230
  };
231
231
  }
232
- function cacheControlFor(cache) {
233
- if (!cache)
234
- return "no-store";
235
- if ((cache.maxAge ?? -1) === 0)
236
- return "no-store";
237
- const parts = [];
238
- if (cache.isPrivate)
239
- parts.push("private");
240
- if (cache.maxAge !== undefined)
241
- parts.push(`max-age=${cache.maxAge}`);
242
- if (cache.sMaxAge !== undefined)
243
- parts.push(`s-maxage=${cache.sMaxAge}`);
244
- if (cache.immutable)
245
- parts.push("immutable");
246
- return parts.length ? parts.join(", ") : "no-store";
247
- }
248
232
  export {
249
233
  makeAuthorizeHandler
250
234
  };
@@ -5,7 +5,13 @@ import { applyResponsePolicy } from "../cookies";
5
5
  import { authorizeDirectErrorResponse } from "../errors";
6
6
  function makeCallbackHandler(deps) {
7
7
  return async (c) => {
8
- const result = await handleCallback({ rawRequest: c.req.raw, cookies: c.get("cookies") }, {
8
+ const tenant = c.get("tenant");
9
+ const result = await handleCallback({
10
+ rawRequest: c.req.raw,
11
+ cookies: c.get("cookies"),
12
+ ...tenant ? { tenant } : {},
13
+ issuerUrl: c.get("issuerUrl")
14
+ }, {
9
15
  configStore: deps.configStore,
10
16
  sessionStore: deps.sessionStore,
11
17
  tokenStore: deps.tokenStore,
@@ -32,6 +32,6 @@ function makeJwksHandler(deps) {
32
32
  };
33
33
  }
34
34
  export {
35
- makeJwksHandler,
36
- makeDiscoveryHandler
35
+ makeDiscoveryHandler,
36
+ makeJwksHandler
37
37
  };
@@ -1,8 +1,11 @@
1
1
  // src/http/handlers/method-route.ts
2
- import { handleMethodRoute } from "../../domain/method-route";
2
+ import {
3
+ handleMethodRoute,
4
+ handlePublicMethodRoute
5
+ } from "../../domain/method-route";
3
6
  import { authError } from "../../types/error";
4
7
  import { isErr } from "../../types/result";
5
- import { applyResponsePolicy } from "../cookies";
8
+ import { applyResponsePolicy, cacheControlHeader } from "../cookies";
6
9
  import { authorizeDirectErrorResponse } from "../errors";
7
10
  function makeMethodRouteHandler(deps) {
8
11
  return async (c) => {
@@ -10,6 +13,48 @@ function makeMethodRouteHandler(deps) {
10
13
  if (!tenant) {
11
14
  return authorizeDirectErrorResponse(authError.invalidRequest("tenant unresolved"));
12
15
  }
16
+ {
17
+ const u = new URL(c.req.url);
18
+ const segs = u.pathname.split("/").filter(Boolean);
19
+ if (segs.length >= 3 && segs[0] === "m") {
20
+ const methodId = segs[1];
21
+ const subPath = "/" + segs.slice(2).join("/");
22
+ const httpMethod = c.req.method.toUpperCase() === "POST" ? "POST" : "GET";
23
+ const routeKey = `${httpMethod} ${subPath}`;
24
+ const resolved = await deps.methodCache.resolve(tenant.config, methodId);
25
+ if (!isErr(resolved) && resolved.value.publicRoutes?.includes(routeKey)) {
26
+ const issuerUrl = c.get("issuerUrl");
27
+ const callbackHost = deps.callbackHostFor?.(tenant.id) ?? new URL(issuerUrl).host;
28
+ const callbackUrl = `${new URL(issuerUrl).protocol}//${callbackHost}/cb/${methodId}`;
29
+ const pub = await handlePublicMethodRoute({
30
+ rawRequest: c.req.raw,
31
+ tenant,
32
+ method: resolved.value,
33
+ route: routeKey,
34
+ subPath,
35
+ dispatch: { state: "", callbackUrl, issuerUrl }
36
+ }, {
37
+ sessionStore: deps.sessionStore,
38
+ tokenStore: deps.tokenStore,
39
+ clock: deps.clock,
40
+ ...deps.auditLog ? { auditLog: deps.auditLog } : {},
41
+ ...deps.onLogout ? { onLogout: deps.onLogout } : {}
42
+ });
43
+ if (isErr(pub))
44
+ return authorizeDirectErrorResponse(pub.error);
45
+ if (pub.value.kind === "denied") {
46
+ return applyResponsePolicy(new Response(`access_denied: ${pub.value.reason}`, {
47
+ status: 403,
48
+ headers: { "content-type": "text/plain" }
49
+ }), { cookieDefaults: deps.cookieDefaults });
50
+ }
51
+ return applyResponsePolicy(pub.value.response, {
52
+ cacheControl: cacheControlHeader(pub.value.cache),
53
+ cookieDefaults: deps.cookieDefaults
54
+ });
55
+ }
56
+ }
57
+ }
13
58
  const cookies = c.get("cookies");
14
59
  const flowId = cookies.get("idp.flow");
15
60
  if (!flowId) {
@@ -93,6 +93,6 @@ async function readForm(req) {
93
93
  return new URLSearchParams(await req.text());
94
94
  }
95
95
  export {
96
- makeRevokeHandler,
97
- makeIntrospectHandler
96
+ makeIntrospectHandler,
97
+ makeRevokeHandler
98
98
  };