@_mustachio/openauth 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) 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 +45 -0
  13. package/dist/esm/adapters/memory/token-store.js +13 -0
  14. package/dist/esm/adapters/postgres/executor.js +2 -2
  15. package/dist/esm/adapters/postgres/index.js +33 -10
  16. package/dist/esm/adapters/postgres/migrations.js +8 -2
  17. package/dist/esm/adapters/postgres/session-store.js +42 -0
  18. package/dist/esm/client.js +35 -28
  19. package/dist/esm/domain/authorize.js +7 -2
  20. package/dist/esm/domain/callback.js +124 -7
  21. package/dist/esm/domain/client-auth.js +2 -2
  22. package/dist/esm/domain/crypto.js +14 -14
  23. package/dist/esm/domain/discovery.js +46 -4
  24. package/dist/esm/domain/dpop.js +122 -0
  25. package/dist/esm/domain/id-token.js +94 -0
  26. package/dist/esm/domain/introspect.js +5 -1
  27. package/dist/esm/domain/jwt.js +30 -2
  28. package/dist/esm/domain/logout.js +75 -0
  29. package/dist/esm/domain/method-dispatch.js +27 -1
  30. package/dist/esm/domain/method-route.js +83 -1
  31. package/dist/esm/domain/par.js +51 -0
  32. package/dist/esm/domain/pkce.js +2 -2
  33. package/dist/esm/domain/refresh.js +12 -1
  34. package/dist/esm/domain/register.js +74 -0
  35. package/dist/esm/domain/revoke.js +2 -2
  36. package/dist/esm/domain/state-envelope.js +2 -2
  37. package/dist/esm/domain/token-exchange.js +1 -0
  38. package/dist/esm/domain/token.js +53 -12
  39. package/dist/esm/domain/userinfo.js +38 -3
  40. package/dist/esm/error.js +8 -8
  41. package/dist/esm/http/cookies.js +19 -2
  42. package/dist/esm/http/errors.js +4 -4
  43. package/dist/esm/http/handlers/authorize.js +92 -21
  44. package/dist/esm/http/handlers/callback.js +7 -1
  45. package/dist/esm/http/handlers/end-session.js +64 -0
  46. package/dist/esm/http/handlers/metadata.js +7 -4
  47. package/dist/esm/http/handlers/method-route.js +47 -2
  48. package/dist/esm/http/handlers/par.js +53 -0
  49. package/dist/esm/http/handlers/register.js +46 -0
  50. package/dist/esm/http/handlers/revocation.js +2 -2
  51. package/dist/esm/http/handlers/token.js +36 -6
  52. package/dist/esm/http/handlers/userinfo.js +35 -29
  53. package/dist/esm/http/middleware/tenant.js +6 -6
  54. package/dist/esm/http/router.js +10 -0
  55. package/dist/esm/http/schemas/authorize.js +8 -2
  56. package/dist/esm/http/schemas/end-session.js +13 -0
  57. package/dist/esm/http/schemas/par.js +9 -0
  58. package/dist/esm/http/schemas/register.js +16 -0
  59. package/dist/esm/http/schemas/revocation.js +2 -2
  60. package/dist/esm/http/schemas/token.js +7 -7
  61. package/dist/esm/index.js +40 -34
  62. package/dist/esm/methods/oauth2-factory.js +2 -2
  63. package/dist/esm/methods/password-hash.js +2 -2
  64. package/dist/esm/methods/providers/index.js +30 -14
  65. package/dist/esm/methods/saml-sp/acs.js +208 -0
  66. package/dist/esm/methods/saml-sp/attributes.js +75 -0
  67. package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
  68. package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
  69. package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
  70. package/dist/esm/methods/saml-sp/factory.js +70 -0
  71. package/dist/esm/methods/saml-sp/index.js +7 -0
  72. package/dist/esm/methods/saml-sp/metadata.js +69 -0
  73. package/dist/esm/methods/saml-sp/method.js +29 -0
  74. package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
  75. package/dist/esm/methods/saml-sp/saml-instance.js +49 -0
  76. package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
  77. package/dist/esm/methods/saml-sp/sls.js +154 -0
  78. package/dist/esm/methods/saml-sp/types.js +0 -0
  79. package/dist/esm/types/error.js +5 -0
  80. package/dist/esm/types/result.js +3 -3
  81. package/dist/esm/ui/forms.js +3 -3
  82. package/dist/esm/ui/index.js +8 -5
  83. package/dist/esm/ui/picker.js +2 -2
  84. package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
  85. package/dist/types/adapters/d1/session-store.d.ts +3 -0
  86. package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
  87. package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
  88. package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
  89. package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
  90. package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
  91. package/dist/types/adapters/memory/session-store.d.ts +6 -1
  92. package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
  93. package/dist/types/adapters/memory/token-store.d.ts +1 -0
  94. package/dist/types/adapters/memory/token-store.d.ts.map +1 -1
  95. package/dist/types/adapters/postgres/migrations.d.ts +1 -1
  96. package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
  97. package/dist/types/adapters/postgres/session-store.d.ts +3 -0
  98. package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
  99. package/dist/types/client.d.ts +19 -0
  100. package/dist/types/client.d.ts.map +1 -1
  101. package/dist/types/domain/authorize.d.ts.map +1 -1
  102. package/dist/types/domain/callback.d.ts +16 -1
  103. package/dist/types/domain/callback.d.ts.map +1 -1
  104. package/dist/types/domain/client-credentials.d.ts +2 -0
  105. package/dist/types/domain/client-credentials.d.ts.map +1 -1
  106. package/dist/types/domain/discovery.d.ts +37 -0
  107. package/dist/types/domain/discovery.d.ts.map +1 -1
  108. package/dist/types/domain/dpop.d.ts +61 -0
  109. package/dist/types/domain/dpop.d.ts.map +1 -0
  110. package/dist/types/domain/id-token.d.ts +83 -0
  111. package/dist/types/domain/id-token.d.ts.map +1 -0
  112. package/dist/types/domain/introspect.d.ts +8 -0
  113. package/dist/types/domain/introspect.d.ts.map +1 -1
  114. package/dist/types/domain/jwt.d.ts +27 -1
  115. package/dist/types/domain/jwt.d.ts.map +1 -1
  116. package/dist/types/domain/logout.d.ts +69 -0
  117. package/dist/types/domain/logout.d.ts.map +1 -0
  118. package/dist/types/domain/method-dispatch.d.ts.map +1 -1
  119. package/dist/types/domain/method-route.d.ts +57 -1
  120. package/dist/types/domain/method-route.d.ts.map +1 -1
  121. package/dist/types/domain/par.d.ts +62 -0
  122. package/dist/types/domain/par.d.ts.map +1 -0
  123. package/dist/types/domain/refresh.d.ts +9 -0
  124. package/dist/types/domain/refresh.d.ts.map +1 -1
  125. package/dist/types/domain/register.d.ts +33 -0
  126. package/dist/types/domain/register.d.ts.map +1 -0
  127. package/dist/types/domain/token-exchange.d.ts +2 -0
  128. package/dist/types/domain/token-exchange.d.ts.map +1 -1
  129. package/dist/types/domain/token.d.ts +41 -1
  130. package/dist/types/domain/token.d.ts.map +1 -1
  131. package/dist/types/domain/userinfo.d.ts +44 -5
  132. package/dist/types/domain/userinfo.d.ts.map +1 -1
  133. package/dist/types/http/context.d.ts +6 -1
  134. package/dist/types/http/context.d.ts.map +1 -1
  135. package/dist/types/http/cookies.d.ts +9 -1
  136. package/dist/types/http/cookies.d.ts.map +1 -1
  137. package/dist/types/http/handlers/authorize.d.ts.map +1 -1
  138. package/dist/types/http/handlers/callback.d.ts.map +1 -1
  139. package/dist/types/http/handlers/end-session.d.ts +3 -0
  140. package/dist/types/http/handlers/end-session.d.ts.map +1 -0
  141. package/dist/types/http/handlers/metadata.d.ts +1 -1
  142. package/dist/types/http/handlers/metadata.d.ts.map +1 -1
  143. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  144. package/dist/types/http/handlers/par.d.ts +3 -0
  145. package/dist/types/http/handlers/par.d.ts.map +1 -0
  146. package/dist/types/http/handlers/register.d.ts +3 -0
  147. package/dist/types/http/handlers/register.d.ts.map +1 -0
  148. package/dist/types/http/handlers/token.d.ts.map +1 -1
  149. package/dist/types/http/handlers/userinfo.d.ts.map +1 -1
  150. package/dist/types/http/router.d.ts.map +1 -1
  151. package/dist/types/http/schemas/authorize.d.ts +40 -0
  152. package/dist/types/http/schemas/authorize.d.ts.map +1 -1
  153. package/dist/types/http/schemas/end-session.d.ts +31 -0
  154. package/dist/types/http/schemas/end-session.d.ts.map +1 -0
  155. package/dist/types/http/schemas/par.d.ts +20 -0
  156. package/dist/types/http/schemas/par.d.ts.map +1 -0
  157. package/dist/types/http/schemas/register.d.ts +42 -0
  158. package/dist/types/http/schemas/register.d.ts.map +1 -0
  159. package/dist/types/index.d.ts +3 -3
  160. package/dist/types/index.d.ts.map +1 -1
  161. package/dist/types/methods/saml-sp/acs.d.ts +4 -0
  162. package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
  163. package/dist/types/methods/saml-sp/attributes.d.ts +40 -0
  164. package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
  165. package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
  166. package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
  167. package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
  168. package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
  169. package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
  170. package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
  171. package/dist/types/methods/saml-sp/factory.d.ts +704 -0
  172. package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
  173. package/dist/types/methods/saml-sp/index.d.ts +24 -0
  174. package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
  175. package/dist/types/methods/saml-sp/metadata.d.ts +42 -0
  176. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
  177. package/dist/types/methods/saml-sp/method.d.ts +17 -0
  178. package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
  179. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
  180. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
  181. package/dist/types/methods/saml-sp/saml-instance.d.ts +81 -0
  182. package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
  183. package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
  184. package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
  185. package/dist/types/methods/saml-sp/sls.d.ts +4 -0
  186. package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
  187. package/dist/types/methods/saml-sp/types.d.ts +204 -0
  188. package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
  189. package/dist/types/ports/audit-log.d.ts +40 -0
  190. package/dist/types/ports/audit-log.d.ts.map +1 -1
  191. package/dist/types/ports/session-store.d.ts +50 -0
  192. package/dist/types/ports/session-store.d.ts.map +1 -1
  193. package/dist/types/ports/token-store.d.ts +12 -0
  194. package/dist/types/ports/token-store.d.ts.map +1 -1
  195. package/dist/types/types/authorization.d.ts +23 -0
  196. package/dist/types/types/authorization.d.ts.map +1 -1
  197. package/dist/types/types/error.d.ts +9 -0
  198. package/dist/types/types/error.d.ts.map +1 -1
  199. package/dist/types/types/flow.d.ts +10 -0
  200. package/dist/types/types/flow.d.ts.map +1 -1
  201. package/dist/types/types/idp.d.ts +162 -1
  202. package/dist/types/types/idp.d.ts.map +1 -1
  203. package/dist/types/types/method.d.ts +109 -0
  204. package/dist/types/types/method.d.ts.map +1 -1
  205. package/dist/types/types/tenant.d.ts +37 -0
  206. package/dist/types/types/tenant.d.ts.map +1 -1
  207. package/dist/types/types/token.d.ts +128 -0
  208. package/dist/types/types/token.d.ts.map +1 -1
  209. package/package.json +9 -1
  210. package/src/adapters/d1/migrations.ts +5 -0
  211. package/src/adapters/d1/session-store.ts +68 -0
  212. package/src/adapters/durable-object/session-store.ts +61 -0
  213. package/src/adapters/dynamo/session-store.ts +63 -0
  214. package/src/adapters/memory/session-store.ts +83 -1
  215. package/src/adapters/memory/token-store.ts +16 -0
  216. package/src/adapters/postgres/migrations.ts +6 -0
  217. package/src/adapters/postgres/session-store.ts +69 -0
  218. package/src/client.ts +32 -0
  219. package/src/domain/authorize.ts +9 -0
  220. package/src/domain/callback.ts +220 -7
  221. package/src/domain/client-credentials.ts +2 -0
  222. package/src/domain/discovery.ts +80 -2
  223. package/src/domain/dpop.ts +293 -0
  224. package/src/domain/id-token.ts +213 -0
  225. package/src/domain/introspect.ts +10 -0
  226. package/src/domain/jwt.ts +80 -1
  227. package/src/domain/logout.ts +180 -0
  228. package/src/domain/method-dispatch.ts +53 -1
  229. package/src/domain/method-route.ts +195 -1
  230. package/src/domain/par.ts +141 -0
  231. package/src/domain/refresh.ts +41 -0
  232. package/src/domain/register.ts +168 -0
  233. package/src/domain/token-exchange.ts +11 -0
  234. package/src/domain/token.ts +134 -8
  235. package/src/domain/userinfo.ts +121 -6
  236. package/src/http/context.ts +10 -0
  237. package/src/http/cookies.ts +19 -1
  238. package/src/http/handlers/authorize.ts +146 -24
  239. package/src/http/handlers/callback.ts +9 -1
  240. package/src/http/handlers/end-session.ts +106 -0
  241. package/src/http/handlers/metadata.ts +7 -2
  242. package/src/http/handlers/method-route.ts +75 -2
  243. package/src/http/handlers/par.ts +84 -0
  244. package/src/http/handlers/register.ts +70 -0
  245. package/src/http/handlers/token.ts +56 -0
  246. package/src/http/handlers/userinfo.ts +62 -38
  247. package/src/http/router.ts +19 -0
  248. package/src/http/schemas/authorize.ts +27 -0
  249. package/src/http/schemas/end-session.ts +23 -0
  250. package/src/http/schemas/par.ts +23 -0
  251. package/src/http/schemas/register.ts +29 -0
  252. package/src/index.ts +21 -1
  253. package/src/methods/saml-sp/acs.ts +399 -0
  254. package/src/methods/saml-sp/attributes.ts +137 -0
  255. package/src/methods/saml-sp/authnrequest.ts +128 -0
  256. package/src/methods/saml-sp/cache-provider.ts +76 -0
  257. package/src/methods/saml-sp/cert-rotation.ts +34 -0
  258. package/src/methods/saml-sp/factory.ts +118 -0
  259. package/src/methods/saml-sp/index.ts +34 -0
  260. package/src/methods/saml-sp/metadata.ts +204 -0
  261. package/src/methods/saml-sp/method.ts +65 -0
  262. package/src/methods/saml-sp/parse-idp-metadata.ts +164 -0
  263. package/src/methods/saml-sp/saml-instance.ts +173 -0
  264. package/src/methods/saml-sp/slo-initiate.ts +177 -0
  265. package/src/methods/saml-sp/sls.ts +306 -0
  266. package/src/methods/saml-sp/types.ts +203 -0
  267. package/src/ports/CONSISTENCY.md +77 -21
  268. package/src/ports/audit-log.ts +42 -0
  269. package/src/ports/session-store.ts +61 -0
  270. package/src/ports/token-store.ts +13 -0
  271. package/src/types/authorization.ts +25 -0
  272. package/src/types/error.ts +23 -0
  273. package/src/types/flow.ts +10 -0
  274. package/src/types/idp.ts +170 -0
  275. package/src/types/method.ts +110 -0
  276. package/src/types/tenant.ts +37 -0
  277. package/src/types/token.ts +135 -0
