@atproto/oauth-provider 0.4.0 → 0.5.1

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 (402) hide show
  1. package/.linguirc +57 -0
  2. package/CHANGELOG.md +29 -0
  3. package/dist/account/account-manager.d.ts +17 -3
  4. package/dist/account/account-manager.d.ts.map +1 -1
  5. package/dist/account/account-manager.js +102 -8
  6. package/dist/account/account-manager.js.map +1 -1
  7. package/dist/account/account-store.d.ts +81 -15
  8. package/dist/account/account-store.d.ts.map +1 -1
  9. package/dist/account/account-store.js +70 -19
  10. package/dist/account/account-store.js.map +1 -1
  11. package/dist/account/sign-in-data.d.ts +28 -0
  12. package/dist/account/sign-in-data.d.ts.map +1 -0
  13. package/dist/account/sign-in-data.js +16 -0
  14. package/dist/account/sign-in-data.js.map +1 -0
  15. package/dist/account/sign-up-data.d.ts +26 -0
  16. package/dist/account/sign-up-data.d.ts.map +1 -0
  17. package/dist/account/sign-up-data.js +11 -0
  18. package/dist/account/sign-up-data.js.map +1 -0
  19. package/dist/assets/app/bundle-manifest.json +598 -6
  20. package/dist/assets/app/index-ItwwtJ8r.js +36 -0
  21. package/dist/assets/app/index-ItwwtJ8r.js.map +1 -0
  22. package/dist/assets/app/main-B_dNxQo_.js +4 -0
  23. package/dist/assets/app/main-B_dNxQo_.js.map +1 -0
  24. package/dist/assets/app/main-CSatvmRR.css +3 -0
  25. package/dist/assets/app/main-CSatvmRR.js +306 -0
  26. package/dist/assets/app/main-CSatvmRR.js.map +1 -0
  27. package/dist/assets/app/messages-BQeltXSF.js +4 -0
  28. package/dist/assets/app/messages-BQeltXSF.js.map +1 -0
  29. package/dist/assets/app/messages-BQkEhfjg.js +4 -0
  30. package/dist/assets/app/messages-BQkEhfjg.js.map +1 -0
  31. package/dist/assets/app/messages-BUjKj_UJ.js +4 -0
  32. package/dist/assets/app/messages-BUjKj_UJ.js.map +1 -0
  33. package/dist/assets/app/messages-BWIQa8fO.js +4 -0
  34. package/dist/assets/app/messages-BWIQa8fO.js.map +1 -0
  35. package/dist/assets/app/messages-BaNVb0bp.js +4 -0
  36. package/dist/assets/app/messages-BaNVb0bp.js.map +1 -0
  37. package/dist/assets/app/messages-BaizVXcF.js +4 -0
  38. package/dist/assets/app/messages-BaizVXcF.js.map +1 -0
  39. package/dist/assets/app/messages-BfoClA1Y.js +4 -0
  40. package/dist/assets/app/messages-BfoClA1Y.js.map +1 -0
  41. package/dist/assets/app/messages-BsKGDZnC.js +4 -0
  42. package/dist/assets/app/messages-BsKGDZnC.js.map +1 -0
  43. package/dist/assets/app/messages-Bu-TJhml.js +4 -0
  44. package/dist/assets/app/messages-Bu-TJhml.js.map +1 -0
  45. package/dist/assets/app/messages-BvOKnBQk.js +4 -0
  46. package/dist/assets/app/messages-BvOKnBQk.js.map +1 -0
  47. package/dist/assets/app/messages-BxDzCiWz.js +4 -0
  48. package/dist/assets/app/messages-BxDzCiWz.js.map +1 -0
  49. package/dist/assets/app/messages-CDgFOy4S.js +4 -0
  50. package/dist/assets/app/messages-CDgFOy4S.js.map +1 -0
  51. package/dist/assets/app/messages-CLbTz0o9.js +4 -0
  52. package/dist/assets/app/messages-CLbTz0o9.js.map +1 -0
  53. package/dist/assets/app/messages-CNwSh0t7.js +4 -0
  54. package/dist/assets/app/messages-CNwSh0t7.js.map +1 -0
  55. package/dist/assets/app/messages-CSMNJ6P8.js +4 -0
  56. package/dist/assets/app/messages-CSMNJ6P8.js.map +1 -0
  57. package/dist/assets/app/messages-CZQUw3mp.js +4 -0
  58. package/dist/assets/app/messages-CZQUw3mp.js.map +1 -0
  59. package/dist/assets/app/messages-CZT41oVp.js +4 -0
  60. package/dist/assets/app/messages-CZT41oVp.js.map +1 -0
  61. package/dist/assets/app/messages-C_b-d3t8.js +4 -0
  62. package/dist/assets/app/messages-C_b-d3t8.js.map +1 -0
  63. package/dist/assets/app/messages-C_u3MTc2.js +4 -0
  64. package/dist/assets/app/messages-C_u3MTc2.js.map +1 -0
  65. package/dist/assets/app/messages-Cn8nHZic.js +4 -0
  66. package/dist/assets/app/messages-Cn8nHZic.js.map +1 -0
  67. package/dist/assets/app/messages-CtDywJUm.js +4 -0
  68. package/dist/assets/app/messages-CtDywJUm.js.map +1 -0
  69. package/dist/assets/app/messages-CurtIjBF.js +4 -0
  70. package/dist/assets/app/messages-CurtIjBF.js.map +1 -0
  71. package/dist/assets/app/messages-Cv6zIbaP.js +4 -0
  72. package/dist/assets/app/messages-Cv6zIbaP.js.map +1 -0
  73. package/dist/assets/app/messages-D1eLQuPE.js +4 -0
  74. package/dist/assets/app/messages-D1eLQuPE.js.map +1 -0
  75. package/dist/assets/app/messages-D8vHEaYW.js +4 -0
  76. package/dist/assets/app/messages-D8vHEaYW.js.map +1 -0
  77. package/dist/assets/app/messages-DJ1Q4GeC.js +4 -0
  78. package/dist/assets/app/messages-DJ1Q4GeC.js.map +1 -0
  79. package/dist/assets/app/messages-DRL3exqd.js +4 -0
  80. package/dist/assets/app/messages-DRL3exqd.js.map +1 -0
  81. package/dist/assets/app/messages-DWLPQRTp.js +4 -0
  82. package/dist/assets/app/messages-DWLPQRTp.js.map +1 -0
  83. package/dist/assets/app/messages-DjVaE9YE.js +4 -0
  84. package/dist/assets/app/messages-DjVaE9YE.js.map +1 -0
  85. package/dist/assets/app/messages-DqpMfFJR.js +4 -0
  86. package/dist/assets/app/messages-DqpMfFJR.js.map +1 -0
  87. package/dist/assets/app/messages-ETjhJBEN.js +4 -0
  88. package/dist/assets/app/messages-ETjhJBEN.js.map +1 -0
  89. package/dist/assets/app/messages-EUKrgrGn.js +4 -0
  90. package/dist/assets/app/messages-EUKrgrGn.js.map +1 -0
  91. package/dist/assets/app/messages-QQrOUcPW.js +4 -0
  92. package/dist/assets/app/messages-QQrOUcPW.js.map +1 -0
  93. package/dist/assets/app/messages-e2QGqFL6.js +4 -0
  94. package/dist/assets/app/messages-e2QGqFL6.js.map +1 -0
  95. package/dist/assets/app/messages-p61py7gD.js +4 -0
  96. package/dist/assets/app/messages-p61py7gD.js.map +1 -0
  97. package/dist/assets/asset.d.ts +1 -0
  98. package/dist/assets/asset.d.ts.map +1 -1
  99. package/dist/assets/assets-middleware.d.ts.map +1 -1
  100. package/dist/assets/assets-middleware.js +12 -7
  101. package/dist/assets/assets-middleware.js.map +1 -1
  102. package/dist/assets/index.d.ts +3 -2
  103. package/dist/assets/index.d.ts.map +1 -1
  104. package/dist/assets/index.js +13 -1
  105. package/dist/assets/index.js.map +1 -1
  106. package/dist/client/client-store.d.ts +3 -3
  107. package/dist/client/client-store.d.ts.map +1 -1
  108. package/dist/client/client-store.js +6 -5
  109. package/dist/client/client-store.js.map +1 -1
  110. package/dist/device/device-manager.d.ts +9 -8
  111. package/dist/device/device-manager.d.ts.map +1 -1
  112. package/dist/device/device-manager.js.map +1 -1
  113. package/dist/device/device-store.d.ts +3 -3
  114. package/dist/device/device-store.d.ts.map +1 -1
  115. package/dist/device/device-store.js +10 -9
  116. package/dist/device/device-store.js.map +1 -1
  117. package/dist/dpop/dpop-manager.d.ts +15 -7
  118. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  119. package/dist/dpop/dpop-manager.js +17 -3
  120. package/dist/dpop/dpop-manager.js.map +1 -1
  121. package/dist/dpop/dpop-nonce.d.ts +11 -5
  122. package/dist/dpop/dpop-nonce.d.ts.map +1 -1
  123. package/dist/dpop/dpop-nonce.js +47 -38
  124. package/dist/dpop/dpop-nonce.js.map +1 -1
  125. package/dist/errors/handle-unavailable-error.d.ts +11 -0
  126. package/dist/errors/handle-unavailable-error.d.ts.map +1 -0
  127. package/dist/errors/handle-unavailable-error.js +19 -0
  128. package/dist/errors/handle-unavailable-error.js.map +1 -0
  129. package/dist/errors/invalid-request-error.d.ts +6 -8
  130. package/dist/errors/invalid-request-error.d.ts.map +1 -1
  131. package/dist/errors/invalid-request-error.js +10 -8
  132. package/dist/errors/invalid-request-error.js.map +1 -1
  133. package/dist/lib/csp/index.d.ts +18 -0
  134. package/dist/lib/csp/index.d.ts.map +1 -0
  135. package/dist/lib/csp/index.js +72 -0
  136. package/dist/lib/csp/index.js.map +1 -0
  137. package/dist/lib/hcaptcha.d.ts +177 -0
  138. package/dist/lib/hcaptcha.d.ts.map +1 -0
  139. package/dist/lib/hcaptcha.js +155 -0
  140. package/dist/lib/hcaptcha.js.map +1 -0
  141. package/dist/lib/html/build-document.d.ts +11 -3
  142. package/dist/lib/html/build-document.d.ts.map +1 -1
  143. package/dist/lib/html/build-document.js +51 -15
  144. package/dist/lib/html/build-document.js.map +1 -1
  145. package/dist/lib/http/middleware.d.ts.map +1 -1
  146. package/dist/lib/http/middleware.js +4 -1
  147. package/dist/lib/http/middleware.js.map +1 -1
  148. package/dist/lib/http/request.d.ts +5 -2
  149. package/dist/lib/http/request.d.ts.map +1 -1
  150. package/dist/lib/http/request.js +16 -1
  151. package/dist/lib/http/request.js.map +1 -1
  152. package/dist/lib/http/response.d.ts +4 -2
  153. package/dist/lib/http/response.d.ts.map +1 -1
  154. package/dist/lib/http/response.js +23 -5
  155. package/dist/lib/http/response.js.map +1 -1
  156. package/dist/lib/locale.d.ts +15 -0
  157. package/dist/lib/locale.d.ts.map +1 -0
  158. package/dist/lib/locale.js +17 -0
  159. package/dist/lib/locale.js.map +1 -0
  160. package/dist/lib/util/function.d.ts +2 -2
  161. package/dist/lib/util/function.d.ts.map +1 -1
  162. package/dist/lib/util/function.js.map +1 -1
  163. package/dist/lib/util/type.d.ts +88 -1
  164. package/dist/lib/util/type.d.ts.map +1 -1
  165. package/dist/lib/util/type.js +41 -0
  166. package/dist/lib/util/type.js.map +1 -1
  167. package/dist/metadata/build-metadata.d.ts +2 -2
  168. package/dist/metadata/build-metadata.d.ts.map +1 -1
  169. package/dist/metadata/build-metadata.js.map +1 -1
  170. package/dist/oauth-errors.d.ts +1 -0
  171. package/dist/oauth-errors.d.ts.map +1 -1
  172. package/dist/oauth-errors.js +3 -1
  173. package/dist/oauth-errors.js.map +1 -1
  174. package/dist/oauth-hooks.d.ts +60 -3
  175. package/dist/oauth-hooks.d.ts.map +1 -1
  176. package/dist/oauth-hooks.js +3 -3
  177. package/dist/oauth-hooks.js.map +1 -1
  178. package/dist/oauth-provider.d.ts +23 -18
  179. package/dist/oauth-provider.d.ts.map +1 -1
  180. package/dist/oauth-provider.js +207 -204
  181. package/dist/oauth-provider.js.map +1 -1
  182. package/dist/oauth-verifier.d.ts +1 -1
  183. package/dist/oauth-verifier.d.ts.map +1 -1
  184. package/dist/oauth-verifier.js +2 -1
  185. package/dist/oauth-verifier.js.map +1 -1
  186. package/dist/output/build-authorize-data.d.ts +0 -1
  187. package/dist/output/build-authorize-data.d.ts.map +1 -1
  188. package/dist/output/build-authorize-data.js +0 -1
  189. package/dist/output/build-authorize-data.js.map +1 -1
  190. package/dist/output/build-customization-data.d.ts +241 -0
  191. package/dist/output/build-customization-data.d.ts.map +1 -0
  192. package/dist/output/build-customization-data.js +174 -0
  193. package/dist/output/build-customization-data.js.map +1 -0
  194. package/dist/output/output-manager.d.ts +16 -9
  195. package/dist/output/output-manager.d.ts.map +1 -1
  196. package/dist/output/output-manager.js +78 -42
  197. package/dist/output/output-manager.js.map +1 -1
  198. package/dist/output/send-authorize-redirect.d.ts +9 -6
  199. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  200. package/dist/output/send-authorize-redirect.js +20 -14
  201. package/dist/output/send-authorize-redirect.js.map +1 -1
  202. package/dist/output/send-web-page.d.ts +7 -2
  203. package/dist/output/send-web-page.d.ts.map +1 -1
  204. package/dist/output/send-web-page.js +37 -21
  205. package/dist/output/send-web-page.js.map +1 -1
  206. package/dist/request/request-manager.d.ts +1 -1
  207. package/dist/request/request-manager.d.ts.map +1 -1
  208. package/dist/request/request-manager.js +4 -4
  209. package/dist/request/request-manager.js.map +1 -1
  210. package/dist/request/request-store.d.ts +3 -3
  211. package/dist/request/request-store.d.ts.map +1 -1
  212. package/dist/request/request-store.js +11 -10
  213. package/dist/request/request-store.js.map +1 -1
  214. package/dist/token/token-store.d.ts +4 -4
  215. package/dist/token/token-store.d.ts.map +1 -1
  216. package/dist/token/token-store.js +13 -12
  217. package/dist/token/token-store.js.map +1 -1
  218. package/package.json +43 -20
  219. package/rollup.config.js +61 -17
  220. package/src/account/account-manager.ts +159 -8
  221. package/src/account/account-store.ts +127 -32
  222. package/src/account/sign-in-data.ts +15 -0
  223. package/src/account/sign-up-data.ts +11 -0
  224. package/src/assets/app/app.tsx +31 -16
  225. package/src/assets/app/backend-data.ts +15 -60
  226. package/src/assets/app/backend-types.ts +66 -0
  227. package/src/assets/app/components/forms/button-toggle-visibility.tsx +43 -0
  228. package/src/assets/app/components/forms/button.tsx +60 -0
  229. package/src/assets/app/components/forms/fieldset.tsx +55 -0
  230. package/src/assets/app/components/forms/form-card-async.tsx +103 -0
  231. package/src/assets/app/components/forms/form-card.tsx +49 -0
  232. package/src/assets/app/components/forms/input-checkbox.tsx +73 -0
  233. package/src/assets/app/components/forms/input-container.tsx +107 -0
  234. package/src/assets/app/components/forms/input-email-address.tsx +66 -0
  235. package/src/assets/app/components/forms/input-new-password.tsx +62 -0
  236. package/src/assets/app/components/forms/input-password.tsx +88 -0
  237. package/src/assets/app/components/forms/input-text.tsx +76 -0
  238. package/src/assets/app/components/forms/input-token.tsx +94 -0
  239. package/src/assets/app/components/forms/wizard-card.tsx +116 -0
  240. package/src/assets/app/components/layouts/layout-title-page.tsx +77 -0
  241. package/src/assets/app/components/layouts/layout-welcome.tsx +73 -0
  242. package/src/assets/app/components/utils/account-identifier.tsx +23 -0
  243. package/src/assets/app/components/utils/account-image.tsx +33 -0
  244. package/src/assets/app/components/utils/admonition.tsx +52 -0
  245. package/src/assets/app/components/utils/client-name.tsx +45 -0
  246. package/src/assets/app/components/utils/error-card.tsx +93 -0
  247. package/src/assets/app/components/utils/error-message.tsx +62 -0
  248. package/src/assets/app/components/utils/help-card.tsx +46 -0
  249. package/src/assets/app/components/utils/icons.tsx +88 -0
  250. package/src/assets/app/components/utils/link-anchor.tsx +28 -0
  251. package/src/assets/app/components/utils/link-title.tsx +26 -0
  252. package/src/assets/app/components/utils/multi-lang-string.tsx +56 -0
  253. package/src/assets/app/components/utils/password-strength-label.tsx +37 -0
  254. package/src/assets/app/components/utils/password-strength-meter.tsx +58 -0
  255. package/src/assets/app/components/{url-viewer.tsx → utils/url-viewer.tsx} +9 -6
  256. package/src/assets/app/hooks/use-api.ts +128 -55
  257. package/src/assets/app/hooks/use-async-action.ts +120 -0
  258. package/src/assets/app/hooks/use-browser-color-scheme.ts +31 -0
  259. package/src/assets/app/hooks/use-csrf-token.ts +1 -1
  260. package/src/assets/app/hooks/use-random-string.ts +37 -0
  261. package/src/assets/app/hooks/use-stepper.ts +87 -0
  262. package/src/assets/app/index.html +182 -0
  263. package/src/assets/app/lib/api.ts +248 -79
  264. package/src/assets/app/lib/clsx.ts +5 -8
  265. package/src/assets/app/lib/json-client.ts +94 -0
  266. package/src/assets/app/lib/password.ts +98 -0
  267. package/src/assets/app/lib/ref.ts +17 -0
  268. package/src/assets/app/locales/an/messages.po +492 -0
  269. package/src/assets/app/locales/ast/messages.po +492 -0
  270. package/src/assets/app/locales/ca/messages.po +492 -0
  271. package/src/assets/app/locales/da/messages.po +492 -0
  272. package/src/assets/app/locales/de/messages.po +492 -0
  273. package/src/assets/app/locales/el/messages.po +492 -0
  274. package/src/assets/app/locales/en/messages.po +492 -0
  275. package/src/assets/app/locales/en-GB/messages.po +492 -0
  276. package/src/assets/app/locales/es/messages.po +492 -0
  277. package/src/assets/app/locales/eu/messages.po +492 -0
  278. package/src/assets/app/locales/fi/messages.po +492 -0
  279. package/src/assets/app/locales/fr/messages.po +492 -0
  280. package/src/assets/app/locales/ga/messages.po +492 -0
  281. package/src/assets/app/locales/gl/messages.po +492 -0
  282. package/src/assets/app/locales/hi/messages.po +492 -0
  283. package/src/assets/app/locales/hu/messages.po +492 -0
  284. package/src/assets/app/locales/ia/messages.po +492 -0
  285. package/src/assets/app/locales/id/messages.po +492 -0
  286. package/src/assets/app/locales/it/messages.po +492 -0
  287. package/src/assets/app/locales/ja/messages.po +492 -0
  288. package/src/assets/app/locales/km/messages.po +492 -0
  289. package/src/assets/app/locales/ko/messages.po +492 -0
  290. package/src/assets/app/locales/load.ts +8 -0
  291. package/src/assets/app/locales/locale-context.ts +19 -0
  292. package/src/assets/app/locales/locale-provider.tsx +112 -0
  293. package/src/assets/app/locales/locale-selector.tsx +58 -0
  294. package/src/assets/app/locales/locales.ts +168 -0
  295. package/src/assets/app/locales/ne/messages.po +492 -0
  296. package/src/assets/app/locales/nl/messages.po +492 -0
  297. package/src/assets/app/locales/pl/messages.po +492 -0
  298. package/src/assets/app/locales/pt-BR/messages.po +492 -0
  299. package/src/assets/app/locales/ro/messages.po +492 -0
  300. package/src/assets/app/locales/ru/messages.po +492 -0
  301. package/src/assets/app/locales/sv/messages.po +492 -0
  302. package/src/assets/app/locales/th/messages.po +492 -0
  303. package/src/assets/app/locales/tr/messages.po +492 -0
  304. package/src/assets/app/locales/uk/messages.po +492 -0
  305. package/src/assets/app/locales/vi/messages.po +492 -0
  306. package/src/assets/app/locales/zh-CN/messages.po +492 -0
  307. package/src/assets/app/locales/zh-HK/messages.po +492 -0
  308. package/src/assets/app/locales/zh-TW/messages.po +492 -0
  309. package/src/assets/app/main.css +23 -2
  310. package/src/assets/app/main.tsx +24 -8
  311. package/src/assets/app/views/authorize/accept/accept-form.tsx +150 -0
  312. package/src/assets/app/views/authorize/accept/accept-view.tsx +70 -0
  313. package/src/assets/app/views/authorize/authorize-view.tsx +180 -0
  314. package/src/assets/app/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
  315. package/src/assets/app/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
  316. package/src/assets/app/views/authorize/reset-password/reset-password-view.tsx +127 -0
  317. package/src/assets/app/views/authorize/sign-in/sign-in-form.tsx +244 -0
  318. package/src/assets/app/views/authorize/sign-in/sign-in-picker.tsx +116 -0
  319. package/src/assets/app/views/authorize/sign-in/sign-in-view.tsx +145 -0
  320. package/src/assets/app/views/authorize/sign-up/sign-up-account-form.tsx +140 -0
  321. package/src/assets/app/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
  322. package/src/assets/app/views/authorize/sign-up/sign-up-handle-form.tsx +289 -0
  323. package/src/assets/app/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
  324. package/src/assets/app/views/authorize/sign-up/sign-up-view.tsx +158 -0
  325. package/src/assets/app/views/authorize/welcome/welcome-view.tsx +56 -0
  326. package/src/assets/app/views/error/error-view.tsx +31 -0
  327. package/src/assets/asset.ts +1 -0
  328. package/src/assets/assets-middleware.ts +13 -8
  329. package/src/assets/index.ts +15 -2
  330. package/src/client/client-store.ts +10 -12
  331. package/src/device/device-manager.ts +8 -12
  332. package/src/device/device-store.ts +9 -15
  333. package/src/dpop/dpop-manager.ts +20 -8
  334. package/src/dpop/dpop-nonce.ts +58 -40
  335. package/src/errors/handle-unavailable-error.ts +18 -0
  336. package/src/errors/invalid-request-error.ts +10 -8
  337. package/src/lib/csp/index.ts +98 -0
  338. package/src/lib/hcaptcha.ts +182 -0
  339. package/src/lib/html/build-document.ts +60 -16
  340. package/src/lib/http/middleware.ts +4 -3
  341. package/src/lib/http/request.ts +31 -1
  342. package/src/lib/http/response.ts +22 -9
  343. package/src/lib/locale.ts +21 -0
  344. package/src/lib/util/function.ts +0 -3
  345. package/src/lib/util/type.ts +130 -1
  346. package/src/metadata/build-metadata.ts +2 -1
  347. package/src/oauth-errors.ts +1 -0
  348. package/src/oauth-hooks.ts +69 -3
  349. package/src/oauth-provider.ts +403 -307
  350. package/src/oauth-verifier.ts +3 -1
  351. package/src/output/build-authorize-data.ts +1 -3
  352. package/src/output/build-customization-data.ts +228 -0
  353. package/src/output/output-manager.ts +111 -48
  354. package/src/output/send-authorize-redirect.ts +43 -36
  355. package/src/output/send-web-page.ts +40 -26
  356. package/src/request/request-manager.ts +4 -4
  357. package/src/request/request-store.ts +12 -16
  358. package/src/token/token-store.ts +14 -18
  359. package/tailwind.config.js +5 -0
  360. package/tsconfig.backend.tsbuildinfo +1 -1
  361. package/tsconfig.frontend.tsbuildinfo +1 -1
  362. package/tsconfig.tools.tsbuildinfo +1 -1
  363. package/vite.config.mjs +16 -0
  364. package/.postcssrc.yml +0 -3
  365. package/dist/assets/app/main.css +0 -3
  366. package/dist/assets/app/main.js +0 -20
  367. package/dist/assets/app/main.js.map +0 -1
  368. package/dist/output/customization.d.ts +0 -27
  369. package/dist/output/customization.d.ts.map +0 -1
  370. package/dist/output/customization.js +0 -88
  371. package/dist/output/customization.js.map +0 -1
  372. package/src/assets/app/components/accept-form.tsx +0 -137
  373. package/src/assets/app/components/account-identifier.tsx +0 -18
  374. package/src/assets/app/components/account-picker.tsx +0 -127
  375. package/src/assets/app/components/button.tsx +0 -34
  376. package/src/assets/app/components/client-name.tsx +0 -37
  377. package/src/assets/app/components/fieldset.tsx +0 -26
  378. package/src/assets/app/components/form-card.tsx +0 -47
  379. package/src/assets/app/components/help-card.tsx +0 -42
  380. package/src/assets/app/components/icons/alert-icon.tsx +0 -5
  381. package/src/assets/app/components/icons/at-symbol-icon.tsx +0 -5
  382. package/src/assets/app/components/icons/caret-right-icon.tsx +0 -5
  383. package/src/assets/app/components/icons/lock-icon.tsx +0 -5
  384. package/src/assets/app/components/icons/token-icon.tsx +0 -5
  385. package/src/assets/app/components/icons/util.tsx +0 -17
  386. package/src/assets/app/components/info-card.tsx +0 -45
  387. package/src/assets/app/components/input-checkbox.tsx +0 -47
  388. package/src/assets/app/components/input-container.tsx +0 -37
  389. package/src/assets/app/components/input-layout.tsx +0 -47
  390. package/src/assets/app/components/input-text.tsx +0 -69
  391. package/src/assets/app/components/layout-title-page.tsx +0 -60
  392. package/src/assets/app/components/layout-welcome.tsx +0 -74
  393. package/src/assets/app/components/sign-in-form.tsx +0 -337
  394. package/src/assets/app/components/sign-up-account-form.tsx +0 -194
  395. package/src/assets/app/components/sign-up-disclaimer.tsx +0 -44
  396. package/src/assets/app/views/accept-view.tsx +0 -55
  397. package/src/assets/app/views/authorize-view.tsx +0 -106
  398. package/src/assets/app/views/error-view.tsx +0 -36
  399. package/src/assets/app/views/sign-in-view.tsx +0 -111
  400. package/src/assets/app/views/sign-up-view.tsx +0 -86
  401. package/src/assets/app/views/welcome-view.tsx +0 -54
  402. package/src/output/customization.ts +0 -118
