@udibo/oauth2 0.1.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 (375) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +174 -0
  3. package/SECURITY.md +77 -0
  4. package/esm/_dnt.polyfills.d.ts +17 -0
  5. package/esm/_dnt.polyfills.js +15 -0
  6. package/esm/adapters/hono/_common.d.ts +66 -0
  7. package/esm/adapters/hono/_common.js +108 -0
  8. package/esm/adapters/hono/authorization-server.d.ts +211 -0
  9. package/esm/adapters/hono/authorization-server.js +214 -0
  10. package/esm/adapters/hono/bff/auth-request-store.d.ts +164 -0
  11. package/esm/adapters/hono/bff/auth-request-store.js +154 -0
  12. package/esm/adapters/hono/bff/bff.d.ts +852 -0
  13. package/esm/adapters/hono/bff/bff.js +1194 -0
  14. package/esm/adapters/hono/bff/cookie-prefix.d.ts +72 -0
  15. package/esm/adapters/hono/bff/cookie-prefix.js +85 -0
  16. package/esm/adapters/hono/bff/mod.d.ts +19 -0
  17. package/esm/adapters/hono/bff/mod.js +19 -0
  18. package/esm/adapters/hono/bff/proxy.d.ts +108 -0
  19. package/esm/adapters/hono/bff/proxy.js +314 -0
  20. package/esm/adapters/hono/bff/session-store.d.ts +250 -0
  21. package/esm/adapters/hono/bff/session-store.js +216 -0
  22. package/esm/adapters/hono/bff/testing.d.ts +165 -0
  23. package/esm/adapters/hono/bff/testing.js +254 -0
  24. package/esm/adapters/hono/identity.d.ts +117 -0
  25. package/esm/adapters/hono/identity.js +215 -0
  26. package/esm/adapters/hono/log.d.ts +49 -0
  27. package/esm/adapters/hono/log.js +66 -0
  28. package/esm/adapters/hono/resource-server.d.ts +101 -0
  29. package/esm/adapters/hono/resource-server.js +108 -0
  30. package/esm/client/_http.d.ts +91 -0
  31. package/esm/client/_http.js +303 -0
  32. package/esm/client/base.d.ts +184 -0
  33. package/esm/client/base.js +93 -0
  34. package/esm/client/bff-client.d.ts +235 -0
  35. package/esm/client/bff-client.js +280 -0
  36. package/esm/client/browser-storage.d.ts +152 -0
  37. package/esm/client/browser-storage.js +246 -0
  38. package/esm/client/check.d.ts +114 -0
  39. package/esm/client/check.js +97 -0
  40. package/esm/client/direct-client.d.ts +707 -0
  41. package/esm/client/direct-client.js +1237 -0
  42. package/esm/client/discovery-cache.d.ts +138 -0
  43. package/esm/client/discovery-cache.js +124 -0
  44. package/esm/client/events.d.ts +54 -0
  45. package/esm/client/events.js +37 -0
  46. package/esm/client/mod.d.ts +35 -0
  47. package/esm/client/mod.js +33 -0
  48. package/esm/client/storage.d.ts +127 -0
  49. package/esm/client/storage.js +81 -0
  50. package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts +29 -0
  51. package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.js +47 -0
  52. package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts +28 -0
  53. package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.js +65 -0
  54. package/esm/deps/jsr.io/@std/assert/1.0.19/assert.d.ts +16 -0
  55. package/esm/deps/jsr.io/@std/assert/1.0.19/assert.js +23 -0
  56. package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts +25 -0
  57. package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.js +30 -0
  58. package/esm/deps/jsr.io/@std/assert/1.0.19/equal.d.ts +24 -0
  59. package/esm/deps/jsr.io/@std/assert/1.0.19/equal.js +193 -0
  60. package/esm/deps/jsr.io/@std/assert/1.0.19/equals.d.ts +36 -0
  61. package/esm/deps/jsr.io/@std/assert/1.0.19/equals.js +61 -0
  62. package/esm/deps/jsr.io/@std/assert/1.0.19/exists.d.ts +17 -0
  63. package/esm/deps/jsr.io/@std/assert/1.0.19/exists.js +27 -0
  64. package/esm/deps/jsr.io/@std/assert/1.0.19/fail.d.ts +14 -0
  65. package/esm/deps/jsr.io/@std/assert/1.0.19/fail.js +20 -0
  66. package/esm/deps/jsr.io/@std/assert/1.0.19/false.d.ts +18 -0
  67. package/esm/deps/jsr.io/@std/assert/1.0.19/false.js +23 -0
  68. package/esm/deps/jsr.io/@std/assert/1.0.19/greater.d.ts +19 -0
  69. package/esm/deps/jsr.io/@std/assert/1.0.19/greater.js +29 -0
  70. package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts +19 -0
  71. package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.js +29 -0
  72. package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts +22 -0
  73. package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.js +52 -0
  74. package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts +24 -0
  75. package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.js +53 -0
  76. package/esm/deps/jsr.io/@std/assert/1.0.19/less.d.ts +18 -0
  77. package/esm/deps/jsr.io/@std/assert/1.0.19/less.js +28 -0
  78. package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts +19 -0
  79. package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.js +29 -0
  80. package/esm/deps/jsr.io/@std/assert/1.0.19/match.d.ts +17 -0
  81. package/esm/deps/jsr.io/@std/assert/1.0.19/match.js +26 -0
  82. package/esm/deps/jsr.io/@std/assert/1.0.19/mod.d.ts +43 -0
  83. package/esm/deps/jsr.io/@std/assert/1.0.19/mod.js +45 -0
  84. package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts +26 -0
  85. package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.js +39 -0
  86. package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts +19 -0
  87. package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.js +29 -0
  88. package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts +17 -0
  89. package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.js +26 -0
  90. package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts +22 -0
  91. package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.js +32 -0
  92. package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts +28 -0
  93. package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.js +177 -0
  94. package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts +41 -0
  95. package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.js +53 -0
  96. package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts +23 -0
  97. package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.js +58 -0
  98. package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts +17 -0
  99. package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.js +26 -0
  100. package/esm/deps/jsr.io/@std/assert/1.0.19/throws.d.ts +44 -0
  101. package/esm/deps/jsr.io/@std/assert/1.0.19/throws.js +44 -0
  102. package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts +14 -0
  103. package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.js +20 -0
  104. package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts +14 -0
  105. package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.js +20 -0
  106. package/esm/deps/jsr.io/@std/async/1.5.0/delay.d.ts +44 -0
  107. package/esm/deps/jsr.io/@std/async/1.5.0/delay.js +84 -0
  108. package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.d.ts +39 -0
  109. package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js +61 -0
  110. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.d.ts +22 -0
  111. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.js +51 -0
  112. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.d.ts +34 -0
  113. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.js +192 -0
  114. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.d.ts +34 -0
  115. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.js +113 -0
  116. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.d.ts +3 -0
  117. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.js +13 -0
  118. package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.d.ts +8 -0
  119. package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.js +2 -0
  120. package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.d.ts +1 -0
  121. package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.js +26 -0
  122. package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.d.ts +60 -0
  123. package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.js +152 -0
  124. package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.d.ts +39 -0
  125. package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.js +87 -0
  126. package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.d.ts +39 -0
  127. package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.js +131 -0
  128. package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.d.ts +39 -0
  129. package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.js +82 -0
  130. package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.d.ts +39 -0
  131. package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.js +72 -0
  132. package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.d.ts +38 -0
  133. package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.js +87 -0
  134. package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.d.ts +97 -0
  135. package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.js +99 -0
  136. package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.d.ts +119 -0
  137. package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.js +209 -0
  138. package/esm/deps/jsr.io/@std/http/1.1.3/status.d.ts +343 -0
  139. package/esm/deps/jsr.io/@std/http/1.1.3/status.js +343 -0
  140. package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.d.ts +159 -0
  141. package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.js +223 -0
  142. package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.d.ts +79 -0
  143. package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.js +116 -0
  144. package/esm/deps/jsr.io/@std/internal/1.0.14/diff.d.ts +139 -0
  145. package/esm/deps/jsr.io/@std/internal/1.0.14/diff.js +277 -0
  146. package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.d.ts +98 -0
  147. package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.js +181 -0
  148. package/esm/deps/jsr.io/@std/internal/1.0.14/format.d.ts +29 -0
  149. package/esm/deps/jsr.io/@std/internal/1.0.14/format.js +68 -0
  150. package/esm/deps/jsr.io/@std/internal/1.0.14/styles.d.ts +158 -0
  151. package/esm/deps/jsr.io/@std/internal/1.0.14/styles.js +206 -0
  152. package/esm/deps/jsr.io/@std/internal/1.0.14/types.d.ts +24 -0
  153. package/esm/deps/jsr.io/@std/internal/1.0.14/types.js +3 -0
  154. package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.d.ts +91 -0
  155. package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.js +347 -0
  156. package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.d.ts +382 -0
  157. package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.js +1110 -0
  158. package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.d.ts +363 -0
  159. package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.js +616 -0
  160. package/esm/errors.d.ts +203 -0
  161. package/esm/errors.js +287 -0
  162. package/esm/identity/captcha.d.ts +147 -0
  163. package/esm/identity/captcha.js +64 -0
  164. package/esm/identity/delivery.d.ts +96 -0
  165. package/esm/identity/delivery.js +41 -0
  166. package/esm/identity/errors.d.ts +39 -0
  167. package/esm/identity/errors.js +46 -0
  168. package/esm/identity/events.d.ts +242 -0
  169. package/esm/identity/events.js +39 -0
  170. package/esm/identity/external/_shared.d.ts +86 -0
  171. package/esm/identity/external/_shared.js +107 -0
  172. package/esm/identity/external/_token-exchange.d.ts +61 -0
  173. package/esm/identity/external/_token-exchange.js +179 -0
  174. package/esm/identity/external/apple-client-secret.d.ts +82 -0
  175. package/esm/identity/external/apple-client-secret.js +133 -0
  176. package/esm/identity/external/apple.d.ts +64 -0
  177. package/esm/identity/external/apple.js +248 -0
  178. package/esm/identity/external/discord.d.ts +38 -0
  179. package/esm/identity/external/discord.js +62 -0
  180. package/esm/identity/external/errors.d.ts +48 -0
  181. package/esm/identity/external/errors.js +36 -0
  182. package/esm/identity/external/flow.d.ts +107 -0
  183. package/esm/identity/external/flow.js +158 -0
  184. package/esm/identity/external/github.d.ts +42 -0
  185. package/esm/identity/external/github.js +164 -0
  186. package/esm/identity/external/google.d.ts +52 -0
  187. package/esm/identity/external/google.js +38 -0
  188. package/esm/identity/external/mod.d.ts +63 -0
  189. package/esm/identity/external/mod.js +61 -0
  190. package/esm/identity/external/oauth2.d.ts +110 -0
  191. package/esm/identity/external/oauth2.js +158 -0
  192. package/esm/identity/external/oidc.d.ts +81 -0
  193. package/esm/identity/external/oidc.js +250 -0
  194. package/esm/identity/external/provider.d.ts +132 -0
  195. package/esm/identity/external/provider.js +12 -0
  196. package/esm/identity/hibp.d.ts +67 -0
  197. package/esm/identity/hibp.js +87 -0
  198. package/esm/identity/identifier.d.ts +44 -0
  199. package/esm/identity/identifier.js +47 -0
  200. package/esm/identity/lockout.d.ts +155 -0
  201. package/esm/identity/lockout.js +117 -0
  202. package/esm/identity/mfa/mod.d.ts +42 -0
  203. package/esm/identity/mfa/mod.js +42 -0
  204. package/esm/identity/mfa/recovery-codes.d.ts +96 -0
  205. package/esm/identity/mfa/recovery-codes.js +80 -0
  206. package/esm/identity/mfa/service.d.ts +338 -0
  207. package/esm/identity/mfa/service.js +351 -0
  208. package/esm/identity/mfa/totp.d.ts +186 -0
  209. package/esm/identity/mfa/totp.js +194 -0
  210. package/esm/identity/migration.d.ts +155 -0
  211. package/esm/identity/migration.js +243 -0
  212. package/esm/identity/mod.d.ts +65 -0
  213. package/esm/identity/mod.js +64 -0
  214. package/esm/identity/otp.d.ts +255 -0
  215. package/esm/identity/otp.js +245 -0
  216. package/esm/identity/password-policy.d.ts +45 -0
  217. package/esm/identity/password-policy.js +60 -0
  218. package/esm/identity/password.d.ts +171 -0
  219. package/esm/identity/password.js +160 -0
  220. package/esm/identity/rate-limit.d.ts +192 -0
  221. package/esm/identity/rate-limit.js +168 -0
  222. package/esm/identity/service.d.ts +579 -0
  223. package/esm/identity/service.js +835 -0
  224. package/esm/identity/session.d.ts +122 -0
  225. package/esm/identity/session.js +48 -0
  226. package/esm/identity/token-flow.d.ts +214 -0
  227. package/esm/identity/token-flow.js +173 -0
  228. package/esm/models/authentication.d.ts +24 -0
  229. package/esm/models/authentication.js +1 -0
  230. package/esm/models/authorization-code.d.ts +63 -0
  231. package/esm/models/authorization-code.js +1 -0
  232. package/esm/models/authorization.d.ts +165 -0
  233. package/esm/models/authorization.js +160 -0
  234. package/esm/models/client.d.ts +78 -0
  235. package/esm/models/client.js +1 -0
  236. package/esm/models/device-authorization.d.ts +30 -0
  237. package/esm/models/device-authorization.js +1 -0
  238. package/esm/models/responses.d.ts +93 -0
  239. package/esm/models/responses.js +1 -0
  240. package/esm/models/scope.d.ts +81 -0
  241. package/esm/models/scope.js +125 -0
  242. package/esm/models/token.d.ts +135 -0
  243. package/esm/models/token.js +1 -0
  244. package/esm/package.json +3 -0
  245. package/esm/react/callback.d.ts +52 -0
  246. package/esm/react/callback.js +71 -0
  247. package/esm/react/components/class-names.d.ts +91 -0
  248. package/esm/react/components/class-names.js +118 -0
  249. package/esm/react/components/internal.d.ts +62 -0
  250. package/esm/react/components/internal.js +102 -0
  251. package/esm/react/components/mfa-challenge-form.d.ts +66 -0
  252. package/esm/react/components/mfa-challenge-form.js +46 -0
  253. package/esm/react/components/mfa-enrollment-form.d.ts +83 -0
  254. package/esm/react/components/mfa-enrollment-form.js +41 -0
  255. package/esm/react/components/mod.d.ts +103 -0
  256. package/esm/react/components/mod.js +93 -0
  257. package/esm/react/components/request-password-reset-form.d.ts +61 -0
  258. package/esm/react/components/request-password-reset-form.js +41 -0
  259. package/esm/react/components/reset-password-form.d.ts +66 -0
  260. package/esm/react/components/reset-password-form.js +56 -0
  261. package/esm/react/components/sign-in-form.d.ts +74 -0
  262. package/esm/react/components/sign-in-form.js +43 -0
  263. package/esm/react/components/sign-up-form.d.ts +85 -0
  264. package/esm/react/components/sign-up-form.js +57 -0
  265. package/esm/react/components/types.d.ts +208 -0
  266. package/esm/react/components/types.js +14 -0
  267. package/esm/react/components/use-auth-form.d.ts +118 -0
  268. package/esm/react/components/use-auth-form.js +172 -0
  269. package/esm/react/components/user-menu.d.ts +79 -0
  270. package/esm/react/components/user-menu.js +73 -0
  271. package/esm/react/context.d.ts +51 -0
  272. package/esm/react/context.js +16 -0
  273. package/esm/react/mod.d.ts +26 -0
  274. package/esm/react/mod.js +26 -0
  275. package/esm/react/provider.d.ts +40 -0
  276. package/esm/react/provider.js +189 -0
  277. package/esm/react/require-auth.d.ts +31 -0
  278. package/esm/react/require-auth.js +37 -0
  279. package/esm/react/testing.d.ts +322 -0
  280. package/esm/react/testing.js +461 -0
  281. package/esm/react/use-authorization.d.ts +31 -0
  282. package/esm/react/use-authorization.js +36 -0
  283. package/esm/react/use-oauth2.d.ts +112 -0
  284. package/esm/react/use-oauth2.js +127 -0
  285. package/esm/server/authorization-server.d.ts +736 -0
  286. package/esm/server/authorization-server.js +1304 -0
  287. package/esm/server/client-authentication.d.ts +28 -0
  288. package/esm/server/client-authentication.js +51 -0
  289. package/esm/server/grants/authorization-code.d.ts +179 -0
  290. package/esm/server/grants/authorization-code.js +232 -0
  291. package/esm/server/grants/client-credentials.d.ts +52 -0
  292. package/esm/server/grants/client-credentials.js +53 -0
  293. package/esm/server/grants/device-authorization.d.ts +103 -0
  294. package/esm/server/grants/device-authorization.js +156 -0
  295. package/esm/server/grants/grant.d.ts +165 -0
  296. package/esm/server/grants/grant.js +173 -0
  297. package/esm/server/grants/password.d.ts +80 -0
  298. package/esm/server/grants/password.js +92 -0
  299. package/esm/server/grants/refresh-token.d.ts +80 -0
  300. package/esm/server/grants/refresh-token.js +136 -0
  301. package/esm/server/introspection-token-reader.d.ts +153 -0
  302. package/esm/server/introspection-token-reader.js +164 -0
  303. package/esm/server/jwks-token-reader.d.ts +263 -0
  304. package/esm/server/jwks-token-reader.js +508 -0
  305. package/esm/server/mod.d.ts +23 -0
  306. package/esm/server/mod.js +15 -0
  307. package/esm/server/public-suffix/mod.d.ts +62 -0
  308. package/esm/server/public-suffix/mod.js +103 -0
  309. package/esm/server/public-suffix/rules.d.ts +23 -0
  310. package/esm/server/public-suffix/rules.js +23 -0
  311. package/esm/server/redirect-uri.d.ts +187 -0
  312. package/esm/server/redirect-uri.js +331 -0
  313. package/esm/server/resource-server.d.ts +286 -0
  314. package/esm/server/resource-server.js +365 -0
  315. package/esm/server/services/authorization-code.d.ts +84 -0
  316. package/esm/server/services/authorization-code.js +35 -0
  317. package/esm/server/services/client.d.ts +35 -0
  318. package/esm/server/services/client.js +1 -0
  319. package/esm/server/services/device-authorization.d.ts +134 -0
  320. package/esm/server/services/device-authorization.js +7 -0
  321. package/esm/server/services/token.d.ts +242 -0
  322. package/esm/server/services/token.js +78 -0
  323. package/esm/server/services/user.d.ts +29 -0
  324. package/esm/server/services/user.js +7 -0
  325. package/esm/server/signing-keys.d.ts +201 -0
  326. package/esm/server/signing-keys.js +225 -0
  327. package/esm/server/utils/hash.d.ts +6 -0
  328. package/esm/server/utils/hash.js +11 -0
  329. package/esm/testing/contract/authorization-code.d.ts +37 -0
  330. package/esm/testing/contract/authorization-code.js +93 -0
  331. package/esm/testing/contract/client.d.ts +85 -0
  332. package/esm/testing/contract/client.js +126 -0
  333. package/esm/testing/contract/device-authorization.d.ts +37 -0
  334. package/esm/testing/contract/device-authorization.js +193 -0
  335. package/esm/testing/contract/lockout-store.d.ts +47 -0
  336. package/esm/testing/contract/lockout-store.js +122 -0
  337. package/esm/testing/contract/mfa-store.d.ts +50 -0
  338. package/esm/testing/contract/mfa-store.js +226 -0
  339. package/esm/testing/contract/mod.d.ts +39 -0
  340. package/esm/testing/contract/mod.js +39 -0
  341. package/esm/testing/contract/otp-store.d.ts +48 -0
  342. package/esm/testing/contract/otp-store.js +197 -0
  343. package/esm/testing/contract/rate-limit-store.d.ts +52 -0
  344. package/esm/testing/contract/rate-limit-store.js +105 -0
  345. package/esm/testing/contract/token-flow-store.d.ts +54 -0
  346. package/esm/testing/contract/token-flow-store.js +179 -0
  347. package/esm/testing/contract/token-reader.d.ts +77 -0
  348. package/esm/testing/contract/token-reader.js +78 -0
  349. package/esm/testing/contract/token.d.ts +61 -0
  350. package/esm/testing/contract/token.js +287 -0
  351. package/esm/testing/contract/user.d.ts +63 -0
  352. package/esm/testing/contract/user.js +75 -0
  353. package/esm/testing/mod.d.ts +34 -0
  354. package/esm/testing/mod.js +34 -0
  355. package/esm/testing/server.d.ts +197 -0
  356. package/esm/testing/server.js +193 -0
  357. package/esm/testing/services.d.ts +373 -0
  358. package/esm/testing/services.js +703 -0
  359. package/esm/utils/_buffer.d.ts +10 -0
  360. package/esm/utils/_buffer.js +12 -0
  361. package/esm/utils/_timing-safe.d.ts +13 -0
  362. package/esm/utils/_timing-safe.js +22 -0
  363. package/esm/utils/authentication-context.d.ts +3 -0
  364. package/esm/utils/authentication-context.js +14 -0
  365. package/esm/utils/basic-auth.d.ts +50 -0
  366. package/esm/utils/basic-auth.js +106 -0
  367. package/esm/utils/crypto.d.ts +97 -0
  368. package/esm/utils/crypto.js +162 -0
  369. package/esm/utils/pkce.d.ts +151 -0
  370. package/esm/utils/pkce.js +179 -0
  371. package/esm/utils/text.d.ts +24 -0
  372. package/esm/utils/text.js +28 -0
  373. package/esm/utils/url.d.ts +66 -0
  374. package/esm/utils/url.js +90 -0
  375. package/package.json +92 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Resource Server implementation for protecting resources with OAuth2 tokens.
