@atproto/oauth-provider 0.6.6 → 0.7.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 (465) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/access-token/access-token-mode.d.ts +5 -0
  3. package/dist/access-token/access-token-mode.d.ts.map +1 -0
  4. package/dist/access-token/access-token-mode.js +9 -0
  5. package/dist/access-token/access-token-mode.js.map +1 -0
  6. package/dist/account/account-manager.d.ts +13 -7
  7. package/dist/account/account-manager.d.ts.map +1 -1
  8. package/dist/account/account-manager.js +69 -52
  9. package/dist/account/account-manager.js.map +1 -1
  10. package/dist/account/account-store.d.ts +88 -77
  11. package/dist/account/account-store.d.ts.map +1 -1
  12. package/dist/account/account-store.js +24 -73
  13. package/dist/account/account-store.js.map +1 -1
  14. package/dist/account/sign-in-data.d.ts +4 -13
  15. package/dist/account/sign-in-data.d.ts.map +1 -1
  16. package/dist/account/sign-in-data.js +9 -9
  17. package/dist/account/sign-in-data.js.map +1 -1
  18. package/dist/account/sign-up-input.d.ts +4 -5
  19. package/dist/account/sign-up-input.d.ts.map +1 -1
  20. package/dist/account/sign-up-input.js +13 -3
  21. package/dist/account/sign-up-input.js.map +1 -1
  22. package/dist/client/client-manager.d.ts +4 -1
  23. package/dist/client/client-manager.d.ts.map +1 -1
  24. package/dist/client/client-manager.js +13 -1
  25. package/dist/client/client-manager.js.map +1 -1
  26. package/dist/client/client-store.d.ts +1 -1
  27. package/dist/client/client-store.d.ts.map +1 -1
  28. package/dist/constants.d.ts +5 -1
  29. package/dist/constants.d.ts.map +1 -1
  30. package/dist/constants.js +6 -2
  31. package/dist/constants.js.map +1 -1
  32. package/dist/customization/branding.d.ts +54 -0
  33. package/dist/customization/branding.d.ts.map +1 -0
  34. package/dist/customization/branding.js +13 -0
  35. package/dist/customization/branding.js.map +1 -0
  36. package/dist/customization/build-customization-css.d.ts +3 -0
  37. package/dist/customization/build-customization-css.d.ts.map +1 -0
  38. package/dist/customization/build-customization-css.js +27 -0
  39. package/dist/customization/build-customization-css.js.map +1 -0
  40. package/dist/customization/build-customization-data.d.ts +4 -0
  41. package/dist/customization/build-customization-data.d.ts.map +1 -0
  42. package/dist/customization/build-customization-data.js +18 -0
  43. package/dist/customization/build-customization-data.js.map +1 -0
  44. package/dist/customization/colors.d.ts +7 -0
  45. package/dist/customization/colors.d.ts.map +1 -0
  46. package/dist/customization/colors.js +27 -0
  47. package/dist/customization/colors.js.map +1 -0
  48. package/dist/customization/customization.d.ts +129 -0
  49. package/dist/customization/customization.d.ts.map +1 -0
  50. package/dist/customization/customization.js +26 -0
  51. package/dist/customization/customization.js.map +1 -0
  52. package/dist/customization/links.d.ts +26 -0
  53. package/dist/customization/links.d.ts.map +1 -0
  54. package/dist/customization/links.js +12 -0
  55. package/dist/customization/links.js.map +1 -0
  56. package/dist/device/device-id.d.ts +1 -0
  57. package/dist/device/device-id.d.ts.map +1 -1
  58. package/dist/device/device-id.js +4 -0
  59. package/dist/device/device-id.js.map +1 -1
  60. package/dist/device/device-manager.d.ts +6 -36
  61. package/dist/device/device-manager.d.ts.map +1 -1
  62. package/dist/device/device-manager.js +49 -43
  63. package/dist/device/device-manager.js.map +1 -1
  64. package/dist/device/device-store.d.ts +1 -0
  65. package/dist/device/device-store.d.ts.map +1 -1
  66. package/dist/device/device-store.js.map +1 -1
  67. package/dist/dpop/dpop-manager.d.ts +3 -3
  68. package/dist/dpop/dpop-nonce.d.ts +3 -3
  69. package/dist/dpop/dpop-nonce.d.ts.map +1 -1
  70. package/dist/errors/access-denied-error.d.ts +4 -3
  71. package/dist/errors/access-denied-error.d.ts.map +1 -1
  72. package/dist/errors/access-denied-error.js +5 -6
  73. package/dist/errors/access-denied-error.js.map +1 -1
  74. package/dist/{output/build-error-payload.d.ts → errors/error-parser.d.ts} +1 -1
  75. package/dist/errors/error-parser.d.ts.map +1 -0
  76. package/dist/{output/build-error-payload.js → errors/error-parser.js} +2 -2
  77. package/dist/errors/error-parser.js.map +1 -0
  78. package/dist/errors/invalid-grant-error.d.ts +1 -0
  79. package/dist/errors/invalid-grant-error.d.ts.map +1 -1
  80. package/dist/errors/invalid-grant-error.js +5 -0
  81. package/dist/errors/invalid-grant-error.js.map +1 -1
  82. package/dist/errors/login-required-error.d.ts +1 -0
  83. package/dist/errors/login-required-error.d.ts.map +1 -1
  84. package/dist/errors/login-required-error.js +5 -0
  85. package/dist/errors/login-required-error.js.map +1 -1
  86. package/dist/index.d.ts +1 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +1 -0
  89. package/dist/index.js.map +1 -1
  90. package/dist/lib/html/build-document.d.ts +2 -2
  91. package/dist/lib/html/build-document.d.ts.map +1 -1
  92. package/dist/lib/html/build-document.js +4 -0
  93. package/dist/lib/html/build-document.js.map +1 -1
  94. package/dist/lib/html/hydration-data.d.ts +4 -0
  95. package/dist/lib/html/hydration-data.d.ts.map +1 -0
  96. package/dist/{output/backend-data.js → lib/html/hydration-data.js} +8 -8
  97. package/dist/lib/html/hydration-data.js.map +1 -0
  98. package/dist/lib/html/tags.d.ts +1 -1
  99. package/dist/lib/html/tags.d.ts.map +1 -1
  100. package/dist/lib/html/tags.js +1 -1
  101. package/dist/lib/html/tags.js.map +1 -1
  102. package/dist/lib/http/accept.d.ts +2 -2
  103. package/dist/lib/http/accept.d.ts.map +1 -1
  104. package/dist/lib/http/accept.js +1 -1
  105. package/dist/lib/http/accept.js.map +1 -1
  106. package/dist/lib/http/context.d.ts +2 -4
  107. package/dist/lib/http/context.d.ts.map +1 -1
  108. package/dist/lib/http/context.js +29 -4
  109. package/dist/lib/http/context.js.map +1 -1
  110. package/dist/lib/http/headers.d.ts +3 -0
  111. package/dist/lib/http/headers.d.ts.map +1 -0
  112. package/dist/lib/http/headers.js +14 -0
  113. package/dist/lib/http/headers.js.map +1 -0
  114. package/dist/lib/http/index.d.ts +1 -0
  115. package/dist/lib/http/index.d.ts.map +1 -1
  116. package/dist/lib/http/index.js +1 -0
  117. package/dist/lib/http/index.js.map +1 -1
  118. package/dist/lib/http/middleware.d.ts +1 -1
  119. package/dist/lib/http/middleware.d.ts.map +1 -1
  120. package/dist/lib/http/middleware.js +8 -24
  121. package/dist/lib/http/middleware.js.map +1 -1
  122. package/dist/lib/http/parser.d.ts +3 -3
  123. package/dist/lib/http/parser.d.ts.map +1 -1
  124. package/dist/lib/http/request.d.ts +13 -9
  125. package/dist/lib/http/request.d.ts.map +1 -1
  126. package/dist/lib/http/request.js +27 -49
  127. package/dist/lib/http/request.js.map +1 -1
  128. package/dist/lib/http/response.d.ts +6 -2
  129. package/dist/lib/http/response.d.ts.map +1 -1
  130. package/dist/lib/http/response.js +31 -11
  131. package/dist/lib/http/response.js.map +1 -1
  132. package/dist/lib/http/route.d.ts +3 -3
  133. package/dist/lib/http/route.d.ts.map +1 -1
  134. package/dist/lib/http/route.js +1 -1
  135. package/dist/lib/http/route.js.map +1 -1
  136. package/dist/lib/http/router.d.ts +12 -11
  137. package/dist/lib/http/router.d.ts.map +1 -1
  138. package/dist/lib/http/router.js +26 -34
  139. package/dist/lib/http/router.js.map +1 -1
  140. package/dist/lib/http/security-headers.js +1 -1
  141. package/dist/lib/http/security-headers.js.map +1 -1
  142. package/dist/lib/http/stream.d.ts +3 -3
  143. package/dist/lib/http/stream.d.ts.map +1 -1
  144. package/dist/lib/http/types.d.ts +1 -1
  145. package/dist/lib/http/types.d.ts.map +1 -1
  146. package/dist/lib/send-web-page.d.ts +8 -0
  147. package/dist/lib/send-web-page.d.ts.map +1 -0
  148. package/dist/{output → lib}/send-web-page.js +9 -7
  149. package/dist/lib/send-web-page.js.map +1 -0
  150. package/dist/lib/util/authorization-header.d.ts.map +1 -1
  151. package/dist/lib/util/color.d.ts +32 -0
  152. package/dist/lib/util/color.d.ts.map +1 -0
  153. package/dist/lib/util/color.js +116 -0
  154. package/dist/lib/util/color.js.map +1 -0
  155. package/dist/lib/util/crypto.d.ts +1 -0
  156. package/dist/lib/util/crypto.d.ts.map +1 -1
  157. package/dist/lib/util/crypto.js +8 -3
  158. package/dist/lib/util/crypto.js.map +1 -1
  159. package/dist/lib/util/function.d.ts +1 -0
  160. package/dist/lib/util/function.d.ts.map +1 -1
  161. package/dist/lib/util/function.js +12 -0
  162. package/dist/lib/util/function.js.map +1 -1
  163. package/dist/lib/util/locale.d.ts +20 -0
  164. package/dist/lib/util/locale.d.ts.map +1 -0
  165. package/dist/lib/util/locale.js +14 -0
  166. package/dist/lib/util/locale.js.map +1 -0
  167. package/dist/lib/util/time.d.ts +1 -1
  168. package/dist/lib/util/time.d.ts.map +1 -1
  169. package/dist/lib/util/time.js +1 -1
  170. package/dist/lib/util/time.js.map +1 -1
  171. package/dist/lib/util/type.d.ts +22 -0
  172. package/dist/lib/util/type.d.ts.map +1 -1
  173. package/dist/lib/util/type.js.map +1 -1
  174. package/dist/lib/util/ui8.d.ts +4 -0
  175. package/dist/lib/util/ui8.d.ts.map +1 -0
  176. package/dist/lib/util/ui8.js +17 -0
  177. package/dist/lib/util/ui8.js.map +1 -0
  178. package/dist/lib/util/zod-error.d.ts +2 -0
  179. package/dist/lib/util/zod-error.d.ts.map +1 -0
  180. package/dist/lib/util/zod-error.js +16 -0
  181. package/dist/lib/util/zod-error.js.map +1 -0
  182. package/dist/oauth-errors.d.ts +22 -22
  183. package/dist/oauth-errors.d.ts.map +1 -1
  184. package/dist/oauth-errors.js +37 -45
  185. package/dist/oauth-errors.js.map +1 -1
  186. package/dist/oauth-hooks.d.ts +11 -23
  187. package/dist/oauth-hooks.d.ts.map +1 -1
  188. package/dist/oauth-hooks.js.map +1 -1
  189. package/dist/oauth-middleware.d.ts +12 -0
  190. package/dist/oauth-middleware.d.ts.map +1 -0
  191. package/dist/oauth-middleware.js +32 -0
  192. package/dist/oauth-middleware.js.map +1 -0
  193. package/dist/oauth-provider.d.ts +109 -113
  194. package/dist/oauth-provider.d.ts.map +1 -1
  195. package/dist/oauth-provider.js +124 -542
  196. package/dist/oauth-provider.js.map +1 -1
  197. package/dist/oauth-verifier.d.ts +7 -26
  198. package/dist/oauth-verifier.d.ts.map +1 -1
  199. package/dist/oauth-verifier.js +6 -16
  200. package/dist/oauth-verifier.js.map +1 -1
  201. package/dist/request/code.d.ts.map +1 -1
  202. package/dist/request/request-data.d.ts +2 -4
  203. package/dist/request/request-data.d.ts.map +1 -1
  204. package/dist/request/request-data.js.map +1 -1
  205. package/dist/request/request-manager.d.ts +4 -2
  206. package/dist/request/request-manager.d.ts.map +1 -1
  207. package/dist/request/request-manager.js +9 -8
  208. package/dist/request/request-manager.js.map +1 -1
  209. package/dist/request/request-store.d.ts +6 -0
  210. package/dist/request/request-store.d.ts.map +1 -1
  211. package/dist/request/request-store.js +3 -1
  212. package/dist/request/request-store.js.map +1 -1
  213. package/dist/result/authorization-redirect-parameters.d.ts +18 -0
  214. package/dist/result/authorization-redirect-parameters.d.ts.map +1 -0
  215. package/dist/result/authorization-redirect-parameters.js +3 -0
  216. package/dist/result/authorization-redirect-parameters.js.map +1 -0
  217. package/dist/result/authorization-result-authorize-page.d.ts +13 -0
  218. package/dist/result/authorization-result-authorize-page.d.ts.map +1 -0
  219. package/dist/result/authorization-result-authorize-page.js +3 -0
  220. package/dist/result/authorization-result-authorize-page.js.map +1 -0
  221. package/dist/result/authorization-result-redirect.d.ts +8 -0
  222. package/dist/result/authorization-result-redirect.d.ts.map +1 -0
  223. package/dist/result/authorization-result-redirect.js +3 -0
  224. package/dist/result/authorization-result-redirect.js.map +1 -0
  225. package/dist/router/assets/assets-manifest.d.ts +10 -0
  226. package/dist/router/assets/assets-manifest.d.ts.map +1 -0
  227. package/dist/router/assets/assets-manifest.js +77 -0
  228. package/dist/router/assets/assets-manifest.js.map +1 -0
  229. package/dist/router/assets/assets.d.ts +16 -0
  230. package/dist/router/assets/assets.d.ts.map +1 -0
  231. package/dist/router/assets/assets.js +43 -0
  232. package/dist/router/assets/assets.js.map +1 -0
  233. package/dist/router/assets/csrf.d.ts +4 -0
  234. package/dist/router/assets/csrf.d.ts.map +1 -0
  235. package/dist/router/assets/csrf.js +51 -0
  236. package/dist/router/assets/csrf.js.map +1 -0
  237. package/dist/router/assets/send-account-page.d.ts +7 -0
  238. package/dist/router/assets/send-account-page.d.ts.map +1 -0
  239. package/dist/router/assets/send-account-page.js +34 -0
  240. package/dist/router/assets/send-account-page.js.map +1 -0
  241. package/dist/router/assets/send-authorization-page.d.ts +5 -0
  242. package/dist/router/assets/send-authorization-page.d.ts.map +1 -0
  243. package/dist/router/assets/send-authorization-page.js +49 -0
  244. package/dist/router/assets/send-authorization-page.js.map +1 -0
  245. package/dist/router/assets/send-error-page.d.ts +4 -0
  246. package/dist/router/assets/send-error-page.d.ts.map +1 -0
  247. package/dist/router/assets/send-error-page.js +34 -0
  248. package/dist/router/assets/send-error-page.js.map +1 -0
  249. package/dist/router/create-account-page-middleware.d.ts +6 -0
  250. package/dist/router/create-account-page-middleware.d.ts.map +1 -0
  251. package/dist/router/create-account-page-middleware.js +39 -0
  252. package/dist/router/create-account-page-middleware.js.map +1 -0
  253. package/dist/router/create-api-middleware.d.ts +8 -0
  254. package/dist/router/create-api-middleware.d.ts.map +1 -0
  255. package/dist/router/create-api-middleware.js +501 -0
  256. package/dist/router/create-api-middleware.js.map +1 -0
  257. package/dist/router/create-authorization-page-middleware.d.ts +6 -0
  258. package/dist/router/create-authorization-page-middleware.d.ts.map +1 -0
  259. package/dist/router/create-authorization-page-middleware.js +104 -0
  260. package/dist/router/create-authorization-page-middleware.js.map +1 -0
  261. package/dist/router/create-oauth-middleware.d.ts +6 -0
  262. package/dist/router/create-oauth-middleware.d.ts.map +1 -0
  263. package/dist/router/create-oauth-middleware.js +142 -0
  264. package/dist/router/create-oauth-middleware.js.map +1 -0
  265. package/dist/router/error-handler.d.ts +3 -0
  266. package/dist/router/error-handler.d.ts.map +1 -0
  267. package/dist/{account/account.js → router/error-handler.js} +1 -1
  268. package/dist/router/error-handler.js.map +1 -0
  269. package/dist/router/middleware-options.d.ts +6 -0
  270. package/dist/router/middleware-options.d.ts.map +1 -0
  271. package/dist/router/middleware-options.js +3 -0
  272. package/dist/router/middleware-options.js.map +1 -0
  273. package/dist/router/send-redirect.d.ts +16 -0
  274. package/dist/router/send-redirect.d.ts.map +1 -0
  275. package/dist/{output/send-authorize-redirect.js → router/send-redirect.js} +40 -24
  276. package/dist/router/send-redirect.js.map +1 -0
  277. package/dist/{token/token-claims.d.ts → signer/api-token-payload.d.ts} +237 -232
  278. package/dist/signer/api-token-payload.d.ts.map +1 -0
  279. package/dist/signer/api-token-payload.js +17 -0
  280. package/dist/signer/api-token-payload.js.map +1 -0
  281. package/dist/signer/signed-token-payload.d.ts +164 -159
  282. package/dist/signer/signed-token-payload.d.ts.map +1 -1
  283. package/dist/signer/signed-token-payload.js +10 -16
  284. package/dist/signer/signed-token-payload.js.map +1 -1
  285. package/dist/signer/signer.d.ts +42 -11246
  286. package/dist/signer/signer.d.ts.map +1 -1
  287. package/dist/signer/signer.js +30 -15
  288. package/dist/signer/signer.js.map +1 -1
  289. package/dist/token/refresh-token.d.ts.map +1 -1
  290. package/dist/token/token-data.d.ts +1 -1
  291. package/dist/token/token-data.d.ts.map +1 -1
  292. package/dist/token/token-id.d.ts.map +1 -1
  293. package/dist/token/token-manager.d.ts +28 -26
  294. package/dist/token/token-manager.d.ts.map +1 -1
  295. package/dist/token/token-manager.js +138 -196
  296. package/dist/token/token-manager.js.map +1 -1
  297. package/dist/token/token-store.d.ts +4 -4
  298. package/dist/token/token-store.d.ts.map +1 -1
  299. package/dist/token/token-store.js +1 -0
  300. package/dist/token/token-store.js.map +1 -1
  301. package/dist/token/verify-token-claims.d.ts +3 -3
  302. package/dist/token/verify-token-claims.d.ts.map +1 -1
  303. package/dist/token/verify-token-claims.js +1 -1
  304. package/dist/token/verify-token-claims.js.map +1 -1
  305. package/dist/types/email-otp.d.ts +3 -0
  306. package/dist/types/email-otp.d.ts.map +1 -0
  307. package/dist/types/email-otp.js +6 -0
  308. package/dist/types/email-otp.js.map +1 -0
  309. package/dist/types/email.d.ts +3 -0
  310. package/dist/types/email.d.ts.map +1 -0
  311. package/dist/types/email.js +29 -0
  312. package/dist/types/email.js.map +1 -0
  313. package/dist/types/handle.d.ts +3 -0
  314. package/dist/types/handle.d.ts.map +1 -0
  315. package/dist/types/handle.js +22 -0
  316. package/dist/types/handle.js.map +1 -0
  317. package/dist/types/invite-code.d.ts +4 -0
  318. package/dist/types/invite-code.d.ts.map +1 -0
  319. package/dist/types/invite-code.js +6 -0
  320. package/dist/types/invite-code.js.map +1 -0
  321. package/dist/types/password.d.ts +4 -0
  322. package/dist/types/password.d.ts.map +1 -0
  323. package/dist/types/password.js +7 -0
  324. package/dist/types/password.js.map +1 -0
  325. package/package.json +11 -14
  326. package/src/access-token/access-token-mode.ts +4 -0
  327. package/src/account/account-manager.ts +105 -75
  328. package/src/account/account-store.ts +118 -114
  329. package/src/account/sign-in-data.ts +10 -10
  330. package/src/account/sign-up-input.ts +13 -4
  331. package/src/client/client-manager.ts +34 -2
  332. package/src/client/client-store.ts +1 -1
  333. package/src/constants.ts +6 -1
  334. package/src/customization/branding.ts +12 -0
  335. package/src/customization/build-customization-css.ts +30 -0
  336. package/src/customization/build-customization-data.ts +22 -0
  337. package/src/customization/colors.ts +30 -0
  338. package/src/customization/customization.ts +25 -0
  339. package/src/customization/links.ts +10 -0
  340. package/src/device/device-id.ts +5 -0
  341. package/src/device/device-manager.ts +76 -66
  342. package/src/device/device-store.ts +2 -0
  343. package/src/errors/access-denied-error.ts +24 -17
  344. package/src/{output/build-error-payload.ts → errors/error-parser.ts} +1 -1
  345. package/src/errors/invalid-grant-error.ts +5 -0
  346. package/src/errors/login-required-error.ts +10 -0
  347. package/src/index.ts +1 -0
  348. package/src/lib/html/build-document.ts +6 -4
  349. package/src/{output/backend-data.ts → lib/html/hydration-data.ts} +7 -5
  350. package/src/lib/html/tags.ts +2 -2
  351. package/src/lib/http/accept.ts +3 -3
  352. package/src/lib/http/context.ts +41 -10
  353. package/src/lib/http/headers.ts +15 -0
  354. package/src/lib/http/index.ts +1 -0
  355. package/src/lib/http/middleware.ts +8 -23
  356. package/src/lib/http/request.ts +40 -75
  357. package/src/lib/http/response.ts +39 -15
  358. package/src/lib/http/route.ts +8 -5
  359. package/src/lib/http/router.ts +40 -46
  360. package/src/lib/http/security-headers.ts +1 -1
  361. package/src/lib/http/types.ts +1 -6
  362. package/src/{output → lib}/send-web-page.ts +10 -9
  363. package/src/lib/util/color.ts +132 -0
  364. package/src/lib/util/crypto.ts +9 -4
  365. package/src/lib/util/function.ts +14 -0
  366. package/src/lib/util/locale.ts +18 -0
  367. package/src/lib/util/time.ts +3 -4
  368. package/src/lib/util/type.ts +24 -0
  369. package/src/lib/util/ui8.ts +14 -0
  370. package/src/lib/util/zod-error.ts +14 -0
  371. package/src/oauth-errors.ts +22 -22
  372. package/src/oauth-hooks.ts +11 -24
  373. package/src/oauth-middleware.ts +53 -0
  374. package/src/oauth-provider.ts +290 -1061
  375. package/src/oauth-verifier.ts +9 -55
  376. package/src/request/request-data.ts +5 -4
  377. package/src/request/request-manager.ts +11 -11
  378. package/src/request/request-store.ts +7 -0
  379. package/src/result/authorization-redirect-parameters.ts +24 -0
  380. package/src/result/authorization-result-authorize-page.ts +14 -0
  381. package/src/result/authorization-result-redirect.ts +8 -0
  382. package/src/router/assets/assets-manifest.ts +115 -0
  383. package/src/router/assets/assets.ts +54 -0
  384. package/src/router/assets/csrf.ts +63 -0
  385. package/src/router/assets/send-account-page.ts +43 -0
  386. package/src/router/assets/send-authorization-page.ts +62 -0
  387. package/src/router/assets/send-error-page.ts +42 -0
  388. package/src/router/create-account-page-middleware.ts +69 -0
  389. package/src/router/create-api-middleware.ts +814 -0
  390. package/src/router/create-authorization-page-middleware.ts +173 -0
  391. package/src/router/create-oauth-middleware.ts +247 -0
  392. package/src/router/error-handler.ts +6 -0
  393. package/src/router/middleware-options.ts +9 -0
  394. package/src/router/send-redirect.ts +142 -0
  395. package/src/signer/api-token-payload.ts +18 -0
  396. package/src/signer/signed-token-payload.ts +18 -28
  397. package/src/signer/signer.ts +49 -34
  398. package/src/token/token-data.ts +1 -1
  399. package/src/token/token-manager.ts +190 -239
  400. package/src/token/token-store.ts +6 -4
  401. package/src/token/verify-token-claims.ts +4 -4
  402. package/src/types/email-otp.ts +3 -0
  403. package/src/types/email.ts +26 -0
  404. package/src/types/handle.ts +18 -0
  405. package/src/types/invite-code.ts +4 -0
  406. package/src/types/password.ts +4 -0
  407. package/tsconfig.build.tsbuildinfo +1 -0
  408. package/tsconfig.json +1 -1
  409. package/dist/access-token/access-token-type.d.ts +0 -6
  410. package/dist/access-token/access-token-type.d.ts.map +0 -1
  411. package/dist/access-token/access-token-type.js +0 -10
  412. package/dist/access-token/access-token-type.js.map +0 -1
  413. package/dist/account/account.d.ts +0 -2
  414. package/dist/account/account.d.ts.map +0 -1
  415. package/dist/account/account.js.map +0 -1
  416. package/dist/assets/assets-middleware.d.ts +0 -5
  417. package/dist/assets/assets-middleware.d.ts.map +0 -1
  418. package/dist/assets/assets-middleware.js +0 -41
  419. package/dist/assets/assets-middleware.js.map +0 -1
  420. package/dist/lib/locale.d.ts +0 -15
  421. package/dist/lib/locale.d.ts.map +0 -1
  422. package/dist/lib/locale.js +0 -17
  423. package/dist/lib/locale.js.map +0 -1
  424. package/dist/output/backend-data.d.ts +0 -4
  425. package/dist/output/backend-data.d.ts.map +0 -1
  426. package/dist/output/backend-data.js.map +0 -1
  427. package/dist/output/build-authorize-data.d.ts +0 -29
  428. package/dist/output/build-authorize-data.d.ts.map +0 -1
  429. package/dist/output/build-authorize-data.js +0 -21
  430. package/dist/output/build-authorize-data.js.map +0 -1
  431. package/dist/output/build-customization-data.d.ts +0 -234
  432. package/dist/output/build-customization-data.d.ts.map +0 -1
  433. package/dist/output/build-customization-data.js +0 -174
  434. package/dist/output/build-customization-data.js.map +0 -1
  435. package/dist/output/build-error-data.d.ts +0 -3
  436. package/dist/output/build-error-data.d.ts.map +0 -1
  437. package/dist/output/build-error-data.js +0 -10
  438. package/dist/output/build-error-data.js.map +0 -1
  439. package/dist/output/build-error-payload.d.ts.map +0 -1
  440. package/dist/output/build-error-payload.js.map +0 -1
  441. package/dist/output/output-manager.d.ts +0 -28
  442. package/dist/output/output-manager.d.ts.map +0 -1
  443. package/dist/output/output-manager.js +0 -134
  444. package/dist/output/output-manager.js.map +0 -1
  445. package/dist/output/send-authorize-redirect.d.ts +0 -25
  446. package/dist/output/send-authorize-redirect.d.ts.map +0 -1
  447. package/dist/output/send-authorize-redirect.js.map +0 -1
  448. package/dist/output/send-web-page.d.ts +0 -8
  449. package/dist/output/send-web-page.d.ts.map +0 -1
  450. package/dist/output/send-web-page.js.map +0 -1
  451. package/dist/token/token-claims.d.ts.map +0 -1
  452. package/dist/token/token-claims.js +0 -27
  453. package/dist/token/token-claims.js.map +0 -1
  454. package/src/access-token/access-token-type.ts +0 -5
  455. package/src/account/account.ts +0 -1
  456. package/src/assets/assets-middleware.ts +0 -44
  457. package/src/lib/locale.ts +0 -21
  458. package/src/output/build-authorize-data.ts +0 -53
  459. package/src/output/build-customization-data.ts +0 -217
  460. package/src/output/build-error-data.ts +0 -8
  461. package/src/output/output-manager.ts +0 -188
  462. package/src/output/send-authorize-redirect.ts +0 -137
  463. package/src/token/token-claims.ts +0 -30
  464. package/tsconfig.backend.tsbuildinfo +0 -1
  465. /package/{tsconfig.backend.json → tsconfig.build.json} +0 -0