@@ -1,5 +1,4 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
- import { mediaType } from '@hapi/accept'
3
2
  import createHttpError from 'http-errors'
4
3
  import type { Redis, RedisOptions } from 'ioredis'
5
4
  import { ZodError, z } from 'zod'
@@ -39,14 +38,16 @@ import { AccountManager } from './account/account-manager.js'
39
38
  import {
40
39
  AccountStore,
41
40
  DeviceAccountInfo,
42
- SignInCredentials,
43
41
  asAccountStore,
44
- signInCredentialsSchema,
42
+ handleSchema,
43
+ resetPasswordConfirmDataSchema,
44
+ resetPasswordRequestDataSchema,
45
45
  } from './account/account-store.js'
46
46
  import { Account } from './account/account.js'
47
+ import { signInDataSchema } from './account/sign-in-data.js'
48
+ import { signUpDataSchema } from './account/sign-up-data.js'
47
49
  import { authorizeAssetsMiddleware } from './assets/assets-middleware.js'
48
50
  import { ClientAuth, authJwkThumbprint } from './client/client-auth.js'
49
- import { ClientId, clientIdSchema } from './client/client-id.js'
50
51
  import {
51
52
  ClientManager,
52
53
  LoopbackMetadataGetter,
@@ -55,7 +56,12 @@ import { ClientStore, ifClientStore } from './client/client-store.js'
55
56
  import { Client } from './client/client.js'
56
57
  import { AUTHENTICATION_MAX_AGE, TOKEN_MAX_AGE } from './constants.js'
57
58
  import { DeviceId } from './device/device-id.js'
58
- import { DeviceManager, DeviceManagerOptions } from './device/device-manager.js'
59
+ import {
60
+ DeviceInfo,
61
+ DeviceManager,
62
+ DeviceManagerOptions,
63
+ deviceManagerOptionsSchema,
64
+ } from './device/device-manager.js'
59
65
  import { DeviceStore, asDeviceStore } from './device/device-store.js'
60
66
  import { AccessDeniedError } from './errors/access-denied-error.js'
61
67
  import { AccountSelectionRequiredError } from './errors/account-selection-required-error.js'
@@ -65,13 +71,14 @@ import { InvalidGrantError } from './errors/invalid-grant-error.js'
65
71
  import { InvalidParametersError } from './errors/invalid-parameters-error.js'
66
72
  import { InvalidRequestError } from './errors/invalid-request-error.js'
67
73
  import { LoginRequiredError } from './errors/login-required-error.js'
68
- import { OAuthError } from './errors/oauth-error.js'
69
74
  import { UnauthorizedClientError } from './errors/unauthorized-client-error.js'
70
75
  import { WWWAuthenticateError } from './errors/www-authenticate-error.js'
76
+ import { HcaptchaConfig } from './lib/hcaptcha.js'
71
77
  import {
72
78
  Handler,
73
79
  Middleware,
74
80
  Router,
81
+ cacheControlMiddleware,
75
82
  combineMiddlewares,
76
83
  parseHttpRequest,
77
84
  setupCsrfToken,
@@ -84,18 +91,28 @@ import {
84
91
  validateSameOrigin,
85
92
  writeJson,
86
93
  } from './lib/http/index.js'
87
- import { RequestMetadata } from './lib/http/request.js'
94
+ import {
95
+ RequestMetadata,
96
+ extractLocales,
97
+ negotiateResponseContent as negotiateContent,
98
+ } from './lib/http/request.js'
88
99
  import { dateToEpoch, dateToRelativeSeconds } from './lib/util/date.js'
89
- import { Override } from './lib/util/type.js'
100
+ import { Awaitable, Override } from './lib/util/type.js'
90
101
  import { CustomMetadata, buildMetadata } from './metadata/build-metadata.js'
91
- import { OAuthHooks } from './oauth-hooks.js'
102
+ import { OAuthHooks, SignInData, SignUpData } from './oauth-hooks.js'
92
103
  import { OAuthVerifier, OAuthVerifierOptions } from './oauth-verifier.js'
93
104
  import { AuthorizationResultAuthorize } from './output/build-authorize-data.js'
105
+ import {
106
+ Branding,
107
+ BrandingInput,
108
+ Customization,
109
+ CustomizationInput,
110
+ customizationSchema,
111
+ } from './output/build-customization-data.js'
94
112
  import {
95
113
  buildErrorPayload,
96
114
  buildErrorStatus,
97
115
  } from './output/build-error-payload.js'
98
- import { Customization } from './output/customization.js'
99
116
  import { OutputManager } from './output/output-manager.js'
100
117
  import {
101
118
  AuthorizationResultRedirect,
@@ -114,32 +131,38 @@ import { TokenManager } from './token/token-manager.js'
114
131
  import { TokenStore, asTokenStore } from './token/token-store.js'
115
132
  import { VerifyTokenClaimsOptions } from './token/verify-token-claims.js'
116
133
 
117
- export type OAuthProviderStore = Partial<
118
- ClientStore &
119
- AccountStore &
120
- DeviceStore &
121
- TokenStore &
122
- RequestStore &
123
- ReplayStore
124
- >
125
-
126
134
  export {
135
+ type Branding,
136
+ type BrandingInput,
127
137
  type CustomMetadata,
128
138
  type Customization,
139
+ type CustomizationInput,
129
140
  type Handler,
141
+ type HcaptchaConfig,
130
142
  Keyset,
131
143
  type OAuthAuthorizationServerMetadata,
132
144
  }
133
145
 
146
+ type ApiContext = {
147
+ requestUri: RequestUri
148
+ deviceId: DeviceId
149
+ deviceMetadata: RequestMetadata
150
+ }
151
+
152
+ export type ErrorHandler<
153
+ Req extends IncomingMessage = IncomingMessage,
154
+ Res extends ServerResponse = ServerResponse,
155
+ > = (req: Req, res: Res, err: unknown, message: string) => void
156
+
134
157
  export type RouterOptions<
135
158
  Req extends IncomingMessage = IncomingMessage,
136
159
  Res extends ServerResponse = ServerResponse,
137
160
  > = {
138
- onError?: (req: Req, res: Res, err: unknown, message: string) => void
161
+ onError?: ErrorHandler<Req, Res>
139
162
  }
140
163
 
141
164
  export type OAuthProviderOptions = Override<
142
- OAuthVerifierOptions & OAuthHooks & DeviceManagerOptions,
165
+ OAuthVerifierOptions & OAuthHooks & DeviceManagerOptions & CustomizationInput,
143
166
  {
144
167
  /**
145
168
  * Maximum age a device/account session can be before requiring
@@ -157,11 +180,6 @@ export type OAuthProviderOptions = Override<
157
180
  */
158
181
  metadata?: CustomMetadata
159
182
 
160
- /**
161
- * UI customizations
162
- */
163
- customization?: Customization
164
-
165
183
  /**
166
184
  * A custom fetch function that can be used to fetch the client metadata from
167
185
  * the internet. By default, the fetch function is a safeFetchWrap() function
@@ -184,11 +202,18 @@ export type OAuthProviderOptions = Override<
184
202
  * this store implements all the interfaces not provided in the other
185
203
  * `<name>Store` options.
186
204
  */
187
- store?: OAuthProviderStore
205
+ store?: Partial<
206
+ AccountStore &
207
+ ClientStore &
208
+ DeviceStore &
209
+ ReplayStore &
210
+ RequestStore &
211
+ TokenStore
212
+ >
188
213
 
189
214
  accountStore?: AccountStore
190
- deviceStore?: DeviceStore
191
215
  clientStore?: ClientStore
216
+ deviceStore?: DeviceStore
192
217
  replayStore?: ReplayStore
193
218
  requestStore?: RequestStore
194
219
  tokenStore?: TokenStore
@@ -235,7 +260,6 @@ export class OAuthProvider extends OAuthVerifier {
235
260
 
236
261
  public constructor({
237
262
  metadata,
238
- customization = undefined,
239
263
  authenticationMaxAge = AUTHENTICATION_MAX_AGE,
240
264
  tokenMaxAge = TOKEN_MAX_AGE,
241
265
 
@@ -264,10 +288,30 @@ export class OAuthProvider extends OAuthVerifier {
264
288
 
265
289
  loopbackMetadata = atprotoLoopbackClientMetadata,
266
290
 
267
- // OAuthHooks & OAuthVerifierOptions & DeviceManagerOptions
291
+ // OAuthHooks &
292
+ // OAuthVerifierOptions &
293
+ // DeviceManagerOptions &
294
+ // Customization
268
295
  ...rest
269
296
  }: OAuthProviderOptions) {
270
- super({ replayStore, redis, ...rest })
297
+ const customization: Customization = customizationSchema.parse(rest)
298
+ const deviceManagerOptions: DeviceManagerOptions =
299
+ deviceManagerOptionsSchema.parse(rest)
300
+
301
+ // @NOTE: hooks don't really need a type parser, as all zod can actually
302
+ // check at runtime is the fact that the values are functions. The only way
303
+ // we would benefit from zod here would be to wrap the functions with a
304
+ // validator for the provided function's return types, which we do not add
305
+ // because it would impact runtime performance and we trust the users of
306
+ // this lib (basically ourselves) to rely on the typing system to ensure the
307
+ // correct types are returned.
308
+ const hooks: OAuthHooks = rest
309
+
310
+ // @NOTE: validation of super params (if we wanted to implement it) should
311
+ // be the responsibility of the super class.
312
+ const superOptions: OAuthVerifierOptions = rest
313
+
314
+ super({ replayStore, redis, ...superOptions })
271
315
 
272
316
  requestStore ??= redis
273
317
  ? new RequestStoreRedis({ redis })
@@ -276,13 +320,18 @@ export class OAuthProvider extends OAuthVerifier {
276
320
  this.authenticationMaxAge = authenticationMaxAge
277
321
  this.metadata = buildMetadata(this.issuer, this.keyset, metadata)
278
322
 
279
- this.deviceManager = new DeviceManager(deviceStore, rest)
323
+ this.deviceManager = new DeviceManager(deviceStore, deviceManagerOptions)
280
324
  this.outputManager = new OutputManager(customization)
281
- this.accountManager = new AccountManager(accountStore)
325
+ this.accountManager = new AccountManager(
326
+ this.issuer,
327
+ accountStore,
328
+ hooks,
329
+ customization,
330
+ )
282
331
  this.clientManager = new ClientManager(
283
332
  this.metadata,
284
333
  this.keyset,
285
- rest,
334
+ hooks,
286
335
  clientStore || null,
287
336
  loopbackMetadata || null,
288
337
  safeFetch,
@@ -293,12 +342,12 @@ export class OAuthProvider extends OAuthVerifier {
293
342
  requestStore,
294
343
  this.signer,
295
344
  this.metadata,
296
- rest,
345
+ hooks,
297
346
  )
298
347
  this.tokenManager = new TokenManager(
299
348
  tokenStore,
300
349
  this.signer,
301
- rest,
350
+ hooks,
302
351
  this.accessTokenType,
303
352
  tokenMaxAge,
304
353
  )
@@ -451,8 +500,7 @@ export class OAuthProvider extends OAuthVerifier {
451
500
  // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3-1
452
501
  // > Since initial processing of the pushed authorization request does not
453
502
  // > involve resource owner interaction, error codes related to user
454
- // > interaction, such as consent_required defined by [OIDC], are never
455
- // > returned.
503
+ // > interaction, such as "access_denied", are never returned.
456
504
  if (err instanceof AccessDeniedError) {
457
505
  throw new InvalidRequestError(err.error_description, err)
458
506
  }
@@ -470,7 +518,7 @@ export class OAuthProvider extends OAuthVerifier {
470
518
  .parseAsync(query.request_uri, { path: ['query', 'request_uri'] })
471
519
  .catch(throwInvalidRequest)
472
520
 
473
- return this.requestManager.get(requestUri, client.id, deviceId)
521
+ return this.requestManager.get(requestUri, deviceId, client.id)
474
522
  }
475
523
 
476
524
  if ('request' in query) {
@@ -514,11 +562,11 @@ export class OAuthProvider extends OAuthVerifier {
514
562
  }
515
563
 
516
564
  private async deleteRequest(
517
- uri: RequestUri,
565
+ requestUri: RequestUri,
518
566
  parameters: OAuthAuthorizationRequestParameters,
519
567
  ) {
520
568
  try {
521
- await this.requestManager.delete(uri)
569
+ await this.requestManager.delete(requestUri)
522
570
  } catch (err) {
523
571
  throw AccessDeniedError.from(parameters, err)
524
572
  }
@@ -690,24 +738,46 @@ export class OAuthProvider extends OAuthVerifier {
690
738
  }))
691
739
  }
692
740
 
693
- protected async signIn(
694
- deviceId: DeviceId,
695
- uri: RequestUri,
696
- clientId: ClientId,
697
- credentials: SignInCredentials,
741
+ protected async signUp(
742
+ { requestUri, deviceId, deviceMetadata }: ApiContext,
743
+ data: SignUpData,
698
744
  ): Promise<{
699
745
  account: Account
700
746
  consentRequired: boolean
701
747
  }> {
748
+ const { clientId } = await this.requestManager.get(requestUri, deviceId)
749
+
702
750
  const client = await this.clientManager.getClient(clientId)
703
751
 
752
+ const { account } = await this.accountManager.signUp(
753
+ data,
754
+ deviceId,
755
+ deviceMetadata,
756
+ )
757
+
758
+ return {
759
+ account,
760
+ consentRequired: !client.info.isFirstParty,
761
+ }
762
+ }
763
+
764
+ protected async signIn(
765
+ { requestUri, deviceId, deviceMetadata }: ApiContext,
766
+ data: SignInData,
767
+ ): Promise<{
768
+ account: Account
769
+ consentRequired: boolean
770
+ }> {
704
771
  // Ensure the request is still valid (and update the request expiration)
705
772
  // @TODO use the returned scopes to determine if consent is required
706
- await this.requestManager.get(uri, clientId, deviceId)
773
+ const { clientId } = await this.requestManager.get(requestUri, deviceId)
774
+
775
+ const client = await this.clientManager.getClient(clientId)
707
776
 
708
777
  const { account, info } = await this.accountManager.signIn(
709
- credentials,
778
+ data,
710
779
  deviceId,
780
+ deviceMetadata,
711
781
  )
712
782
 
713
783
  return {
@@ -722,22 +792,21 @@ export class OAuthProvider extends OAuthVerifier {
722
792
  }
723
793
 
724
794
  protected async acceptRequest(
725
- uri: RequestUri,
726
- clientId: ClientId,
795
+ { requestUri, deviceId, deviceMetadata }: ApiContext,
727
796
  sub: string,
728
- deviceId: DeviceId,
729
- deviceMetadata: RequestMetadata,
730
797
  ): Promise<AuthorizationResultRedirect> {
731
798
  const { issuer } = this
732
- const client = await this.clientManager.getClient(clientId)
733
799
 
734
- const { parameters, clientAuth } = await this.requestManager.get(
735
- uri,
736
- clientId,
800
+ const { parameters, clientId, clientAuth } = await this.requestManager.get(
801
+ requestUri,
737
802
  deviceId,
738
803
  )
739
804
 
805
+ const client = await this.clientManager.getClient(clientId)
806
+
740
807
  try {
808
+ // @TODO Currently, a user can "accept" a request for any did that sing-in
809
+ // on the device, even if "remember" was set to false.
741
810
  const { account, info } = await this.accountManager.get(deviceId, sub)
742
811
 
743
812
  // The user is trying to authorize without a fresh login
@@ -749,7 +818,7 @@ export class OAuthProvider extends OAuthVerifier {
749
818
  }
750
819
 
751
820
  const code = await this.requestManager.setAuthorized(
752
- uri,
821
+ requestUri,
753
822
  client,
754
823
  account,
755
824
  deviceId,
@@ -765,24 +834,19 @@ export class OAuthProvider extends OAuthVerifier {
765
834
 
766
835
  return { issuer, parameters, redirect: { code } }
767
836
  } catch (err) {
768
- await this.deleteRequest(uri, parameters)
837
+ await this.deleteRequest(requestUri, parameters)
769
838
 
770
839
  throw AccessDeniedError.from(parameters, err)
771
840
  }
772
841
  }
773
842
 
774
- protected async rejectRequest(
775
- deviceId: DeviceId,
776
- uri: RequestUri,
777
- clientId: ClientId,
778
- ): Promise<AuthorizationResultRedirect> {
779
- const { parameters } = await this.requestManager.get(
780
- uri,
781
- clientId,
782
- deviceId,
783
- )
843
+ protected async rejectRequest({
844
+ requestUri,
845
+ deviceId,
846
+ }: ApiContext): Promise<AuthorizationResultRedirect> {
847
+ const { parameters } = await this.requestManager.get(requestUri, deviceId)
784
848
 
785
- await this.deleteRequest(uri, parameters)
849
+ await this.deleteRequest(requestUri, parameters)
786
850
 
787
851
  return {
788
852
  issuer: this.issuer,
@@ -1033,57 +1097,67 @@ export class OAuthProvider extends OAuthVerifier {
1033
1097
 
1034
1098
  // Utils
1035
1099
 
1036
- const csrfCookie = (uri: RequestUri) => `csrf-${uri}`
1037
- const onError =
1100
+ const csrfCookie = (requestUri: RequestUri) => `csrf-${requestUri}`
1101
+ const onError: null | ErrorHandler<Req, Res> =
1038
1102
  options?.onError ??
1039
1103
  (process.env['NODE_ENV'] === 'development'
1040
- ? (req, res, err, msg): void =>
1104
+ ? (req, res, err, msg) => {
1041
1105
  console.error(`OAuthProvider error (${msg}):`, err)
1042
- : undefined)
1106
+ }
1107
+ : null)
1043
1108
 
1044
- /**
1045
- * Creates a middleware that will serve static JSON content.
1046
- */
1047
- const staticJson = (json: unknown): Middleware<void, Req, Res> =>
1048
- combineMiddlewares([
1049
- function (req, res, next) {
1050
- res.setHeader('Access-Control-Allow-Origin', '*')
1051
- res.setHeader('Access-Control-Allow-Headers', '*')
1109
+ // CORS preflight
1110
+ const corsHeaders: Middleware = function (req, res, next) {
1111
+ res.setHeader('Access-Control-Max-Age', '86400') // 1 day
1052
1112
 
1053
- res.setHeader('Cache-Control', 'max-age=300')
1054
- next()
1055
- },
1056
- staticJsonMiddleware(json),
1057
- ])
1113
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
1114
+ //
1115
+ // > For requests without credentials, the literal value "*" can be
1116
+ // > specified as a wildcard; the value tells browsers to allow
1117
+ // > requesting code from any origin to access the resource.
1118
+ // > Attempting to use the wildcard with credentials results in an
1119
+ // > error.
1120
+ //
1121
+ // A "*" is safer to use than reflecting the request origin.
1122
+ res.setHeader('Access-Control-Allow-Origin', '*')
1123
+
1124
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
1125
+ // > The value "*" only counts as a special wildcard value for
1126
+ // > requests without credentials (requests without HTTP cookies or
1127
+ // > HTTP authentication information). In requests with credentials,
1128
+ // > it is treated as the literal method name "*" without special
1129
+ // > semantics.
1130
+ res.setHeader('Access-Control-Allow-Methods', '*')
1131
+
1132
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,DPoP')
1133
+
1134
+ next()
1135
+ }
1136
+
1137
+ const corsPreflight: Middleware = combineMiddlewares([
1138
+ corsHeaders,
1139
+ (req, res) => {
1140
+ res.writeHead(200).end()
1141
+ },
1142
+ ])
1058
1143
 
1059
1144
  /**
1060
1145
  * Wrap an OAuth endpoint in a middleware that will set the appropriate
1061
1146
  * response headers and format the response as JSON.
1062
1147
  */
1063
1148
  const jsonHandler = <T, TReq extends Req, TRes extends Res, Json>(
1064
- buildJson: (this: T, req: TReq, res: TRes) => Json | Promise<Json>,
1149
+ buildJson: (this: T, req: TReq, res: TRes) => Awaitable<Json>,
1065
1150
  status?: number,
1066
1151
  ): Handler<T, TReq, TRes> =>
1067
1152
  async function (req, res) {
1068
- res.setHeader('Access-Control-Allow-Origin', '*')
1069
- res.setHeader('Access-Control-Allow-Headers', '*')
1070
-
1071
- // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
1072
- res.setHeader('Cache-Control', 'no-store')
1073
- res.setHeader('Pragma', 'no-cache')
1074
-
1075
- // https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
1076
- const dpopNonce = server.nextDpopNonce()
1077
- if (dpopNonce) {
1078
- const name = 'DPoP-Nonce'
1079
- res.setHeader(name, dpopNonce)
1080
- res.appendHeader('Access-Control-Expose-Headers', name)
1081
- }
1082
-
1083
1153
  try {
1154
+ // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
1155
+ res.setHeader('Cache-Control', 'no-store')
1156
+ res.setHeader('Pragma', 'no-cache')
1157
+
1084
1158
  // Ensure we can agree on a content encoding & type before starting to
1085
1159
  // build the JSON response.
1086
- if (!mediaType(req.headers['accept'], ['application/json'])) {
1160
+ if (!negotiateContent(req, ['application/json'])) {
1087
1161
  throw createHttpError(406, 'Unsupported media type')
1088
1162
  }
1089
1163
 
@@ -1094,39 +1168,107 @@ export class OAuthProvider extends OAuthVerifier {
1094
1168
  res.writeHead(status ?? 204).end()
1095
1169
  }
1096
1170
  } catch (err) {
1097
- if (!res.headersSent) {
1098
- if (err instanceof WWWAuthenticateError) {
1099
- const name = 'WWW-Authenticate'
1100
- res.setHeader(name, err.wwwAuthenticateHeader)
1101
- res.appendHeader('Access-Control-Expose-Headers', name)
1102
- }
1171
+ onError?.(req, res, err, 'OAuth request error')
1103
1172
 
1173
+ if (!res.headersSent) {
1104
1174
  const payload = buildErrorPayload(err)
1105
1175
  const status = buildErrorStatus(err)
1106
1176
  writeJson(res, payload, { status })
1107
1177
  } else {
1108
1178
  res.destroy()
1109
1179
  }
1110
-
1111
- // OAuthError are used to build expected responses, so we don't log
1112
- // them as errors.
1113
- if (!(err instanceof OAuthError) || err.statusCode >= 500) {
1114
- onError?.(req, res, err, 'Unexpected error')
1115
- }
1116
1180
  }
1117
1181
  }
1118
1182
 
1183
+ const oauthHandler = <T, TReq extends Req, TRes extends Res, Json>(
1184
+ buildOAuthResponse: (this: T, req: TReq, res: TRes) => Awaitable<Json>,
1185
+ status?: number,
1186
+ ) =>
1187
+ combineMiddlewares([
1188
+ corsHeaders,
1189
+ jsonHandler<T, TReq, TRes, Json>(async function (req, res) {
1190
+ try {
1191
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
1192
+ const dpopNonce = server.nextDpopNonce()
1193
+ if (dpopNonce) {
1194
+ const name = 'DPoP-Nonce'
1195
+ res.setHeader(name, dpopNonce)
1196
+ res.appendHeader('Access-Control-Expose-Headers', name)
1197
+ }
1198
+
1199
+ return await buildOAuthResponse.call(this, req, res)
1200
+ } catch (err) {
1201
+ if (!res.headersSent && err instanceof WWWAuthenticateError) {
1202
+ const name = 'WWW-Authenticate'
1203
+ res.setHeader(name, err.wwwAuthenticateHeader)
1204
+ res.appendHeader('Access-Control-Expose-Headers', name)
1205
+ }
1206
+
1207
+ throw err
1208
+ }
1209
+ }, status),
1210
+ ])
1211
+
1212
+ const apiHandler = <
1213
+ T,
1214
+ TReq extends Req,
1215
+ TRes extends Res,
1216
+ S extends z.ZodTypeAny,
1217
+ Json,
1218
+ >(
1219
+ inputSchema: S,
1220
+ buildJson: (
1221
+ this: T,
1222
+ req: TReq,
1223
+ res: TRes,
1224
+ input: z.infer<S>,
1225
+ context: ApiContext,
1226
+ ) => Json | Promise<Json>,
1227
+ status?: number,
1228
+ ) =>
1229
+ jsonHandler<T, TReq, TRes, Json>(async function (req, res) {
1230
+ validateFetchMode(req, res, ['same-origin'])
1231
+ validateFetchSite(req, res, ['same-origin'])
1232
+ validateSameOrigin(req, res, issuerOrigin)
1233
+ const referer = validateReferer(req, res, {
1234
+ origin: issuerOrigin,
1235
+ pathname: '/oauth/authorize',
1236
+ })
1237
+
1238
+ const requestUri = await requestUriSchema.parseAsync(
1239
+ referer.searchParams.get('request_uri'),
1240
+ { path: ['query', 'request_uri'] },
1241
+ )
1242
+
1243
+ validateCsrfToken(
1244
+ req,
1245
+ res,
1246
+ req.headers['x-csrf-token'],
1247
+ csrfCookie(requestUri),
1248
+ )
1249
+
1250
+ const { deviceId, deviceMetadata } = await server.deviceManager.load(
1251
+ req,
1252
+ res,
1253
+ )
1254
+
1255
+ const payload = await parseHttpRequest(req, ['json'])
1256
+ const input = await inputSchema.parseAsync(payload, { path: ['body'] })
1257
+
1258
+ const context: ApiContext = { requestUri, deviceId, deviceMetadata }
1259
+ return buildJson.call(this, req, res, input, context)
1260
+ }, status)
1261
+
1119
1262
  const navigationHandler = <T, TReq extends Req, TRes extends Res>(
1120
- handler: (this: T, req: TReq, res: TRes) => void | Promise<void>,
1263
+ handler: (this: T, req: TReq, res: TRes) => Awaitable<void>,
1121
1264
  ): Handler<T, TReq, TRes> =>
1122
1265
  async function (req, res) {
1123
- res.setHeader('Access-Control-Allow-Origin', '*')
1124
- res.setHeader('Access-Control-Allow-Headers', '*')
1266
+ try {
1267
+ res.setHeader('Cache-Control', 'no-store')
1268
+ res.setHeader('Pragma', 'no-cache')
1125
1269
 
1126
- res.setHeader('Cache-Control', 'no-store')
1127
- res.setHeader('Pragma', 'no-cache')
1270
+ res.setHeader('Referrer-Policy', 'same-origin')
1128
1271
 
1129
- try {
1130
1272
  validateFetchMode(req, res, ['navigate'])
1131
1273
  validateFetchDest(req, res, ['document'])
1132
1274
  validateSameOrigin(req, res, issuerOrigin)
@@ -1141,11 +1283,78 @@ export class OAuthProvider extends OAuthVerifier {
1141
1283
  )
1142
1284
 
1143
1285
  if (!res.headersSent) {
1144
- await server.outputManager.sendErrorPage(res, err)
1286
+ await server.outputManager.sendErrorPage(res, err, {
1287
+ preferredLocales: extractLocales(req),
1288
+ })
1145
1289
  }
1146
1290
  }
1147
1291
  }
1148
1292
 
1293
+ // Simple GET requests fall under the category of "no-cors" request, meaning
1294
+ // that the browser will allow any cross-origin request, with credentials,
1295
+ // to be sent to the oauth server. The OAuth Server will, however:
1296
+ // 1) validate the request origin (see navigationHandler),
1297
+ // 2) validate the CSRF token,
1298
+ // 3) validate the referer,
1299
+ // 4) validate the sec-fetch-site header,
1300
+ // 4) validate the sec-fetch-mode header (see navigationHandler),
1301
+ // 5) validate the sec-fetch-dest header (see navigationHandler).
1302
+ // And will error (refuse to serve the request) if any of these checks fail.
1303
+ const sameOriginNavigationHandler = <
1304
+ T extends { url: URL },
1305
+ TReq extends Req,
1306
+ TRes extends Res,
1307
+ >(
1308
+ handler: (
1309
+ this: T,
1310
+ req: TReq,
1311
+ res: TRes,
1312
+ deviceInfo: DeviceInfo,
1313
+ ) => Awaitable<void>,
1314
+ ): Handler<T, TReq, TRes> =>
1315
+ navigationHandler(async function (req, res) {
1316
+ validateFetchSite(req, res, ['same-origin'])
1317
+
1318
+ const deviceInfo = await server.deviceManager.load(req, res)
1319
+
1320
+ return handler.call(this, req, res, deviceInfo)
1321
+ })
1322
+
1323
+ const authorizeRedirectNavigationHandler = <
1324
+ T extends { url: URL },
1325
+ TReq extends Req,
1326
+ TRes extends Res,
1327
+ >(
1328
+ handler: (
1329
+ this: T,
1330
+ req: TReq,
1331
+ res: TRes,
1332
+ context: ApiContext,
1333
+ ) => Awaitable<AuthorizationResultRedirect>,
1334
+ ): Handler<T, TReq, TRes> =>
1335
+ sameOriginNavigationHandler(async function (req, res, deviceInfo) {
1336
+ const referer = validateReferer(req, res, {
1337
+ origin: issuerOrigin,
1338
+ pathname: '/oauth/authorize',
1339
+ })
1340
+
1341
+ const requestUri = await requestUriSchema.parseAsync(
1342
+ referer.searchParams.get('request_uri'),
1343
+ )
1344
+
1345
+ const csrfToken = this.url.searchParams.get('csrf_token')
1346
+ const csrfCookieName = csrfCookie(requestUri)
1347
+
1348
+ // Next line will "clear" the CSRF token cookie, preventing replay of
1349
+ // this request (navigating "back" will result in an error).
1350
+ validateCsrfToken(req, res, csrfToken, csrfCookieName, true)
1351
+
1352
+ const context: ApiContext = { ...deviceInfo, requestUri }
1353
+
1354
+ const redirect = await handler.call(this, req, res, context)
1355
+ return sendAuthorizeRedirect(res, redirect)
1356
+ })
1357
+
1149
1358
  /**
1150
1359
  * Provides a better UX when a request is denied by redirecting to the
1151
1360
  * client with the error details. This will also log any error that caused
@@ -1172,44 +1381,26 @@ export class OAuthProvider extends OAuthVerifier {
1172
1381
 
1173
1382
  //- Public OAuth endpoints
1174
1383
 
1384
+ router.options('/.well-known/oauth-authorization-server', corsPreflight)
1175
1385
  router.get(
1176
1386
  '/.well-known/oauth-authorization-server',
1177
- staticJson(server.metadata),
1387
+ corsHeaders,
1388
+ cacheControlMiddleware(300),
1389
+ staticJsonMiddleware(server.metadata),
1178
1390
  )
1179
1391
 
1180
- // CORS preflight
1181
- const corsPreflight: Middleware = function (req, res, _next) {
1182
- res
1183
- .writeHead(204, {
1184
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
1185
- //
1186
- // > For requests without credentials, the literal value "*" can be
1187
- // > specified as a wildcard; the value tells browsers to allow
1188
- // > requesting code from any origin to access the resource.
1189
- // > Attempting to use the wildcard with credentials results in an
1190
- // > error.
1191
- //
1192
- // A "*" is safer to use than reflecting the request origin.
1193
- 'Access-Control-Allow-Origin': '*',
1194
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
1195
- // > The value "*" only counts as a special wildcard value for
1196
- // > requests without credentials (requests without HTTP cookies or
1197
- // > HTTP authentication information). In requests with credentials,
1198
- // > it is treated as the literal method name "*" without special
1199
- // > semantics.
1200
- 'Access-Control-Allow-Methods': '*',
1201
- 'Access-Control-Allow-Headers': 'Content-Type,Authorization,DPoP',
1202
- 'Access-Control-Max-Age': '86400', // 1 day
1203
- })
1204
- .end()
1205
- }
1206
-
1207
- router.get('/oauth/jwks', staticJson(server.jwks))
1392
+ router.options('/oauth/jwks', corsPreflight)
1393
+ router.get(
1394
+ '/oauth/jwks',
1395
+ corsHeaders,
1396
+ cacheControlMiddleware(300),
1397
+ staticJsonMiddleware(server.jwks),
1398
+ )
1208
1399
 
1209
1400
  router.options('/oauth/par', corsPreflight)
1210
1401
  router.post(
1211
1402
  '/oauth/par',
1212
- jsonHandler(async function (req, _res) {
1403
+ oauthHandler(async function (req, _res) {
1213
1404
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1214
1405
 
1215
1406
  const credentials = await oauthClientCredentialsSchema
@@ -1233,11 +1424,9 @@ export class OAuthProvider extends OAuthVerifier {
1233
1424
  )
1234
1425
  }, 201),
1235
1426
  )
1236
-
1237
1427
  // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
1238
1428
  // > If the request did not use the POST method, the authorization server
1239
1429
  // > responds with an HTTP 405 (Method Not Allowed) status code.
1240
- router.options('/oauth/par', corsPreflight)
1241
1430
  router.all('/oauth/par', (req, res) => {
1242
1431
  res.writeHead(405).end()
1243
1432
  })
@@ -1245,7 +1434,7 @@ export class OAuthProvider extends OAuthVerifier {
1245
1434
  router.options('/oauth/token', corsPreflight)
1246
1435
  router.post(
1247
1436
  '/oauth/token',
1248
- jsonHandler(async function (req, _res) {
1437
+ oauthHandler(async function (req, _res) {
1249
1438
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1250
1439
 
1251
1440
  const clientMetadata =
@@ -1277,7 +1466,7 @@ export class OAuthProvider extends OAuthVerifier {
1277
1466
  router.options('/oauth/revoke', corsPreflight)
1278
1467
  router.post(
1279
1468
  '/oauth/revoke',
1280
- jsonHandler(async function (req, res) {
1469
+ oauthHandler(async function (req, res) {
1281
1470
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1282
1471
 
1283
1472
  const tokenIdentification = await oauthTokenIdentificationSchema
@@ -1291,8 +1480,6 @@ export class OAuthProvider extends OAuthVerifier {
1291
1480
  }
1292
1481
  }),
1293
1482
  )
1294
-
1295
- router.options('/oauth/revoke', corsPreflight)
1296
1483
  router.get(
1297
1484
  '/oauth/revoke',
1298
1485
  navigationHandler(async function (req, res) {
@@ -1317,9 +1504,10 @@ export class OAuthProvider extends OAuthVerifier {
1317
1504
  }),
1318
1505
  )
1319
1506
 
1507
+ router.options('/oauth/introspect', corsPreflight)
1320
1508
  router.post(
1321
1509
  '/oauth/introspect',
1322
- jsonHandler(async function (req, _res) {
1510
+ oauthHandler(async function (req, _res) {
1323
1511
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1324
1512
 
1325
1513
  const credentials = await oauthClientCredentialsSchema
@@ -1346,7 +1534,7 @@ export class OAuthProvider extends OAuthVerifier {
1346
1534
  const query = Object.fromEntries(this.url.searchParams)
1347
1535
 
1348
1536
  const clientCredentials = await oauthClientCredentialsSchema
1349
- .parseAsync(query, { path: ['body'] })
1537
+ .parseAsync(query, { path: ['query'] })
1350
1538
  .catch(throwInvalidRequest)
1351
1539
 
1352
1540
  if ('client_secret' in clientCredentials) {
@@ -1362,7 +1550,9 @@ export class OAuthProvider extends OAuthVerifier {
1362
1550
  res,
1363
1551
  )
1364
1552
 
1365
- const data = await server
1553
+ const result:
1554
+ | AuthorizationResultRedirect
1555
+ | AuthorizationResultAuthorize = await server
1366
1556
  .authorize(
1367
1557
  clientCredentials,
1368
1558
  authorizationRequest,
@@ -1371,173 +1561,79 @@ export class OAuthProvider extends OAuthVerifier {
1371
1561
  )
1372
1562
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1373
1563
 
1374
- switch (true) {
1375
- case 'redirect' in data: {
1376
- return sendAuthorizeRedirect(res, data)
1377
- }
1378
- case 'authorize' in data: {
1379
- await setupCsrfToken(req, res, csrfCookie(data.authorize.uri))
1380
- return server.outputManager.sendAuthorizePage(res, data)
1381
- }
1382
- default: {
1383
- // Should never happen
1384
- throw new Error('Unexpected authorization result')
1385
- }
1564
+ if ('redirect' in result) {
1565
+ return sendAuthorizeRedirect(res, result)
1566
+ } else {
1567
+ await setupCsrfToken(req, res, csrfCookie(result.authorize.uri))
1568
+ return server.outputManager.sendAuthorizePage(res, result, {
1569
+ preferredLocales: extractLocales(req),
1570
+ })
1386
1571
  }
1387
1572
  }),
1388
1573
  )
1389
1574
 
1390
- const signInPayloadSchema = z.object({
1391
- csrf_token: z.string(),
1392
- request_uri: requestUriSchema,
1393
- client_id: clientIdSchema,
1394
- credentials: signInCredentialsSchema,
1395
- })
1396
-
1397
- router.options('/oauth/authorize/sign-in', corsPreflight)
1398
1575
  router.post(
1399
- '/oauth/authorize/sign-in',
1400
- jsonHandler(async function (req, res) {
1401
- validateFetchMode(req, res, ['same-origin'])
1402
- validateFetchSite(req, res, ['same-origin'])
1403
- validateSameOrigin(req, res, issuerOrigin)
1404
-
1405
- const payload = await parseHttpRequest(req, ['json'])
1406
- const input = await signInPayloadSchema.parseAsync(payload, {
1407
- path: ['body'],
1408
- })
1409
-
1410
- validateReferer(req, res, {
1411
- origin: issuerOrigin,
1412
- pathname: '/oauth/authorize',
1413
- })
1414
- validateCsrfToken(
1415
- req,
1416
- res,
1417
- input.csrf_token,
1418
- csrfCookie(input.request_uri),
1419
- )
1576
+ '/oauth/authorize/verify-handle-availability',
1577
+ apiHandler(
1578
+ z.object({ handle: handleSchema }).strict(),
1579
+ async function (req, res, data) {
1580
+ return server.accountManager.verifyHandleAvailability(data.handle)
1581
+ },
1582
+ ),
1583
+ )
1420
1584
 
1421
- const { deviceId } = await server.deviceManager.load(req, res, true)
1585
+ router.post(
1586
+ '/oauth/authorize/sign-up',
1587
+ apiHandler(signUpDataSchema, async function (req, res, data, ctx) {
1588
+ return server.signUp(ctx, data)
1589
+ }),
1590
+ )
1422
1591
 
1423
- return server.signIn(
1424
- deviceId,
1425
- input.request_uri,
1426
- input.client_id,
1427
- input.credentials,
1428
- )
1592
+ router.post(
1593
+ '/oauth/authorize/sign-in',
1594
+ apiHandler(signInDataSchema, async function (req, res, data, ctx) {
1595
+ return server.signIn(ctx, data)
1429
1596
  }),
1430
1597
  )
1431
1598
 
1432
- const acceptQuerySchema = z.object({
1433
- csrf_token: z.string(),
1434
- request_uri: requestUriSchema,
1435
- client_id: clientIdSchema,
1436
- account_sub: z.string(),
1437
- })
1599
+ router.post(
1600
+ '/oauth/authorize/reset-password-request',
1601
+ apiHandler(
1602
+ resetPasswordRequestDataSchema,
1603
+ async function (req, res, data) {
1604
+ await server.accountManager.resetPasswordRequest(data)
1605
+ },
1606
+ ),
1607
+ )
1608
+
1609
+ router.post(
1610
+ '/oauth/authorize/reset-password-confirm',
1611
+ apiHandler(
1612
+ resetPasswordConfirmDataSchema,
1613
+ async function (req, res, data) {
1614
+ await server.accountManager.resetPasswordConfirm(data)
1615
+ },
1616
+ ),
1617
+ )
1438
1618
 
1439
- // Though this is a "no-cors" request, meaning that the browser will allow
1440
- // any cross-origin request, with credentials, to be sent, the handler will
1441
- // 1) validate the request origin,
1442
- // 2) validate the CSRF token,
1443
- // 3) validate the referer,
1444
- // 4) validate the sec-fetch-site header,
1445
- // 4) validate the sec-fetch-mode header,
1446
- // 5) validate the sec-fetch-dest header (see navigationHandler).
1447
- // And will error if any of these checks fail.
1448
1619
  router.get(
1449
1620
  '/oauth/authorize/accept',
1450
- navigationHandler(async function (req, res) {
1451
- validateFetchSite(req, res, ['same-origin'])
1452
-
1453
- const query = Object.fromEntries(this.url.searchParams)
1454
- const input = await acceptQuerySchema.parseAsync(query, {
1455
- path: ['query'],
1456
- })
1457
-
1458
- validateReferer(req, res, {
1459
- origin: issuerOrigin,
1460
- pathname: '/oauth/authorize',
1461
- searchParams: [
1462
- ['request_uri', input.request_uri],
1463
- ['client_id', input.client_id],
1464
- ],
1465
- })
1466
- validateCsrfToken(
1467
- req,
1468
- res,
1469
- input.csrf_token,
1470
- csrfCookie(input.request_uri),
1471
- true,
1472
- )
1473
-
1474
- const { deviceId, deviceMetadata } = await server.deviceManager.load(
1475
- req,
1476
- res,
1477
- )
1621
+ authorizeRedirectNavigationHandler(async function (req, res, ctx) {
1622
+ const sub = this.url.searchParams.get('account_sub')
1623
+ if (!sub) throw new InvalidRequestError('Account sub not provided')
1478
1624
 
1479
- const data = await server
1480
- .acceptRequest(
1481
- input.request_uri,
1482
- input.client_id,
1483
- input.account_sub,
1484
- deviceId,
1485
- deviceMetadata,
1486
- )
1625
+ return server
1626
+ .acceptRequest(ctx, sub)
1487
1627
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1488
-
1489
- return await sendAuthorizeRedirect(res, data)
1490
1628
  }),
1491
1629
  )
1492
1630
 
1493
- const rejectQuerySchema = z.object({
1494
- csrf_token: z.string(),
1495
- request_uri: requestUriSchema,
1496
- client_id: clientIdSchema,
1497
- })
1498
-
1499
- // Though this is a "no-cors" request, meaning that the browser will allow
1500
- // any cross-origin request, with credentials, to be sent, the handler will
1501
- // 1) validate the request origin,
1502
- // 2) validate the CSRF token,
1503
- // 3) validate the referer,
1504
- // 4) validate the sec-fetch-site header,
1505
- // 4) validate the sec-fetch-mode header,
1506
- // 5) validate the sec-fetch-dest header (see navigationHandler).
1507
- // And will error if any of these checks fail.
1508
1631
  router.get(
1509
1632
  '/oauth/authorize/reject',
1510
- navigationHandler(async function (req, res) {
1511
- validateFetchSite(req, res, ['same-origin'])
1512
-
1513
- const query = Object.fromEntries(this.url.searchParams)
1514
- const input = await rejectQuerySchema.parseAsync(query, {
1515
- path: ['query'],
1516
- })
1517
-
1518
- validateReferer(req, res, {
1519
- origin: issuerOrigin,
1520
- pathname: '/oauth/authorize',
1521
- searchParams: [
1522
- ['request_uri', input.request_uri],
1523
- ['client_id', input.client_id],
1524
- ],
1525
- })
1526
- validateCsrfToken(
1527
- req,
1528
- res,
1529
- input.csrf_token,
1530
- csrfCookie(input.request_uri),
1531
- true,
1532
- )
1533
-
1534
- const { deviceId } = await server.deviceManager.load(req, res)
1535
-
1536
- const data = await server
1537
- .rejectRequest(deviceId, input.request_uri, input.client_id)
1633
+ authorizeRedirectNavigationHandler(async function (req, res, ctx) {
1634
+ return server
1635
+ .rejectRequest(ctx)
1538
1636
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1539
-
1540
- return await sendAuthorizeRedirect(res, data)
1541
1637
  }),
1542
1638
  )
1543
1639