3
+ *
4
+ * Implements bearer token validation per RFC 6750.
5
+ *
6
+ * @see https://datatracker.ietf.org/doc/html/rfc6750
7
+ * @module
8
+ */
9
+ import "../_dnt.polyfills.js";
10
+ import { BasicScope } from "../models/scope.js";
11
+ import { AccessDeniedError, InsufficientPermissionsError, InsufficientScopeError, InvalidTokenError, toOAuth2Error, } from "../errors.js";
12
+ import { authorizationFromClaims, } from "../models/authorization.js";
13
+ /** Bearer token regex pattern. */
14
+ export const BEARER_TOKEN = /^ *(?:[Bb][Ee][Aa][Rr][Ee][Rr]) +([\w-.~+/]+=*) *$/;
15
+ const FAILURE_LABELS = {
16
+ organization: "requires organization",
17
+ role: "requires role",
18
+ orgRole: "requires organization role",
19
+ permission: "requires permission",
20
+ };
21
+ const CHALLENGE_ERROR_CODES = new Set([
22
+ "invalid_request",
23
+ "invalid_token",
24
+ "insufficient_scope",
25
+ ]);
26
+ /**
27
+ * Validates OAuth2 bearer access tokens (RFC 6750) so API endpoints can require
28
+ * authentication. Framework-agnostic: it works on web `Request`/`Response`, so
29
+ * an adapter (Hono, etc.) wires {@linkcode authenticate} into its middleware.
30
+ *
31
+ * A single instance serves every tenant/origin; per-request services come from
32
+ * the `resolve` callback in {@linkcode ResourceServerOptions}. For a
33
+ * single-tenant server, `resolve` is a constant function returning the same
34
+ * services every time.
35
+ *
36
+ * {@linkcode authenticate} throws {@linkcode HttpError} subclasses on failure
37
+ * ({@linkcode AccessDeniedError} when no token is sent,
38
+ * {@linkcode InvalidTokenError} for an invalid/expired token, and
39
+ * {@linkcode InsufficientScopeError} when scope is lacking); use
40
+ * {@linkcode handleAuthError} to turn them into RFC 6750 responses.
41
+ *
42
+ * @template Client The client entity type.
43
+ * @template User The resource-owner entity type.
44
+ * @template S The scope type used for granted/required scope comparisons.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { Hono } from "hono";
49
+ * import type { ClientInterface, TokenReaderInterface } from "@udibo/oauth2/server";
50
+ * import { ResourceServer } from "@udibo/oauth2/server/resource";
51
+ *
52
+ * interface User { id: string }
53
+ *
54
+ * declare const app: Hono;
55
+ * declare const tokenService: TokenReaderInterface<ClientInterface, User>;
56
+ *
57
+ * const server = new ResourceServer({
58
+ * resolve: () => ({ services: { tokenService } }),
59
+ * });
60
+ *
61
+ * app.get("/api/me", async (c) => {
62
+ * try {
63
+ * const { user } = await server.authenticate(c.req.raw, "identity:read");
64
+ * return c.json(user ?? null);
65
+ * } catch (error) {
66
+ * return server.handleAuthError(error);
67
+ * }
68
+ * });
69
+ * ```
70
+ */
71
+ export class ResourceServer {
72
+ /** Scope constructor used to parse and compare scopes. */
73
+ Scope;
74
+ /** Protection space named in `WWW-Authenticate` challenges. */
75
+ realm;
76
+ /** Leeway in seconds applied to the access token's expiry. Defaults to `0`. */
77
+ clockSkewSeconds;
78
+ /** Body format used for error responses: OAuth2 or RFC 9457 problem details. */
79
+ errorFormat;
80
+ /** When true, handler boundaries rethrow {@linkcode OAuth2Error} instead of returning a response. */
81
+ throwOnError;
82
+ /** Resolves the per-request {@link ResourceServerContext}. */
83
+ resolveContext;
84
+ /** Creates a resource server from the given {@linkcode ResourceServerOptions}. */
85
+ constructor(options) {
86
+ this.Scope = options.Scope ??
87
+ BasicScope;
88
+ this.realm = options.realm ?? "Service";
89
+ this.clockSkewSeconds = options.clockSkewSeconds ?? 0;
90
+ this.errorFormat = options.errorFormat ?? "oauth2";
91
+ this.throwOnError = options.throwOnError ?? false;
92
+ this.resolveContext = options.resolve;
93
+ }
94
+ /**
95
+ * Builds a WWW-Authenticate header value per RFC 6750 Section 3.
96
+ *
97
+ * The header includes:
98
+ * - realm: The protection space
99
+ * - error: The error code (for 401/403 responses)
100
+ * - error_description: Human-readable error description
101
+ * - scope: Required scope (for insufficient_scope errors)
102
+ *
103
+ * RFC 6750 Section 3.1 registers exactly three challenge codes —
104
+ * `invalid_request`, `invalid_token`, `insufficient_scope` — so an error
105
+ * carrying anything else (including the `access_denied` this server raises
106
+ * when a request presents no credentials at all, which the same section says
107
+ * MUST NOT be given a code) produces a bare `realm` challenge. Pick the error
108
+ * class accordingly: a client reads a coded challenge and acts on it, and
109
+ * {@linkcode InsufficientScopeError} is the one that tells it "your token is
110
+ * fine, it just lacks scope" rather than sending it off to refresh.
111
+ *
112
+ * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
113
+ */
114
+ buildWwwAuthenticate(error, requiredScope) {
115
+ const quote = (value) =>
116
+ // deno-lint-ignore no-control-regex
117
+ value.replace(/[\x00-\x1f\x7f]/g, "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
118
+ const parts = [`realm="${quote(this.realm)}"`];
119
+ const code = error?.extensions.error;
120
+ if (error && code && CHALLENGE_ERROR_CODES.has(code)) {
121
+ parts.push(`error="${quote(code)}"`);
122
+ if (error.message) {
123
+ parts.push(`error_description="${quote(error.message)}"`);
124
+ }
125
+ }
126
+ if (requiredScope) {
127
+ parts.push(`scope="${quote(requiredScope)}"`);
128
+ }
129
+ else if (error instanceof InsufficientScopeError) {
130
+ const scope = error.extensions.requiredScope;
131
+ if (scope) {
132
+ parts.push(`scope="${quote(scope)}"`);
133
+ }
134
+ }
135
+ return `Bearer ${parts.join(", ")}`;
136
+ }
137
+ /**
138
+ * Creates an error response for an OAuth2 error.
139
+ *
140
+ * The response body is formatted according to {@link errorFormat}:
141
+ * - `"oauth2"`: `{ error, error_description?, error_uri? }` as JSON.
142
+ * - `"problem-details"`: RFC 9457 Problem Details with OAuth2 extensions.
143
+ *
144
+ * Does **not** set any response headers beyond `Content-Type`. Any extra
145
+ * response headers (`WWW-Authenticate`, `Cache-Control`, `Pragma`, …) must
146
+ * be set on the error via its `headers` property before calling this
147
+ * method; they are copied through verbatim.
148
+ */
149
+ createErrorResponse(error) {
150
+ const oauth2Error = toOAuth2Error(error);
151
+ const response = this.errorFormat === "problem-details"
152
+ ? oauth2Error.getResponse()
153
+ : this.#createOAuth2ErrorResponse(oauth2Error);
154
+ for (const [key, value] of oauth2Error.headers) {
155
+ if (key.toLowerCase() === "content-type")
156
+ continue;
157
+ response.headers.set(key, value);
158
+ }
159
+ return response;
160
+ }
161
+ #createOAuth2ErrorResponse(oauth2Error) {
162
+ const body = {
163
+ error: oauth2Error.extensions.error ?? "server_error",
164
+ };
165
+ const description = oauth2Error.extensions.error_description ??
166
+ oauth2Error.exposedMessage;
167
+ if (description) {
168
+ body.error_description = description;
169
+ }
170
+ const errorUri = oauth2Error.extensions.error_uri ?? oauth2Error.type;
171
+ if (errorUri) {
172
+ body.error_uri = errorUri;
173
+ }
174
+ return new Response(JSON.stringify(body), {
175
+ status: oauth2Error.status,
176
+ headers: { "Content-Type": "application/json;charset=UTF-8" },
177
+ });
178
+ }
179
+ /**
180
+ * Normalizes an error at a handler boundary and either rethrows it (when
181
+ * {@link throwOnError} is true) or converts it to a response.
182
+ *
183
+ * The optional `prepare` callback lets a handler annotate the error —
184
+ * usually by setting headers like `Cache-Control` or `WWW-Authenticate`
185
+ * that belong on both the thrown error and the response — before the
186
+ * throw/respond decision is made.
187
+ */
188
+ handleError(error, prepare) {
189
+ const oauth2Error = toOAuth2Error(error);
190
+ prepare?.(oauth2Error);
191
+ if (this.throwOnError)
192
+ throw oauth2Error;
193
+ return this.createErrorResponse(oauth2Error);
194
+ }
195
+ /**
196
+ * {@link handleError} variant that automatically attaches a
197
+ * `WWW-Authenticate` header (per RFC 6750 Section 3) when the error's
198
+ * status is 401 or 403. Use this at bearer-token boundaries to avoid the
199
+ * boilerplate of building the header for each error response.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * import { Hono } from "hono";
204
+ * import type { ClientInterface } from "@udibo/oauth2/server";
205
+ * import { ResourceServer } from "@udibo/oauth2/server/resource";
206
+ *
207
+ * interface User { id: string }
208
+ *
209
+ * declare const app: Hono;
210
+ * declare const server: ResourceServer<ClientInterface, User>;
211
+ *
212
+ * app.get("/api/me", async (c) => {
213
+ * try {
214
+ * const ctx = await server.authenticate(c.req.raw);
215
+ * return c.json(ctx.user ?? null);
216
+ * } catch (error) {
217
+ * return server.handleAuthError(error);
218
+ * }
219
+ * });
220
+ * ```
221
+ */
222
+ handleAuthError(error) {
223
+ return this.handleError(error, (e) => {
224
+ if (e.status === 401 || e.status === 403) {
225
+ e.headers.set("WWW-Authenticate", this.buildWwwAuthenticate(e));
226
+ }
227
+ });
228
+ }
229
+ /**
230
+ * Extracts an access token from a request.
231
+ * Looks in the Authorization header (Bearer token) first,
232
+ * then in the request body for form-encoded requests.
233
+ */
234
+ async getAccessToken(request) {
235
+ const authorization = request.headers.get("authorization");
236
+ if (authorization) {
237
+ const match = BEARER_TOKEN.exec(authorization);
238
+ if (match)
239
+ return match[1];
240
+ }
241
+ const contentType = request.headers.get("content-type");
242
+ if (request.method === "POST" &&
243
+ contentType?.includes("application/x-www-form-urlencoded")) {
244
+ try {
245
+ const body = await request.clone().formData();
246
+ const accessToken = body.get("access_token");
247
+ if (typeof accessToken === "string")
248
+ return accessToken;
249
+ }
250
+ catch {
251
+ // ignore
252
+ }
253
+ }
254
+ return null;
255
+ }
256
+ /**
257
+ * Retrieves and validates a token by access token string.
258
+ *
259
+ * The token's expiry is compared against now with {@linkcode
260
+ * clockSkewSeconds} of leeway, so the same tolerance applies whichever
261
+ * token reader produced the token.
262
+ *
263
+ * @throws {InvalidTokenError} If the token is invalid, expired beyond the
264
+ * configured leeway, or revoked
265
+ * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
266
+ */
267
+ async getToken(accessToken, services) {
268
+ const { tokenService } = services;
269
+ const token = await tokenService.getToken(accessToken);
270
+ if (!token) {
271
+ throw new InvalidTokenError("invalid access token");
272
+ }
273
+ if (token.accessTokenExpiresAt) {
274
+ const deadline = token.accessTokenExpiresAt.getTime() +
275
+ this.clockSkewSeconds * 1000;
276
+ if (deadline < Date.now()) {
277
+ throw new InvalidTokenError("access token has expired");
278
+ }
279
+ }
280
+ return token;
281
+ }
282
+ /**
283
+ * Authenticates a request and verifies the token has the required scope.
284
+ *
285
+ * @param request The incoming request
286
+ * @param requiredScope Optional scope required to access the resource
287
+ * @returns The authenticated context with token, client, user, and scope
288
+ * @throws {AccessDeniedError} If no token is provided
289
+ * @throws {InvalidTokenError} If the token is invalid or expired
290
+ * @throws {InsufficientScopeError} If the token lacks required scope (HTTP 403)
291
+ * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
292
+ */
293
+ async authenticate(request, requiredScope) {
294
+ const accessToken = await this.getAccessToken(request);
295
+ if (!accessToken) {
296
+ throw new AccessDeniedError("authentication required");
297
+ }
298
+ const { services } = await this.resolveContext(request);
299
+ const token = await this.getToken(accessToken, services);
300
+ if (requiredScope) {
301
+ this.assertScope(token.scope, requiredScope);
302
+ }
303
+ return {
304
+ token,
305
+ client: token.client,
306
+ user: token.user,
307
+ scope: token.scope,
308
+ authorization: authorizationFromClaims(token.claims, token.scope),
309
+ };
310
+ }
311
+ /**
312
+ * Throws unless the authenticated context satisfies every condition —
313
+ * the assertion behind the adapters' `require()` middleware, public so a
314
+ * handler authenticating manually can reuse the same checks.
315
+ *
316
+ * Scope conditions go through {@linkcode assertScope}, so their failure is
317
+ * the same RFC 6750 `insufficient_scope` challenge `requireScope` sends.
318
+ * Every other condition is answered by the context's
319
+ * {@linkcode AuthenticatedContext.authorization}; the first unmet one
320
+ * throws a plain 403 naming the failed check.
321
+ *
322
+ * @throws {InsufficientScopeError} If the token lacks a required scope (403,
323
+ * `insufficient_scope` challenge).
324
+ * @throws {InsufficientPermissionsError} If a permission, role, or
325
+ * organization condition fails (403, no challenge code).
326
+ */
327
+ assertAuthorized(context, conditions) {
328
+ if (conditions.scope !== undefined) {
329
+ const required = typeof conditions.scope === "string"
330
+ ? conditions.scope
331
+ : conditions.scope.join(" ");
332
+ this.assertScope(context.scope, required);
333
+ }
334
+ const failure = context.authorization.unmet(conditions);
335
+ if (failure) {
336
+ const required = failure.required === true
337
+ ? "an active organization"
338
+ : failure.required;
339
+ throw new InsufficientPermissionsError(`${FAILURE_LABELS[failure.kind]}: ${required}`);
340
+ }
341
+ }
342
+ /**
343
+ * Throws {@link InsufficientScopeError} unless `granted` contains
344
+ * `requiredScope`. Shared by {@link authenticate} and the adapters'
345
+ * scope-only guards (`requireScope`) so the comparison and the RFC 6750
346
+ * error stay identical — a guard that checks an already-authenticated
347
+ * request's scope (without re-validating the token) calls this with the
348
+ * context's `scope`. Public so a handler authenticating manually can reuse
349
+ * the same check.
350
+ *
351
+ * @throws {InsufficientScopeError} If `granted` lacks `requiredScope` (403).
352
+ */
353
+ assertScope(granted, requiredScope) {
354
+ const scope = typeof requiredScope === "string"
355
+ ? new this.Scope(requiredScope)
356
+ : requiredScope;
357
+ if (!granted?.has(scope)) {
358
+ throw new InsufficientScopeError("insufficient scope", {
359
+ extensions: { requiredScope: scope.toString() },
360
+ });
361
+ }
362
+ }
363
+ }
364
+ export { IntrospectionTokenReader } from "./introspection-token-reader.js";
365
+ export { JwksTokenReader } from "./jwks-token-reader.js";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Authorization code service interface and abstract implementation.
3
+ * @module
4
+ */
5
+ import type { AuthorizationCode } from "../../models/authorization-code.js";
6
+ import type { ClientInterface } from "../../models/client.js";
7
+ import type { AbstractScope, BasicScope } from "../../models/scope.js";
8
+ import type { ClientServiceInterface } from "./client.js";
9
+ import type { UserServiceInterface } from "./user.js";
10
+ /**
11
+ * Service interface for authorization code management.
12
+ */
13
+ export interface AuthorizationCodeServiceInterface<Client extends ClientInterface, User, Scope extends AbstractScope = BasicScope> {
14
+ /** Lifetime of authorization codes in seconds. */
15
+ lifetime: number;
16
+ /** Generates an authorization code string. */
17
+ generateCode(client: Client, user: User, scope?: Scope | null): Promise<string>;
18
+ /** Gets the date that a new authorization code would expire at. */
19
+ expiresAt(client: Client, user: User, scope?: Scope | null): Promise<Date>;
20
+ /** Retrieves an existing authorization code. */
21
+ get(code: string): Promise<AuthorizationCode<Client, User, Scope> | undefined>;
22
+ /**
23
+ * Saves an authorization code, preserving its `authenticationContext`
24
+ * snapshot on save and retrieval. Leave legacy context absent.
25
+ */
26
+ save(authorizationCode: AuthorizationCode<Client, User, Scope>): Promise<AuthorizationCode<Client, User, Scope>>;
27
+ /**
28
+ * Revokes an authorization code, accepting the record or its raw string.
29
+ *
30
+ * **Implementations must compare and delete atomically.** The authorization
31
+ * code grant claims a code by revoking it and issues a token only to the
32
+ * caller whose revoke resolved `true`, so this boolean is the single-use
33
+ * guarantee (RFC 6749 §4.1.2): resolve `true` only for the call that removed
34
+ * a live code, and `false` for every other call — a code already revoked,
35
+ * expired, or never stored. An implementation that reads then deletes in two
36
+ * steps, or that returns `true` unconditionally, lets two concurrent
37
+ * exchanges of one code both mint tokens. In SQL this is a
38
+ * `DELETE ... WHERE code = $1` whose affected-row count is the answer.
39
+ */
40
+ revoke(authorizationCode: AuthorizationCode<Client, User, Scope> | string): Promise<boolean>;
41
+ }
42
+ /** Options for configuring an abstract authorization code service. */
43
+ export interface AbstractAuthorizationCodeServiceOptions<Client extends ClientInterface, User> {
44
+ /** Lifetime of authorization codes in seconds. Defaults to 600 (10 minutes). */
45
+ lifetime?: number;
46
+ /** Client service for hydrating authorization codes. */
47
+ clientService?: ClientServiceInterface<Client, User>;
48
+ /** User service for hydrating authorization codes. */
49
+ userService?: UserServiceInterface<User>;
50
+ }
51
+ /**
52
+ * Abstract authorization code service with sensible defaults.
53
+ *
54
+ * Provides default implementations for:
55
+ * - `generateCode` — crypto.randomUUID()
56
+ * - `expiresAt` — based on lifetime setting
57
+ *
58
+ * Subclasses must implement the storage methods: `get`, `save`, `revoke`
59
+ */
60
+ export declare abstract class AbstractAuthorizationCodeService<Client extends ClientInterface, User, Scope extends AbstractScope = BasicScope> implements AuthorizationCodeServiceInterface<Client, User, Scope> {
61
+ /** Lifetime of authorization codes in seconds. */
62
+ lifetime: number;
63
+ /** Client service used by subclasses to hydrate authorization codes. */
64
+ protected clientService?: ClientServiceInterface<Client, User>;
65
+ /** User service used by subclasses to hydrate authorization codes. */
66
+ protected userService?: UserServiceInterface<User>;
67
+ /** Lifetime defaults to 600 seconds (10 minutes) when not supplied. */
68
+ constructor(options?: AbstractAuthorizationCodeServiceOptions<Client, User>);
69
+ /** Generates an opaque authorization code string via `crypto.randomUUID()`. */
70
+ generateCode(_client: Client, _user: User, _scope?: Scope | null): Promise<string>;
71
+ /** Computes the code expiry as now plus {@linkcode lifetime}. */
72
+ expiresAt(_client: Client, _user: User, _scope?: Scope | null): Promise<Date>;
73
+ /** Retrieves a persisted authorization code by its string value. */
74
+ abstract get(code: string): Promise<AuthorizationCode<Client, User, Scope> | undefined>;
75
+ /** Persists an authorization code and returns the saved record. */
76
+ abstract save(authorizationCode: AuthorizationCode<Client, User, Scope>): Promise<AuthorizationCode<Client, User, Scope>>;
77
+ /**
78
+ * Revokes an authorization code given as a record or string. Resolves `true`
79
+ * only for the call that removed a live code, and `false` for every other
80
+ * call; the compare and delete must be atomic, since the authorization code
81
+ * grant relies on this boolean for single use.
82
+ */
83
+ abstract revoke(authorizationCode: AuthorizationCode<Client, User, Scope> | string): Promise<boolean>;
84
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Authorization code service interface and abstract implementation.
3
+ * @module
4
+ */
5
+ /**
6
+ * Abstract authorization code service with sensible defaults.
7
+ *
8
+ * Provides default implementations for:
9
+ * - `generateCode` — crypto.randomUUID()
10
+ * - `expiresAt` — based on lifetime setting
11
+ *
12
+ * Subclasses must implement the storage methods: `get`, `save`, `revoke`
13
+ */
14
+ export class AbstractAuthorizationCodeService {
15
+ /** Lifetime of authorization codes in seconds. */
16
+ lifetime;
17
+ /** Client service used by subclasses to hydrate authorization codes. */
18
+ clientService;
19
+ /** User service used by subclasses to hydrate authorization codes. */
20
+ userService;
21
+ /** Lifetime defaults to 600 seconds (10 minutes) when not supplied. */
22
+ constructor(options) {
23
+ this.lifetime = options?.lifetime ?? 600;
24
+ this.clientService = options?.clientService;
25
+ this.userService = options?.userService;
26
+ }
27
+ /** Generates an opaque authorization code string via `crypto.randomUUID()`. */
28
+ generateCode(_client, _user, _scope) {
29
+ return Promise.resolve(crypto.randomUUID());
30
+ }
31
+ /** Computes the code expiry as now plus {@linkcode lifetime}. */
32
+ expiresAt(_client, _user, _scope) {
33
+ return Promise.resolve(new Date(Date.now() + this.lifetime * 1000));
34
+ }
35
+ }
@@ -0,0 +1,35 @@
1
+ import type { ClientInterface } from "../../models/client.js";
2
+ /**
3
+ * Service interface for client management.
4
+ */
5
+ export interface ClientServiceInterface<Client extends ClientInterface, User> {
6
+ /** Retrieves a client by ID. */
7
+ get(id: string): Promise<Client | undefined>;
8
+ /**
9
+ * Authenticates a client from the credentials a request presented, resolving
10
+ * the client on success and `undefined` on failure. The authorization server
11
+ * answers `undefined` with `invalid_client`.
12
+ *
13
+ * A client registered with a secret is confidential; one registered without
14
+ * a secret is public:
15
+ *
16
+ * - A **public** client must resolve when `secret` is omitted. The server
17
+ * always advertises `none` in `token_endpoint_auth_methods_supported`, so
18
+ * an implementation that demands a secret from every client makes that
19
+ * advertisement false.
20
+ * - A **confidential** client resolves only with its correct `secret`. With
21
+ * the secret missing or wrong, resolve `undefined`.
22
+ *
23
+ * `runClientServiceContractTests` from `@udibo/oauth2/testing/contract` pins
24
+ * both rules; run it against your implementation. It does not pin whether a
25
+ * public client that presents a `secret` resolves, so do not rely on either
26
+ * answer.
27
+ *
28
+ * @param id The presented `client_id` — unauthenticated input.
29
+ * @param secret The presented `client_secret`, omitted when the request
30
+ * carried none.
31
+ */
32
+ getAuthenticated(id: string, secret?: string): Promise<Client | undefined>;
33
+ /** Retrieves a user associated with a client (for client-credentials grant). */
34
+ getUser(client: Client | string): Promise<User | undefined>;
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Device authorization service interface for the Device Authorization Grant (RFC 8628).
3
+ *
4
+ * @see https://datatracker.ietf.org/doc/html/rfc8628
5
+ * @module
6
+ */
7
+ import type { ClientInterface } from "../../models/client.js";
8
+ import type { DeviceAuthorization } from "../../models/device-authorization.js";
9
+ import type { AbstractScope } from "../../models/scope.js";
10
+ /**
11
+ * Service interface for managing device authorizations.
12
+ *
13
+ * Implementations must handle:
14
+ * - Generating and storing device codes and user codes
15
+ * - Looking up device codes for polling
16
+ * - Looking up user codes for user authorization
17
+ * - Marking codes as authorized or denied
18
+ * - Cleaning up expired codes
19
+ *
20
+ * **Every method taking a `DeviceAuthorization` must accept the records this
21
+ * same service returns**, not only a record the caller assembled. A store that
22
+ * looks a record up by one code commonly returns it carrying only that code —
23
+ * {@linkcode DeviceAuthorizationServiceInterface.getByUserCode} has no device
24
+ * code to fill in — so resolve the stored authorization by whichever code the
25
+ * argument carries rather than by assuming both are populated. The grant hands
26
+ * {@linkcode DeviceAuthorizationServiceInterface.updateLastPolled} and
27
+ * {@linkcode DeviceAuthorizationServiceInterface.revoke} the record from
28
+ * `getByDeviceCode`, and a verification page hands
29
+ * {@linkcode DeviceAuthorizationServiceInterface.approve},
30
+ * {@linkcode DeviceAuthorizationServiceInterface.deny} and `revoke` the record
31
+ * from `getByUserCode`.
32
+ *
33
+ * @see https://datatracker.ietf.org/doc/html/rfc8628
34
+ */
35
+ export interface DeviceAuthorizationServiceInterface<Client extends ClientInterface, User, Scope extends AbstractScope> {
36
+ /**
37
+ * Lifetime of device codes in seconds.
38
+ * RFC 8628 does not specify a default, but 15-30 minutes is common.
39
+ * @default 1800 (30 minutes)
40
+ */
41
+ lifetime: number;
42
+ /**
43
+ * Minimum polling interval in seconds.
44
+ * Clients should not poll more frequently than this.
45
+ * @default 5
46
+ */
47
+ interval: number;
48
+ /**
49
+ * Generates a unique device code.
50
+ *
51
+ * The device code is a long, high-entropy string used by the client
52
+ * to poll for authorization status.
53
+ */
54
+ generateDeviceCode(client: Client, scope?: Scope | null): Promise<string>;
55
+ /**
56
+ * Generates a user code.
57
+ *
58
+ * The user code should be short and easy for users to type.
59
+ * Common formats include: ABCD-1234, WDJB-MJHT, 12345678
60
+ */
61
+ generateUserCode(client: Client, scope?: Scope | null): Promise<string>;
62
+ /**
63
+ * Returns when the codes should expire.
64
+ */
65
+ expiresAt(client: Client, scope?: Scope | null): Promise<Date>;
66
+ /**
67
+ * Saves a device authorization request.
68
+ */
69
+ save(authorization: DeviceAuthorization<Client, User, Scope>): Promise<DeviceAuthorization<Client, User, Scope>>;
70
+ /**
71
+ * Gets a device authorization by device code.
72
+ *
73
+ * Used by the client when polling for authorization status.
74
+ */
75
+ getByDeviceCode(deviceCode: string): Promise<DeviceAuthorization<Client, User, Scope> | undefined>;
76
+ /**
77
+ * Gets a device authorization by user code.
78
+ *
79
+ * Used during user authorization to look up the pending request.
80
+ */
81
+ getByUserCode(userCode: string): Promise<DeviceAuthorization<Client, User, Scope> | undefined>;
82
+ /**
83
+ * Marks a device authorization as authorized by the user.
84
+ *
85
+ * @param authorization The authorization to approve
86
+ * @param user The user who approved the request
87
+ * @param scope Optional scope (may be narrower than originally requested)
88
+ */
89
+ approve(authorization: DeviceAuthorization<Client, User, Scope>, user: User, scope?: Scope | null): Promise<DeviceAuthorization<Client, User, Scope>>;
90
+ /**
91
+ * Marks a device authorization as denied by the user.
92
+ */
93
+ deny(authorization: DeviceAuthorization<Client, User, Scope>): Promise<DeviceAuthorization<Client, User, Scope>>;
94
+ /**
95
+ * Updates the last polled time for a device authorization.
96
+ *
97
+ * Used to track polling frequency and enforce slow_down errors.
98
+ *
99
+ * **A failure here is re-classified by re-reading the device code.** The
100
+ * grant calls this only on the non-terminal polling path, and when it
101
+ * rejects, the grant asks {@linkcode
102
+ * DeviceAuthorizationServiceInterface.getByDeviceCode} whether the record is
103
+ * still stored: gone means a concurrent poll claimed it, and the caller is
104
+ * refused `invalid_grant`; still there means the store itself failed, and
105
+ * the original error propagates. **That read must therefore be at least as
106
+ * fresh as the write that just failed** — serve it from the primary, never a
107
+ * replica that can lag behind it, or a transient write failure on a live
108
+ * authorization will tell a legitimate device its code was already used,
109
+ * which RFC 8628 §3.5 makes terminal.
110
+ */
111
+ updateLastPolled(authorization: DeviceAuthorization<Client, User, Scope>): Promise<DeviceAuthorization<Client, User, Scope>>;
112
+ /**
113
+ * Revokes a device authorization, accepting the record or its raw device code.
114
+ *
115
+ * **Implementations must compare and delete atomically.** The device
116
+ * authorization grant claims an approved authorization by revoking it and
117
+ * issues a token only to the caller whose revoke resolved `true`, so this
118
+ * boolean is the single-use guarantee (RFC 8628 §3.5): resolve `true` only
119
+ * for the call that removed a live authorization, and `false` for every other
120
+ * call — one already revoked, expired, or never stored. An implementation
121
+ * that reads then deletes in two steps, or that returns `true`
122
+ * unconditionally, lets two concurrent polls of one device code both mint
123
+ * tokens. In SQL this is a `DELETE ... WHERE device_code = $1` whose
124
+ * affected-row count is the answer.
125
+ *
126
+ * **The authorization must become unreachable by both of its codes.** A
127
+ * verification page revokes the record it read from
128
+ * {@linkcode DeviceAuthorizationServiceInterface.getByUserCode} to cancel a
129
+ * request; if that leaves the device-code entry behind, the device's next
130
+ * poll still redeems the cancelled — possibly already approved —
131
+ * authorization for a token.
132
+ */
133
+ revoke(authorization: DeviceAuthorization<Client, User, Scope> | string): Promise<boolean>;
134
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Device authorization service interface for the Device Authorization Grant (RFC 8628).
3
+ *
4
+ * @see https://datatracker.ietf.org/doc/html/rfc8628
5
+ * @module
6
+ */
7
+ export {};