@@ -1,8 +1,6 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http'
2
- import createHttpError from 'http-errors'
3
1
  import type { Redis, RedisOptions } from 'ioredis'
4
- import { ZodError, z } from 'zod'
5
2
  import { Jwks, Keyset } from '@atproto/jwk'
3
+ import type { Account } from '@atproto/oauth-provider-api'
6
4
  import {
7
5
  CLIENT_ASSERTION_TYPE_JWT_BEARER,
8
6
  OAuthAccessToken,
@@ -15,7 +13,6 @@ import {
15
13
  OAuthClientCredentials,
16
14
  OAuthClientCredentialsNone,
17
15
  OAuthClientMetadata,
18
- OAuthIntrospectionResponse,
19
16
  OAuthParResponse,
20
17
  OAuthRefreshTokenGrantTokenRequest,
21
18
  OAuthTokenIdentification,
@@ -23,31 +20,21 @@ import {
23
20
  OAuthTokenResponse,
24
21
  OAuthTokenType,
25
22
  atprotoLoopbackClientMetadata,
26
- oauthAuthorizationRequestParSchema,
27
23
  oauthAuthorizationRequestParametersSchema,
28
- oauthAuthorizationRequestQuerySchema,
29
- oauthClientCredentialsSchema,
30
- oauthTokenIdentificationSchema,
31
- oauthTokenRequestSchema,
32
24
  } from '@atproto/oauth-types'
33
25
  import { safeFetchWrap } from '@atproto-labs/fetch-node'
34
26
  import { SimpleStore } from '@atproto-labs/simple-store'
35
27
  import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
36
- import { AccessTokenType } from './access-token/access-token-type.js'
28
+ import { AccessTokenMode } from './access-token/access-token-mode.js'
37
29
  import { AccountManager } from './account/account-manager.js'
38
30
  import {
39
31
  AccountStore,
40
- DeviceAccountInfo,
32
+ AuthorizedClientData,
33
+ DeviceAccount,
41
34
  asAccountStore,
42
- handleSchema,
43
- resetPasswordConfirmDataSchema,
44
- resetPasswordRequestDataSchema,
45
35
  } from './account/account-store.js'
46
- import { Account } from './account/account.js'
47
- import { signInDataSchema } from './account/sign-in-data.js'
48
- import { signUpInputSchema } from './account/sign-up-input.js'
49
- import { authorizeAssetsMiddleware } from './assets/assets-middleware.js'
50
36
  import { ClientAuth, authJwkThumbprint } from './client/client-auth.js'
37
+ import { ClientId } from './client/client-id.js'
51
38
  import {
52
39
  ClientManager,
53
40
  LoopbackMetadataGetter,
@@ -55,9 +42,14 @@ import {
55
42
  import { ClientStore, ifClientStore } from './client/client-store.js'
56
43
  import { Client } from './client/client.js'
57
44
  import { AUTHENTICATION_MAX_AGE, TOKEN_MAX_AGE } from './constants.js'
45
+ import { Branding, BrandingInput } from './customization/branding.js'
46
+ import {
47
+ Customization,
48
+ CustomizationInput,
49
+ customizationSchema,
50
+ } from './customization/customization.js'
58
51
  import { DeviceId } from './device/device-id.js'
59
52
  import {
60
- DeviceInfo,
61
53
  DeviceManager,
62
54
  DeviceManagerOptions,
63
55
  deviceManagerOptionsSchema,
@@ -66,58 +58,18 @@ import { DeviceStore, asDeviceStore } from './device/device-store.js'
66
58
  import { AccessDeniedError } from './errors/access-denied-error.js'
67
59
  import { AccountSelectionRequiredError } from './errors/account-selection-required-error.js'
68
60
  import { ConsentRequiredError } from './errors/consent-required-error.js'
69
- import { InvalidClientError } from './errors/invalid-client-error.js'
70
61
  import { InvalidGrantError } from './errors/invalid-grant-error.js'
71
62
  import { InvalidParametersError } from './errors/invalid-parameters-error.js'
72
63
  import { InvalidRequestError } from './errors/invalid-request-error.js'
73
64
  import { LoginRequiredError } from './errors/login-required-error.js'
74
- import { UnauthorizedClientError } from './errors/unauthorized-client-error.js'
75
- import { WWWAuthenticateError } from './errors/www-authenticate-error.js'
76
65
  import { HcaptchaConfig } from './lib/hcaptcha.js'
77
- import {
78
- Handler,
79
- Middleware,
80
- Router,
81
- cacheControlMiddleware,
82
- combineMiddlewares,
83
- parseHttpRequest,
84
- setupCsrfToken,
85
- staticJsonMiddleware,
86
- validateCsrfToken,
87
- validateFetchDest,
88
- validateFetchMode,
89
- validateFetchSite,
90
- validateReferer,
91
- validateSameOrigin,
92
- writeJson,
93
- } from './lib/http/index.js'
94
- import {
95
- RequestMetadata,
96
- extractLocales,
97
- negotiateResponseContent as negotiateContent,
98
- } from './lib/http/request.js'
99
- import { dateToEpoch, dateToRelativeSeconds } from './lib/util/date.js'
100
- import { Awaitable, Override } from './lib/util/type.js'
66
+ import { RequestMetadata } from './lib/http/request.js'
67
+ import { dateToRelativeSeconds } from './lib/util/date.js'
68
+ import { LocalizedString, MultiLangString } from './lib/util/locale.js'
69
+ import { extractZodErrorMessage } from './lib/util/zod-error.js'
101
70
  import { CustomMetadata, buildMetadata } from './metadata/build-metadata.js'
102
- import { OAuthHooks, SignInData, SignUpData } from './oauth-hooks.js'
71
+ import { OAuthHooks } from './oauth-hooks.js'
103
72
  import { OAuthVerifier, OAuthVerifierOptions } from './oauth-verifier.js'
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'
112
- import {
113
- buildErrorPayload,
114
- buildErrorStatus,
115
- } from './output/build-error-payload.js'
116
- import { OutputManager } from './output/output-manager.js'
117
- import {
118
- AuthorizationResultRedirect,
119
- sendAuthorizeRedirect,
120
- } from './output/send-authorize-redirect.js'
121
73
  import { ReplayStore, ifReplayStore } from './replay/replay-store.js'
122
74
  import { codeSchema } from './request/code.js'
123
75
  import { RequestInfo } from './request/request-info.js'
@@ -125,129 +77,149 @@ import { RequestManager } from './request/request-manager.js'
125
77
  import { RequestStoreMemory } from './request/request-store-memory.js'
126
78
  import { RequestStoreRedis } from './request/request-store-redis.js'
127
79
  import { RequestStore, ifRequestStore } from './request/request-store.js'
128
- import { RequestUri, requestUriSchema } from './request/request-uri.js'
129
- import { isTokenId } from './token/token-id.js'
80
+ import { requestUriSchema } from './request/request-uri.js'
81
+ import { AuthorizationRedirectParameters } from './result/authorization-redirect-parameters.js'
82
+ import { AuthorizationResultAuthorizePage } from './result/authorization-result-authorize-page.js'
83
+ import { AuthorizationResultRedirect } from './result/authorization-result-redirect.js'
84
+ import { ErrorHandler } from './router/error-handler.js'
130
85
  import { TokenManager } from './token/token-manager.js'
131
86
  import { TokenStore, asTokenStore } from './token/token-store.js'
132
- import { VerifyTokenClaimsOptions } from './token/verify-token-claims.js'
133
-
134
- export {
135
- type Branding,
136
- type BrandingInput,
137
- type CustomMetadata,
138
- type Customization,
139
- type CustomizationInput,
140
- type Handler,
141
- type HcaptchaConfig,
142
- Keyset,
143
- type OAuthAuthorizationServerMetadata,
87
+ import {
88
+ VerifyTokenClaimsOptions,
89
+ VerifyTokenClaimsResult,
90
+ } from './token/verify-token-claims.js'
91
+
92
+ export { AccessTokenMode, Keyset }
93
+ export type {
94
+ AuthorizationRedirectParameters,
95
+ AuthorizationResultAuthorizePage as AuthorizationResultAuthorize,
96
+ AuthorizationResultRedirect,
97
+ Branding,
98
+ BrandingInput,
99
+ CustomMetadata,
100
+ Customization,
101
+ CustomizationInput,
102
+ ErrorHandler,
103
+ HcaptchaConfig,
104
+ LocalizedString,
105
+ MultiLangString,
106
+ OAuthAuthorizationServerMetadata,
144
107
  }
145
108
 
146
- type ApiContext = {
147
- requestUri: RequestUri
148
- deviceId: DeviceId
149
- deviceMetadata: RequestMetadata
150
- }
109
+ type OAuthProviderConfig = {
110
+ /**
111
+ * Maximum age a device/account session can be before requiring
112
+ * re-authentication.
113
+ */
114
+ authenticationMaxAge?: number
115
+
116
+ /**
117
+ * Maximum age an ephemeral session (one where "remember me" was not
118
+ * checked) can be before requiring re-authentication.
119
+ */
120
+
121
+ /**
122
+ * Maximum age access & id tokens can be before requiring a refresh.
123
+ */
124
+ tokenMaxAge?: number
125
+
126
+ /**
127
+ * If set to {@link AccessTokenMode.stateless}, the generated access tokens
128
+ * will contain all the necessary information to validate the token without
129
+ * needing to query the database. This is useful for cases where the Resource
130
+ * Server is on a different host/server than the Authorization Server.
131
+ *
132
+ * When set to {@link AccessTokenMode.light}, the access tokens will contain
133
+ * only the necessary information to validate the token, but the token id
134
+ * will need to be queried from the database to retrieve the full token
135
+ * information (scope, audience, etc.)
136
+ *
137
+ * @see {@link AccessTokenMode}
138
+ * @default {AccessTokenMode.stateless}
139
+ */
140
+ accessTokenMode?: AccessTokenMode
141
+
142
+ /**
143
+ * Additional metadata to be included in the discovery document.
144
+ */
145
+ metadata?: CustomMetadata
146
+
147
+ /**
148
+ * A custom fetch function that can be used to fetch the client metadata from
149
+ * the internet. By default, the fetch function is a safeFetchWrap() function
150
+ * that protects against SSRF attacks, large responses & known bad domains. If
151
+ * you want to disable all protections, you can provide `globalThis.fetch` as
152
+ * fetch function.
153
+ */
154
+ safeFetch?: typeof globalThis.fetch
155
+
156
+ /**
157
+ * A redis instance to use for replay protection. If not provided, replay
158
+ * protection will use memory storage.
159
+ */
160
+ redis?: Redis | RedisOptions | string
151
161
 
152
- export type ErrorHandler<
153
- Req extends IncomingMessage = IncomingMessage,
154
- Res extends ServerResponse = ServerResponse,
155
- > = (req: Req, res: Res, err: unknown, message: string) => void
162
+ /**
163
+ * This will be used as the default store for all the stores. If a store is
164
+ * not provided, this store will be used instead. If the `store` does not
165
+ * implement a specific store, a runtime error will be thrown. Make sure that
166
+ * this store implements all the interfaces not provided in the other
167
+ * `<name>Store` options.
168
+ */
169
+ store?: Partial<
170
+ AccountStore &
171
+ ClientStore &
172
+ DeviceStore &
173
+ ReplayStore &
174
+ RequestStore &
175
+ TokenStore
176
+ >
177
+
178
+ accountStore?: AccountStore
179
+ clientStore?: ClientStore
180
+ deviceStore?: DeviceStore
181
+ replayStore?: ReplayStore
182
+ requestStore?: RequestStore
183
+ tokenStore?: TokenStore
184
+
185
+ /**
186
+ * In order to speed up the client fetching process, you can provide a cache
187
+ * to store HTTP responses.
188
+ *
189
+ * @note the cached entries should automatically expire after a certain time (typically 10 minutes)
190
+ */
191
+ clientJwksCache?: SimpleStore<string, Jwks>
156
192
 
157
- export type RouterOptions<
158
- Req extends IncomingMessage = IncomingMessage,
159
- Res extends ServerResponse = ServerResponse,
160
- > = {
161
- onError?: ErrorHandler<Req, Res>
193
+ /**
194
+ * In order to speed up the client fetching process, you can provide a cache
195
+ * to store HTTP responses.
196
+ *
197
+ * @note the cached entries should automatically expire after a certain time (typically 10 minutes)
198
+ */
199
+ clientMetadataCache?: SimpleStore<string, OAuthClientMetadata>
200
+
201
+ /**
202
+ * In order to enable loopback clients, you can provide a function that
203
+ * returns the client metadata for a given loopback URL. This is useful for
204
+ * development and testing purposes. This function is not called for internet
205
+ * clients.
206
+ *
207
+ * @default is as specified by ATPROTO
208
+ */
209
+ loopbackMetadata?: null | false | LoopbackMetadataGetter
162
210
  }
163
211
 
164
- export type OAuthProviderOptions = Override<
165
- OAuthVerifierOptions & OAuthHooks & DeviceManagerOptions & CustomizationInput,
166
- {
167
- /**
168
- * Maximum age a device/account session can be before requiring
169
- * re-authentication.
170
- */
171
- authenticationMaxAge?: number
172
-
173
- /**
174
- * Maximum age access & id tokens can be before requiring a refresh.
175
- */
176
- tokenMaxAge?: number
177
-
178
- /**
179
- * Additional metadata to be included in the discovery document.
180
- */
181
- metadata?: CustomMetadata
182
-
183
- /**
184
- * A custom fetch function that can be used to fetch the client metadata from
185
- * the internet. By default, the fetch function is a safeFetchWrap() function
186
- * that protects against SSRF attacks, large responses & known bad domains. If
187
- * you want to disable all protections, you can provide `globalThis.fetch` as
188
- * fetch function.
189
- */
190
- safeFetch?: typeof globalThis.fetch
191
-
192
- /**
193
- * A redis instance to use for replay protection. If not provided, replay
194
- * protection will use memory storage.
195
- */
196
- redis?: Redis | RedisOptions | string
197
-
198
- /**
199
- * This will be used as the default store for all the stores. If a store is
200
- * not provided, this store will be used instead. If the `store` does not
201
- * implement a specific store, a runtime error will be thrown. Make sure that
202
- * this store implements all the interfaces not provided in the other
203
- * `<name>Store` options.
204
- */
205
- store?: Partial<
206
- AccountStore &
207
- ClientStore &
208
- DeviceStore &
209
- ReplayStore &
210
- RequestStore &
211
- TokenStore
212
- >
213
-
214
- accountStore?: AccountStore
215
- clientStore?: ClientStore
216
- deviceStore?: DeviceStore
217
- replayStore?: ReplayStore
218
- requestStore?: RequestStore
219
- tokenStore?: TokenStore
220
-
221
- /**
222
- * In order to speed up the client fetching process, you can provide a cache
223
- * to store HTTP responses.
224
- *
225
- * @note the cached entries should automatically expire after a certain time (typically 10 minutes)
226
- */
227
- clientJwksCache?: SimpleStore<string, Jwks>
228
-
229
- /**
230
- * In order to speed up the client fetching process, you can provide a cache
231
- * to store HTTP responses.
232
- *
233
- * @note the cached entries should automatically expire after a certain time (typically 10 minutes)
234
- */
235
- clientMetadataCache?: SimpleStore<string, OAuthClientMetadata>
236
-
237
- /**
238
- * In order to enable loopback clients, you can provide a function that
239
- * returns the client metadata for a given loopback URL. This is useful for
240
- * development and testing purposes. This function is not called for internet
241
- * clients.
242
- *
243
- * @default is as specified by ATPROTO
244
- */
245
- loopbackMetadata?: null | false | LoopbackMetadataGetter
246
- }
247
- >
212
+ export type OAuthProviderOptions = OAuthProviderConfig &
213
+ OAuthVerifierOptions &
214
+ OAuthHooks &
215
+ DeviceManagerOptions &
216
+ CustomizationInput
248
217
 
249
218
  export class OAuthProvider extends OAuthVerifier {
219
+ protected readonly accessTokenMode: AccessTokenMode
220
+
250
221
  public readonly metadata: OAuthAuthorizationServerMetadata
222
+ public readonly customization: Customization
251
223
 
252
224
  public readonly authenticationMaxAge: number
253
225
 
@@ -256,12 +228,14 @@ export class OAuthProvider extends OAuthVerifier {
256
228
  public readonly clientManager: ClientManager
257
229
  public readonly requestManager: RequestManager
258
230
  public readonly tokenManager: TokenManager
259
- public readonly outputManager: OutputManager
260
231
 
261
232
  public constructor({
262
- metadata,
233
+ // OAuthProviderConfig
263
234
  authenticationMaxAge = AUTHENTICATION_MAX_AGE,
264
235
  tokenMaxAge = TOKEN_MAX_AGE,
236
+ accessTokenMode = AccessTokenMode.stateless,
237
+
238
+ metadata,
265
239
 
266
240
  safeFetch = safeFetchWrap(),
267
241
  redis,
@@ -294,7 +268,6 @@ export class OAuthProvider extends OAuthVerifier {
294
268
  // Customization
295
269
  ...rest
296
270
  }: OAuthProviderOptions) {
297
- const customization: Customization = customizationSchema.parse(rest)
298
271
  const deviceManagerOptions: DeviceManagerOptions =
299
272
  deviceManagerOptionsSchema.parse(rest)
300
273
 
@@ -317,16 +290,17 @@ export class OAuthProvider extends OAuthVerifier {
317
290
  ? new RequestStoreRedis({ redis })
318
291
  : new RequestStoreMemory()
319
292
 
293
+ this.accessTokenMode = accessTokenMode
320
294
  this.authenticationMaxAge = authenticationMaxAge
321
295
  this.metadata = buildMetadata(this.issuer, this.keyset, metadata)
296
+ this.customization = customizationSchema.parse(rest)
322
297
 
323
298
  this.deviceManager = new DeviceManager(deviceStore, deviceManagerOptions)
324
- this.outputManager = new OutputManager(customization)
325
299
  this.accountManager = new AccountManager(
326
300
  this.issuer,
327
301
  accountStore,
328
302
  hooks,
329
- customization,
303
+ this.customization,
330
304
  )
331
305
  this.clientManager = new ClientManager(
332
306
  this.metadata,
@@ -348,7 +322,7 @@ export class OAuthProvider extends OAuthVerifier {
348
322
  tokenStore,
349
323
  this.signer,
350
324
  hooks,
351
- this.accessTokenType,
325
+ this.accessTokenMode,
352
326
  tokenMaxAge,
353
327
  )
354
328
  }
@@ -357,20 +331,31 @@ export class OAuthProvider extends OAuthVerifier {
357
331
  return this.keyset.publicJwks
358
332
  }
359
333
 
360
- protected loginRequired(
361
- client: Client,
334
+ /**
335
+ * @returns true if the user's consent is required for the requested scopes
336
+ */
337
+ public checkConsentRequired(
362
338
  parameters: OAuthAuthorizationRequestParameters,
363
- info: DeviceAccountInfo,
339
+ clientData?: AuthorizedClientData,
364
340
  ) {
365
- /** in seconds */
366
- const authAge = (Date.now() - info.authenticatedAt.getTime()) / 1e3
341
+ // Client was never authorized before
342
+ if (!clientData) return true
367
343
 
368
- // Fool-proof (invalid date, or suspiciously in the future)
369
- if (!Number.isFinite(authAge) || authAge < 0) {
370
- return true
371
- }
344
+ // Client explicitly asked for consent
345
+ if (parameters.prompt === 'consent') return true
372
346
 
373
- return authAge >= this.authenticationMaxAge
347
+ // No scope requested, and client is known by user, no consent required
348
+ const requestedScopes = parameters.scope?.split(' ')
349
+ if (requestedScopes == null) return false
350
+
351
+ // Ensure that all requested scopes were previously authorized by the user
352
+ const { authorizedScopes } = clientData
353
+ return !requestedScopes.every((scope) => authorizedScopes.includes(scope))
354
+ }
355
+
356
+ public checkLoginRequired(deviceAccount: DeviceAccount) {
357
+ const authAge = Date.now() - deviceAccount.updatedAt.getTime()
358
+ return authAge > this.authenticationMaxAge
374
359
  }
375
360
 
376
361
  protected async authenticateClient(
@@ -470,7 +455,7 @@ export class OAuthProvider extends OAuthVerifier {
470
455
  /**
471
456
  * @see {@link https://datatracker.ietf.org/doc/html/rfc9126}
472
457
  */
473
- protected async pushedAuthorizationRequest(
458
+ public async pushedAuthorizationRequest(
474
459
  credentials: OAuthClientCredentials,
475
460
  authorizationRequest: OAuthAuthorizationRequestPar,
476
461
  dpopJkt: null | string,
@@ -516,7 +501,12 @@ export class OAuthProvider extends OAuthVerifier {
516
501
  if ('request_uri' in query) {
517
502
  const requestUri = await requestUriSchema
518
503
  .parseAsync(query.request_uri, { path: ['query', 'request_uri'] })
519
- .catch(throwInvalidRequest)
504
+ .catch((err) => {
505
+ throw new InvalidRequestError(
506
+ extractZodErrorMessage(err) ?? 'Input validation error',
507
+ err,
508
+ )
509
+ })
520
510
 
521
511
  return this.requestManager.get(requestUri, deviceId, client.id)
522
512
  }
@@ -561,26 +551,15 @@ export class OAuthProvider extends OAuthVerifier {
561
551
  )
562
552
  }
563
553
 
564
- private async deleteRequest(
565
- requestUri: RequestUri,
566
- parameters: OAuthAuthorizationRequestParameters,
567
- ) {
568
- try {
569
- await this.requestManager.delete(requestUri)
570
- } catch (err) {
571
- throw AccessDeniedError.from(parameters, err)
572
- }
573
- }
574
-
575
554
  /**
576
555
  * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.1}
577
556
  */
578
- protected async authorize(
557
+ public async authorize(
579
558
  clientCredentials: OAuthClientCredentialsNone,
580
559
  query: OAuthAuthorizationRequestQuery,
581
560
  deviceId: DeviceId,
582
561
  deviceMetadata: RequestMetadata,
583
- ): Promise<AuthorizationResultRedirect | AuthorizationResultAuthorize> {
562
+ ): Promise<AuthorizationResultRedirect | AuthorizationResultAuthorizePage> {
584
563
  const { issuer } = this
585
564
 
586
565
  // If there is a chance to redirect the user to the client, let's do
@@ -597,18 +576,14 @@ export class OAuthProvider extends OAuthVerifier {
597
576
  .getClient(clientCredentials.client_id)
598
577
  .catch(accessDeniedCatcher)
599
578
 
600
- const { clientAuth, parameters, uri } =
601
- await this.processAuthorizationRequest(client, deviceId, query).catch(
602
- accessDeniedCatcher,
603
- )
579
+ const { parameters, uri } = await this.processAuthorizationRequest(
580
+ client,
581
+ deviceId,
582
+ query,
583
+ ).catch(accessDeniedCatcher)
604
584
 
605
585
  try {
606
- const sessions = await this.getSessions(
607
- client,
608
- clientAuth,
609
- deviceId,
610
- parameters,
611
- )
586
+ const sessions = await this.getSessions(client.id, deviceId, parameters)
612
587
 
613
588
  if (parameters.prompt === 'none') {
614
589
  const ssoSessions = sessions.filter((s) => s.matchesHint)
@@ -635,7 +610,7 @@ export class OAuthProvider extends OAuthVerifier {
635
610
  deviceMetadata,
636
611
  )
637
612
 
638
- return { issuer, client, parameters, redirect: { code } }
613
+ return { issuer, parameters, redirect: { code } }
639
614
  }
640
615
 
641
616
  // Automatic SSO when a did was provided
@@ -652,7 +627,7 @@ export class OAuthProvider extends OAuthVerifier {
652
627
  deviceMetadata,
653
628
  )
654
629
 
655
- return { issuer, client, parameters, redirect: { code } }
630
+ return { issuer, parameters, redirect: { code } }
656
631
  }
657
632
  }
658
633
  }
@@ -661,39 +636,48 @@ export class OAuthProvider extends OAuthVerifier {
661
636
  issuer,
662
637
  client,
663
638
  parameters,
664
- authorize: {
665
- uri,
666
- sessions,
667
- scopeDetails: parameters.scope
668
- ?.split(/\s+/)
669
- .filter(Boolean)
670
- .sort((a, b) => a.localeCompare(b))
671
- .map((scope) => ({
672
- scope,
673
- // @TODO Allow to customize the scope descriptions (e.g.
674
- // using a hook)
675
- description: undefined,
676
- })),
677
- },
639
+ uri,
640
+ sessions: sessions.map((session) => ({
641
+ // Map to avoid leaking other data that might be present in the session
642
+ account: session.account,
643
+ selected: session.selected,
644
+ loginRequired: session.loginRequired,
645
+ consentRequired: session.consentRequired,
646
+ })),
647
+ scopeDetails: parameters.scope
648
+ ?.split(/\s+/)
649
+ .filter(Boolean)
650
+ .sort((a, b) => a.localeCompare(b))
651
+ .map((scope) => ({
652
+ scope,
653
+ // @TODO Allow to customize the scope descriptions (e.g.
654
+ // using a hook)
655
+ description: undefined,
656
+ })),
678
657
  }
679
658
  } catch (err) {
680
- await this.deleteRequest(uri, parameters)
659
+ try {
660
+ await this.requestManager.delete(uri)
661
+ } catch {
662
+ // There are two error here. Better keep the outer one.
663
+ //
664
+ // @TODO Maybe move this entire code to the /authorize endpoint
665
+ // (allowing to log this error)
666
+ }
681
667
 
682
668
  // Not using accessDeniedCatcher here because "parameters" will most
683
669
  // likely contain the redirect_uri (using the client default).
684
- throw AccessDeniedError.from(parameters, err)
670
+ throw AccessDeniedError.from(parameters, err, 'server_error')
685
671
  }
686
672
  }
687
673
 
688
674
  protected async getSessions(
689
- client: Client,
690
- clientAuth: ClientAuth,
675
+ clientId: ClientId,
691
676
  deviceId: DeviceId,
692
677
  parameters: OAuthAuthorizationRequestParameters,
693
678
  ): Promise<
694
679
  {
695
680
  account: Account
696
- info: DeviceAccountInfo
697
681
 
698
682
  selected: boolean
699
683
  loginRequired: boolean
@@ -702,163 +686,34 @@ export class OAuthProvider extends OAuthVerifier {
702
686
  matchesHint: boolean
703
687
  }[]
704
688
  > {
705
- const accounts = await this.accountManager.list(deviceId)
689
+ const deviceAccounts =
690
+ await this.accountManager.listDeviceAccounts(deviceId)
706
691
 
707
692
  const hint = parameters.login_hint
708
693
  const matchesHint = (account: Account): boolean =>
709
694
  (!!account.sub && account.sub === hint) ||
710
695
  (!!account.preferred_username && account.preferred_username === hint)
711
696
 
712
- return accounts.map(({ account, info }) => ({
713
- account,
714
- info,
697
+ return deviceAccounts.map((deviceAccount) => ({
698
+ account: deviceAccount.account,
715
699
 
716
700
  selected:
717
701
  parameters.prompt !== 'select_account' &&
718
- matchesHint(account) &&
719
- // If an account uses the sub of another account as preferred_username,
720
- // there might be multiple accounts matching the hint. In that case,
721
- // selecting the account automatically may have unexpected results (i.e.
722
- // not able to login using desired account).
723
- accounts.reduce(
724
- (acc, a) => acc + (matchesHint(a.account) ? 1 : 0),
725
- 0,
726
- ) === 1,
702
+ matchesHint(deviceAccount.account),
703
+ // @TODO Return the session expiration date instead of a boolean to
704
+ // avoid having to rely on a leeway when "accepting" the request.
727
705
  loginRequired:
728
- parameters.prompt === 'login' ||
729
- this.loginRequired(client, parameters, info),
730
- consentRequired:
731
- parameters.prompt === 'consent' ||
732
- // @TODO the "authorizedClients" should also include the scopes that
733
- // were already authorized for the client. Otherwise a client could
734
- // use silent authentication to get additional scopes without consent.
735
- !info.authorizedClients.includes(client.id),
736
-
737
- matchesHint: hint == null || matchesHint(account),
738
- }))
739
- }
740
-
741
- protected async signUp(
742
- { requestUri, deviceId, deviceMetadata }: ApiContext,
743
- data: SignUpData,
744
- ): Promise<{
745
- account: Account
746
- consentRequired: boolean
747
- }> {
748
- const { clientId } = await this.requestManager.get(requestUri, deviceId)
749
-
750
- const client = await this.clientManager.getClient(clientId)
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
- }> {
771
- // Ensure the request is still valid (and update the request expiration)
772
- // @TODO use the returned scopes to determine if consent is required
773
- const { clientId } = await this.requestManager.get(requestUri, deviceId)
774
-
775
- const client = await this.clientManager.getClient(clientId)
776
-
777
- const { account, info } = await this.accountManager.signIn(
778
- data,
779
- deviceId,
780
- deviceMetadata,
781
- )
782
-
783
- return {
784
- account,
785
- consentRequired: client.info.isFirstParty
786
- ? false
787
- : // @TODO: the "authorizedClients" should also include the scopes that
788
- // were already authorized for the client. Otherwise a client could
789
- // use silent authentication to get additional scopes without consent.
790
- !info.authorizedClients.includes(client.id),
791
- }
792
- }
793
-
794
- protected async acceptRequest(
795
- { requestUri, deviceId, deviceMetadata }: ApiContext,
796
- sub: string,
797
- ): Promise<AuthorizationResultRedirect> {
798
- const { issuer } = this
799
-
800
- const { parameters, clientId, clientAuth } = await this.requestManager.get(
801
- requestUri,
802
- deviceId,
803
- )
804
-
805
- const client = await this.clientManager.getClient(clientId)
806
-
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.
810
- const { account, info } = await this.accountManager.get(deviceId, sub)
811
-
812
- // The user is trying to authorize without a fresh login
813
- if (this.loginRequired(client, parameters, info)) {
814
- throw new LoginRequiredError(
815
- parameters,
816
- 'Account authentication required.',
817
- )
818
- }
819
-
820
- const code = await this.requestManager.setAuthorized(
821
- requestUri,
822
- client,
823
- account,
824
- deviceId,
825
- deviceMetadata,
826
- )
827
-
828
- await this.accountManager.addAuthorizedClient(
829
- deviceId,
830
- account,
831
- client,
832
- clientAuth,
833
- )
834
-
835
- return { issuer, parameters, redirect: { code } }
836
- } catch (err) {
837
- await this.deleteRequest(requestUri, parameters)
838
-
839
- throw AccessDeniedError.from(parameters, err)
840
- }
841
- }
706
+ parameters.prompt === 'login' || this.checkLoginRequired(deviceAccount),
707
+ consentRequired: this.checkConsentRequired(
708
+ parameters,
709
+ deviceAccount.authorizedClients.get(clientId),
710
+ ),
842
711
 
843
- protected async rejectRequest({
844
- requestUri,
845
- deviceId,
846
- }: ApiContext): Promise<AuthorizationResultRedirect> {
847
- const { parameters } = await this.requestManager.get(requestUri, deviceId)
848
-
849
- await this.deleteRequest(requestUri, parameters)
850
-
851
- return {
852
- issuer: this.issuer,
853
- parameters: parameters,
854
- redirect: {
855
- error: 'access_denied',
856
- error_description: 'Access denied',
857
- },
858
- }
712
+ matchesHint: hint == null || matchesHint(deviceAccount.account),
713
+ }))
859
714
  }
860
715
 
861
- protected async token(
716
+ public async token(
862
717
  clientCredentials: OAuthClientCredentials,
863
718
  clientMetadata: RequestMetadata,
864
719
  request: OAuthTokenRequest,
@@ -911,9 +766,8 @@ export class OAuthProvider extends OAuthVerifier {
911
766
  input: OAuthAuthorizationCodeGrantTokenRequest,
912
767
  dpopJkt: null | string,
913
768
  ): Promise<OAuthTokenResponse> {
769
+ const code = codeSchema.parse(input.code)
914
770
  try {
915
- const code = codeSchema.parse(input.code)
916
-
917
771
  const { sub, deviceId, parameters } = await this.requestManager.findCode(
918
772
  client,
919
773
  clientAuth,
@@ -933,27 +787,24 @@ export class OAuthProvider extends OAuthVerifier {
933
787
  // a good enough incentive to follow the best practices, until we have a
934
788
  // better implementation.
935
789
  //
936
- // @TODO: Use tokenManager to ensure uniqueness of code_challenge
790
+ // @TODO Use tokenManager to ensure uniqueness of code_challenge
937
791
  if (parameters.code_challenge) {
938
792
  const unique = await this.replayManager.uniqueCodeChallenge(
939
793
  parameters.code_challenge,
940
794
  )
941
795
  if (!unique) {
942
- throw new InvalidGrantError(
943
- 'code_challenge',
944
- 'Code challenge already used',
945
- )
796
+ throw new InvalidGrantError('Code challenge already used')
946
797
  }
947
798
  }
948
799
 
949
- const { account, info } = await this.accountManager.get(deviceId, sub)
800
+ const { account } = await this.accountManager.getAccount(sub)
950
801
 
951
802
  return await this.tokenManager.create(
952
803
  client,
953
804
  clientAuth,
954
805
  clientMetadata,
955
806
  account,
956
- { id: deviceId, info },
807
+ deviceId,
957
808
  parameters,
958
809
  input,
959
810
  dpopJkt,
@@ -962,10 +813,18 @@ export class OAuthProvider extends OAuthVerifier {
962
813
  // If a token is replayed, requestManager.findCode will throw. In that
963
814
  // case, we need to revoke any token that was issued for this code.
964
815
 
965
- await this.tokenManager.revoke(input.code)
816
+ const tokenInfo = await this.tokenManager.findByCode(code)
817
+ if (tokenInfo) {
818
+ await this.tokenManager.deleteToken(tokenInfo.id)
966
819
 
967
- // @TODO (?) in order to protect the user, we should maybe also mark the
968
- // account-device association as expired ?
820
+ // As an additional security measure, we also sign the device out, so
821
+ // that the device cannot be used to access the account anymore without
822
+ // a new authentication.
823
+ const { deviceId, sub } = tokenInfo.data
824
+ if (deviceId) {
825
+ await this.accountManager.removeDeviceAccount(deviceId, sub)
826
+ }
827
+ }
969
828
 
970
829
  throw err
971
830
  }
@@ -990,693 +849,63 @@ export class OAuthProvider extends OAuthVerifier {
990
849
  /**
991
850
  * @see {@link https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 rfc7009}
992
851
  */
993
- protected async revoke({ token }: OAuthTokenIdentification) {
994
- // @TODO this should also remove the account-device association (or, at
995
- // least, mark it as expired)
996
- await this.tokenManager.revoke(token)
997
- }
998
-
999
- /**
1000
- * @see {@link https://datatracker.ietf.org/doc/html/rfc7662#section-2.1 rfc7662}
1001
- */
1002
- protected async introspect(
852
+ public async revoke(
1003
853
  credentials: OAuthClientCredentials,
1004
854
  { token }: OAuthTokenIdentification,
1005
- ): Promise<OAuthIntrospectionResponse> {
855
+ ) {
856
+ // > The authorization server first validates the client credentials (in
857
+ // > case of a confidential client)
1006
858
  const [client, clientAuth] = await this.authenticateClient(credentials)
1007
859
 
1008
- // RFC7662 states the following:
1009
- //
1010
- // > To prevent token scanning attacks, the endpoint MUST also require some
1011
- // > form of authorization to access this endpoint, such as client
1012
- // > authentication as described in OAuth 2.0 [RFC6749] or a separate OAuth
1013
- // > 2.0 access token such as the bearer token described in OAuth 2.0 Bearer
1014
- // > Token Usage [RFC6750]. The methods of managing and validating these
1015
- // > authentication credentials are out of scope of this specification.
1016
- if (clientAuth.method === 'none') {
1017
- throw new UnauthorizedClientError('Client authentication required')
1018
- }
860
+ const tokenInfo = await this.tokenManager.findToken(token)
1019
861
 
1020
- const start = Date.now()
1021
- try {
1022
- const tokenInfo = await this.tokenManager.clientTokenInfo(
1023
- client,
1024
- clientAuth,
1025
- token,
1026
- )
1027
-
1028
- return {
1029
- active: true,
1030
-
1031
- scope: tokenInfo.data.parameters.scope,
1032
- client_id: tokenInfo.data.clientId,
1033
- username: tokenInfo.account.preferred_username,
1034
- token_type: tokenInfo.data.parameters.dpop_jkt ? 'DPoP' : 'Bearer',
1035
- authorization_details: tokenInfo.data.details ?? undefined,
1036
-
1037
- aud: tokenInfo.account.aud,
1038
- exp: dateToEpoch(tokenInfo.data.expiresAt),
1039
- iat: dateToEpoch(tokenInfo.data.updatedAt),
1040
- iss: this.signer.issuer,
1041
- jti: tokenInfo.id,
1042
- sub: tokenInfo.account.sub,
1043
- }
1044
- } catch (err) {
1045
- // Prevent brute force & timing attack (only for inactive tokens)
1046
- await new Promise((r) => setTimeout(r, 750 - (Date.now() - start)))
862
+ // > [...] and then verifies whether the token was issued to the client
863
+ // > making the revocation request. If this validation fails, the request is
864
+ // > refused and the client is informed of the error by the authorization
865
+ // > server as described below.
866
+ await this.tokenManager.validateAccess(client, clientAuth, tokenInfo)
1047
867
 
1048
- return {
1049
- active: false,
1050
- }
1051
- }
868
+ // > In the next step, the authorization server invalidates the token. The
869
+ // > invalidation takes place immediately, and the token cannot be used
870
+ // > again after the revocation.
871
+ await this.tokenManager.deleteToken(tokenInfo.id)
1052
872
  }
1053
873
 
1054
- protected override async authenticateToken(
874
+ protected override async verifyToken(
1055
875
  tokenType: OAuthTokenType,
1056
876
  token: OAuthAccessToken,
1057
877
  dpopJkt: string | null,
1058
878
  verifyOptions?: VerifyTokenClaimsOptions,
1059
- ) {
1060
- if (isTokenId(token)) {
1061
- this.assertTokenTypeAllowed(tokenType, AccessTokenType.id)
879
+ ): Promise<VerifyTokenClaimsResult> {
880
+ if (this.accessTokenMode === AccessTokenMode.stateless) {
881
+ return super.verifyToken(tokenType, token, dpopJkt, verifyOptions)
882
+ }
1062
883
 
1063
- return this.tokenManager.authenticateTokenId(
884
+ if (this.accessTokenMode === AccessTokenMode.light) {
885
+ const { claims } = await super.verifyToken(
1064
886
  tokenType,
1065
887
  token,
1066
888
  dpopJkt,
1067
- verifyOptions,
889
+ // Do not verify the scope and audience in case of "light" tokens.
890
+ // these will be checked through the tokenManager hereafter.
891
+ undefined,
1068
892
  )
1069
- }
1070
-
1071
- return super.authenticateToken(tokenType, token, dpopJkt, verifyOptions)
1072
- }
1073
-
1074
- /**
1075
- * @returns An http request handler that can be used with node's http server
1076
- * or as a middleware with express / connect.
1077
- */
1078
- public httpHandler<
1079
- T = void,
1080
- Req extends IncomingMessage = IncomingMessage,
1081
- Res extends ServerResponse = ServerResponse,
1082
- >(options?: RouterOptions<Req, Res>): Handler<T, Req, Res> {
1083
- const router = this.buildRouter<T, Req, Res>(options)
1084
- return router.buildHandler()
1085
- }
1086
893
 
1087
- public buildRouter<
1088
- T = void,
1089
- Req extends IncomingMessage = IncomingMessage,
1090
- Res extends ServerResponse = ServerResponse,
1091
- >(options?: RouterOptions<Req, Res>): Router<T, Req, Res> {
1092
- // eslint-disable-next-line @typescript-eslint/no-this-alias
1093
- const server = this
1094
- const issuerUrl = new URL(server.issuer)
1095
- const issuerOrigin = issuerUrl.origin
1096
- const router = new Router<T, Req, Res>(issuerUrl)
1097
-
1098
- // Utils
1099
-
1100
- const csrfCookie = (requestUri: RequestUri) => `csrf-${requestUri}`
1101
- const onError: null | ErrorHandler<Req, Res> =
1102
- options?.onError ??
1103
- (process.env['NODE_ENV'] === 'development'
1104
- ? (req, res, err, msg) => {
1105
- console.error(`OAuthProvider error (${msg}):`, err)
1106
- }
1107
- : null)
1108
-
1109
- // CORS preflight
1110
- const corsHeaders: Middleware = function (req, res, next) {
1111
- res.setHeader('Access-Control-Max-Age', '86400') // 1 day
1112
-
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', '*')
894
+ const tokenId = claims.jti
1123
895
 
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
- ])
1143
-
1144
- /**
1145
- * Wrap an OAuth endpoint in a middleware that will set the appropriate
1146
- * response headers and format the response as JSON.
1147
- */
1148
- const jsonHandler = <T, TReq extends Req, TRes extends Res, Payload>(
1149
- buildJson: (
1150
- this: T,
1151
- req: TReq,
1152
- res: TRes,
1153
- ) => Awaitable<{ payload: Payload; status?: number }>,
1154
- ): Handler<T, TReq, TRes> =>
1155
- async function (req, res) {
1156
- // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
1157
- res.setHeader('Cache-Control', 'no-store')
1158
- res.setHeader('Pragma', 'no-cache')
1159
-
1160
- // Ensure we can agree on a content encoding & type before starting to
1161
- // build the JSON response.
1162
- if (!negotiateContent(req, ['application/json'])) {
1163
- throw createHttpError(406, 'Unsupported media type')
1164
- }
1165
-
1166
- try {
1167
- const { payload, status = 200 } = await buildJson.call(this, req, res)
1168
- writeJson(res, payload, { status })
1169
- } catch (err) {
1170
- onError?.(req, res, err, 'OAuth request error')
1171
-
1172
- if (!res.headersSent) {
1173
- const payload = buildErrorPayload(err)
1174
- const status = buildErrorStatus(err)
1175
- writeJson(res, payload, { status })
1176
- } else {
1177
- res.destroy()
1178
- }
1179
- }
1180
- }
1181
-
1182
- const oauthHandler = <T, TReq extends Req, TRes extends Res, Payload>(
1183
- buildOAuthResponse: (this: T, req: TReq, res: TRes) => Awaitable<Payload>,
1184
- status?: number,
1185
- ) =>
1186
- combineMiddlewares([
1187
- corsHeaders,
1188
- jsonHandler<T, TReq, TRes, Payload>(async function (req, res) {
1189
- try {
1190
- // https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
1191
- const dpopNonce = server.nextDpopNonce()
1192
- if (dpopNonce) {
1193
- const name = 'DPoP-Nonce'
1194
- res.setHeader(name, dpopNonce)
1195
- res.appendHeader('Access-Control-Expose-Headers', name)
1196
- }
1197
-
1198
- const payload = await buildOAuthResponse.call(this, req, res)
1199
- return { payload, status }
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
- }),
1210
- ])
1211
-
1212
- const apiHandler = <
1213
- T,
1214
- TReq extends Req,
1215
- TRes extends Res,
1216
- S extends z.ZodTypeAny,
1217
- Payload,
1218
- >(
1219
- inputSchema: S,
1220
- buildJson: (
1221
- this: T,
1222
- req: TReq,
1223
- res: TRes,
1224
- input: z.infer<S>,
1225
- context: ApiContext,
1226
- ) => Awaitable<Payload>,
1227
- status?: number,
1228
- ) =>
1229
- jsonHandler<T, TReq, TRes, Payload>(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 inputRaw = await parseHttpRequest(req, ['json'])
1256
- const input = await inputSchema.parseAsync(inputRaw, { path: ['body'] })
1257
-
1258
- const context: ApiContext = { requestUri, deviceId, deviceMetadata }
1259
- const payload = await buildJson.call(this, req, res, input, context)
1260
- return { payload, status }
1261
- })
1262
-
1263
- const navigationHandler = <T, TReq extends Req, TRes extends Res>(
1264
- handler: (this: T, req: TReq, res: TRes) => Awaitable<void>,
1265
- ): Handler<T, TReq, TRes> =>
1266
- async function (req, res) {
1267
- try {
1268
- res.setHeader('Cache-Control', 'no-store')
1269
- res.setHeader('Pragma', 'no-cache')
1270
-
1271
- res.setHeader('Referrer-Policy', 'same-origin')
1272
-
1273
- validateFetchMode(req, res, ['navigate'])
1274
- validateFetchDest(req, res, ['document'])
1275
- validateSameOrigin(req, res, issuerOrigin)
1276
-
1277
- await handler.call(this, req, res)
1278
- } catch (err) {
1279
- onError?.(
1280
- req,
1281
- res,
1282
- err,
1283
- `Failed to handle navigation request to "${req.url}"`,
1284
- )
1285
-
1286
- if (!res.headersSent) {
1287
- await server.outputManager.sendErrorPage(res, err, {
1288
- preferredLocales: extractLocales(req),
1289
- })
1290
- }
1291
- }
1292
- }
1293
-
1294
- // Simple GET requests fall under the category of "no-cors" request, meaning
1295
- // that the browser will allow any cross-origin request, with credentials,
1296
- // to be sent to the oauth server. The OAuth Server will, however:
1297
- // 1) validate the request origin (see navigationHandler),
1298
- // 2) validate the CSRF token,
1299
- // 3) validate the referer,
1300
- // 4) validate the sec-fetch-site header,
1301
- // 4) validate the sec-fetch-mode header (see navigationHandler),
1302
- // 5) validate the sec-fetch-dest header (see navigationHandler).
1303
- // And will error (refuse to serve the request) if any of these checks fail.
1304
- const sameOriginNavigationHandler = <
1305
- T extends { url: URL },
1306
- TReq extends Req,
1307
- TRes extends Res,
1308
- >(
1309
- handler: (
1310
- this: T,
1311
- req: TReq,
1312
- res: TRes,
1313
- deviceInfo: DeviceInfo,
1314
- ) => Awaitable<void>,
1315
- ): Handler<T, TReq, TRes> =>
1316
- navigationHandler(async function (req, res) {
1317
- validateFetchSite(req, res, ['same-origin'])
1318
-
1319
- const deviceInfo = await server.deviceManager.load(req, res)
1320
-
1321
- return handler.call(this, req, res, deviceInfo)
1322
- })
1323
-
1324
- const authorizeRedirectNavigationHandler = <
1325
- T extends { url: URL },
1326
- TReq extends Req,
1327
- TRes extends Res,
1328
- >(
1329
- handler: (
1330
- this: T,
1331
- req: TReq,
1332
- res: TRes,
1333
- context: ApiContext,
1334
- ) => Awaitable<AuthorizationResultRedirect>,
1335
- ): Handler<T, TReq, TRes> =>
1336
- sameOriginNavigationHandler(async function (req, res, deviceInfo) {
1337
- const referer = validateReferer(req, res, {
1338
- origin: issuerOrigin,
1339
- pathname: '/oauth/authorize',
1340
- })
1341
-
1342
- const requestUri = await requestUriSchema.parseAsync(
1343
- referer.searchParams.get('request_uri'),
1344
- )
1345
-
1346
- const csrfToken = this.url.searchParams.get('csrf_token')
1347
- const csrfCookieName = csrfCookie(requestUri)
1348
-
1349
- // Next line will "clear" the CSRF token cookie, preventing replay of
1350
- // this request (navigating "back" will result in an error).
1351
- validateCsrfToken(req, res, csrfToken, csrfCookieName, true)
1352
-
1353
- const context: ApiContext = { ...deviceInfo, requestUri }
1354
-
1355
- const redirect = await handler.call(this, req, res, context)
1356
- return sendAuthorizeRedirect(res, redirect)
1357
- })
1358
-
1359
- /**
1360
- * Provides a better UX when a request is denied by redirecting to the
1361
- * client with the error details. This will also log any error that caused
1362
- * the access to be denied (such as system errors).
1363
- */
1364
- const accessDeniedToRedirectCatcher = (
1365
- req: Req,
1366
- res: Res,
1367
- err: unknown,
1368
- ): AuthorizationResultRedirect => {
1369
- if (err instanceof AccessDeniedError && err.parameters.redirect_uri) {
1370
- const { cause } = err
1371
- if (cause) onError?.(req, res, cause, 'Access denied')
1372
-
1373
- return {
1374
- issuer: server.issuer,
1375
- parameters: err.parameters,
1376
- redirect: err.toJSON(),
1377
- }
1378
- }
1379
-
1380
- throw err
896
+ // In addition to verifying the signature (through the verifier above), we
897
+ // also verify the tokenId is still valid using a database to fetch
898
+ // missing data from "light" token.
899
+ return this.tokenManager.verifyToken(
900
+ token,
901
+ tokenType,
902
+ tokenId,
903
+ dpopJkt,
904
+ verifyOptions,
905
+ )
1381
906
  }
1382
907
 
1383
- //- Public OAuth endpoints
1384
-
1385
- router.options('/.well-known/oauth-authorization-server', corsPreflight)
1386
- router.get(
1387
- '/.well-known/oauth-authorization-server',
1388
- corsHeaders,
1389
- cacheControlMiddleware(300),
1390
- staticJsonMiddleware(server.metadata),
1391
- )
1392
-
1393
- router.options('/oauth/jwks', corsPreflight)
1394
- router.get(
1395
- '/oauth/jwks',
1396
- corsHeaders,
1397
- cacheControlMiddleware(300),
1398
- staticJsonMiddleware(server.jwks),
1399
- )
1400
-
1401
- router.options('/oauth/par', corsPreflight)
1402
- router.post(
1403
- '/oauth/par',
1404
- oauthHandler(async function (req, _res) {
1405
- const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1406
-
1407
- const credentials = await oauthClientCredentialsSchema
1408
- .parseAsync(payload, { path: ['body'] })
1409
- .catch(throwInvalidRequest)
1410
-
1411
- const authorizationRequest = await oauthAuthorizationRequestParSchema
1412
- .parseAsync(payload, { path: ['body'] })
1413
- .catch(throwInvalidRequest)
1414
-
1415
- const dpopJkt = await server.checkDpopProof(
1416
- req.headers['dpop'],
1417
- req.method!,
1418
- this.url,
1419
- )
1420
-
1421
- return server.pushedAuthorizationRequest(
1422
- credentials,
1423
- authorizationRequest,
1424
- dpopJkt,
1425
- )
1426
- }, 201),
1427
- )
1428
- // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
1429
- // > If the request did not use the POST method, the authorization server
1430
- // > responds with an HTTP 405 (Method Not Allowed) status code.
1431
- router.all('/oauth/par', (req, res) => {
1432
- res.writeHead(405).end()
1433
- })
1434
-
1435
- router.options('/oauth/token', corsPreflight)
1436
- router.post(
1437
- '/oauth/token',
1438
- oauthHandler(async function (req, _res) {
1439
- const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1440
-
1441
- const clientMetadata =
1442
- await server.deviceManager.getRequestMetadata(req)
1443
-
1444
- const clientCredentials = await oauthClientCredentialsSchema
1445
- .parseAsync(payload, { path: ['body'] })
1446
- .catch(throwInvalidClient)
1447
-
1448
- const tokenRequest = await oauthTokenRequestSchema
1449
- .parseAsync(payload, { path: ['body'] })
1450
- .catch(throwInvalidGrant)
1451
-
1452
- const dpopJkt = await server.checkDpopProof(
1453
- req.headers['dpop'],
1454
- req.method!,
1455
- this.url,
1456
- )
1457
-
1458
- return server.token(
1459
- clientCredentials,
1460
- clientMetadata,
1461
- tokenRequest,
1462
- dpopJkt,
1463
- )
1464
- }),
1465
- )
1466
-
1467
- router.options('/oauth/revoke', corsPreflight)
1468
- router.post(
1469
- '/oauth/revoke',
1470
- oauthHandler(async function (req, res) {
1471
- const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1472
-
1473
- const tokenIdentification = await oauthTokenIdentificationSchema
1474
- .parseAsync(payload, { path: ['body'] })
1475
- .catch(throwInvalidRequest)
1476
-
1477
- try {
1478
- await server.revoke(tokenIdentification)
1479
- } catch (err) {
1480
- onError?.(req, res, err, 'Failed to revoke token')
1481
- }
1482
-
1483
- return {}
1484
- }),
1485
- )
1486
- router.get(
1487
- '/oauth/revoke',
1488
- navigationHandler(async function (req, res) {
1489
- const query = Object.fromEntries(this.url.searchParams)
1490
-
1491
- const tokenIdentification = await oauthTokenIdentificationSchema
1492
- .parseAsync(query, { path: ['query'] })
1493
- .catch(throwInvalidRequest)
1494
-
1495
- try {
1496
- await server.revoke(tokenIdentification)
1497
- } catch (err) {
1498
- onError?.(req, res, err, 'Failed to revoke token')
1499
- }
1500
-
1501
- // Same as POST + redirect to callback URL
1502
- // todo: generate JSONP response (if "callback" is provided)
1503
-
1504
- throw new Error(
1505
- 'You are successfully logged out. Redirect not implemented',
1506
- )
1507
- }),
1508
- )
1509
-
1510
- router.options('/oauth/introspect', corsPreflight)
1511
- router.post(
1512
- '/oauth/introspect',
1513
- oauthHandler(async function (req, _res) {
1514
- const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1515
-
1516
- const credentials = await oauthClientCredentialsSchema
1517
- .parseAsync(payload, { path: ['body'] })
1518
- .catch(throwInvalidRequest)
1519
-
1520
- const tokenIdentification = await oauthTokenIdentificationSchema
1521
- .parseAsync(payload, { path: ['body'] })
1522
- .catch(throwInvalidRequest)
1523
-
1524
- return server.introspect(credentials, tokenIdentification)
1525
- }),
1526
- )
1527
-
1528
- //- Private authorization endpoints
1529
-
1530
- router.use(authorizeAssetsMiddleware())
1531
-
1532
- router.get(
1533
- '/oauth/authorize',
1534
- navigationHandler(async function (req, res) {
1535
- validateFetchSite(req, res, ['cross-site', 'none'])
1536
-
1537
- const query = Object.fromEntries(this.url.searchParams)
1538
-
1539
- const clientCredentials = await oauthClientCredentialsSchema
1540
- .parseAsync(query, { path: ['query'] })
1541
- .catch(throwInvalidRequest)
1542
-
1543
- if ('client_secret' in clientCredentials) {
1544
- throw new InvalidRequestError('Client secret must not be provided')
1545
- }
1546
-
1547
- const authorizationRequest = await oauthAuthorizationRequestQuerySchema
1548
- .parseAsync(query, { path: ['query'] })
1549
- .catch(throwInvalidRequest)
1550
-
1551
- const { deviceId, deviceMetadata } = await server.deviceManager.load(
1552
- req,
1553
- res,
1554
- )
1555
-
1556
- const result:
1557
- | AuthorizationResultRedirect
1558
- | AuthorizationResultAuthorize = await server
1559
- .authorize(
1560
- clientCredentials,
1561
- authorizationRequest,
1562
- deviceId,
1563
- deviceMetadata,
1564
- )
1565
- .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1566
-
1567
- if ('redirect' in result) {
1568
- return sendAuthorizeRedirect(res, result)
1569
- } else {
1570
- await setupCsrfToken(req, res, csrfCookie(result.authorize.uri))
1571
- return server.outputManager.sendAuthorizePage(res, result, {
1572
- preferredLocales: extractLocales(req),
1573
- })
1574
- }
1575
- }),
1576
- )
1577
-
1578
- router.post(
1579
- '/oauth/authorize/verify-handle-availability',
1580
- apiHandler(
1581
- z.object({ handle: handleSchema }).strict(),
1582
- async function (req, res, data) {
1583
- await server.accountManager.verifyHandleAvailability(data.handle)
1584
- return { available: true }
1585
- },
1586
- ),
1587
- )
1588
-
1589
- router.post(
1590
- '/oauth/authorize/sign-up',
1591
- apiHandler(signUpInputSchema, async function (req, res, data, ctx) {
1592
- return server.signUp(ctx, data)
1593
- }),
1594
- )
1595
-
1596
- router.post(
1597
- '/oauth/authorize/sign-in',
1598
- apiHandler(signInDataSchema, async function (req, res, data, ctx) {
1599
- return server.signIn(ctx, data)
1600
- }),
1601
- )
1602
-
1603
- router.post(
1604
- '/oauth/authorize/reset-password-request',
1605
- apiHandler(
1606
- resetPasswordRequestDataSchema,
1607
- async function (req, res, data) {
1608
- await server.accountManager.resetPasswordRequest(data)
1609
- return { success: true }
1610
- },
1611
- ),
1612
- )
1613
-
1614
- router.post(
1615
- '/oauth/authorize/reset-password-confirm',
1616
- apiHandler(
1617
- resetPasswordConfirmDataSchema,
1618
- async function (req, res, data) {
1619
- await server.accountManager.resetPasswordConfirm(data)
1620
- return { success: true }
1621
- },
1622
- ),
1623
- )
1624
-
1625
- router.get(
1626
- '/oauth/authorize/accept',
1627
- authorizeRedirectNavigationHandler(async function (req, res, ctx) {
1628
- const sub = this.url.searchParams.get('account_sub')
1629
- if (!sub) throw new InvalidRequestError('Account sub not provided')
1630
-
1631
- return server
1632
- .acceptRequest(ctx, sub)
1633
- .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1634
- }),
1635
- )
1636
-
1637
- router.get(
1638
- '/oauth/authorize/reject',
1639
- authorizeRedirectNavigationHandler(async function (req, res, ctx) {
1640
- return server
1641
- .rejectRequest(ctx)
1642
- .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1643
- }),
1644
- )
1645
-
1646
- return router
1647
- }
1648
- }
1649
-
1650
- function throwInvalidGrant(err: unknown): never {
1651
- throw new InvalidGrantError(
1652
- extractZodErrorMessage(err) || 'Invalid grant',
1653
- err,
1654
- )
1655
- }
1656
-
1657
- function throwInvalidClient(err: unknown): never {
1658
- throw new InvalidClientError(
1659
- extractZodErrorMessage(err) || 'Client authentication failed',
1660
- err,
1661
- )
1662
- }
1663
-
1664
- function throwInvalidRequest(err: unknown): never {
1665
- throw new InvalidRequestError(
1666
- extractZodErrorMessage(err) || 'Input validation error',
1667
- err,
1668
- )
1669
- }
1670
-
1671
- function extractZodErrorMessage(err: unknown): string | undefined {
1672
- if (err instanceof ZodError) {
1673
- const issue = err.issues[0]
1674
- if (issue?.path.length) {
1675
- // "part" will typically be "body" or "query"
1676
- const [part, ...path] = issue.path
1677
- return `Validation of "${path.join('.')}" ${part} parameter failed: ${issue.message}`
1678
- }
908
+ // Fool-proof
909
+ throw new Error('Invalid access token mode')
1679
910
  }
1680
-
1681
- return undefined
1682
911
  }