@@ -73,6 +73,8 @@ async function startAuthorize(input, deps) {
73
73
  prompt: req.prompt,
74
74
  uiLocales: req.uiLocales,
75
75
  nonce,
76
+ ...req.nonce !== undefined ? { appNonce: req.nonce } : {},
77
+ ...req.claimsRequest !== undefined ? { claimsRequest: req.claimsRequest } : {},
76
78
  clientPkce: req.codeChallenge ? { challenge: req.codeChallenge, method: "S256" } : undefined,
77
79
  methodState: null,
78
80
  context: tenant.request.custom,
@@ -194,6 +196,9 @@ async function issueCodeFromInlineSuccess(result, record, deps) {
194
196
  context: flow.context ?? null,
195
197
  providerSubject: result.providerSubject,
196
198
  properties: result.properties,
199
+ ...flow.appNonce !== undefined ? { appNonce: flow.appNonce } : {},
200
+ ...flow.claimsRequest !== undefined ? { claimsRequest: flow.claimsRequest } : {},
201
+ authTime: Math.floor(now / 1000),
197
202
  expiresAt: now + AUTH_CODE_TTL_MS
198
203
  }, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
199
204
  if (isErr(saved))
@@ -206,7 +211,7 @@ async function issueCodeFromInlineSuccess(result, record, deps) {
206
211
  });
