@draftlab/auth 0.15.0 → 0.16.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 (272) hide show
  1. package/dist/esm/allow.js +26 -0
  2. package/dist/esm/client.js +254 -0
  3. package/dist/esm/core.js +597 -0
  4. package/dist/esm/css.d.js +0 -0
  5. package/dist/esm/error.js +88 -0
  6. package/dist/esm/index.js +5 -0
  7. package/dist/esm/keys.js +126 -0
  8. package/dist/esm/mutex.js +53 -0
  9. package/dist/esm/pkce.js +87 -0
  10. package/dist/esm/provider/apple.js +15 -0
  11. package/dist/esm/provider/code.js +62 -0
  12. package/dist/esm/provider/discord.js +15 -0
  13. package/dist/esm/provider/facebook.js +15 -0
  14. package/dist/esm/provider/github.js +15 -0
  15. package/dist/esm/provider/gitlab.js +15 -0
  16. package/dist/esm/provider/google.js +16 -0
  17. package/dist/esm/provider/linkedin.js +15 -0
  18. package/dist/esm/provider/magiclink.js +83 -0
  19. package/dist/esm/provider/microsoft.js +15 -0
  20. package/dist/esm/provider/oauth2.js +130 -0
  21. package/dist/esm/provider/password.js +331 -0
  22. package/dist/esm/provider/provider.js +18 -0
  23. package/dist/esm/provider/reddit.js +15 -0
  24. package/dist/esm/provider/slack.js +15 -0
  25. package/dist/esm/provider/spotify.js +15 -0
  26. package/dist/esm/provider/twitch.js +15 -0
  27. package/dist/esm/provider/vercel.js +17 -0
  28. package/dist/esm/random.js +40 -0
  29. package/dist/esm/revocation.js +27 -0
  30. package/dist/esm/storage/memory.js +110 -0
  31. package/dist/esm/storage/storage.js +56 -0
  32. package/dist/esm/storage/turso.js +93 -0
  33. package/dist/esm/storage/unstorage.js +78 -0
  34. package/dist/esm/subject.js +7 -0
  35. package/dist/esm/themes/theme.js +115 -0
  36. package/dist/esm/toolkit/client.js +119 -0
  37. package/dist/esm/toolkit/index.js +25 -0
  38. package/dist/esm/toolkit/providers/facebook.js +11 -0
  39. package/dist/esm/toolkit/providers/github.js +11 -0
  40. package/dist/esm/toolkit/providers/google.js +11 -0
  41. package/dist/esm/toolkit/providers/strategy.js +0 -0
  42. package/dist/esm/toolkit/storage.js +81 -0
  43. package/dist/esm/toolkit/utils.js +18 -0
  44. package/dist/esm/types.js +0 -0
  45. package/dist/esm/ui/base.js +478 -0
  46. package/dist/esm/ui/code.js +186 -0
  47. package/dist/esm/ui/form.js +46 -0
  48. package/dist/esm/ui/icon.js +242 -0
  49. package/dist/esm/ui/magiclink.js +158 -0
  50. package/dist/esm/ui/password.js +435 -0
  51. package/dist/esm/ui/select.js +102 -0
  52. package/dist/esm/util.js +59 -0
  53. package/dist/{allow.d.mts → types/allow.d.ts} +9 -11
  54. package/dist/types/allow.d.ts.map +1 -0
  55. package/dist/types/client.d.ts +462 -0
  56. package/dist/types/client.d.ts.map +1 -0
  57. package/dist/types/core.d.ts +113 -0
  58. package/dist/types/core.d.ts.map +1 -0
  59. package/dist/{error.d.mts → types/error.d.ts} +95 -97
  60. package/dist/types/error.d.ts.map +1 -0
  61. package/dist/types/index.d.ts +2 -0
  62. package/dist/types/index.d.ts.map +1 -0
  63. package/dist/{keys.d.mts → types/keys.d.ts} +20 -24
  64. package/dist/types/keys.d.ts.map +1 -0
  65. package/dist/types/mutex.d.ts +42 -0
  66. package/dist/types/mutex.d.ts.map +1 -0
  67. package/dist/{pkce.d.mts → types/pkce.d.ts} +10 -11
  68. package/dist/types/pkce.d.ts.map +1 -0
  69. package/dist/types/provider/apple.d.ts +197 -0
  70. package/dist/types/provider/apple.d.ts.map +1 -0
  71. package/dist/types/provider/code.d.ts +288 -0
  72. package/dist/types/provider/code.d.ts.map +1 -0
  73. package/dist/types/provider/discord.d.ts +206 -0
  74. package/dist/types/provider/discord.d.ts.map +1 -0
  75. package/dist/types/provider/facebook.d.ts +200 -0
  76. package/dist/types/provider/facebook.d.ts.map +1 -0
  77. package/dist/types/provider/github.d.ts +220 -0
  78. package/dist/types/provider/github.d.ts.map +1 -0
  79. package/dist/types/provider/gitlab.d.ts +180 -0
  80. package/dist/types/provider/gitlab.d.ts.map +1 -0
  81. package/dist/types/provider/google.d.ts +158 -0
  82. package/dist/types/provider/google.d.ts.map +1 -0
  83. package/dist/types/provider/linkedin.d.ts +190 -0
  84. package/dist/types/provider/linkedin.d.ts.map +1 -0
  85. package/dist/types/provider/magiclink.d.ts +141 -0
  86. package/dist/types/provider/magiclink.d.ts.map +1 -0
  87. package/dist/types/provider/microsoft.d.ts +247 -0
  88. package/dist/types/provider/microsoft.d.ts.map +1 -0
  89. package/dist/types/provider/oauth2.d.ts +229 -0
  90. package/dist/types/provider/oauth2.d.ts.map +1 -0
  91. package/dist/types/provider/password.d.ts +408 -0
  92. package/dist/types/provider/password.d.ts.map +1 -0
  93. package/dist/types/provider/provider.d.ts +226 -0
  94. package/dist/types/provider/provider.d.ts.map +1 -0
  95. package/dist/types/provider/reddit.d.ts +159 -0
  96. package/dist/types/provider/reddit.d.ts.map +1 -0
  97. package/dist/types/provider/slack.d.ts +171 -0
  98. package/dist/types/provider/slack.d.ts.map +1 -0
  99. package/dist/types/provider/spotify.d.ts +168 -0
  100. package/dist/types/provider/spotify.d.ts.map +1 -0
  101. package/dist/types/provider/twitch.d.ts +163 -0
  102. package/dist/types/provider/twitch.d.ts.map +1 -0
  103. package/dist/types/provider/vercel.d.ts +294 -0
  104. package/dist/types/provider/vercel.d.ts.map +1 -0
  105. package/dist/{random.d.mts → types/random.d.ts} +4 -6
  106. package/dist/types/random.d.ts.map +1 -0
  107. package/dist/types/revocation.d.ts +76 -0
  108. package/dist/types/revocation.d.ts.map +1 -0
  109. package/dist/{storage/memory.d.mts → types/storage/memory.d.ts} +17 -21
  110. package/dist/types/storage/memory.d.ts.map +1 -0
  111. package/dist/types/storage/storage.d.ts +177 -0
  112. package/dist/types/storage/storage.d.ts.map +1 -0
  113. package/dist/{storage/turso.d.mts → types/storage/turso.d.ts} +4 -8
  114. package/dist/types/storage/turso.d.ts.map +1 -0
  115. package/dist/{storage/unstorage.d.mts → types/storage/unstorage.d.ts} +12 -11
  116. package/dist/types/storage/unstorage.d.ts.map +1 -0
  117. package/dist/types/subject.d.ts +115 -0
  118. package/dist/types/subject.d.ts.map +1 -0
  119. package/dist/types/themes/theme.d.ts +207 -0
  120. package/dist/types/themes/theme.d.ts.map +1 -0
  121. package/dist/types/toolkit/client.d.ts +235 -0
  122. package/dist/types/toolkit/client.d.ts.map +1 -0
  123. package/dist/types/toolkit/index.d.ts +45 -0
  124. package/dist/types/toolkit/index.d.ts.map +1 -0
  125. package/dist/types/toolkit/providers/facebook.d.ts +8 -0
  126. package/dist/types/toolkit/providers/facebook.d.ts.map +1 -0
  127. package/dist/types/toolkit/providers/github.d.ts +8 -0
  128. package/dist/types/toolkit/providers/github.d.ts.map +1 -0
  129. package/dist/types/toolkit/providers/google.d.ts +8 -0
  130. package/dist/types/toolkit/providers/google.d.ts.map +1 -0
  131. package/dist/types/toolkit/providers/strategy.d.ts +38 -0
  132. package/dist/types/toolkit/providers/strategy.d.ts.map +1 -0
  133. package/dist/{toolkit/storage.d.mts → types/toolkit/storage.d.ts} +37 -39
  134. package/dist/types/toolkit/storage.d.ts.map +1 -0
  135. package/dist/{toolkit/utils.d.mts → types/toolkit/utils.d.ts} +2 -4
  136. package/dist/types/toolkit/utils.d.ts.map +1 -0
  137. package/dist/types/types.d.ts +92 -0
  138. package/dist/types/types.d.ts.map +1 -0
  139. package/dist/types/ui/base.d.ts +18 -0
  140. package/dist/types/ui/base.d.ts.map +1 -0
  141. package/dist/types/ui/code.d.ts +43 -0
  142. package/dist/types/ui/code.d.ts.map +1 -0
  143. package/dist/types/ui/form.d.ts +24 -0
  144. package/dist/types/ui/form.d.ts.map +1 -0
  145. package/dist/types/ui/icon.d.ts +60 -0
  146. package/dist/types/ui/icon.d.ts.map +1 -0
  147. package/dist/types/ui/magiclink.d.ts +41 -0
  148. package/dist/types/ui/magiclink.d.ts.map +1 -0
  149. package/dist/types/ui/password.d.ts +43 -0
  150. package/dist/types/ui/password.d.ts.map +1 -0
  151. package/dist/types/ui/select.d.ts +33 -0
  152. package/dist/types/ui/select.d.ts.map +1 -0
  153. package/dist/{util.d.mts → types/util.d.ts} +11 -13
  154. package/dist/types/util.d.ts.map +1 -0
  155. package/package.json +10 -16
  156. package/dist/adapters/node.d.mts +0 -18
  157. package/dist/adapters/node.mjs +0 -69
  158. package/dist/allow.mjs +0 -63
  159. package/dist/client.d.mts +0 -456
  160. package/dist/client.mjs +0 -283
  161. package/dist/core.d.mts +0 -110
  162. package/dist/core.mjs +0 -595
  163. package/dist/error.mjs +0 -237
  164. package/dist/index.d.mts +0 -2
  165. package/dist/index.mjs +0 -3
  166. package/dist/keys.mjs +0 -146
  167. package/dist/mutex.d.mts +0 -44
  168. package/dist/mutex.mjs +0 -110
  169. package/dist/pkce.mjs +0 -157
  170. package/dist/provider/apple.d.mts +0 -111
  171. package/dist/provider/apple.mjs +0 -164
  172. package/dist/provider/code.d.mts +0 -228
  173. package/dist/provider/code.mjs +0 -246
  174. package/dist/provider/discord.d.mts +0 -146
  175. package/dist/provider/discord.mjs +0 -156
  176. package/dist/provider/facebook.d.mts +0 -142
  177. package/dist/provider/facebook.mjs +0 -150
  178. package/dist/provider/github.d.mts +0 -140
  179. package/dist/provider/github.mjs +0 -169
  180. package/dist/provider/gitlab.d.mts +0 -106
  181. package/dist/provider/gitlab.mjs +0 -147
  182. package/dist/provider/google.d.mts +0 -112
  183. package/dist/provider/google.mjs +0 -109
  184. package/dist/provider/linkedin.d.mts +0 -132
  185. package/dist/provider/linkedin.mjs +0 -142
  186. package/dist/provider/magiclink.d.mts +0 -89
  187. package/dist/provider/magiclink.mjs +0 -143
  188. package/dist/provider/microsoft.d.mts +0 -178
  189. package/dist/provider/microsoft.mjs +0 -177
  190. package/dist/provider/oauth2.d.mts +0 -176
  191. package/dist/provider/oauth2.mjs +0 -222
  192. package/dist/provider/passkey.d.mts +0 -104
  193. package/dist/provider/passkey.mjs +0 -320
  194. package/dist/provider/password.d.mts +0 -412
  195. package/dist/provider/password.mjs +0 -363
  196. package/dist/provider/provider.d.mts +0 -227
  197. package/dist/provider/provider.mjs +0 -44
  198. package/dist/provider/reddit.d.mts +0 -107
  199. package/dist/provider/reddit.mjs +0 -127
  200. package/dist/provider/slack.d.mts +0 -114
  201. package/dist/provider/slack.mjs +0 -138
  202. package/dist/provider/spotify.d.mts +0 -113
  203. package/dist/provider/spotify.mjs +0 -135
  204. package/dist/provider/totp.d.mts +0 -112
  205. package/dist/provider/totp.mjs +0 -191
  206. package/dist/provider/twitch.d.mts +0 -108
  207. package/dist/provider/twitch.mjs +0 -131
  208. package/dist/provider/vercel.d.mts +0 -177
  209. package/dist/provider/vercel.mjs +0 -230
  210. package/dist/random.mjs +0 -86
  211. package/dist/revocation.d.mts +0 -55
  212. package/dist/revocation.mjs +0 -63
  213. package/dist/router/context.d.mts +0 -21
  214. package/dist/router/context.mjs +0 -193
  215. package/dist/router/cookies.d.mts +0 -8
  216. package/dist/router/cookies.mjs +0 -13
  217. package/dist/router/index.d.mts +0 -21
  218. package/dist/router/index.mjs +0 -107
  219. package/dist/router/matcher.d.mts +0 -15
  220. package/dist/router/matcher.mjs +0 -76
  221. package/dist/router/middleware/cors.d.mts +0 -15
  222. package/dist/router/middleware/cors.mjs +0 -114
  223. package/dist/router/safe-request.d.mts +0 -52
  224. package/dist/router/safe-request.mjs +0 -160
  225. package/dist/router/types.d.mts +0 -67
  226. package/dist/router/types.mjs +0 -1
  227. package/dist/router/variables.d.mts +0 -12
  228. package/dist/router/variables.mjs +0 -20
  229. package/dist/storage/memory.mjs +0 -125
  230. package/dist/storage/storage.d.mts +0 -179
  231. package/dist/storage/storage.mjs +0 -104
  232. package/dist/storage/turso.mjs +0 -117
  233. package/dist/storage/unstorage.mjs +0 -103
  234. package/dist/subject.d.mts +0 -62
  235. package/dist/subject.mjs +0 -36
  236. package/dist/themes/theme.d.mts +0 -209
  237. package/dist/themes/theme.mjs +0 -120
  238. package/dist/toolkit/client.d.mts +0 -169
  239. package/dist/toolkit/client.mjs +0 -209
  240. package/dist/toolkit/index.d.mts +0 -9
  241. package/dist/toolkit/index.mjs +0 -9
  242. package/dist/toolkit/providers/facebook.d.mts +0 -12
  243. package/dist/toolkit/providers/facebook.mjs +0 -16
  244. package/dist/toolkit/providers/github.d.mts +0 -12
  245. package/dist/toolkit/providers/github.mjs +0 -16
  246. package/dist/toolkit/providers/google.d.mts +0 -12
  247. package/dist/toolkit/providers/google.mjs +0 -20
  248. package/dist/toolkit/providers/strategy.d.mts +0 -40
  249. package/dist/toolkit/providers/strategy.mjs +0 -1
  250. package/dist/toolkit/storage.mjs +0 -157
  251. package/dist/toolkit/utils.mjs +0 -30
  252. package/dist/types.d.mts +0 -94
  253. package/dist/types.mjs +0 -1
  254. package/dist/ui/base.d.mts +0 -30
  255. package/dist/ui/base.mjs +0 -407
  256. package/dist/ui/code.d.mts +0 -43
  257. package/dist/ui/code.mjs +0 -173
  258. package/dist/ui/form.d.mts +0 -32
  259. package/dist/ui/form.mjs +0 -49
  260. package/dist/ui/icon.d.mts +0 -58
  261. package/dist/ui/icon.mjs +0 -247
  262. package/dist/ui/magiclink.d.mts +0 -41
  263. package/dist/ui/magiclink.mjs +0 -152
  264. package/dist/ui/passkey.d.mts +0 -27
  265. package/dist/ui/passkey.mjs +0 -323
  266. package/dist/ui/password.d.mts +0 -42
  267. package/dist/ui/password.mjs +0 -402
  268. package/dist/ui/select.d.mts +0 -34
  269. package/dist/ui/select.mjs +0 -98
  270. package/dist/ui/totp.d.mts +0 -34
  271. package/dist/ui/totp.mjs +0 -270
  272. package/dist/util.mjs +0 -128
