@atproto/oauth-provider 0.4.0 → 0.5.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 (402) hide show
  1. package/.linguirc +57 -0
  2. package/CHANGELOG.md +21 -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 +232 -0
  191. package/dist/output/build-customization-data.d.ts.map +1 -0
  192. package/dist/output/build-customization-data.js +145 -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 +399 -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 +189 -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,26 @@ 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
+ BrandingConfig,
107
+ Customization,
108
+ customizationSchema,
109
+ } from './output/build-customization-data.js'
94
110
  import {
95
111
  buildErrorPayload,
96
112
  buildErrorStatus,
97
113
  } from './output/build-error-payload.js'
98
- import { Customization } from './output/customization.js'
99
114
  import { OutputManager } from './output/output-manager.js'
100
115
  import {
101
116
  AuthorizationResultRedirect,
@@ -114,32 +129,36 @@ import { TokenManager } from './token/token-manager.js'
114
129
  import { TokenStore, asTokenStore } from './token/token-store.js'
115
130
  import { VerifyTokenClaimsOptions } from './token/verify-token-claims.js'
116
131
 
117
- export type OAuthProviderStore = Partial<
118
- ClientStore &
119
- AccountStore &
120
- DeviceStore &
121
- TokenStore &
122
- RequestStore &
123
- ReplayStore
124
- >
125
-
126
132
  export {
133
+ type BrandingConfig,
127
134
  type CustomMetadata,
128
135
  type Customization,
129
136
  type Handler,
137
+ type HcaptchaConfig,
130
138
  Keyset,
131
139
  type OAuthAuthorizationServerMetadata,
132
140
  }
133
141
 
142
+ type ApiContext = {
143
+ requestUri: RequestUri
144
+ deviceId: DeviceId
145
+ deviceMetadata: RequestMetadata
146
+ }
147
+
148
+ export type ErrorHandler<
149
+ Req extends IncomingMessage = IncomingMessage,
150
+ Res extends ServerResponse = ServerResponse,
151
+ > = (req: Req, res: Res, err: unknown, message: string) => void
152
+
134
153
  export type RouterOptions<
135
154
  Req extends IncomingMessage = IncomingMessage,
136
155
  Res extends ServerResponse = ServerResponse,
137
156
  > = {
138
- onError?: (req: Req, res: Res, err: unknown, message: string) => void
157
+ onError?: ErrorHandler<Req, Res>
139
158
  }
140
159
 
141
160
  export type OAuthProviderOptions = Override<
142
- OAuthVerifierOptions & OAuthHooks & DeviceManagerOptions,
161
+ OAuthVerifierOptions & OAuthHooks & DeviceManagerOptions & Customization,
143
162
  {
144
163
  /**
145
164
  * Maximum age a device/account session can be before requiring
@@ -157,11 +176,6 @@ export type OAuthProviderOptions = Override<
157
176
  */
158
177
  metadata?: CustomMetadata
159
178
 
160
- /**
161
- * UI customizations
162
- */
163
- customization?: Customization
164
-
165
179
  /**
166
180
  * A custom fetch function that can be used to fetch the client metadata from
167
181
  * the internet. By default, the fetch function is a safeFetchWrap() function
@@ -184,11 +198,18 @@ export type OAuthProviderOptions = Override<
184
198
  * this store implements all the interfaces not provided in the other
185
199
  * `<name>Store` options.
186
200
  */
187
- store?: OAuthProviderStore
201
+ store?: Partial<
202
+ AccountStore &
203
+ ClientStore &
204
+ DeviceStore &
205
+ ReplayStore &
206
+ RequestStore &
207
+ TokenStore
208
+ >
188
209
 
189
210
  accountStore?: AccountStore
190
- deviceStore?: DeviceStore
191
211
  clientStore?: ClientStore
212
+ deviceStore?: DeviceStore
192
213
  replayStore?: ReplayStore
193
214
  requestStore?: RequestStore
194
215
  tokenStore?: TokenStore
@@ -235,7 +256,6 @@ export class OAuthProvider extends OAuthVerifier {
235
256
 
236
257
  public constructor({
237
258
  metadata,
238
- customization = undefined,
239
259
  authenticationMaxAge = AUTHENTICATION_MAX_AGE,
240
260
  tokenMaxAge = TOKEN_MAX_AGE,
241
261
 
@@ -264,10 +284,30 @@ export class OAuthProvider extends OAuthVerifier {
264
284
 
265
285
  loopbackMetadata = atprotoLoopbackClientMetadata,
266
286
 
267
- // OAuthHooks & OAuthVerifierOptions & DeviceManagerOptions
287
+ // OAuthHooks &
288
+ // OAuthVerifierOptions &
289
+ // DeviceManagerOptions &
290
+ // Customization
268
291
  ...rest
269
292
  }: OAuthProviderOptions) {
270
- super({ replayStore, redis, ...rest })
293
+ const customization: Customization = customizationSchema.parse(rest)
294
+ const deviceManagerOptions: DeviceManagerOptions =
295
+ deviceManagerOptionsSchema.parse(rest)
296
+
297
+ // @NOTE: hooks don't really need a type parser, as all zod can actually
298
+ // check at runtime is the fact that the values are functions. The only way
299
+ // we would benefit from zod here would be to wrap the functions with a
300
+ // validator for the provided function's return types, which we do not add
301
+ // because it would impact runtime performance and we trust the users of
302
+ // this lib (basically ourselves) to rely on the typing system to ensure the
303
+ // correct types are returned.
304
+ const hooks: OAuthHooks = rest
305
+
306
+ // @NOTE: validation of super params (if we wanted to implement it) should
307
+ // be the responsibility of the super class.
308
+ const superOptions: OAuthVerifierOptions = rest
309
+
310
+ super({ replayStore, redis, ...superOptions })
271
311
 
272
312
  requestStore ??= redis
273
313
  ? new RequestStoreRedis({ redis })
@@ -276,13 +316,18 @@ export class OAuthProvider extends OAuthVerifier {
276
316
  this.authenticationMaxAge = authenticationMaxAge
277
317
  this.metadata = buildMetadata(this.issuer, this.keyset, metadata)
278
318
 
279
- this.deviceManager = new DeviceManager(deviceStore, rest)
319
+ this.deviceManager = new DeviceManager(deviceStore, deviceManagerOptions)
280
320
  this.outputManager = new OutputManager(customization)
281
- this.accountManager = new AccountManager(accountStore)
321
+ this.accountManager = new AccountManager(
322
+ this.issuer,
323
+ accountStore,
324
+ hooks,
325
+ customization,
326
+ )
282
327
  this.clientManager = new ClientManager(
283
328
  this.metadata,
284
329
  this.keyset,
285
- rest,
330
+ hooks,
286
331
  clientStore || null,
287
332
  loopbackMetadata || null,
288
333
  safeFetch,
@@ -293,12 +338,12 @@ export class OAuthProvider extends OAuthVerifier {
293
338
  requestStore,
294
339
  this.signer,
295
340
  this.metadata,
296
- rest,
341
+ hooks,
297
342
  )
298
343
  this.tokenManager = new TokenManager(
299
344
  tokenStore,
300
345
  this.signer,
301
- rest,
346
+ hooks,
302
347
  this.accessTokenType,
303
348
  tokenMaxAge,
304
349
  )
@@ -451,8 +496,7 @@ export class OAuthProvider extends OAuthVerifier {
451
496
  // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3-1
452
497
  // > Since initial processing of the pushed authorization request does not
453
498
  // > involve resource owner interaction, error codes related to user
454
- // > interaction, such as consent_required defined by [OIDC], are never
455
- // > returned.
499
+ // > interaction, such as "access_denied", are never returned.
456
500
  if (err instanceof AccessDeniedError) {
457
501
  throw new InvalidRequestError(err.error_description, err)
458
502
  }
@@ -470,7 +514,7 @@ export class OAuthProvider extends OAuthVerifier {
470
514
  .parseAsync(query.request_uri, { path: ['query', 'request_uri'] })
471
515
  .catch(throwInvalidRequest)
472
516
 
473
- return this.requestManager.get(requestUri, client.id, deviceId)
517
+ return this.requestManager.get(requestUri, deviceId, client.id)
474
518
  }
475
519
 
476
520
  if ('request' in query) {
@@ -514,11 +558,11 @@ export class OAuthProvider extends OAuthVerifier {
514
558
  }
515
559
 
516
560
  private async deleteRequest(
517
- uri: RequestUri,
561
+ requestUri: RequestUri,
518
562
  parameters: OAuthAuthorizationRequestParameters,
519
563
  ) {
520
564
  try {
521
- await this.requestManager.delete(uri)
565
+ await this.requestManager.delete(requestUri)
522
566
  } catch (err) {
523
567
  throw AccessDeniedError.from(parameters, err)
524
568
  }
@@ -690,24 +734,46 @@ export class OAuthProvider extends OAuthVerifier {
690
734
  }))
691
735
  }
692
736
 
693
- protected async signIn(
694
- deviceId: DeviceId,
695
- uri: RequestUri,
696
- clientId: ClientId,
697
- credentials: SignInCredentials,
737
+ protected async signUp(
738
+ { requestUri, deviceId, deviceMetadata }: ApiContext,
739
+ data: SignUpData,
698
740
  ): Promise<{
699
741
  account: Account
700
742
  consentRequired: boolean
701
743
  }> {
744
+ const { clientId } = await this.requestManager.get(requestUri, deviceId)
745
+
702
746
  const client = await this.clientManager.getClient(clientId)
703
747
 
748
+ const { account } = await this.accountManager.signUp(
749
+ data,
750
+ deviceId,
751
+ deviceMetadata,
752
+ )
753
+
754
+ return {
755
+ account,
756
+ consentRequired: !client.info.isFirstParty,
757
+ }
758
+ }
759
+
760
+ protected async signIn(
761
+ { requestUri, deviceId, deviceMetadata }: ApiContext,
762
+ data: SignInData,
763
+ ): Promise<{
764
+ account: Account
765
+ consentRequired: boolean
766
+ }> {
704
767
  // Ensure the request is still valid (and update the request expiration)
705
768
  // @TODO use the returned scopes to determine if consent is required
706
- await this.requestManager.get(uri, clientId, deviceId)
769
+ const { clientId } = await this.requestManager.get(requestUri, deviceId)
770
+
771
+ const client = await this.clientManager.getClient(clientId)
707
772
 
708
773
  const { account, info } = await this.accountManager.signIn(
709
- credentials,
774
+ data,
710
775
  deviceId,
776
+ deviceMetadata,
711
777
  )
712
778
 
713
779
  return {
@@ -722,22 +788,21 @@ export class OAuthProvider extends OAuthVerifier {
722
788
  }
723
789
 
724
790
  protected async acceptRequest(
725
- uri: RequestUri,
726
- clientId: ClientId,
791
+ { requestUri, deviceId, deviceMetadata }: ApiContext,
727
792
  sub: string,
728
- deviceId: DeviceId,
729
- deviceMetadata: RequestMetadata,
730
793
  ): Promise<AuthorizationResultRedirect> {
731
794
  const { issuer } = this
732
- const client = await this.clientManager.getClient(clientId)
733
795
 
734
- const { parameters, clientAuth } = await this.requestManager.get(
735
- uri,
736
- clientId,
796
+ const { parameters, clientId, clientAuth } = await this.requestManager.get(
797
+ requestUri,
737
798
  deviceId,
738
799
  )
739
800
 
801
+ const client = await this.clientManager.getClient(clientId)
802
+
740
803
  try {
804
+ // @TODO Currently, a user can "accept" a request for any did that sing-in
805
+ // on the device, even if "remember" was set to false.
741
806
  const { account, info } = await this.accountManager.get(deviceId, sub)
742
807
 
743
808
  // The user is trying to authorize without a fresh login
@@ -749,7 +814,7 @@ export class OAuthProvider extends OAuthVerifier {
749
814
  }
750
815
 
751
816
  const code = await this.requestManager.setAuthorized(
752
- uri,
817
+ requestUri,
753
818
  client,
754
819
  account,
755
820
  deviceId,
@@ -765,24 +830,19 @@ export class OAuthProvider extends OAuthVerifier {
765
830
 
766
831
  return { issuer, parameters, redirect: { code } }
767
832
  } catch (err) {
768
- await this.deleteRequest(uri, parameters)
833
+ await this.deleteRequest(requestUri, parameters)
769
834
 
770
835
  throw AccessDeniedError.from(parameters, err)
771
836
  }
772
837
  }
773
838
 
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
- )
839
+ protected async rejectRequest({
840
+ requestUri,
841
+ deviceId,
842
+ }: ApiContext): Promise<AuthorizationResultRedirect> {
843
+ const { parameters } = await this.requestManager.get(requestUri, deviceId)
784
844
 
785
- await this.deleteRequest(uri, parameters)
845
+ await this.deleteRequest(requestUri, parameters)
786
846
 
787
847
  return {
788
848
  issuer: this.issuer,
@@ -1033,57 +1093,67 @@ export class OAuthProvider extends OAuthVerifier {
1033
1093
 
1034
1094
  // Utils
1035
1095
 
1036
- const csrfCookie = (uri: RequestUri) => `csrf-${uri}`
1037
- const onError =
1096
+ const csrfCookie = (requestUri: RequestUri) => `csrf-${requestUri}`
1097
+ const onError: null | ErrorHandler<Req, Res> =
1038
1098
  options?.onError ??
1039
1099
  (process.env['NODE_ENV'] === 'development'
1040
- ? (req, res, err, msg): void =>
1100
+ ? (req, res, err, msg) => {
1041
1101
  console.error(`OAuthProvider error (${msg}):`, err)
1042
- : undefined)
1102
+ }
1103
+ : null)
1043
1104
 
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', '*')
1105
+ // CORS preflight
1106
+ const corsHeaders: Middleware = function (req, res, next) {
1107
+ res.setHeader('Access-Control-Max-Age', '86400') // 1 day
1052
1108
 
1053
- res.setHeader('Cache-Control', 'max-age=300')
1054
- next()
1055
- },
1056
- staticJsonMiddleware(json),
1057
- ])
1109
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
1110
+ //
1111
+ // > For requests without credentials, the literal value "*" can be
1112
+ // > specified as a wildcard; the value tells browsers to allow
1113
+ // > requesting code from any origin to access the resource.
1114
+ // > Attempting to use the wildcard with credentials results in an
1115
+ // > error.
1116
+ //
1117
+ // A "*" is safer to use than reflecting the request origin.
1118
+ res.setHeader('Access-Control-Allow-Origin', '*')
1119
+
1120
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
1121
+ // > The value "*" only counts as a special wildcard value for
1122
+ // > requests without credentials (requests without HTTP cookies or
1123
+ // > HTTP authentication information). In requests with credentials,
1124
+ // > it is treated as the literal method name "*" without special
1125
+ // > semantics.
1126
+ res.setHeader('Access-Control-Allow-Methods', '*')
1127
+
1128
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,DPoP')
1129
+
1130
+ next()
1131
+ }
1132
+
1133
+ const corsPreflight: Middleware = combineMiddlewares([
1134
+ corsHeaders,
1135
+ (req, res) => {
1136
+ res.writeHead(200).end()
1137
+ },
1138
+ ])
1058
1139
 
1059
1140
  /**
1060
1141
  * Wrap an OAuth endpoint in a middleware that will set the appropriate
1061
1142
  * response headers and format the response as JSON.
1062
1143
  */
1063
1144
  const jsonHandler = <T, TReq extends Req, TRes extends Res, Json>(
1064
- buildJson: (this: T, req: TReq, res: TRes) => Json | Promise<Json>,
1145
+ buildJson: (this: T, req: TReq, res: TRes) => Awaitable<Json>,
1065
1146
  status?: number,
1066
1147
  ): Handler<T, TReq, TRes> =>
1067
1148
  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
1149
  try {
1150
+ // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
1151
+ res.setHeader('Cache-Control', 'no-store')
1152
+ res.setHeader('Pragma', 'no-cache')
1153
+
1084
1154
  // Ensure we can agree on a content encoding & type before starting to
1085
1155
  // build the JSON response.
1086
- if (!mediaType(req.headers['accept'], ['application/json'])) {
1156
+ if (!negotiateContent(req, ['application/json'])) {
1087
1157
  throw createHttpError(406, 'Unsupported media type')
1088
1158
  }
1089
1159
 
@@ -1094,39 +1164,107 @@ export class OAuthProvider extends OAuthVerifier {
1094
1164
  res.writeHead(status ?? 204).end()
1095
1165
  }
1096
1166
  } 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
- }
1167
+ onError?.(req, res, err, 'OAuth request error')
1103
1168
 
1169
+ if (!res.headersSent) {
1104
1170
  const payload = buildErrorPayload(err)
1105
1171
  const status = buildErrorStatus(err)
1106
1172
  writeJson(res, payload, { status })
1107
1173
  } else {
1108
1174
  res.destroy()
1109
1175
  }
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
1176
  }
1117
1177
  }
1118
1178
 
1179
+ const oauthHandler = <T, TReq extends Req, TRes extends Res, Json>(
1180
+ buildOAuthResponse: (this: T, req: TReq, res: TRes) => Awaitable<Json>,
1181
+ status?: number,
1182
+ ) =>
1183
+ combineMiddlewares([
1184
+ corsHeaders,
1185
+ jsonHandler<T, TReq, TRes, Json>(async function (req, res) {
1186
+ try {
1187
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
1188
+ const dpopNonce = server.nextDpopNonce()
1189
+ if (dpopNonce) {
1190
+ const name = 'DPoP-Nonce'
1191
+ res.setHeader(name, dpopNonce)
1192
+ res.appendHeader('Access-Control-Expose-Headers', name)
1193
+ }
1194
+
1195
+ return await buildOAuthResponse.call(this, req, res)
1196
+ } catch (err) {
1197
+ if (!res.headersSent && err instanceof WWWAuthenticateError) {
1198
+ const name = 'WWW-Authenticate'
1199
+ res.setHeader(name, err.wwwAuthenticateHeader)
1200
+ res.appendHeader('Access-Control-Expose-Headers', name)
1201
+ }
1202
+
1203
+ throw err
1204
+ }
1205
+ }, status),
1206
+ ])
1207
+
1208
+ const apiHandler = <
1209
+ T,
1210
+ TReq extends Req,
1211
+ TRes extends Res,
1212
+ S extends z.ZodTypeAny,
1213
+ Json,
1214
+ >(
1215
+ inputSchema: S,
1216
+ buildJson: (
1217
+ this: T,
1218
+ req: TReq,
1219
+ res: TRes,
1220
+ input: z.infer<S>,
1221
+ context: ApiContext,
1222
+ ) => Json | Promise<Json>,
1223
+ status?: number,
1224
+ ) =>
1225
+ jsonHandler<T, TReq, TRes, Json>(async function (req, res) {
1226
+ validateFetchMode(req, res, ['same-origin'])
1227
+ validateFetchSite(req, res, ['same-origin'])
1228
+ validateSameOrigin(req, res, issuerOrigin)
1229
+ const referer = validateReferer(req, res, {
1230
+ origin: issuerOrigin,
1231
+ pathname: '/oauth/authorize',
1232
+ })
1233
+
1234
+ const requestUri = await requestUriSchema.parseAsync(
1235
+ referer.searchParams.get('request_uri'),
1236
+ { path: ['query', 'request_uri'] },
1237
+ )
1238
+
1239
+ validateCsrfToken(
1240
+ req,
1241
+ res,
1242
+ req.headers['x-csrf-token'],
1243
+ csrfCookie(requestUri),
1244
+ )
1245
+
1246
+ const { deviceId, deviceMetadata } = await server.deviceManager.load(
1247
+ req,
1248
+ res,
1249
+ )
1250
+
1251
+ const payload = await parseHttpRequest(req, ['json'])
1252
+ const input = await inputSchema.parseAsync(payload, { path: ['body'] })
1253
+
1254
+ const context: ApiContext = { requestUri, deviceId, deviceMetadata }
1255
+ return buildJson.call(this, req, res, input, context)
1256
+ }, status)
1257
+
1119
1258
  const navigationHandler = <T, TReq extends Req, TRes extends Res>(
1120
- handler: (this: T, req: TReq, res: TRes) => void | Promise<void>,
1259
+ handler: (this: T, req: TReq, res: TRes) => Awaitable<void>,
1121
1260
  ): Handler<T, TReq, TRes> =>
1122
1261
  async function (req, res) {
1123
- res.setHeader('Access-Control-Allow-Origin', '*')
1124
- res.setHeader('Access-Control-Allow-Headers', '*')
1262
+ try {
1263
+ res.setHeader('Cache-Control', 'no-store')
1264
+ res.setHeader('Pragma', 'no-cache')
1125
1265
 
1126
- res.setHeader('Cache-Control', 'no-store')
1127
- res.setHeader('Pragma', 'no-cache')
1266
+ res.setHeader('Referrer-Policy', 'same-origin')
1128
1267
 
1129
- try {
1130
1268
  validateFetchMode(req, res, ['navigate'])
1131
1269
  validateFetchDest(req, res, ['document'])
1132
1270
  validateSameOrigin(req, res, issuerOrigin)
@@ -1141,11 +1279,78 @@ export class OAuthProvider extends OAuthVerifier {
1141
1279
  )
1142
1280
 
1143
1281
  if (!res.headersSent) {
1144
- await server.outputManager.sendErrorPage(res, err)
1282
+ await server.outputManager.sendErrorPage(res, err, {
1283
+ preferredLocales: extractLocales(req),
1284
+ })
1145
1285
  }
1146
1286
  }
1147
1287
  }
1148
1288
 
1289
+ // Simple GET requests fall under the category of "no-cors" request, meaning
1290
+ // that the browser will allow any cross-origin request, with credentials,
1291
+ // to be sent to the oauth server. The OAuth Server will, however:
1292
+ // 1) validate the request origin (see navigationHandler),
1293
+ // 2) validate the CSRF token,
1294
+ // 3) validate the referer,
1295
+ // 4) validate the sec-fetch-site header,
1296
+ // 4) validate the sec-fetch-mode header (see navigationHandler),
1297
+ // 5) validate the sec-fetch-dest header (see navigationHandler).
1298
+ // And will error (refuse to serve the request) if any of these checks fail.
1299
+ const sameOriginNavigationHandler = <
1300
+ T extends { url: URL },
1301
+ TReq extends Req,
1302
+ TRes extends Res,
1303
+ >(
1304
+ handler: (
1305
+ this: T,
1306
+ req: TReq,
1307
+ res: TRes,
1308
+ deviceInfo: DeviceInfo,
1309
+ ) => Awaitable<void>,
1310
+ ): Handler<T, TReq, TRes> =>
1311
+ navigationHandler(async function (req, res) {
1312
+ validateFetchSite(req, res, ['same-origin'])
1313
+
1314
+ const deviceInfo = await server.deviceManager.load(req, res)
1315
+
1316
+ return handler.call(this, req, res, deviceInfo)
1317
+ })
1318
+
1319
+ const authorizeRedirectNavigationHandler = <
1320
+ T extends { url: URL },
1321
+ TReq extends Req,
1322
+ TRes extends Res,
1323
+ >(
1324
+ handler: (
1325
+ this: T,
1326
+ req: TReq,
1327
+ res: TRes,
1328
+ context: ApiContext,
1329
+ ) => Awaitable<AuthorizationResultRedirect>,
1330
+ ): Handler<T, TReq, TRes> =>
1331
+ sameOriginNavigationHandler(async function (req, res, deviceInfo) {
1332
+ const referer = validateReferer(req, res, {
1333
+ origin: issuerOrigin,
1334
+ pathname: '/oauth/authorize',
1335
+ })
1336
+
1337
+ const requestUri = await requestUriSchema.parseAsync(
1338
+ referer.searchParams.get('request_uri'),
1339
+ )
1340
+
1341
+ const csrfToken = this.url.searchParams.get('csrf_token')
1342
+ const csrfCookieName = csrfCookie(requestUri)
1343
+
1344
+ // Next line will "clear" the CSRF token cookie, preventing replay of
1345
+ // this request (navigating "back" will result in an error).
1346
+ validateCsrfToken(req, res, csrfToken, csrfCookieName, true)
1347
+
1348
+ const context: ApiContext = { ...deviceInfo, requestUri }
1349
+
1350
+ const redirect = await handler.call(this, req, res, context)
1351
+ return sendAuthorizeRedirect(res, redirect)
1352
+ })
1353
+
1149
1354
  /**
1150
1355
  * Provides a better UX when a request is denied by redirecting to the
1151
1356
  * client with the error details. This will also log any error that caused
@@ -1172,44 +1377,26 @@ export class OAuthProvider extends OAuthVerifier {
1172
1377
 
1173
1378
  //- Public OAuth endpoints
1174
1379
 
1380
+ router.options('/.well-known/oauth-authorization-server', corsPreflight)
1175
1381
  router.get(
1176
1382
  '/.well-known/oauth-authorization-server',
1177
- staticJson(server.metadata),
1383
+ corsHeaders,
1384
+ cacheControlMiddleware(300),
1385
+ staticJsonMiddleware(server.metadata),
1178
1386
  )
1179
1387
 
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))
1388
+ router.options('/oauth/jwks', corsPreflight)
1389
+ router.get(
1390
+ '/oauth/jwks',
1391
+ corsHeaders,
1392
+ cacheControlMiddleware(300),
1393
+ staticJsonMiddleware(server.jwks),
1394
+ )
1208
1395
 
1209
1396
  router.options('/oauth/par', corsPreflight)
1210
1397
  router.post(
1211
1398
  '/oauth/par',
1212
- jsonHandler(async function (req, _res) {
1399
+ oauthHandler(async function (req, _res) {
1213
1400
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1214
1401
 
1215
1402
  const credentials = await oauthClientCredentialsSchema
@@ -1233,11 +1420,9 @@ export class OAuthProvider extends OAuthVerifier {
1233
1420
  )
1234
1421
  }, 201),
1235
1422
  )
1236
-
1237
1423
  // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
1238
1424
  // > If the request did not use the POST method, the authorization server
1239
1425
  // > responds with an HTTP 405 (Method Not Allowed) status code.
1240
- router.options('/oauth/par', corsPreflight)
1241
1426
  router.all('/oauth/par', (req, res) => {
1242
1427
  res.writeHead(405).end()
1243
1428
  })
@@ -1245,7 +1430,7 @@ export class OAuthProvider extends OAuthVerifier {
1245
1430
  router.options('/oauth/token', corsPreflight)
1246
1431
  router.post(
1247
1432
  '/oauth/token',
1248
- jsonHandler(async function (req, _res) {
1433
+ oauthHandler(async function (req, _res) {
1249
1434
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1250
1435
 
1251
1436
  const clientMetadata =
@@ -1277,7 +1462,7 @@ export class OAuthProvider extends OAuthVerifier {
1277
1462
  router.options('/oauth/revoke', corsPreflight)
1278
1463
  router.post(
1279
1464
  '/oauth/revoke',
1280
- jsonHandler(async function (req, res) {
1465
+ oauthHandler(async function (req, res) {
1281
1466
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1282
1467
 
1283
1468
  const tokenIdentification = await oauthTokenIdentificationSchema
@@ -1291,8 +1476,6 @@ export class OAuthProvider extends OAuthVerifier {
1291
1476
  }
1292
1477
  }),
1293
1478
  )
1294
-
1295
- router.options('/oauth/revoke', corsPreflight)
1296
1479
  router.get(
1297
1480
  '/oauth/revoke',
1298
1481
  navigationHandler(async function (req, res) {
@@ -1317,9 +1500,10 @@ export class OAuthProvider extends OAuthVerifier {
1317
1500
  }),
1318
1501
  )
1319
1502
 
1503
+ router.options('/oauth/introspect', corsPreflight)
1320
1504
  router.post(
1321
1505
  '/oauth/introspect',
1322
- jsonHandler(async function (req, _res) {
1506
+ oauthHandler(async function (req, _res) {
1323
1507
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1324
1508
 
1325
1509
  const credentials = await oauthClientCredentialsSchema
@@ -1346,7 +1530,7 @@ export class OAuthProvider extends OAuthVerifier {
1346
1530
  const query = Object.fromEntries(this.url.searchParams)
1347
1531
 
1348
1532
  const clientCredentials = await oauthClientCredentialsSchema
1349
- .parseAsync(query, { path: ['body'] })
1533
+ .parseAsync(query, { path: ['query'] })
1350
1534
  .catch(throwInvalidRequest)
1351
1535
 
1352
1536
  if ('client_secret' in clientCredentials) {
@@ -1362,7 +1546,9 @@ export class OAuthProvider extends OAuthVerifier {
1362
1546
  res,
1363
1547
  )
1364
1548
 
1365
- const data = await server
1549
+ const result:
1550
+ | AuthorizationResultRedirect
1551
+ | AuthorizationResultAuthorize = await server
1366
1552
  .authorize(
1367
1553
  clientCredentials,
1368
1554
  authorizationRequest,
@@ -1371,173 +1557,79 @@ export class OAuthProvider extends OAuthVerifier {
1371
1557
  )
1372
1558
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1373
1559
 
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
- }
1560
+ if ('redirect' in result) {
1561
+ return sendAuthorizeRedirect(res, result)
1562
+ } else {
1563
+ await setupCsrfToken(req, res, csrfCookie(result.authorize.uri))
1564
+ return server.outputManager.sendAuthorizePage(res, result, {
1565
+ preferredLocales: extractLocales(req),
1566
+ })
1386
1567
  }
1387
1568
  }),
1388
1569
  )
1389
1570
 
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
1571
  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
- )
1572
+ '/oauth/authorize/verify-handle-availability',
1573
+ apiHandler(
1574
+ z.object({ handle: handleSchema }).strict(),
1575
+ async function (req, res, data) {
1576
+ return server.accountManager.verifyHandleAvailability(data.handle)
1577
+ },
1578
+ ),
1579
+ )
1420
1580
 
1421
- const { deviceId } = await server.deviceManager.load(req, res, true)
1581
+ router.post(
1582
+ '/oauth/authorize/sign-up',
1583
+ apiHandler(signUpDataSchema, async function (req, res, data, ctx) {
1584
+ return server.signUp(ctx, data)
1585
+ }),
1586
+ )
1422
1587
 
1423
- return server.signIn(
1424
- deviceId,
1425
- input.request_uri,
1426
- input.client_id,
1427
- input.credentials,
1428
- )
1588
+ router.post(
1589
+ '/oauth/authorize/sign-in',
1590
+ apiHandler(signInDataSchema, async function (req, res, data, ctx) {
1591
+ return server.signIn(ctx, data)
1429
1592
  }),
1430
1593
  )
1431
1594
 
1432
- const acceptQuerySchema = z.object({
1433
- csrf_token: z.string(),
1434
- request_uri: requestUriSchema,
1435
- client_id: clientIdSchema,
1436
- account_sub: z.string(),
1437
- })
1595
+ router.post(
1596
+ '/oauth/authorize/reset-password-request',
1597
+ apiHandler(
1598
+ resetPasswordRequestDataSchema,
1599
+ async function (req, res, data) {
1600
+ await server.accountManager.resetPasswordRequest(data)
1601
+ },
1602
+ ),
1603
+ )
1604
+
1605
+ router.post(
1606
+ '/oauth/authorize/reset-password-confirm',
1607
+ apiHandler(
1608
+ resetPasswordConfirmDataSchema,
1609
+ async function (req, res, data) {
1610
+ await server.accountManager.resetPasswordConfirm(data)
1611
+ },
1612
+ ),
1613
+ )
1438
1614
 
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
1615
  router.get(
1449
1616
  '/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
- )
1617
+ authorizeRedirectNavigationHandler(async function (req, res, ctx) {
1618
+ const sub = this.url.searchParams.get('account_sub')
1619
+ if (!sub) throw new InvalidRequestError('Account sub not provided')
1478
1620
 
1479
- const data = await server
1480
- .acceptRequest(
1481
- input.request_uri,
1482
- input.client_id,
1483
- input.account_sub,
1484
- deviceId,
1485
- deviceMetadata,
1486
- )
1621
+ return server
1622
+ .acceptRequest(ctx, sub)
1487
1623
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1488
-
1489
- return await sendAuthorizeRedirect(res, data)
1490
1624
  }),
1491
1625
  )
1492
1626
 
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
1627
  router.get(
1509
1628
  '/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)
1629
+ authorizeRedirectNavigationHandler(async function (req, res, ctx) {
1630
+ return server
1631
+ .rejectRequest(ctx)
1538
1632
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1539
-
1540
- return await sendAuthorizeRedirect(res, data)
1541
1633
  }),
1542
1634
  )
1543
1635