207
212
  }
208
213
  export {
209
- startAuthorize,
214
+ AUTH_CODE_TTL_MS,
210
215
  DEFAULT_FLOW_TTL_MS,
211
- AUTH_CODE_TTL_MS
216
+ startAuthorize
212
217
  };
@@ -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);
@@ -117,6 +138,9 @@ async function translate(result, flow, deps) {
117
138
  context: flow.context ?? null,
118
139
  providerSubject: result.providerSubject,
119
140
  properties: result.properties,
141
+ ...flow.appNonce !== undefined ? { appNonce: flow.appNonce } : {},
142
+ ...flow.claimsRequest !== undefined ? { claimsRequest: flow.claimsRequest } : {},
143
+ authTime: Math.floor(now / 1000),
120
144
  expiresAt: now + AUTH_CODE_TTL_MS
121
145
  }, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
122
146
  if (isErr(saved))
@@ -165,6 +189,99 @@ async function translate(result, flow, deps) {
165
189
  return err(result.error);
166
190
  }
167
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
+ }
168
285
  function normalizePath(p) {
169
286
  return p.replace(/\/+$/, "");
170
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
  };
@@ -4,6 +4,12 @@ import { buildJwksDocument } from "./jwt";
4
4
  function buildDiscoveryDocument(deps) {
5
5
  const base = deps.issuerUrl.replace(/\/+$/, "");
6
6
  const p = deps.paths ?? {};
7
+ const customScopeKeys = Object.keys(deps.customScopeClaims ?? {});
8
+ const customClaimNames = new Set;
9
+ for (const list of Object.values(deps.customScopeClaims ?? {})) {
10
+ for (const name of list)
11
+ customClaimNames.add(name);
12
+ }
7
13
  return {
8
14
  issuer: deps.issuerUrl,
9
15
  authorization_endpoint: `${base}${p.authorize ?? "/authorize"}`,
@@ -12,15 +18,51 @@ function buildDiscoveryDocument(deps) {
12
18
  jwks_uri: `${base}${p.jwks ?? "/.well-known/jwks.json"}`,
13
19
  revocation_endpoint: `${base}${p.revoke ?? "/revoke"}`,
14
20
  introspection_endpoint: `${base}${p.introspect ?? "/introspect"}`,
21
+ end_session_endpoint: `${base}${p.endSession ?? "/end_session"}`,
22
+ registration_endpoint: `${base}${p.register ?? "/register"}`,
23
+ pushed_authorization_request_endpoint: `${base}${p.par ?? "/par"}`,
24
+ require_pushed_authorization_requests: false,
25
+ dpop_signing_alg_values_supported: ["ES256", "EdDSA"],
15
26
  response_types_supported: ["code"],
16
27
  grant_types_supported: [
17
28
  "authorization_code",
18
29
  "refresh_token",
19
30
  "client_credentials"
20
31
  ],
21
- subject_types_supported: ["public"],
32
+ subject_types_supported: ["public", "pairwise"],
22
33
  id_token_signing_alg_values_supported: ["ES256", "EdDSA"],
23
- scopes_supported: deps.scopes ?? ["openid", "email", "profile"],
34
+ scopes_supported: [
35
+ ...deps.scopes ?? ["openid", "email", "profile"],
36
+ ...customScopeKeys
37
+ ],
38
+ claims_supported: [
39
+ "sub",
40
+ "iss",
41
+ "aud",
42
+ "exp",
43
+ "iat",
44
+ "auth_time",
45
+ "nonce",
46
+ "amr",
47
+ "at_hash",
48
+ "email",
49
+ "email_verified",
50
+ "name",
51
+ "given_name",
52
+ "family_name",
53
+ "preferred_username",
54
+ "picture",
55
+ "locale",
56
+ "phone_number",
57
+ "phone_number_verified",
58
+ "address",
59
+ ...customClaimNames
60
+ ],
61
+ claims_parameter_supported: true,
62
+ request_parameter_supported: false,
63
+ request_uri_parameter_supported: false,
64
+ require_request_uri_registration: false,
65
+ ui_locales_supported: deps.uiLocales ?? ["en"],
24
66
  token_endpoint_auth_methods_supported: [
25
67
  "client_secret_basic",
26
68
  "client_secret_post",
@@ -36,6 +78,6 @@ async function buildJwks(keyStore) {
36
78
  return ok(buildJwksDocument(res.value));
37
79
  }
38
80
  export {
39
- buildJwks,
40
- buildDiscoveryDocument
81
+ buildDiscoveryDocument,
82
+ buildJwks
41
83
  };
@@ -0,0 +1,122 @@
1
+ // src/domain/dpop.ts
2
+ import {
3
+ calculateJwkThumbprint,
4
+ decodeProtectedHeader,
5
+ importJWK,
6
+ jwtVerify
7
+ } from "jose";
8
+ import { authError } from "../types/error";
9
+ import { err, isErr, ok } from "../types/result";
10
+ import { base64url, sha256, utf8 } from "./crypto";
11
+ var DPOP_ASYMMETRIC_ALGS = new Set(["ES256", "EdDSA"]);
12
+ var DEFAULT_DPOP_IAT_TOLERANCE_SEC = 60;
13
+ var DEFAULT_DPOP_JTI_TTL_MS = 2 * DEFAULT_DPOP_IAT_TOLERANCE_SEC * 1000;
14
+ async function verifyDpopProof(input, deps) {
15
+ if (!deps.tokenStore.recordDpopJti) {
16
+ return err(authError.invalidDpopProof("token-store adapter does not support DPoP replay protection"));
17
+ }
18
+ let header;
19
+ try {
20
+ header = decodeProtectedHeader(input.proofJwt);
21
+ } catch (e) {
22
+ return err(authError.invalidDpopProof(`dpop proof header is not a valid compact JWS: ${stringifyError(e)}`));
23
+ }
24
+ if (header.typ !== "dpop+jwt") {
25
+ return err(authError.invalidDpopProof(`dpop proof header.typ must be "dpop+jwt", got "${String(header.typ)}"`));
26
+ }
27
+ if (!header.alg || !DPOP_ASYMMETRIC_ALGS.has(header.alg)) {
28
+ return err(authError.invalidDpopProof(`dpop proof alg "${String(header.alg)}" not in {ES256, EdDSA}`));
29
+ }
30
+ const jwk = header.jwk;
31
+ if (!jwk || typeof jwk !== "object") {
32
+ return err(authError.invalidDpopProof("dpop proof header.jwk missing or not an object"));
33
+ }
34
+ if ("d" in jwk) {
35
+ return err(authError.invalidDpopProof("dpop proof header.jwk must be a public key (private material present)"));
36
+ }
37
+ let key;
38
+ try {
39
+ key = await importJWK(jwk, header.alg);
40
+ } catch (e) {
41
+ return err(authError.invalidDpopProof(`dpop proof JWK import failed: ${stringifyError(e)}`));
42
+ }
43
+ let verified;
44
+ try {
45
+ verified = await jwtVerify(input.proofJwt, key, {
46
+ algorithms: [header.alg]
47
+ });
48
+ } catch (e) {
49
+ return err(authError.invalidDpopProof(`dpop proof signature verification failed: ${stringifyError(e)}`));
50
+ }
51
+ const payload = verified.payload;
52
+ if (typeof payload.htm !== "string") {
53
+ return err(authError.invalidDpopProof("dpop proof missing string htm"));
54
+ }
55
+ if (typeof payload.htu !== "string") {
56
+ return err(authError.invalidDpopProof("dpop proof missing string htu"));
57
+ }
58
+ if (typeof payload.iat !== "number") {
59
+ return err(authError.invalidDpopProof("dpop proof missing numeric iat"));
60
+ }
61
+ if (typeof payload.jti !== "string" || payload.jti.length === 0) {
62
+ return err(authError.invalidDpopProof("dpop proof missing non-empty string jti"));
63
+ }
64
+ if (payload.htm !== input.htm) {
65
+ return err(authError.invalidDpopProof(`dpop htm "${payload.htm}" does not match request method "${input.htm}"`));
66
+ }
67
+ if (payload.htu !== input.htu) {
68
+ return err(authError.invalidDpopProof(`dpop htu "${payload.htu}" does not match request uri "${input.htu}"`));
69
+ }
70
+ const tolerance = input.iatToleranceSec ?? DEFAULT_DPOP_IAT_TOLERANCE_SEC;
71
+ const skew = Math.abs(input.nowSec - payload.iat);
72
+ if (skew > tolerance) {
73
+ return err(authError.invalidDpopProof(`dpop iat skew ${skew}s exceeds tolerance ${tolerance}s`));
74
+ }
75
+ if (input.expectedAth !== undefined) {
76
+ if (typeof payload.ath !== "string" || payload.ath !== input.expectedAth) {
77
+ return err(authError.invalidDpopProof("dpop ath does not match SHA-256 of the access token"));
78
+ }
79
+ }
80
+ const recorded = await deps.tokenStore.recordDpopJti(payload.jti, deps.jtiTtlMs ?? DEFAULT_DPOP_JTI_TTL_MS);
81
+ if (isErr(recorded)) {
82
+ return err(authError.invalidDpopProof(`dpop jti "${payload.jti}" replayed`, {
83
+ jti: payload.jti
84
+ }));
85
+ }
86
+ let jkt;
87
+ try {
88
+ jkt = await calculateJwkThumbprint(jwk, "sha256");
89
+ } catch (e) {
90
+ return err(authError.invalidDpopProof(`dpop jkt thumbprint computation failed: ${stringifyError(e)}`));
91
+ }
92
+ return ok({
93
+ jkt,
94
+ payload: {
95
+ htm: payload.htm,
96
+ htu: payload.htu,
97
+ iat: payload.iat,
98
+ jti: payload.jti,
99
+ ...payload.ath !== undefined ? { ath: payload.ath } : {},
100
+ ...payload.nonce !== undefined ? { nonce: payload.nonce } : {}
101
+ }
102
+ });
103
+ }
104
+ function canonicalHtu(rawUrl) {
105
+ const u = new URL(rawUrl);
106
+ u.search = "";
107
+ u.hash = "";
108
+ return u.toString();
109
+ }
110
+ async function computeAth(accessToken) {
111
+ return base64url.encode(await sha256(utf8.encode(accessToken)));
112
+ }
113
+ function stringifyError(e) {
114
+ return e instanceof Error ? e.message : String(e);
115
+ }
116
+ export {
117
+ DEFAULT_DPOP_IAT_TOLERANCE_SEC,
118
+ DEFAULT_DPOP_JTI_TTL_MS,
119
+ canonicalHtu,
120
+ computeAth,
121
+ verifyDpopProof
122
+ };
@@ -0,0 +1,94 @@
1
+ // src/domain/id-token.ts
2
+ import { base64url, sha256, utf8 } from "./crypto";
3
+ var SCOPE_TO_CLAIMS = {
4
+ profile: [
5
+ "name",
6
+ "given_name",
7
+ "family_name",
8
+ "middle_name",
9
+ "nickname",
10
+ "preferred_username",
11
+ "profile",
12
+ "picture",
13
+ "website",
14
+ "gender",
15
+ "birthdate",
16
+ "zoneinfo",
17
+ "locale",
18
+ "updated_at"
19
+ ],
20
+ email: ["email", "email_verified"],
21
+ phone: ["phone_number", "phone_number_verified"],
22
+ address: ["address"]
23
+ };
24
+ function deriveAmr(methodKind) {
25
+ switch (methodKind) {
26
+ case "password":
27
+ return ["pwd"];
28
+ case "code":
29
+ return ["otp"];
30
+ case "passkey":
31
+ return ["hwk"];
32
+ default:
33
+ return;
34
+ }
35
+ }
36
+ async function computeAtHash(accessToken) {
37
+ const digest = await sha256(utf8.encode(accessToken));
38
+ const half = digest.slice(0, digest.byteLength / 2);
39
+ return base64url.encode(half);
40
+ }
41
+ function pickScopedClaims(claim, scopes, extra = [], customMappings = {}) {
42
+ const props = claim.properties;
43
+ if (!props || typeof props !== "object")
44
+ return {};
45
+ const mappings = {
46
+ ...customMappings,
47
+ ...SCOPE_TO_CLAIMS
48
+ };
49
+ const granted = new Set;
50
+ for (const scope of scopes) {
51
+ const list = mappings[scope];
52
+ if (!list)
53
+ continue;
54
+ for (const c of list)
55
+ granted.add(c);
56
+ }
57
+ for (const name of extra)
58
+ granted.add(name);
59
+ const out = {};
60
+ for (const key of granted) {
61
+ if (key in props && props[key] !== undefined)
62
+ out[key] = props[key];
63
+ }
64
+ return out;
65
+ }
66
+ var DEFAULT_ID_TOKEN_TTL_MS = 5 * 60 * 1000;
67
+ async function buildIdTokenClaims(input) {
68
+ const ttl = input.ttlMs ?? DEFAULT_ID_TOKEN_TTL_MS;
69
+ const amr = deriveAmr(input.methodKind);
70
+ const extraClaims = Object.keys(input.claimsRequest?.id_token ?? {});
71
+ return {
72
+ iss: input.issuerUrl,
73
+ sub: input.subjectId,
74
+ aud: input.audience,
75
+ exp: Math.floor((input.now + ttl) / 1000),
76
+ iat: Math.floor(input.now / 1000),
77
+ auth_time: input.authTime,
78
+ at_hash: await computeAtHash(input.accessToken),
79
+ ...input.appNonce !== undefined ? { nonce: input.appNonce } : {},
80
+ ...amr ? { amr } : {},
81
+ ...pickScopedClaims(input.claim, input.scopes, extraClaims, input.customScopeClaims ?? {})
82
+ };
83
+ }
84
+ function shouldIssueIdToken(scopes) {
85
+ return scopes.includes("openid");
86
+ }
87
+ export {
88
+ DEFAULT_ID_TOKEN_TTL_MS,
89
+ buildIdTokenClaims,
90
+ computeAtHash,
91
+ deriveAmr,
92
+ pickScopedClaims,
93
+ shouldIssueIdToken
94
+ };
@@ -32,6 +32,7 @@ async function introspect(req, deps) {
32
32
  if (claims.aud !== req.clientId) {
33
33
  return ok({ active: false });
34
34
  }
35
+ const subjectType = claims.claim?.type;
35
36
  return ok({
36
37
  active: true,
37
38
  sub: claims.sub,
@@ -41,9 +42,12 @@ async function introspect(req, deps) {
41
42
  iat: claims.iat,
42
43
  scope: claims.scope,
43
44
  client_id: claims.aud,
45
+ token_type: claims.cnf?.jkt !== undefined ? "DPoP" : "Bearer",
44
46
  tid: claims.tid,
45
47
  mid: claims.mid,
46
- mkind: claims.mkind
48
+ mkind: claims.mkind,
49
+ ...subjectType !== undefined ? { subject_type: subjectType } : {},
50
+ ...claims.cnf !== undefined ? { cnf: claims.cnf } : {}
47
51
  });
48
52
  }
49
53
  export {
@@ -3,6 +3,32 @@ import { importJWK, jwtVerify, SignJWT } from "jose";
3
3
  async function signAccessToken(claims, privateKey, alg, kid) {
4
4
  return new SignJWT(claims).setProtectedHeader({ alg, typ: "JWT", kid }).sign(privateKey);
5
5
  }
6
+ async function signIdToken(claims, privateKey, alg, kid) {
7
+ return new SignJWT(claims).setProtectedHeader({ alg, typ: "JWT", kid }).sign(privateKey);
8
+ }
9
+ async function verifyIdToken(token, keys, options = {}) {
10
+ const algorithms = Array.from(new Set(keys.map((k) => k.alg).filter((a) => ASYMMETRIC_ALGS.has(a))));
11
+ const { payload } = await jwtVerify(token, async (header) => {
12
+ if (!header.alg || header.alg === "none") {
13
+ throw new Error(`verifyIdToken: refusing alg "${header.alg ?? ""}"`);
14
+ }
15
+ const match = keys.find((k) => k.kid === header.kid);
16
+ if (!match) {
17
+ throw new Error(`verifyIdToken: unknown kid "${header.kid}"`);
18
+ }
19
+ if (match.alg !== header.alg) {
20
+ throw new Error(`verifyIdToken: header.alg "${header.alg}" does not match key alg "${match.alg}"`);
21
+ }
22
+ const imported = await importJWK(match.publicJwk, match.alg);
23
+ return imported;
24
+ }, {
25
+ algorithms,
26
+ ...options.issuer ? { issuer: options.issuer } : {},
27
+ ...options.audience ? { audience: options.audience } : {},
28
+ ...options.acceptExpired ? { clockTolerance: Number.MAX_SAFE_INTEGER } : {}
29
+ });
30
+ return payload;
31
+ }
6
32
  var ASYMMETRIC_ALGS = new Set(["ES256", "EdDSA"]);
7
33
  async function verifyAccessToken(token, keys, options = {}) {
8
34
  const algorithms = Array.from(new Set(keys.map((k) => k.alg).filter((a) => ASYMMETRIC_ALGS.has(a))));
@@ -37,7 +63,9 @@ function buildJwksDocument(keys) {
37
63
  };
38
64
  }
39
65
  export {
40
- verifyAccessToken,
66
+ buildJwksDocument,
41
67
  signAccessToken,
42
- buildJwksDocument
68
+ signIdToken,
69
+ verifyAccessToken,
70
+ verifyIdToken
43
71
  };
@@ -0,0 +1,75 @@
1
+ // src/domain/logout.ts
2
+ import { authError } from "../types/error";
3
+ import { err, isErr, ok } from "../types/result";
4
+ import { safeAudit } from "./audit";
5
+ import { verifyIdToken } from "./jwt";
6
+ import { revokeAllForSubject } from "./revoke";
7
+ async function endSession(req, tenant, deps) {
8
+ let hintSubject;
9
+ let hintAud;
10
+ if (req.idTokenHint) {
11
+ const keysRes = await deps.keyStore.signingKeys();
12
+ if (isErr(keysRes))
13
+ return err(keysRes.error);
14
+ try {
15
+ const claims = await verifyIdToken(req.idTokenHint, keysRes.value, {
16
+ issuer: deps.issuerUrl,
17
+ acceptExpired: true
18
+ });
19
+ hintSubject = claims.sub;
20
+ hintAud = claims.aud;
21
+ } catch (e) {
22
+ const reason = e instanceof Error ? e.message : String(e);
23
+ return err(authError.invalidRequest(`id_token_hint failed signature/issuer verification: ${reason}`, "id_token_hint"));
24
+ }
25
+ }
26
+ if (req.clientId && hintAud && req.clientId !== hintAud) {
27
+ return err(authError.invalidRequest("client_id does not match id_token_hint.aud", "client_id"));
28
+ }
29
+ const resolvedClientId = req.clientId ?? hintAud;
30
+ let client;
31
+ if (resolvedClientId) {
32
+ client = tenant.config.clients.find((c) => c.id === resolvedClientId);
33
+ if (!client) {
34
+ return err(authError.invalidRequest(`unknown client "${resolvedClientId}"`, "client_id"));
35
+ }
36
+ }
37
+ if (req.postLogoutRedirectUri) {
38
+ if (!client) {
39
+ return err(authError.invalidRequest("post_logout_redirect_uri requires identifying the client via client_id or id_token_hint", "post_logout_redirect_uri"));
40
+ }
41
+ const registered = client.postLogoutRedirectUris ?? [];
42
+ if (!registered.includes(req.postLogoutRedirectUri)) {
43
+ return err(authError.invalidRequest("post_logout_redirect_uri is not registered for this client", "post_logout_redirect_uri"));
44
+ }
45
+ }
46
+ if (hintSubject) {
47
+ const revoked = await revokeAllForSubject(tenant.id, hintSubject, {
48
+ tokenStore: deps.tokenStore,
49
+ ...deps.auditLog ? { auditLog: deps.auditLog } : {},
50
+ clock: deps.clock
51
+ });
52
+ if (isErr(revoked))
53
+ return err(revoked.error);
54
+ }
55
+ await safeAudit(deps, {
56
+ kind: "session_logout",
57
+ tenantId: tenant.id,
58
+ ...resolvedClientId ? { clientId: resolvedClientId } : {},
59
+ ...hintSubject ? { subjectId: hintSubject } : {},
60
+ timestamp: deps.clock()
61
+ });
62
+ if (req.postLogoutRedirectUri) {
63
+ const url = new URL(req.postLogoutRedirectUri);
64
+ if (req.state !== undefined)
65
+ url.searchParams.set("state", req.state);
66
+ return ok({ kind: "redirect", url: url.toString() });
67
+ }
68
+ return ok({
69
+ kind: "ok",
70
+ ...hintSubject ? { subjectId: hintSubject } : {}
71
+ });
72
+ }
73
+ export {
74
+ endSession
75
+ };