@@ -1,222 +0,0 @@
1
- import { getRelativeUrl } from "../util.mjs";
2
- import { OauthError } from "../error.mjs";
3
- import { generatePKCE } from "../pkce.mjs";
4
- import { generateSecureToken, timingSafeCompare } from "../random.mjs";
5
- import { createRemoteJWKSet, jwtVerify } from "jose";
6
-
7
- //#region src/provider/oauth2.ts
8
- /**
9
- * OAuth 2.0 authentication provider for Draft Auth.
10
- * Implements the Authorization Code Grant flow with optional PKCE support.
11
- *
12
- * ## Quick Setup
13
- *
14
- * ```ts
15
- * import { Oauth2Provider } from "@draftlab/auth/provider/oauth2"
16
- *
17
- * export default issuer({
18
- * providers: {
19
- * github: Oauth2Provider({
20
- * clientID: process.env.GITHUB_CLIENT_ID,
21
- * clientSecret: process.env.GITHUB_CLIENT_SECRET,
22
- * endpoint: {
23
- * authorization: "https://github.com/login/oauth/authorize",
24
- * token: "https://github.com/login/oauth/access_token"
25
- * },
26
- * scopes: ["user:email", "read:user"]
27
- * }),
28
- * discord: Oauth2Provider({
29
- * clientID: process.env.DISCORD_CLIENT_ID,
30
- * clientSecret: process.env.DISCORD_CLIENT_SECRET,
31
- * endpoint: {
32
- * authorization: "https://discord.com/api/oauth2/authorize",
33
- * token: "https://discord.com/api/oauth2/token"
34
- * },
35
- * scopes: ["identify", "email"],
36
- * pkce: true // Required by some providers
37
- * })
38
- * }
39
- * })
40
- * ```
41
- *
42
- * ## Features
43
- *
44
- * - **Authorization Code Grant**: Secure server-side OAuth 2.0 flow
45
- * - **PKCE Support**: Optional Proof Key for Code Exchange for enhanced security
46
- * - **Flexible Endpoints**: Configure custom authorization and token endpoints
47
- * - **Custom Parameters**: Support for provider-specific authorization parameters
48
- *
49
- * ## User Data
50
- *
51
- * The provider returns access tokens:
52
- *
53
- * ```ts
54
- * success: async (ctx, value) => {
55
- * if (value.provider === "oauth2") {
56
- * // Access token for API calls: value.tokenset.access
57
- * // Refresh token (if provided): value.tokenset.refresh
58
- * // Client ID used: value.clientID
59
- * }
60
- * }
61
- * ```
62
- *
63
- * @packageDocumentation
64
- */
65
- /**
66
- * Creates an OAuth 2.0 authentication provider.
67
- * Implements the Authorization Code Grant flow with optional PKCE support.
68
- *
69
- * @param config - OAuth 2.0 provider configuration
70
- * @returns Provider instance implementing OAuth 2.0 authentication
71
- *
72
- * @example
73
- * ```ts
74
- * // GitHub provider with basic configuration
75
- * const githubProvider = Oauth2Provider({
76
- * clientID: process.env.GITHUB_CLIENT_ID,
77
- * clientSecret: process.env.GITHUB_CLIENT_SECRET,
78
- * endpoint: {
79
- * authorization: "https://github.com/login/oauth/authorize",
80
- * token: "https://github.com/login/oauth/access_token"
81
- * },
82
- * scopes: ["user:email", "read:user"]
83
- * })
84
- *
85
- * // Provider with PKCE and custom parameters
86
- * const customProvider = Oauth2Provider({
87
- * clientID: "my-client-id",
88
- * clientSecret: "my-client-secret",
89
- * endpoint: {
90
- * authorization: "https://provider.com/oauth/authorize",
91
- * token: "https://provider.com/oauth/token",
92
- * jwks: "https://provider.com/.well-known/jwks.json"
93
- * },
94
- * scopes: ["read", "write"],
95
- * pkce: true,
96
- * query: {
97
- * prompt: "consent",
98
- * access_type: "offline"
99
- * }
100
- * })
101
- * ```
102
- */
103
- const Oauth2Provider = (config) => {
104
- const authQuery = config.query || {};
105
- /**
106
- * Handles the OAuth 2.0 callback logic for both GET and POST requests.
107
- * Exchanges the authorization code for tokens and processes the response.
108
- */
109
- const handleCallbackLogic = async (c, ctx, provider, code) => {
110
- if (!(provider && code)) return c.redirect(getRelativeUrl(c, "./authorize"));
111
- const tokenRequestBody = new URLSearchParams({
112
- client_id: config.clientID,
113
- client_secret: config.clientSecret,
114
- code,
115
- grant_type: "authorization_code",
116
- redirect_uri: provider.redirect,
117
- ...provider.codeVerifier ? { code_verifier: provider.codeVerifier } : {}
118
- });
119
- try {
120
- const response = await fetch(config.endpoint.token, {
121
- method: "POST",
122
- headers: {
123
- "Content-Type": "application/x-www-form-urlencoded",
124
- Accept: "application/json"
125
- },
126
- body: tokenRequestBody.toString()
127
- });
128
- if (!response.ok) throw new Error(`Token request failed with status ${response.status}`);
129
- const tokenData = await response.json();
130
- if (tokenData.error) throw new OauthError(tokenData.error, tokenData.error_description || "");
131
- if (tokenData.id_token && config.endpoint.jwks) try {
132
- const jwks = createRemoteJWKSet(new URL(config.endpoint.jwks));
133
- await jwtVerify(tokenData.id_token, jwks, {
134
- issuer: config.endpoint.authorization.split("/").slice(0, 3).join("/"),
135
- clockTolerance: 60
136
- });
137
- } catch (error) {
138
- throw new OauthError("invalid_request", `ID token validation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
139
- }
140
- return await ctx.success(c, {
141
- clientID: config.clientID,
142
- tokenset: {
143
- get access() {
144
- return tokenData.access_token;
145
- },
146
- get refresh() {
147
- return tokenData.refresh_token || "";
148
- },
149
- get expiry() {
150
- return tokenData.expires_in || 0;
151
- },
152
- get raw() {
153
- return tokenData;
154
- }
155
- }
156
- });
157
- } catch (error) {
158
- if (error instanceof OauthError) throw error;
159
- throw new OauthError("server_error", `Token exchange failed: ${error instanceof Error ? error.message : "Unknown error"}`);
160
- }
161
- };
162
- return {
163
- type: config.type || "oauth2",
164
- init(routes, ctx) {
165
- /**
166
- * Initiates OAuth 2.0 authorization flow.
167
- * Redirects user to the provider's authorization endpoint with proper parameters.
168
- */
169
- routes.get("/authorize", async (c) => {
170
- const state = generateSecureToken();
171
- const pkce = config.pkce ? await generatePKCE() : void 0;
172
- await ctx.set(c, "provider", 600, {
173
- state,
174
- redirect: getRelativeUrl(c, "./callback"),
175
- codeVerifier: pkce?.verifier
176
- });
177
- const authorizationUrl = new URL(config.endpoint.authorization);
178
- authorizationUrl.searchParams.set("client_id", config.clientID);
179
- authorizationUrl.searchParams.set("redirect_uri", getRelativeUrl(c, "./callback"));
180
- authorizationUrl.searchParams.set("response_type", "code");
181
- authorizationUrl.searchParams.set("state", state);
182
- authorizationUrl.searchParams.set("scope", config.scopes.join(" "));
183
- if (pkce) {
184
- authorizationUrl.searchParams.set("code_challenge", pkce.challenge);
185
- authorizationUrl.searchParams.set("code_challenge_method", pkce.method);
186
- }
187
- for (const [key, value] of Object.entries(authQuery)) authorizationUrl.searchParams.set(key, value);
188
- return c.redirect(authorizationUrl.toString());
189
- });
190
- /**
191
- * Handles OAuth 2.0 callback via query parameters (GET request).
192
- * Standard OAuth 2.0 callback method for most providers.
193
- */
194
- routes.get("/callback", async (c) => {
195
- const provider = await ctx.get(c, "provider");
196
- const code = c.query("code");
197
- const state = c.query("state");
198
- const error = c.query("error");
199
- if (error) throw new OauthError(error, c.query("error_description") || "");
200
- if (!(provider && code) || provider.state && !timingSafeCompare(state || "", provider.state)) return c.redirect(getRelativeUrl(c, "./authorize"));
201
- return await handleCallbackLogic(c, ctx, provider, code);
202
- });
203
- /**
204
- * Handles OAuth 2.0 callback via form data (POST request).
205
- * Alternative callback method supported by some providers.
206
- */
207
- routes.post("/callback", async (c) => {
208
- const provider = await ctx.get(c, "provider");
209
- const formData = await c.formData();
210
- const code = formData.get("code")?.toString();
211
- const state = formData.get("state")?.toString();
212
- const error = formData.get("error")?.toString();
213
- if (error) throw new OauthError(error, formData.get("error_description")?.toString() || "");
214
- if (!(provider && code) || provider.state && !timingSafeCompare(state || "", provider.state)) return c.redirect(getRelativeUrl(c, "./authorize"));
215
- return await handleCallbackLogic(c, ctx, provider, code);
216
- });
217
- }
218
- };
219
- };
220
-
221
- //#endregion
222
- export { Oauth2Provider };
@@ -1,104 +0,0 @@
1
- import { Provider } from "./provider.mjs";
2
- import { AuthenticatorSelectionCriteria, AuthenticatorTransportFuture, Base64URLString, CredentialDeviceType } from "@simplewebauthn/server";
3
-
4
- //#region src/provider/passkey.d.ts
5
-
6
- /**
7
- * User model for passkey authentication.
8
- * Contains the core user data needed for WebAuthn operations.
9
- */
10
- interface UserModel {
11
- id: string;
12
- username: string;
13
- }
14
- /**
15
- * Original PasskeyModel structure for in-memory use.
16
- * Represents a registered credential with public key as Uint8Array.
17
- */
18
- interface PasskeyModel {
19
- id: string;
20
- publicKey: Uint8Array<ArrayBuffer>;
21
- userId: string;
22
- webauthnUserID: string;
23
- counter: number;
24
- deviceType: CredentialDeviceType;
25
- backedUp: boolean;
26
- transports?: AuthenticatorTransportFuture[];
27
- }
28
- /**
29
- * PasskeyModel version for KV storage with publicKey as string.
30
- * Used for storing credentials in a key-value store.
31
- */
32
- interface PasskeyModelStored extends Omit<PasskeyModel, "publicKey"> {
33
- publicKey: string;
34
- }
35
- declare const DEFAULT_COPY: {
36
- error_user_not_allowed: string;
37
- };
38
- /**
39
- * Configuration for the PasskeyProvider.
40
- * Defines how the passkey authentication flow should behave.
41
- */
42
- interface PasskeyProviderConfig {
43
- /**
44
- * Custom authorization handler that generates the UI for authorization.
45
- */
46
- authorize: (req: Request) => Promise<Response>;
47
- /**
48
- * Custom registration handler that generates the UI for registration.
49
- */
50
- register: (req: Request) => Promise<Response>;
51
- /**
52
- * The human-readable name of the relying party (your application).
53
- */
54
- rpName: string;
55
- /**
56
- * The ID of the relying party, typically the domain name without protocol.
57
- */
58
- rpID?: string;
59
- /**
60
- * The origin URL(s) that are allowed to initiate WebAuthn ceremonies.
61
- */
62
- origin?: string | string[];
63
- /**
64
- * Optional function to check if a user is allowed to register a passkey.
65
- */
66
- userCanRegisterPasskey?: (userId: string, req: Request) => Promise<boolean>;
67
- /**
68
- * Optional WebAuthn authenticator selection criteria.
69
- */
70
- authenticatorSelection?: AuthenticatorSelectionCriteria;
71
- /**
72
- * Optional attestation type.
73
- */
74
- attestationType?: "none" | "direct" | "enterprise";
75
- /**
76
- * Optional timeout for challenges in milliseconds.
77
- */
78
- timeout?: number;
79
- /**
80
- * Custom copy texts for error messages and UI elements.
81
- */
82
- copy?: Partial<typeof DEFAULT_COPY>;
83
- }
84
- /**
85
- * Creates a passkey (WebAuthn) authentication provider.
86
- *
87
- * This provider enables passwordless authentication using biometrics, hardware security
88
- * keys, or platform authenticators. It implements the Web Authentication (WebAuthn) standard.
89
- *
90
- * It handles:
91
- * - Passkey registration (creating new credentials)
92
- * - Authentication with existing passkeys
93
- * - Secure storage of credentials
94
- * - Challenge verification
95
- *
96
- * @param config Configuration options for the passkey provider
97
- * @returns A Provider instance configured for passkey authentication
98
- */
99
- declare const PasskeyProvider: (config: PasskeyProviderConfig) => Provider<{
100
- userId: string;
101
- credentialId?: Base64URLString;
102
- }>;
103
- //#endregion
104
- export { PasskeyModel, PasskeyModelStored, PasskeyProvider, PasskeyProviderConfig, UserModel };
@@ -1,320 +0,0 @@
1
- import { Storage } from "../storage/storage.mjs";
2
- import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
3
-
4
- //#region src/provider/passkey.ts
5
- /**
6
- * Converts a Uint8Array to a Base64URL encoded string.
7
- * This is used to convert binary data for storage in databases or JSON.
8
- *
9
- * @param bytes - The Uint8Array to convert
10
- * @returns Base64URL encoded string
11
- */
12
- const uint8ArrayToBase64Url = (bytes) => {
13
- let str = "";
14
- for (const charCode of bytes) str += String.fromCharCode(charCode);
15
- return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
16
- };
17
- /**
18
- * Converts a Base64URL encoded string back to a Uint8Array.
19
- * This is used to convert stored data back to binary format for WebAuthn operations.
20
- *
21
- * @param base64urlString - The Base64URL encoded string to convert
22
- * @returns Uint8Array containing the decoded data
23
- */
24
- const base64UrlToUint8Array = (base64urlString) => {
25
- const base64 = base64urlString.replace(/-/g, "+").replace(/_/g, "/");
26
- /**
27
- * Pad with '=' until it's a multiple of four
28
- * (4 - (85 % 4 = 1) = 3) % 4 = 3 padding
29
- * (4 - (86 % 4 = 2) = 2) % 4 = 2 padding
30
- * (4 - (87 % 4 = 3) = 1) % 4 = 1 padding
31
- * (4 - (88 % 4 = 0) = 4) % 4 = 0 padding
32
- */
33
- const padLength = (4 - base64.length % 4) % 4;
34
- const padded = base64.padEnd(base64.length + padLength, "=");
35
- const binary = atob(padded);
36
- const buffer = new ArrayBuffer(binary.length);
37
- const bytes = new Uint8Array(buffer);
38
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
39
- return bytes;
40
- };
41
- const userKey = (userId) => [
42
- "passkey",
43
- "user",
44
- userId
45
- ];
46
- const passkeyKey = (userId, credentialId) => [
47
- "passkey",
48
- "user",
49
- userId,
50
- "credential",
51
- credentialId,
52
- "passkey"
53
- ];
54
- const optionsKey = (userId) => [
55
- "passkey",
56
- "user",
57
- userId,
58
- "options"
59
- ];
60
- const userPasskeysIndexKey = (userId) => [
61
- "passkey",
62
- "user",
63
- userId,
64
- "passkeys"
65
- ];
66
- const DEFAULT_COPY = { error_user_not_allowed: "There is already an account with this email. Login to add a passkey." };
67
- /**
68
- * Creates a passkey (WebAuthn) authentication provider.
69
- *
70
- * This provider enables passwordless authentication using biometrics, hardware security
71
- * keys, or platform authenticators. It implements the Web Authentication (WebAuthn) standard.
72
- *
73
- * It handles:
74
- * - Passkey registration (creating new credentials)
75
- * - Authentication with existing passkeys
76
- * - Secure storage of credentials
77
- * - Challenge verification
78
- *
79
- * @param config Configuration options for the passkey provider
80
- * @returns A Provider instance configured for passkey authentication
81
- */
82
- const PasskeyProvider = (config) => {
83
- const copy = {
84
- ...DEFAULT_COPY,
85
- ...config.copy
86
- };
87
- return {
88
- type: "passkey",
89
- init(routes, ctx) {
90
- const { rpName, authenticatorSelection, attestationType = "none", timeout = 300 * 1e3 } = config;
91
- const getStoredUserById = async (userId) => {
92
- return await Storage.get(ctx.storage, userKey(userId));
93
- };
94
- const saveUser = async (user) => {
95
- await Storage.set(ctx.storage, userKey(user.id), user);
96
- };
97
- const getStoredPasskeyById = async (userId, credentialID) => {
98
- const storedPasskey = await Storage.get(ctx.storage, passkeyKey(userId, credentialID));
99
- if (!storedPasskey) return null;
100
- return {
101
- ...storedPasskey,
102
- publicKey: base64UrlToUint8Array(storedPasskey.publicKey)
103
- };
104
- };
105
- const getStoredUserPasskeys = async (userId) => {
106
- const passkeyIds = await Storage.get(ctx.storage, userPasskeysIndexKey(userId)) || [];
107
- const passkeys = [];
108
- for (const id of passkeyIds) {
109
- const pk = await getStoredPasskeyById(userId, id);
110
- if (pk) passkeys.push(pk);
111
- }
112
- return passkeys;
113
- };
114
- const saveNewStoredPasskey = async (passkeyData) => {
115
- const storablePasskey = {
116
- ...passkeyData,
117
- publicKey: uint8ArrayToBase64Url(passkeyData.publicKey)
118
- };
119
- await Storage.set(ctx.storage, passkeyKey(passkeyData.userId, passkeyData.id), storablePasskey);
120
- const passkeyIds = await Storage.get(ctx.storage, userPasskeysIndexKey(passkeyData.userId)) || [];
121
- if (!passkeyIds.includes(passkeyData.id)) {
122
- passkeyIds.push(passkeyData.id);
123
- await Storage.set(ctx.storage, userPasskeysIndexKey(passkeyData.userId), passkeyIds);
124
- }
125
- };
126
- const updateStoredPasskeyCounter = async (userId, credentialID, newCounter) => {
127
- const passkey = await getStoredPasskeyById(userId, credentialID);
128
- if (passkey) {
129
- passkey.counter = newCounter;
130
- const storablePasskey = {
131
- ...passkey,
132
- publicKey: uint8ArrayToBase64Url(passkey.publicKey)
133
- };
134
- await Storage.set(ctx.storage, passkeyKey(userId, credentialID), storablePasskey);
135
- }
136
- };
137
- routes.get("/authorize", async (c) => {
138
- return ctx.forward(c, await config.authorize(c.request));
139
- });
140
- routes.get("/register", async (c) => {
141
- return ctx.forward(c, await config.register(c.request));
142
- });
143
- routes.get("/register-request", async (c) => {
144
- const userId = c.query("userId");
145
- const rpID = config.rpID || c.query("rpID");
146
- const otherDevice = c.query("otherDevice") === "true";
147
- if (!userId) return c.json({ error: "User ID for registration is required." }, { status: 400 });
148
- if (!rpID) return c.json({ error: "RP ID for registration is required." }, { status: 400 });
149
- const username = c.query("username") || userId;
150
- let user = await getStoredUserById(userId);
151
- if (config.userCanRegisterPasskey) {
152
- if (!await config.userCanRegisterPasskey(userId, c.request)) return c.json({ error: copy.error_user_not_allowed }, { status: 403 });
153
- }
154
- if (!user) {
155
- user = {
156
- id: userId,
157
- username
158
- };
159
- await saveUser(user);
160
- }
161
- const userPasskeys = await getStoredUserPasskeys(user.id);
162
- const regOptions = await generateRegistrationOptions({
163
- rpName,
164
- rpID,
165
- userName: user.username,
166
- attestationType,
167
- excludeCredentials: userPasskeys.map((pk) => ({
168
- id: pk.id,
169
- transports: pk.transports
170
- })),
171
- authenticatorSelection: authenticatorSelection ?? {
172
- residentKey: "preferred",
173
- userVerification: "preferred",
174
- authenticatorAttachment: otherDevice ? void 0 : "platform"
175
- },
176
- timeout
177
- });
178
- await Storage.set(ctx.storage, optionsKey(user.id), regOptions);
179
- return c.json(regOptions);
180
- });
181
- routes.post("/register-verify", async (c) => {
182
- const body = await c.parseJson();
183
- const userId = c.query("userId");
184
- const rpID = config.rpID || c.query("rpID");
185
- const origin = config.origin || c.query("origin");
186
- if (!userId) return c.json({
187
- verified: false,
188
- error: "User ID for verification is required."
189
- }, { status: 400 });
190
- if (!rpID) return c.json({ error: "RP ID for verification is required." }, { status: 400 });
191
- if (!origin) return c.json({ error: "Origin for verification is required." }, { status: 400 });
192
- const user = await getStoredUserById(userId);
193
- if (!user) return c.json({
194
- verified: false,
195
- error: "User not found during verification."
196
- }, { status: 404 });
197
- const regOptions = await Storage.get(ctx.storage, optionsKey(user.id));
198
- if (!regOptions) return c.json({
199
- verified: false,
200
- error: "Registration options not found."
201
- }, { status: 400 });
202
- const challenge = regOptions.challenge;
203
- let verification;
204
- try {
205
- verification = await verifyRegistrationResponse({
206
- response: body,
207
- expectedChallenge: challenge,
208
- expectedOrigin: origin,
209
- expectedRPID: rpID,
210
- requireUserVerification: authenticatorSelection?.userVerification !== "discouraged"
211
- });
212
- } catch (error) {
213
- console.error("Passkey Registration Verification Error:", error);
214
- const message = error instanceof Error ? error.message : "Unknown error";
215
- return c.json({
216
- verified: false,
217
- error: message
218
- }, { status: 400 });
219
- }
220
- const { verified, registrationInfo } = verification;
221
- if (verified && registrationInfo) {
222
- const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo;
223
- if (credential) {
224
- const newPasskey = {
225
- id: credential.id,
226
- userId: user.id,
227
- webauthnUserID: regOptions.user.id,
228
- publicKey: credential.publicKey,
229
- counter: credential.counter,
230
- transports: credential.transports,
231
- deviceType: credentialDeviceType,
232
- backedUp: credentialBackedUp
233
- };
234
- await saveNewStoredPasskey(newPasskey);
235
- return ctx.success(c, {
236
- userId: user.id,
237
- credentialId: newPasskey.id,
238
- verified: true
239
- });
240
- }
241
- }
242
- return c.json({
243
- verified: false,
244
- error: "Registration verification failed."
245
- }, { status: 400 });
246
- });
247
- routes.get("/authenticate-options", async (c) => {
248
- const userId = c.query("userId");
249
- if (!userId) return c.json({ error: "User ID for authentication is required." }, { status: 400 });
250
- const rpID = config.rpID || c.query("rpID");
251
- if (!rpID) return c.json({ error: "RP ID for authentication is required." }, { status: 400 });
252
- const userForAuth = await getStoredUserById(userId);
253
- if (!userForAuth) return c.json({ error: "User not found for authentication." }, { status: 404 });
254
- const authOptions = await generateAuthenticationOptions({
255
- rpID,
256
- allowCredentials: (await getStoredUserPasskeys(userForAuth.id)).map((pk) => ({
257
- id: pk.id,
258
- transports: pk.transports
259
- })),
260
- userVerification: authenticatorSelection?.userVerification ?? "preferred",
261
- timeout
262
- });
263
- await Storage.set(ctx.storage, optionsKey(userForAuth.id), authOptions);
264
- return c.json(authOptions);
265
- });
266
- routes.post("/authenticate-verify", async (c) => {
267
- const body = await c.parseJson();
268
- const userId = c.query("userId");
269
- if (!userId) return c.json({ error: "User ID for authentication is required." }, { status: 400 });
270
- const rpID = config.rpID || c.query("rpID");
271
- if (!rpID) return c.json({ error: "RP ID for authentication is required." }, { status: 400 });
272
- const origin = config.origin || c.query("origin");
273
- if (!origin) return c.json({ error: "Origin for authentication is required." }, { status: 400 });
274
- const user = await getStoredUserById(userId);
275
- if (!user) return c.json({
276
- verified: false,
277
- error: `User ${userId} not found.`
278
- }, { status: 404 });
279
- const authOptions = await Storage.get(ctx.storage, optionsKey(user.id));
280
- if (!authOptions) return c.json({ error: "Authentication options not found." }, { status: 400 });
281
- const passkey = await getStoredPasskeyById(userId, body.id);
282
- if (!passkey) return c.json({
283
- verified: false,
284
- error: `Passkey ${body.id} not found for user ${user.username}.`
285
- }, { status: 400 });
286
- const { publicKey, counter, transports } = passkey;
287
- if (!publicKey || typeof counter !== "number" || !transports) return c.json({ error: "Passkey not found for authentication." }, { status: 400 });
288
- const challenge = authOptions.challenge;
289
- if (!challenge) return c.json({ error: "Authentication challenge not found." }, { status: 400 });
290
- const { verified, authenticationInfo } = await verifyAuthenticationResponse({
291
- response: body,
292
- expectedChallenge: challenge,
293
- expectedOrigin: origin || "",
294
- expectedRPID: rpID,
295
- credential: {
296
- id: passkey.id,
297
- publicKey,
298
- counter,
299
- transports
300
- }
301
- });
302
- if (verified) {
303
- await updateStoredPasskeyCounter(user.id, passkey.id, authenticationInfo.newCounter);
304
- return ctx.success(c, {
305
- userId: user.id,
306
- credentialId: passkey.id,
307
- verified: true
308
- });
309
- }
310
- return c.json({
311
- verified: false,
312
- error: "Authentication verification failed."
313
- }, { status: 400 });
314
- });
315
- }
316
- };
317
- };
318
-
319
- //#endregion
320
- export { PasskeyProvider };