@draftlab/auth 0.0.3 → 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.
- package/dist/allow.d.ts +58 -1
- package/dist/allow.js +61 -2
- package/dist/client.d.ts +2 -3
- package/dist/client.js +2 -2
- package/dist/core.d.ts +128 -8
- package/dist/core.js +496 -12
- package/dist/error.d.ts +242 -1
- package/dist/error.js +235 -1
- package/dist/index.d.ts +1 -8
- package/dist/index.js +1 -12
- package/dist/keys.d.ts +1 -1
- package/dist/keys.js +138 -3
- package/dist/pkce.js +160 -1
- package/dist/provider/code.d.ts +227 -3
- package/dist/provider/code.js +27 -14
- package/dist/provider/facebook.d.ts +2 -3
- package/dist/provider/facebook.js +1 -5
- package/dist/provider/github.d.ts +2 -3
- package/dist/provider/github.js +1 -5
- package/dist/provider/google.d.ts +2 -3
- package/dist/provider/google.js +1 -5
- package/dist/provider/oauth2.d.ts +175 -3
- package/dist/provider/oauth2.js +153 -5
- package/dist/provider/password.d.ts +384 -3
- package/dist/provider/password.js +4 -4
- package/dist/provider/provider.d.ts +226 -2
- package/dist/random.js +85 -1
- package/dist/storage/memory.d.ts +2 -2
- package/dist/storage/memory.js +1 -1
- package/dist/storage/storage.d.ts +161 -1
- package/dist/storage/storage.js +60 -1
- package/dist/storage/turso.d.ts +1 -1
- package/dist/storage/turso.js +1 -1
- package/dist/storage/unstorage.d.ts +2 -2
- package/dist/storage/unstorage.js +2 -2
- package/dist/subject.d.ts +61 -2
- package/dist/themes/theme.d.ts +208 -1
- package/dist/themes/theme.js +118 -1
- package/dist/ui/base.d.ts +22 -35
- package/dist/ui/base.js +388 -3
- package/dist/ui/code.d.ts +22 -137
- package/dist/ui/code.js +199 -161
- package/dist/ui/form.d.ts +8 -6
- package/dist/ui/form.js +57 -1
- package/dist/ui/icon.d.ts +7 -84
- package/dist/ui/icon.js +69 -2
- package/dist/ui/password.d.ts +30 -37
- package/dist/ui/password.js +340 -237
- package/dist/ui/select.d.ts +19 -218
- package/dist/ui/select.js +91 -4
- package/dist/util.d.ts +71 -1
- package/dist/util.js +106 -1
- package/package.json +5 -3
- package/dist/allow-CixonwTW.d.ts +0 -59
- package/dist/allow-DX5cehSc.js +0 -63
- package/dist/base-DRutbxgL.js +0 -422
- package/dist/code-DJxdFR7p.d.ts +0 -212
- package/dist/core-BZHEAefX.d.ts +0 -129
- package/dist/core-CDM5o4rs.js +0 -498
- package/dist/error-CWAdNAzm.d.ts +0 -243
- package/dist/error-DgAKK7b2.js +0 -237
- package/dist/form-6XKM_cOk.js +0 -61
- package/dist/icon-Ci5uqGB_.js +0 -192
- package/dist/keys-EEfxEGfO.js +0 -140
- package/dist/oauth2-B7-6Z7Lc.js +0 -155
- package/dist/oauth2-CXHukHf2.d.ts +0 -176
- package/dist/password-C4KLmO0O.d.ts +0 -385
- package/dist/pkce-276Za_rZ.js +0 -162
- package/dist/provider-tndlqCzp.d.ts +0 -227
- package/dist/random-SXMYlaVr.js +0 -87
- package/dist/select-BjySLL8I.js +0 -280
- package/dist/storage-BEaqEPNQ.js +0 -62
- package/dist/storage-CxKerLlc.d.ts +0 -162
- package/dist/subject-DMIMVtaT.d.ts +0 -62
- package/dist/theme-C9by7VXf.d.ts +0 -209
- package/dist/theme-CswaLtbW.js +0 -120
- package/dist/util-CSdHUFOo.js +0 -108
- package/dist/util-DbSKG1Xm.d.ts +0 -72
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
import { Provider } from "./provider-tndlqCzp.js";
|
|
2
|
-
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
|
-
|
|
4
|
-
//#region src/provider/password.d.ts
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Password-based authentication provider for Draft Auth.
|
|
8
|
-
* Supports user registration, login, and password changes with email verification.
|
|
9
|
-
*
|
|
10
|
-
* ## Quick Setup
|
|
11
|
-
*
|
|
12
|
-
* ```ts
|
|
13
|
-
* import { PasswordUI } from "@draftauth/core/ui/password"
|
|
14
|
-
* import { PasswordProvider } from "@draftauth/core/provider/password"
|
|
15
|
-
*
|
|
16
|
-
* export default issuer({
|
|
17
|
-
* providers: {
|
|
18
|
-
* password: PasswordProvider(
|
|
19
|
-
* PasswordUI({
|
|
20
|
-
* copy: {
|
|
21
|
-
* error_email_taken: "This email is already taken."
|
|
22
|
-
* },
|
|
23
|
-
* sendCode: async (email, code) => {
|
|
24
|
-
* await sendEmail(email, `Your verification code: ${code}`)
|
|
25
|
-
* }
|
|
26
|
-
* })
|
|
27
|
-
* )
|
|
28
|
-
* }
|
|
29
|
-
* })
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* ## Custom UI Implementation
|
|
33
|
-
*
|
|
34
|
-
* For full control over the user interface, implement the handlers directly:
|
|
35
|
-
*
|
|
36
|
-
* ```ts
|
|
37
|
-
* PasswordProvider({
|
|
38
|
-
* login: async (req, form, error) => {
|
|
39
|
-
* return new Response(renderLoginPage(form, error))
|
|
40
|
-
* },
|
|
41
|
-
* register: async (req, state, form, error) => {
|
|
42
|
-
* return new Response(renderRegisterPage(state, form, error))
|
|
43
|
-
* },
|
|
44
|
-
* change: async (req, state, form, error) => {
|
|
45
|
-
* return new Response(renderChangePage(state, form, error))
|
|
46
|
-
* },
|
|
47
|
-
* sendCode: async (email, code) => {
|
|
48
|
-
* await yourEmailService.send(email, code)
|
|
49
|
-
* }
|
|
50
|
-
* })
|
|
51
|
-
* ```
|
|
52
|
-
*
|
|
53
|
-
* ## Features
|
|
54
|
-
*
|
|
55
|
-
* - **Email verification**: Secure registration with email confirmation codes
|
|
56
|
-
* - **Password hashing**: Built-in Scrypt and PBKDF2 support with secure defaults
|
|
57
|
-
* - **Password validation**: Configurable password strength requirements
|
|
58
|
-
* - **Password reset**: Secure password change flow with email verification
|
|
59
|
-
* - **Session management**: Automatic invalidation on password changes
|
|
60
|
-
*
|
|
61
|
-
* @packageDocumentation
|
|
62
|
-
*/
|
|
63
|
-
/**
|
|
64
|
-
* Password hashing interface for secure password storage.
|
|
65
|
-
* Implement this interface to use custom password hashing algorithms.
|
|
66
|
-
*
|
|
67
|
-
* @template T - The hash storage format (usually an object with hash, salt, and params)
|
|
68
|
-
* @internal
|
|
69
|
-
*/
|
|
70
|
-
interface PasswordHasher<T> {
|
|
71
|
-
/**
|
|
72
|
-
* Hashes a plaintext password for secure storage.
|
|
73
|
-
*
|
|
74
|
-
* @param password - The plaintext password to hash
|
|
75
|
-
* @returns Promise resolving to the hash data structure
|
|
76
|
-
*/
|
|
77
|
-
hash(password: string): Promise<T>;
|
|
78
|
-
/**
|
|
79
|
-
* Verifies a plaintext password against a stored hash.
|
|
80
|
-
*
|
|
81
|
-
* @param password - The plaintext password to verify
|
|
82
|
-
* @param compare - The stored hash data to compare against
|
|
83
|
-
* @returns Promise resolving to true if password matches
|
|
84
|
-
*/
|
|
85
|
-
verify(password: string, compare: T): Promise<boolean>;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Configuration for the password authentication provider.
|
|
89
|
-
*/
|
|
90
|
-
interface PasswordConfig {
|
|
91
|
-
/**
|
|
92
|
-
* Length of verification codes sent to users.
|
|
93
|
-
* @internal
|
|
94
|
-
* @default 6
|
|
95
|
-
*/
|
|
96
|
-
readonly length?: number;
|
|
97
|
-
/**
|
|
98
|
-
* Password hashing implementation to use.
|
|
99
|
-
* @internal
|
|
100
|
-
* @default ScryptHasher()
|
|
101
|
-
*/
|
|
102
|
-
readonly hasher?: PasswordHasher<unknown>;
|
|
103
|
-
/**
|
|
104
|
-
* Request handler for rendering the login screen.
|
|
105
|
-
* Receives the request, optional form data, and any login errors.
|
|
106
|
-
*
|
|
107
|
-
* @param req - The HTTP request object
|
|
108
|
-
* @param form - Form data from POST requests (if any)
|
|
109
|
-
* @param error - Login error to display (if any)
|
|
110
|
-
* @returns Promise resolving to the login page response
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* ```ts
|
|
114
|
-
* login: async (req, form, error) => {
|
|
115
|
-
* const html = renderLoginPage({
|
|
116
|
-
* email: form?.get('email'),
|
|
117
|
-
* error: error?.type
|
|
118
|
-
* })
|
|
119
|
-
* return new Response(html, {
|
|
120
|
-
* headers: { 'Content-Type': 'text/html' }
|
|
121
|
-
* })
|
|
122
|
-
* }
|
|
123
|
-
* ```
|
|
124
|
-
*/
|
|
125
|
-
login: (req: Request, form?: FormData, error?: PasswordLoginError) => Promise<Response>;
|
|
126
|
-
/**
|
|
127
|
-
* Request handler for rendering the registration screen.
|
|
128
|
-
* Handles both initial registration form and email verification.
|
|
129
|
-
*
|
|
130
|
-
* @param req - The HTTP request object
|
|
131
|
-
* @param state - Current registration state (start or code verification)
|
|
132
|
-
* @param form - Form data from POST requests (if any)
|
|
133
|
-
* @param error - Registration error to display (if any)
|
|
134
|
-
* @returns Promise resolving to the registration page response
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* ```ts
|
|
138
|
-
* register: async (req, state, form, error) => {
|
|
139
|
-
* if (state.type === 'start') {
|
|
140
|
-
* return new Response(renderRegistrationForm(error))
|
|
141
|
-
* } else {
|
|
142
|
-
* return new Response(renderCodeVerification(state.email, error))
|
|
143
|
-
* }
|
|
144
|
-
* }
|
|
145
|
-
* ```
|
|
146
|
-
*/
|
|
147
|
-
register: (req: Request, state: PasswordRegisterState, form?: FormData, error?: PasswordRegisterError) => Promise<Response>;
|
|
148
|
-
/**
|
|
149
|
-
* Request handler for rendering the password change screen.
|
|
150
|
-
* Handles email entry, code verification, and password update steps.
|
|
151
|
-
*
|
|
152
|
-
* @param req - The HTTP request object
|
|
153
|
-
* @param state - Current password change state
|
|
154
|
-
* @param form - Form data from POST requests (if any)
|
|
155
|
-
* @param error - Password change error to display (if any)
|
|
156
|
-
* @returns Promise resolving to the password change page response
|
|
157
|
-
*
|
|
158
|
-
* @example
|
|
159
|
-
* ```ts
|
|
160
|
-
* change: async (req, state, form, error) => {
|
|
161
|
-
* switch (state.type) {
|
|
162
|
-
* case 'start':
|
|
163
|
-
* return new Response(renderEmailForm(error))
|
|
164
|
-
* case 'code':
|
|
165
|
-
* return new Response(renderCodeForm(state.email, error))
|
|
166
|
-
* case 'update':
|
|
167
|
-
* return new Response(renderPasswordForm(error))
|
|
168
|
-
* }
|
|
169
|
-
* }
|
|
170
|
-
* ```
|
|
171
|
-
*/
|
|
172
|
-
change: (req: Request, state: PasswordChangeState, form?: FormData, error?: PasswordChangeError) => Promise<Response>;
|
|
173
|
-
/**
|
|
174
|
-
* Callback for sending verification codes to users via email.
|
|
175
|
-
* Implement this to integrate with your email service provider.
|
|
176
|
-
*
|
|
177
|
-
* @param email - The recipient's email address
|
|
178
|
-
* @param code - The verification code to send
|
|
179
|
-
* @returns Promise that resolves when email is sent
|
|
180
|
-
*
|
|
181
|
-
* @example
|
|
182
|
-
* ```ts
|
|
183
|
-
* sendCode: async (email, code) => {
|
|
184
|
-
* await emailService.send({
|
|
185
|
-
* to: email,
|
|
186
|
-
* subject: 'Your verification code',
|
|
187
|
-
* text: `Your verification code is: ${code}`
|
|
188
|
-
* })
|
|
189
|
-
* }
|
|
190
|
-
* ```
|
|
191
|
-
*/
|
|
192
|
-
sendCode: (email: string, code: string) => Promise<void>;
|
|
193
|
-
/**
|
|
194
|
-
* Optional password validation function or schema.
|
|
195
|
-
* Can be either a validation function or a standard-schema validator.
|
|
196
|
-
*
|
|
197
|
-
* @param password - The password to validate
|
|
198
|
-
* @returns Error message if invalid, undefined if valid
|
|
199
|
-
*
|
|
200
|
-
* @example
|
|
201
|
-
* ```ts
|
|
202
|
-
* // Function-based validation
|
|
203
|
-
* validatePassword: (password) => {
|
|
204
|
-
* if (password.length < 8) return "Password must be at least 8 characters"
|
|
205
|
-
* if (!/[A-Z]/.test(password)) return "Password must contain uppercase letter"
|
|
206
|
-
* return undefined
|
|
207
|
-
* }
|
|
208
|
-
*
|
|
209
|
-
* // Schema-based validation
|
|
210
|
-
* validatePassword: pipe(
|
|
211
|
-
* string(),
|
|
212
|
-
* minLength(8, "Password must be at least 8 characters"),
|
|
213
|
-
* regex(/[A-Z]/, "Password must contain uppercase letter")
|
|
214
|
-
* )
|
|
215
|
-
* ```
|
|
216
|
-
*/
|
|
217
|
-
readonly validatePassword?: StandardSchemaV1 | ((password: string) => Promise<string | undefined> | string | undefined);
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Registration flow states that determine which UI to show.
|
|
221
|
-
* The registration process moves through these states sequentially.
|
|
222
|
-
*/
|
|
223
|
-
type PasswordRegisterState = {
|
|
224
|
-
/** Initial state: user enters email and password */
|
|
225
|
-
readonly type: "start";
|
|
226
|
-
} | {
|
|
227
|
-
/** Code verification state: user enters emailed verification code */
|
|
228
|
-
readonly type: "code";
|
|
229
|
-
/** The verification code sent to the user */
|
|
230
|
-
readonly code: string;
|
|
231
|
-
/** The user's email address */
|
|
232
|
-
readonly email: string;
|
|
233
|
-
/** The hashed password (ready for storage) */
|
|
234
|
-
readonly password: unknown;
|
|
235
|
-
};
|
|
236
|
-
/**
|
|
237
|
-
* Possible errors during user registration.
|
|
238
|
-
*/
|
|
239
|
-
type PasswordRegisterError = {
|
|
240
|
-
/** The verification code entered is incorrect */
|
|
241
|
-
readonly type: "invalid_code";
|
|
242
|
-
} | {
|
|
243
|
-
/** The email address is already registered */
|
|
244
|
-
readonly type: "email_taken";
|
|
245
|
-
} | {
|
|
246
|
-
/** The email address format is invalid */
|
|
247
|
-
readonly type: "invalid_email";
|
|
248
|
-
} | {
|
|
249
|
-
/** The password does not meet requirements */
|
|
250
|
-
readonly type: "invalid_password";
|
|
251
|
-
} | {
|
|
252
|
-
/** Password and confirmation password don't match */
|
|
253
|
-
readonly type: "password_mismatch";
|
|
254
|
-
} | {
|
|
255
|
-
/** Custom validation error from validatePassword callback */
|
|
256
|
-
readonly type: "validation_error";
|
|
257
|
-
readonly message?: string;
|
|
258
|
-
};
|
|
259
|
-
/**
|
|
260
|
-
* Password change flow states that determine which UI to show.
|
|
261
|
-
*/
|
|
262
|
-
type PasswordChangeState = {
|
|
263
|
-
/** Initial state: user enters their email address */
|
|
264
|
-
readonly type: "start";
|
|
265
|
-
/** URL to redirect to after successful password change */
|
|
266
|
-
readonly redirect: string;
|
|
267
|
-
} | {
|
|
268
|
-
/** Code verification state: user enters emailed verification code */
|
|
269
|
-
readonly type: "code";
|
|
270
|
-
/** The verification code sent to the user */
|
|
271
|
-
readonly code: string;
|
|
272
|
-
/** The user's email address */
|
|
273
|
-
readonly email: string;
|
|
274
|
-
/** URL to redirect to after completion */
|
|
275
|
-
readonly redirect: string;
|
|
276
|
-
} | {
|
|
277
|
-
/** Password update state: user enters new password */
|
|
278
|
-
readonly type: "update";
|
|
279
|
-
/** URL to redirect to after completion */
|
|
280
|
-
readonly redirect: string;
|
|
281
|
-
/** The verified email address */
|
|
282
|
-
readonly email: string;
|
|
283
|
-
};
|
|
284
|
-
/**
|
|
285
|
-
* Possible errors during password changes.
|
|
286
|
-
*/
|
|
287
|
-
type PasswordChangeError = {
|
|
288
|
-
/** The email address format is invalid */
|
|
289
|
-
readonly type: "invalid_email";
|
|
290
|
-
} | {
|
|
291
|
-
/** The verification code entered is incorrect */
|
|
292
|
-
readonly type: "invalid_code";
|
|
293
|
-
} | {
|
|
294
|
-
/** The new password does not meet requirements */
|
|
295
|
-
readonly type: "invalid_password";
|
|
296
|
-
} | {
|
|
297
|
-
/** New password and confirmation don't match */
|
|
298
|
-
readonly type: "password_mismatch";
|
|
299
|
-
} | {
|
|
300
|
-
/** Custom validation error from validatePassword callback */
|
|
301
|
-
readonly type: "validation_error";
|
|
302
|
-
readonly message: string;
|
|
303
|
-
};
|
|
304
|
-
/**
|
|
305
|
-
* Possible errors during login attempts.
|
|
306
|
-
*/
|
|
307
|
-
type PasswordLoginError = {
|
|
308
|
-
/** The email address format is invalid */
|
|
309
|
-
readonly type: "invalid_email";
|
|
310
|
-
} | {
|
|
311
|
-
/** The password is incorrect or email not found */
|
|
312
|
-
readonly type: "invalid_password";
|
|
313
|
-
};
|
|
314
|
-
/**
|
|
315
|
-
* User data returned by successful password authentication.
|
|
316
|
-
*/
|
|
317
|
-
interface PasswordUserData {
|
|
318
|
-
/** The authenticated user's email address */
|
|
319
|
-
readonly email: string;
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Creates a password authentication provider with email verification.
|
|
323
|
-
* Implements secure registration, login, and password change flows.
|
|
324
|
-
*
|
|
325
|
-
* @param config - Provider configuration including UI handlers and email service
|
|
326
|
-
* @returns Provider instance implementing password authentication
|
|
327
|
-
*
|
|
328
|
-
* @example
|
|
329
|
-
* ```ts
|
|
330
|
-
* const provider = PasswordProvider({
|
|
331
|
-
* login: async (req, form, error) => {
|
|
332
|
-
* return new Response(renderLogin(form, error))
|
|
333
|
-
* },
|
|
334
|
-
* register: async (req, state, form, error) => {
|
|
335
|
-
* return new Response(renderRegister(state, form, error))
|
|
336
|
-
* },
|
|
337
|
-
* change: async (req, state, form, error) => {
|
|
338
|
-
* return new Response(renderChange(state, form, error))
|
|
339
|
-
* },
|
|
340
|
-
* sendCode: async (email, code) => {
|
|
341
|
-
* await emailService.send(email, `Code: ${code}`)
|
|
342
|
-
* },
|
|
343
|
-
* validatePassword: (pwd) => {
|
|
344
|
-
* return pwd.length >= 8 ? undefined : "Too short"
|
|
345
|
-
* }
|
|
346
|
-
* })
|
|
347
|
-
* ```
|
|
348
|
-
*/
|
|
349
|
-
declare const PasswordProvider: (config: PasswordConfig) => Provider<PasswordUserData>;
|
|
350
|
-
/**
|
|
351
|
-
* PBKDF2 password hasher with configurable iterations.
|
|
352
|
-
* Good choice for compatibility but slower than Scrypt.
|
|
353
|
-
*
|
|
354
|
-
* @param opts - Configuration options
|
|
355
|
-
* @returns Password hasher using PBKDF2 algorithm
|
|
356
|
-
* @internal
|
|
357
|
-
*/
|
|
358
|
-
declare const PBKDF2Hasher: (opts?: {
|
|
359
|
-
iterations?: number;
|
|
360
|
-
}) => PasswordHasher<{
|
|
361
|
-
hash: string;
|
|
362
|
-
salt: string;
|
|
363
|
-
iterations: number;
|
|
364
|
-
}>;
|
|
365
|
-
/**
|
|
366
|
-
* Scrypt password hasher with secure defaults.
|
|
367
|
-
* Recommended choice for new applications due to memory-hard properties.
|
|
368
|
-
*
|
|
369
|
-
* @param opts - Scrypt parameters (N, r, p)
|
|
370
|
-
* @returns Password hasher using Scrypt algorithm
|
|
371
|
-
* @internal
|
|
372
|
-
*/
|
|
373
|
-
declare const ScryptHasher: (opts?: {
|
|
374
|
-
N?: number;
|
|
375
|
-
r?: number;
|
|
376
|
-
p?: number;
|
|
377
|
-
}) => PasswordHasher<{
|
|
378
|
-
hash: string;
|
|
379
|
-
salt: string;
|
|
380
|
-
N: number;
|
|
381
|
-
r: number;
|
|
382
|
-
p: number;
|
|
383
|
-
}>;
|
|
384
|
-
//#endregion
|
|
385
|
-
export { PBKDF2Hasher, PasswordChangeError, PasswordChangeState, PasswordConfig, PasswordHasher, PasswordLoginError, PasswordProvider, PasswordRegisterError, PasswordRegisterState, PasswordUserData, ScryptHasher };
|
package/dist/pkce-276Za_rZ.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { base64url } from "jose";
|
|
2
|
-
|
|
3
|
-
//#region src/pkce.ts
|
|
4
|
-
/**
|
|
5
|
-
* Performs a timing-safe comparison of two strings to prevent timing attacks.
|
|
6
|
-
* This implementation is platform-agnostic, uses a constant-time algorithm,
|
|
7
|
-
* and correctly handles all Unicode characters by operating on their UTF-8 byte representation.
|
|
8
|
-
* It always takes a time proportional to the length of the expected string,
|
|
9
|
-
* regardless of where the strings differ, making it safe for comparing sensitive values.
|
|
10
|
-
*
|
|
11
|
-
* @param a - The first string to compare (often the expected, secret value).
|
|
12
|
-
* @param b - The second string to compare (often the user-provided value).
|
|
13
|
-
* @returns True if the strings are identical, false otherwise.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* // Safe for comparing sensitive values like PKCE verifiers or tokens
|
|
18
|
-
* const isValid = await timingSafeCompare(receivedVerifier, expectedChallenge);
|
|
19
|
-
*
|
|
20
|
-
* // Safe for password hash verification
|
|
21
|
-
* const isValidPassword = timingSafeCompare(hashedInput, storedHash);
|
|
22
|
-
*
|
|
23
|
-
* // Returns false for different types or lengths without leaking timing info
|
|
24
|
-
* timingSafeCompare("abc", 123 as any); // false
|
|
25
|
-
* timingSafeCompare("abc", "abcd"); // false
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
const timingSafeCompare = (a, b) => {
|
|
29
|
-
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
30
|
-
const encoder = new TextEncoder();
|
|
31
|
-
const aBytes = encoder.encode(a);
|
|
32
|
-
const bBytes = encoder.encode(b);
|
|
33
|
-
let diff = aBytes.length ^ bBytes.length;
|
|
34
|
-
for (const [i, aByte] of aBytes.entries()) diff |= aByte ^ (bBytes[i] ?? 0);
|
|
35
|
-
return diff === 0;
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Generates a cryptographically secure code verifier for PKCE.
|
|
39
|
-
* The verifier is a URL-safe base64-encoded string of random bytes.
|
|
40
|
-
*
|
|
41
|
-
* @param length - Length of the random buffer in bytes
|
|
42
|
-
* @returns Base64url-encoded verifier string
|
|
43
|
-
*/
|
|
44
|
-
const generateVerifier = (length) => {
|
|
45
|
-
const buffer = new Uint8Array(length);
|
|
46
|
-
crypto.getRandomValues(buffer);
|
|
47
|
-
return base64url.encode(buffer);
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Generates a code challenge from a verifier using the specified method.
|
|
51
|
-
* For 'S256', applies SHA-256 hash then base64url encoding.
|
|
52
|
-
* For 'plain', returns the verifier unchanged (not recommended for production).
|
|
53
|
-
*
|
|
54
|
-
* @param verifier - The code verifier string
|
|
55
|
-
* @param method - Challenge generation method
|
|
56
|
-
* @returns Promise resolving to the code challenge string
|
|
57
|
-
*/
|
|
58
|
-
const generateChallenge = async (verifier, method) => {
|
|
59
|
-
if (method === "plain") return verifier;
|
|
60
|
-
const encoder = new TextEncoder();
|
|
61
|
-
const data = encoder.encode(verifier);
|
|
62
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
63
|
-
return base64url.encode(new Uint8Array(hash));
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* Generates a complete PKCE challenge for OAuth authorization requests.
|
|
67
|
-
* Creates a cryptographically secure verifier and corresponding S256 challenge.
|
|
68
|
-
* Validates that the generated verifier meets standard requirements (43-128 characters).
|
|
69
|
-
*
|
|
70
|
-
* @param length - Length of the random buffer in bytes (32-96 range to generate 43-128 character verifier)
|
|
71
|
-
* @returns Promise resolving to PKCE challenge data
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```ts
|
|
75
|
-
* const pkce = await generatePKCE()
|
|
76
|
-
*
|
|
77
|
-
* // Use challenge in authorization URL
|
|
78
|
-
* authUrl.searchParams.set('code_challenge', pkce.challenge)
|
|
79
|
-
* authUrl.searchParams.set('code_challenge_method', pkce.method)
|
|
80
|
-
*
|
|
81
|
-
* // Store verifier for token exchange
|
|
82
|
-
* sessionStorage.setItem('code_verifier', pkce.verifier)
|
|
83
|
-
* ```
|
|
84
|
-
*
|
|
85
|
-
* @throws {RangeError} If length is outside valid range or generated verifier doesn't meet requirements
|
|
86
|
-
*/
|
|
87
|
-
const generatePKCE = async (length = 48) => {
|
|
88
|
-
if (!Number.isInteger(length) || length < 32 || length > 96) throw new RangeError("Random buffer length must be between 32 and 96 bytes (generates 43-128 character verifier)");
|
|
89
|
-
const verifier = generateVerifier(length);
|
|
90
|
-
if (verifier.length < 43 || verifier.length > 128) throw new Error("Generated verifier does not meet requirements");
|
|
91
|
-
if (!/^[A-Za-z0-9_-]+$/.test(verifier)) throw new Error("Generated verifier is not valid base64url format");
|
|
92
|
-
const challenge = await generateChallenge(verifier, "S256");
|
|
93
|
-
return {
|
|
94
|
-
verifier,
|
|
95
|
-
challenge,
|
|
96
|
-
method: "S256"
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
/**
|
|
100
|
-
* Validates a PKCE code verifier against a previously generated challenge.
|
|
101
|
-
* Uses timing-safe comparison and timing normalization to prevent timing attacks.
|
|
102
|
-
* All validation paths take the same computational time regardless of input validity,
|
|
103
|
-
* making it resistant to timing-based side-channel attacks.
|
|
104
|
-
*
|
|
105
|
-
* @param verifier - The code verifier received from the client
|
|
106
|
-
* @param challenge - The code challenge stored during authorization
|
|
107
|
-
* @param method - The challenge method used during generation
|
|
108
|
-
* @returns Promise resolving to true if verifier matches challenge
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* ```ts
|
|
112
|
-
* // During token exchange
|
|
113
|
-
* const isValid = await validatePKCE(
|
|
114
|
-
* receivedVerifier,
|
|
115
|
-
* storedChallenge,
|
|
116
|
-
* 'S256'
|
|
117
|
-
* )
|
|
118
|
-
*
|
|
119
|
-
* if (!isValid) {
|
|
120
|
-
* throw new Error('Invalid PKCE verifier')
|
|
121
|
-
* }
|
|
122
|
-
* ```
|
|
123
|
-
*/
|
|
124
|
-
const validatePKCE = async (verifier, challenge, method = "S256") => {
|
|
125
|
-
const MIN_PROCESSING_TIME = 50;
|
|
126
|
-
const RANDOM_JITTER_MAX = 20;
|
|
127
|
-
const startTime = performance.now();
|
|
128
|
-
let isValid = false;
|
|
129
|
-
let hasEarlyFailure = false;
|
|
130
|
-
const normalizedVerifier = String(verifier || "");
|
|
131
|
-
const normalizedChallenge = String(challenge || "");
|
|
132
|
-
const validations = [
|
|
133
|
-
typeof verifier === "string" && typeof challenge === "string" && verifier && challenge,
|
|
134
|
-
normalizedVerifier.length >= 43 && normalizedVerifier.length <= 128,
|
|
135
|
-
normalizedChallenge.length >= 43 && normalizedChallenge.length <= 128,
|
|
136
|
-
/^[A-Za-z0-9_-]+$/.test(normalizedVerifier),
|
|
137
|
-
/^[A-Za-z0-9_-]+$/.test(normalizedChallenge)
|
|
138
|
-
];
|
|
139
|
-
hasEarlyFailure = !validations.every(Boolean);
|
|
140
|
-
const verifierToUse = hasEarlyFailure ? "dummyverifier_".repeat(6) : normalizedVerifier;
|
|
141
|
-
try {
|
|
142
|
-
const generatedChallenge = await generateChallenge(verifierToUse, method);
|
|
143
|
-
const challengeToCompare = hasEarlyFailure ? "dummychallenge_".repeat(6) : normalizedChallenge;
|
|
144
|
-
const comparisonResult = timingSafeCompare(generatedChallenge, challengeToCompare);
|
|
145
|
-
isValid = !hasEarlyFailure && comparisonResult;
|
|
146
|
-
} catch {
|
|
147
|
-
isValid = false;
|
|
148
|
-
}
|
|
149
|
-
const elapsed = performance.now() - startTime;
|
|
150
|
-
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
151
|
-
if (remainingTime > 0 || elapsed < MIN_PROCESSING_TIME) {
|
|
152
|
-
const jitterArray = new Uint32Array(1);
|
|
153
|
-
crypto.getRandomValues(jitterArray);
|
|
154
|
-
const jitter = (jitterArray[0] ?? 0) / 4294967295 * RANDOM_JITTER_MAX;
|
|
155
|
-
const totalDelay = Math.max(remainingTime, MIN_PROCESSING_TIME - elapsed) + jitter;
|
|
156
|
-
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
157
|
-
}
|
|
158
|
-
return isValid;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
//#endregion
|
|
162
|
-
export { generatePKCE, validatePKCE };
|