better-auth 1.7.6 → 1.7.8
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/api/rate-limiter/index.mjs +4 -1
- package/dist/api/routes/password.mjs +5 -11
- package/dist/api/routes/sign-in.mjs +1 -1
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +10 -5
- package/dist/client/config.mjs +3 -4
- package/dist/cookies/index.d.mts +5 -5
- package/dist/crypto/purpose.mjs +19 -0
- package/dist/db/get-migration.mjs +13 -11
- package/dist/db/get-schema.mjs +27 -11
- package/dist/db/internal-adapter.mjs +73 -56
- package/dist/db/to-zod.d.mts +8 -6
- package/dist/db/to-zod.mjs +12 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/custom-session/index.mjs +6 -13
- package/dist/plugins/email-otp/routes.mjs +19 -10
- package/dist/plugins/generic-oauth/index.d.mts +13 -0
- package/dist/plugins/generic-oauth/index.mjs +232 -189
- package/dist/plugins/last-login-method/index.mjs +3 -3
- package/dist/plugins/magic-link/index.mjs +29 -16
- package/dist/plugins/oauth-proxy/index.d.mts +5 -6
- package/dist/plugins/oauth-proxy/index.mjs +11 -10
- package/dist/plugins/organization/client.mjs +2 -1
- package/dist/plugins/organization/routes/crud-members.d.mts +1 -42
- package/dist/plugins/organization/routes/crud-members.mjs +1 -1
- package/dist/plugins/phone-number/routes.mjs +33 -15
- package/dist/state.mjs +9 -6
- package/dist/utils/hashing.mjs +1 -1
- package/dist/utils/middleware-response.mjs +4 -1
- package/dist/utils/password.mjs +33 -1
- package/package.json +13 -13
|
@@ -41,12 +41,11 @@ interface OAuthProxyOptions {
|
|
|
41
41
|
* A dedicated secret used to encrypt and decrypt data passed between
|
|
42
42
|
* servers during the OAuth proxy flow.
|
|
43
43
|
*
|
|
44
|
-
* When set, this secret
|
|
45
|
-
* `BETTER_AUTH_SECRET` for
|
|
46
|
-
* This limits
|
|
47
|
-
* environments (production, preview,
|
|
48
|
-
*
|
|
49
|
-
* protected by the main secret.
|
|
44
|
+
* When set, keys derived from this secret are used **instead of** keys
|
|
45
|
+
* derived from `BETTER_AUTH_SECRET` for OAuth proxy encryption.
|
|
46
|
+
* This limits exposure of data protected only by the main secret if
|
|
47
|
+
* the proxy secret is shared across environments (production, preview,
|
|
48
|
+
* development). A compromised proxy secret can still affect OAuth flows.
|
|
50
49
|
*
|
|
51
50
|
* All environments participating in the OAuth proxy flow must share
|
|
52
51
|
* the same `secret` value.
|
|
@@ -9,7 +9,8 @@ import { redirectOnError } from "../../oauth2/errors.mjs";
|
|
|
9
9
|
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
10
10
|
import { handleOAuthUserInfo, linkOAuthAccount } from "../../oauth2/link-account.mjs";
|
|
11
11
|
import { setOAuthState } from "../../api/state/oauth.mjs";
|
|
12
|
-
import {
|
|
12
|
+
import { derivePurposeKey } from "../../crypto/purpose.mjs";
|
|
13
|
+
import { getAuthStateVerificationIdentifier, parseGenericState } from "../../state.mjs";
|
|
13
14
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
14
15
|
import { parseJSON } from "../../client/parser.mjs";
|
|
15
16
|
import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
|
|
@@ -66,7 +67,7 @@ const oauthCallbackQuerySchema = z.object({
|
|
|
66
67
|
});
|
|
67
68
|
const oAuthProxy = (opts) => {
|
|
68
69
|
const maxAge = opts?.maxAge ?? 60;
|
|
69
|
-
const getEncryptionKey = (ctx) => opts?.secret ?? ctx.context.secretConfig;
|
|
70
|
+
const getEncryptionKey = (ctx, purpose) => derivePurposeKey(opts?.secret ?? ctx.context.secretConfig, purpose);
|
|
70
71
|
const oauthProxyCompletion = createAuthEndpoint("/callback/:id/oauth-proxy", {
|
|
71
72
|
method: "GET",
|
|
72
73
|
operationId: "oauthProxyCompletion",
|
|
@@ -84,7 +85,7 @@ const oAuthProxy = (opts) => {
|
|
|
84
85
|
let decryptedPayload;
|
|
85
86
|
try {
|
|
86
87
|
decryptedPayload = await symmetricDecrypt({
|
|
87
|
-
key: getEncryptionKey(ctx),
|
|
88
|
+
key: getEncryptionKey(ctx, "oauth-proxy-profile"),
|
|
88
89
|
data: encryptedProfile
|
|
89
90
|
});
|
|
90
91
|
} catch (e) {
|
|
@@ -225,7 +226,7 @@ const oAuthProxy = (opts) => {
|
|
|
225
226
|
let statePackage;
|
|
226
227
|
try {
|
|
227
228
|
statePackage = parseJSON(await symmetricDecrypt({
|
|
228
|
-
key: getEncryptionKey(ctx),
|
|
229
|
+
key: getEncryptionKey(ctx, "oauth-proxy-package"),
|
|
229
230
|
data: state
|
|
230
231
|
}));
|
|
231
232
|
} catch {
|
|
@@ -245,7 +246,7 @@ const oAuthProxy = (opts) => {
|
|
|
245
246
|
let stateData;
|
|
246
247
|
try {
|
|
247
248
|
stateData = parseJSON(await symmetricDecrypt({
|
|
248
|
-
key: getEncryptionKey(ctx),
|
|
249
|
+
key: getEncryptionKey(ctx, "oauth-proxy-state"),
|
|
249
250
|
data: statePackage.stateCookie
|
|
250
251
|
}));
|
|
251
252
|
} catch (e) {
|
|
@@ -338,7 +339,7 @@ const oAuthProxy = (opts) => {
|
|
|
338
339
|
timestamp: Date.now()
|
|
339
340
|
};
|
|
340
341
|
const encryptedPayload = await symmetricEncrypt({
|
|
341
|
-
key: getEncryptionKey(ctx),
|
|
342
|
+
key: getEncryptionKey(ctx, "oauth-proxy-profile"),
|
|
342
343
|
data: JSON.stringify(payload)
|
|
343
344
|
});
|
|
344
345
|
proxyCallbackURL.searchParams.set("profile", encryptedPayload);
|
|
@@ -366,11 +367,11 @@ const oAuthProxy = (opts) => {
|
|
|
366
367
|
const oauthStateCookie = ctx.context.createAuthCookie("oauth_state");
|
|
367
368
|
const encryptedCookieValue = parseSetCookieHeader(setCookieHeader).get(oauthStateCookie.name)?.value;
|
|
368
369
|
if (encryptedCookieValue) plaintextState = await symmetricDecrypt({
|
|
369
|
-
key: ctx.context.secretConfig,
|
|
370
|
+
key: derivePurposeKey(ctx.context.secretConfig, "oauth-state-cookie"),
|
|
370
371
|
data: encryptedCookieValue
|
|
371
372
|
});
|
|
372
373
|
}
|
|
373
|
-
} else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(originalState))?.value;
|
|
374
|
+
} else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(getAuthStateVerificationIdentifier(originalState)))?.value;
|
|
374
375
|
if (!plaintextState) {
|
|
375
376
|
ctx.context.logger.warn("No OAuth state found for proxy");
|
|
376
377
|
return;
|
|
@@ -378,13 +379,13 @@ const oAuthProxy = (opts) => {
|
|
|
378
379
|
const statePackage = {
|
|
379
380
|
state: originalState,
|
|
380
381
|
stateCookie: await symmetricEncrypt({
|
|
381
|
-
key: getEncryptionKey(ctx),
|
|
382
|
+
key: getEncryptionKey(ctx, "oauth-proxy-state"),
|
|
382
383
|
data: plaintextState
|
|
383
384
|
}),
|
|
384
385
|
isOAuthProxy: true
|
|
385
386
|
};
|
|
386
387
|
const encryptedPackage = await symmetricEncrypt({
|
|
387
|
-
key: getEncryptionKey(ctx),
|
|
388
|
+
key: getEncryptionKey(ctx, "oauth-proxy-package"),
|
|
388
389
|
data: JSON.stringify(statePackage)
|
|
389
390
|
});
|
|
390
391
|
oauthURL.searchParams.set("state", encryptedPackage);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
2
2
|
import { useAuthQuery } from "../../client/query.mjs";
|
|
3
|
+
import { matchesSessionSignal } from "../../client/config.mjs";
|
|
3
4
|
import { adminAc, defaultRoles, memberAc, ownerAc } from "./access/statement.mjs";
|
|
4
5
|
import { ORGANIZATION_ERROR_CODES } from "./error-codes.mjs";
|
|
5
6
|
import { hasPermissionFn } from "./permission.mjs";
|
|
@@ -71,7 +72,7 @@ const organizationClient = (options) => {
|
|
|
71
72
|
},
|
|
72
73
|
{
|
|
73
74
|
matcher(path) {
|
|
74
|
-
return path
|
|
75
|
+
return matchesSessionSignal(path) || path.startsWith("/organization");
|
|
75
76
|
},
|
|
76
77
|
signal: "$activeOrgSignal"
|
|
77
78
|
},
|
|
@@ -1009,48 +1009,7 @@ declare const getActiveMemberRole: <O extends OrganizationOptions>(options: O) =
|
|
|
1009
1009
|
};
|
|
1010
1010
|
}>>)[];
|
|
1011
1011
|
}, {
|
|
1012
|
-
role:
|
|
1013
|
-
enabled: true;
|
|
1014
|
-
} ? {
|
|
1015
|
-
id: string;
|
|
1016
|
-
organizationId: string;
|
|
1017
|
-
role: InferOrganizationRolesFromOption<O>;
|
|
1018
|
-
createdAt: Date;
|
|
1019
|
-
userId: string;
|
|
1020
|
-
teamId?: string | undefined;
|
|
1021
|
-
user: {
|
|
1022
|
-
id: string;
|
|
1023
|
-
email: string;
|
|
1024
|
-
name: string;
|
|
1025
|
-
image?: string | undefined;
|
|
1026
|
-
};
|
|
1027
|
-
} : {
|
|
1028
|
-
id: string;
|
|
1029
|
-
organizationId: string;
|
|
1030
|
-
role: InferOrganizationRolesFromOption<O>;
|
|
1031
|
-
createdAt: Date;
|
|
1032
|
-
userId: string;
|
|
1033
|
-
user: {
|
|
1034
|
-
id: string;
|
|
1035
|
-
email: string;
|
|
1036
|
-
name: string;
|
|
1037
|
-
image?: string | undefined;
|
|
1038
|
-
};
|
|
1039
|
-
}) & (O["schema"] extends {
|
|
1040
|
-
member?: {
|
|
1041
|
-
additionalFields: infer Field extends Record<string, _$_better_auth_core_db0.DBFieldAttribute>;
|
|
1042
|
-
} | undefined;
|
|
1043
|
-
} ? FieldAttributeToObject<Field> : {}) extends infer T ? { [K in keyof T]: T[K] } : never) & {
|
|
1044
|
-
user: {
|
|
1045
|
-
id: string;
|
|
1046
|
-
createdAt: Date;
|
|
1047
|
-
updatedAt: Date;
|
|
1048
|
-
email: string;
|
|
1049
|
-
emailVerified: boolean;
|
|
1050
|
-
name: string;
|
|
1051
|
-
image?: string | null | undefined;
|
|
1052
|
-
};
|
|
1053
|
-
})["role"];
|
|
1012
|
+
role: InferOrganizationRolesFromOption<O>;
|
|
1054
1013
|
}>;
|
|
1055
1014
|
//#endregion
|
|
1056
1015
|
export { addMember, getActiveMember, getActiveMemberRole, leaveOrganization, listMembers, removeMember, updateMemberRole };
|
|
@@ -512,7 +512,7 @@ const getActiveMemberRole = (options) => createAuthEndpoint("/organization/get-a
|
|
|
512
512
|
organizationId
|
|
513
513
|
});
|
|
514
514
|
if (!member) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION);
|
|
515
|
-
return ctx.json({ role: member
|
|
515
|
+
return ctx.json({ role: member.role });
|
|
516
516
|
});
|
|
517
517
|
//#endregion
|
|
518
518
|
export { addMember, getActiveMember, getActiveMemberRole, leaveOrganization, listMembers, removeMember, updateMemberRole };
|
|
@@ -4,7 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
|
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
6
6
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
7
|
-
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
7
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort, resetCredentialPassword } from "../../utils/password.mjs";
|
|
8
8
|
import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
|
|
9
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -175,15 +175,19 @@ const sendPhoneNumberOTP = (opts) => createAuthEndpoint("/phone-number/send-otp"
|
|
|
175
175
|
async function verifyAndConsumePhoneNumberOTP(ctx, opts, body) {
|
|
176
176
|
const { phoneNumber, code } = body;
|
|
177
177
|
if (opts.verifyOTP) {
|
|
178
|
-
|
|
179
|
-
phoneNumber,
|
|
180
|
-
code
|
|
181
|
-
}, ctx)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
|
|
182
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(phoneNumber);
|
|
178
|
+
await verifyWithCustomVerifier(ctx, opts.verifyOTP, phoneNumber, code, phoneNumber);
|
|
183
179
|
return;
|
|
184
180
|
}
|
|
185
181
|
await verifyPhoneNumberOTP(ctx, opts, phoneNumber, code);
|
|
186
182
|
}
|
|
183
|
+
/** Checks the code with `verifyOTP`, then drops the code stored under `identifier`. */
|
|
184
|
+
async function verifyWithCustomVerifier(ctx, verifyOTP, phoneNumber, code, identifier) {
|
|
185
|
+
if (!await verifyOTP({
|
|
186
|
+
phoneNumber,
|
|
187
|
+
code
|
|
188
|
+
}, ctx)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
|
|
189
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
190
|
+
}
|
|
187
191
|
/**
|
|
188
192
|
* ### API Methods
|
|
189
193
|
*
|
|
@@ -459,7 +463,17 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
459
463
|
} }
|
|
460
464
|
} }
|
|
461
465
|
}, async (ctx) => {
|
|
462
|
-
|
|
466
|
+
const phoneResetIdentifier = `${ctx.body.phoneNumber}-request-password-reset`;
|
|
467
|
+
let otpConsumed = false;
|
|
468
|
+
if (opts.verifyOTP) {
|
|
469
|
+
assertPasswordNotTooShort(ctx, ctx.body.newPassword);
|
|
470
|
+
assertPasswordNotTooLong(ctx, ctx.body.newPassword);
|
|
471
|
+
await verifyWithCustomVerifier(ctx, opts.verifyOTP, ctx.body.phoneNumber, ctx.body.otp, phoneResetIdentifier);
|
|
472
|
+
otpConsumed = true;
|
|
473
|
+
} else if (!await matchesUsableStoredPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp)) {
|
|
474
|
+
await verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp);
|
|
475
|
+
otpConsumed = true;
|
|
476
|
+
}
|
|
463
477
|
const user = await ctx.context.adapter.findOne({
|
|
464
478
|
model: "user",
|
|
465
479
|
where: [{
|
|
@@ -471,15 +485,8 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
471
485
|
assertPasswordNotTooShort(ctx, ctx.body.newPassword);
|
|
472
486
|
assertPasswordNotTooLong(ctx, ctx.body.newPassword);
|
|
473
487
|
const hashedPassword = await ctx.context.password.hash(ctx.body.newPassword);
|
|
474
|
-
|
|
475
|
-
userId: user.id,
|
|
476
|
-
providerId: "credential",
|
|
477
|
-
accountId: user.id,
|
|
478
|
-
password: hashedPassword
|
|
479
|
-
});
|
|
480
|
-
else await ctx.context.internalAdapter.updatePassword(user.id, hashedPassword);
|
|
488
|
+
await resetCredentialPassword(ctx, user.id, hashedPassword, otpConsumed ? void 0 : () => verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp));
|
|
481
489
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|
|
482
|
-
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(user.id);
|
|
483
490
|
return ctx.json({ status: true });
|
|
484
491
|
});
|
|
485
492
|
/**
|
|
@@ -518,6 +525,17 @@ async function verifyPhoneNumberOTP(ctx, opts, identifier, providedCode) {
|
|
|
518
525
|
throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
|
|
519
526
|
}
|
|
520
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Reports whether the provided code matches a stored, unexpired record with
|
|
530
|
+
* attempts left, without consuming it or counting an attempt.
|
|
531
|
+
*/
|
|
532
|
+
async function matchesUsableStoredPhoneNumberOTP(ctx, opts, identifier, providedCode) {
|
|
533
|
+
const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
534
|
+
if (!existing || existing.expiresAt < /* @__PURE__ */ new Date()) return false;
|
|
535
|
+
const [otpValue, rawAttempts] = existing.value.split(":");
|
|
536
|
+
if (parseVerificationAttempts(rawAttempts) >= (opts?.allowedAttempts ?? 3)) return false;
|
|
537
|
+
return otpValue === providedCode;
|
|
538
|
+
}
|
|
521
539
|
function parseVerificationAttempts(value) {
|
|
522
540
|
const attempts = Number(value ?? 0);
|
|
523
541
|
return Number.isSafeInteger(attempts) && attempts > 0 ? attempts : 0;
|
package/dist/state.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { generateRandomString } from "./crypto/random.mjs";
|
|
2
2
|
import { symmetricDecrypt, symmetricEncrypt } from "./crypto/index.mjs";
|
|
3
3
|
import { expireCookie } from "./cookies/index.mjs";
|
|
4
|
+
import { derivePurposeKey } from "./crypto/purpose.mjs";
|
|
4
5
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
6
|
import * as z from "zod";
|
|
6
7
|
//#region src/state.ts
|
|
@@ -32,6 +33,8 @@ const stateDataSchema = z.looseObject({
|
|
|
32
33
|
*/
|
|
33
34
|
serverContext: z.record(z.string(), z.unknown()).optional()
|
|
34
35
|
});
|
|
36
|
+
/** Verification key for database-backed OAuth and SAML relay state. */
|
|
37
|
+
const getAuthStateVerificationIdentifier = (state) => `auth-state:${state}`;
|
|
35
38
|
const INTERNAL_STATE_KEYS = new Set(Object.keys(stateDataSchema.shape));
|
|
36
39
|
var StateError = class extends BetterAuthError {
|
|
37
40
|
code;
|
|
@@ -59,7 +62,7 @@ async function generateGenericState(c, stateData, settings) {
|
|
|
59
62
|
oauthState: state
|
|
60
63
|
};
|
|
61
64
|
const encryptedData = await symmetricEncrypt({
|
|
62
|
-
key: c.context.secretConfig,
|
|
65
|
+
key: derivePurposeKey(c.context.secretConfig, "oauth-state-cookie"),
|
|
63
66
|
data: JSON.stringify(payload)
|
|
64
67
|
});
|
|
65
68
|
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "oauth_state", { maxAge: 600 });
|
|
@@ -78,7 +81,7 @@ async function generateGenericState(c, stateData, settings) {
|
|
|
78
81
|
...stateData,
|
|
79
82
|
oauthState: state
|
|
80
83
|
}),
|
|
81
|
-
identifier: state,
|
|
84
|
+
identifier: getAuthStateVerificationIdentifier(state),
|
|
82
85
|
expiresAt
|
|
83
86
|
})) throw new StateError("Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", { code: "state_generation_error" });
|
|
84
87
|
return {
|
|
@@ -99,7 +102,7 @@ async function parseGenericState(c, state, settings) {
|
|
|
99
102
|
});
|
|
100
103
|
try {
|
|
101
104
|
const decryptedData = await symmetricDecrypt({
|
|
102
|
-
key: c.context.secretConfig,
|
|
105
|
+
key: derivePurposeKey(c.context.secretConfig, "oauth-state-cookie"),
|
|
103
106
|
data: encryptedData
|
|
104
107
|
});
|
|
105
108
|
parsedData = stateDataSchema.parse(JSON.parse(decryptedData));
|
|
@@ -117,7 +120,7 @@ async function parseGenericState(c, state, settings) {
|
|
|
117
120
|
});
|
|
118
121
|
expireCookie(c, stateCookie);
|
|
119
122
|
} else {
|
|
120
|
-
const data = await c.context.internalAdapter.findVerificationValue(state);
|
|
123
|
+
const data = await c.context.internalAdapter.findVerificationValue(getAuthStateVerificationIdentifier(state));
|
|
121
124
|
if (!data) throw new StateError("State mismatch: verification not found", {
|
|
122
125
|
code: "state_mismatch",
|
|
123
126
|
details: { state }
|
|
@@ -136,7 +139,7 @@ async function parseGenericState(c, state, settings) {
|
|
|
136
139
|
errorURL: parsedData.errorURL
|
|
137
140
|
});
|
|
138
141
|
expireCookie(c, stateCookie);
|
|
139
|
-
await c.context.internalAdapter.deleteVerificationByIdentifier(state);
|
|
142
|
+
await c.context.internalAdapter.deleteVerificationByIdentifier(getAuthStateVerificationIdentifier(state));
|
|
140
143
|
}
|
|
141
144
|
if (parsedData.expiresAt < Date.now()) throw new StateError("Invalid state: request expired", {
|
|
142
145
|
code: "state_mismatch",
|
|
@@ -146,4 +149,4 @@ async function parseGenericState(c, state, settings) {
|
|
|
146
149
|
return parsedData;
|
|
147
150
|
}
|
|
148
151
|
//#endregion
|
|
149
|
-
export { INTERNAL_STATE_KEYS, StateError, generateGenericState, parseGenericState };
|
|
152
|
+
export { INTERNAL_STATE_KEYS, StateError, generateGenericState, getAuthStateVerificationIdentifier, parseGenericState };
|
package/dist/utils/hashing.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
1
|
import { utf8ToBytes } from "@noble/hashes/utils.js";
|
|
2
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
3
3
|
//#region src/utils/hashing.ts
|
|
4
4
|
/**
|
|
5
5
|
* TS implementation of ERC-55 ("Mixed-case checksum address encoding") using @noble/hashes
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
const middlewareResponse = ({ message, status, code }) => ({ response: new Response(JSON.stringify({
|
|
3
3
|
message,
|
|
4
4
|
code
|
|
5
|
-
}), {
|
|
5
|
+
}), {
|
|
6
|
+
status,
|
|
7
|
+
headers: { "Content-Type": "application/json" }
|
|
8
|
+
}) });
|
|
6
9
|
//#endregion
|
|
7
10
|
export { middlewareResponse };
|
package/dist/utils/password.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runWithTransaction } from "@better-auth/core/context";
|
|
1
2
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
2
3
|
//#region src/utils/password.ts
|
|
3
4
|
function assertPasswordNotTooShort(ctx, password) {
|
|
@@ -12,6 +13,37 @@ function assertPasswordNotTooLong(ctx, password) {
|
|
|
12
13
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
13
14
|
}
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Sets a user's credential password after a reset, revoking their sessions
|
|
18
|
+
* when `emailAndPassword.revokeSessionsOnPasswordReset` is enabled.
|
|
19
|
+
*
|
|
20
|
+
* `consumeResetToken` spends the single-use reset token or OTP and throws when
|
|
21
|
+
* it is no longer valid. It runs first inside the same transaction as
|
|
22
|
+
* revocation and the password write, so a failure in any of them leaves all
|
|
23
|
+
* three unchanged and the user can retry. Verification values held only in
|
|
24
|
+
* secondary storage are consumed with `getAndDelete`, which can't roll back,
|
|
25
|
+
* so a later failure there still spends the token. Secondary-storage sessions are
|
|
26
|
+
* removed by an after-commit hook, so they can't roll back with the write.
|
|
27
|
+
* Sessions are revoked before the write so that on an adapter without
|
|
28
|
+
* transactions a failure still keeps the old password rather than committing a
|
|
29
|
+
* new one while existing sessions stay valid.
|
|
30
|
+
*/
|
|
31
|
+
async function resetCredentialPassword(ctx, userId, passwordHash, consumeResetToken) {
|
|
32
|
+
await runWithTransaction(ctx.context.adapter, async () => {
|
|
33
|
+
await consumeResetToken?.();
|
|
34
|
+
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
35
|
+
if (!await ctx.context.internalAdapter.findCredentialAccount(userId)) {
|
|
36
|
+
await ctx.context.internalAdapter.createAccount({
|
|
37
|
+
userId,
|
|
38
|
+
providerId: "credential",
|
|
39
|
+
accountId: userId,
|
|
40
|
+
password: passwordHash
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
await ctx.context.internalAdapter.updatePassword(userId, passwordHash);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
15
47
|
async function validatePassword(ctx, data) {
|
|
16
48
|
assertPasswordNotTooLong(ctx, data.password);
|
|
17
49
|
const credentialAccount = await ctx.context.internalAdapter.findCredentialAccount(data.userId);
|
|
@@ -43,4 +75,4 @@ async function shouldRequirePassword(ctx, userId, allowPasswordless) {
|
|
|
43
75
|
return Boolean(credentialAccount?.password);
|
|
44
76
|
}
|
|
45
77
|
//#endregion
|
|
46
|
-
export { assertPasswordNotTooLong, assertPasswordNotTooShort, checkPassword, shouldRequirePassword, validatePassword };
|
|
78
|
+
export { assertPasswordNotTooLong, assertPasswordNotTooShort, checkPassword, resetCredentialPassword, shouldRequirePassword, validatePassword };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.8",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -463,23 +463,23 @@
|
|
|
463
463
|
}
|
|
464
464
|
},
|
|
465
465
|
"dependencies": {
|
|
466
|
-
"@better-auth/utils": "0.
|
|
466
|
+
"@better-auth/utils": "0.5.0",
|
|
467
467
|
"@better-fetch/fetch": "1.3.2",
|
|
468
468
|
"@noble/ciphers": "^2.2.0",
|
|
469
469
|
"@noble/hashes": "^2.2.0",
|
|
470
|
-
"better-call": "1.4.
|
|
470
|
+
"better-call": "1.4.2",
|
|
471
471
|
"defu": "^6.1.4",
|
|
472
472
|
"jose": "^6.2.3",
|
|
473
473
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
474
474
|
"nanostores": "^1.3.0",
|
|
475
475
|
"zod": "^4.5.4",
|
|
476
|
-
"@better-auth/core": "1.7.
|
|
477
|
-
"@better-auth/drizzle-adapter": "1.7.
|
|
478
|
-
"@better-auth/kysely-adapter": "1.7.
|
|
479
|
-
"@better-auth/memory-adapter": "1.7.
|
|
480
|
-
"@better-auth/mongo-adapter": "1.7.
|
|
481
|
-
"@better-auth/prisma-adapter": "1.7.
|
|
482
|
-
"@better-auth/telemetry": "1.7.
|
|
476
|
+
"@better-auth/core": "1.7.8",
|
|
477
|
+
"@better-auth/drizzle-adapter": "1.7.8",
|
|
478
|
+
"@better-auth/kysely-adapter": "1.7.8",
|
|
479
|
+
"@better-auth/memory-adapter": "1.7.8",
|
|
480
|
+
"@better-auth/mongo-adapter": "1.7.8",
|
|
481
|
+
"@better-auth/prisma-adapter": "1.7.8",
|
|
482
|
+
"@better-auth/telemetry": "1.7.8"
|
|
483
483
|
},
|
|
484
484
|
"devDependencies": {
|
|
485
485
|
"@lynx-js/react": "^0.121.2",
|
|
@@ -487,8 +487,8 @@
|
|
|
487
487
|
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
488
488
|
"@opentelemetry/sdk-trace-node": "^2.10.0",
|
|
489
489
|
"@sveltejs/kit": "^2.70.1",
|
|
490
|
-
"@tanstack/react-start": "^1.168.
|
|
491
|
-
"@tanstack/solid-start": "^1.168.
|
|
490
|
+
"@tanstack/react-start": "^1.168.60",
|
|
491
|
+
"@tanstack/solid-start": "^1.168.57",
|
|
492
492
|
"@types/bun": "^1.3.14",
|
|
493
493
|
"@types/google.accounts": "^0.0.18",
|
|
494
494
|
"@types/pg": "^8.20.0",
|
|
@@ -511,7 +511,7 @@
|
|
|
511
511
|
"peerDependencies": {
|
|
512
512
|
"@lynx-js/react": "*",
|
|
513
513
|
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
514
|
-
"@sveltejs/kit": "^2.0.0",
|
|
514
|
+
"@sveltejs/kit": "^2.0.0 || ^3.0.0",
|
|
515
515
|
"@tanstack/react-start": "^1.0.0",
|
|
516
516
|
"@tanstack/solid-start": "^1.0.0",
|
|
517
517
|
"drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
|