mirror of
https://github.com/better-auth/better-auth.git
synced 2026-07-16 14:38:30 -05:00
fix(username): only store valid displayUsername fallbacks as usernames (#10182)
This commit is contained in:
5
.changeset/slow-rules-reflect.md
Normal file
5
.changeset/slow-rules-reflect.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Only store display username fallbacks as usernames when they pass username validation during email sign-up.
|
||||
@@ -144,28 +144,38 @@ export const username = (options?: UsernameOptions | undefined) => {
|
||||
|
||||
const pathsWithHttpHookValidation = ["/sign-up/email", "/update-user"];
|
||||
|
||||
const getUsernameToValidate = (username: string) =>
|
||||
options?.validationOrder?.username === "post-normalization"
|
||||
? normalizer(username)
|
||||
: username;
|
||||
|
||||
async function validateUsernameValue(username: string) {
|
||||
const usernameToValidate = getUsernameToValidate(username);
|
||||
if (usernameToValidate.length < minUsernameLength) {
|
||||
return ERROR_CODES.USERNAME_TOO_SHORT;
|
||||
}
|
||||
|
||||
if (usernameToValidate.length > maxUsernameLength) {
|
||||
return ERROR_CODES.USERNAME_TOO_LONG;
|
||||
}
|
||||
|
||||
const valid = await validator(usernameToValidate);
|
||||
if (!valid) {
|
||||
return ERROR_CODES.INVALID_USERNAME;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function validateUsername(
|
||||
username: string,
|
||||
displayUsername: string | null,
|
||||
adapter: { findOne: <T>(opts: any) => Promise<T | null> },
|
||||
currentUserId?: string | null,
|
||||
) {
|
||||
const usernameToValidate =
|
||||
options?.validationOrder?.username === "post-normalization"
|
||||
? normalizer(username)
|
||||
: username;
|
||||
|
||||
if (usernameToValidate.length < minUsernameLength) {
|
||||
throw APIError.from("BAD_REQUEST", ERROR_CODES.USERNAME_TOO_SHORT);
|
||||
}
|
||||
|
||||
if (usernameToValidate.length > maxUsernameLength) {
|
||||
throw APIError.from("BAD_REQUEST", ERROR_CODES.USERNAME_TOO_LONG);
|
||||
}
|
||||
|
||||
const valid = await validator(usernameToValidate);
|
||||
if (!valid) {
|
||||
throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_USERNAME);
|
||||
const validationError = await validateUsernameValue(username);
|
||||
if (validationError) {
|
||||
throw APIError.from("BAD_REQUEST", validationError);
|
||||
}
|
||||
|
||||
const normalizedUsername = normalizer(username);
|
||||
@@ -614,6 +624,26 @@ export const username = (options?: UsernameOptions | undefined) => {
|
||||
),
|
||||
hooks: {
|
||||
before: [
|
||||
// Apply valid displayUsername fallback before validation so fallback usernames are checked.
|
||||
{
|
||||
matcher(context) {
|
||||
return context.path === "/sign-up/email";
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
if (
|
||||
typeof ctx.body.displayUsername !== "string" ||
|
||||
ctx.body.username !== undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const validationError = await validateUsernameValue(
|
||||
ctx.body.displayUsername,
|
||||
);
|
||||
if (!validationError) {
|
||||
ctx.body.username = ctx.body.displayUsername;
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
matcher(context) {
|
||||
return (
|
||||
@@ -622,40 +652,14 @@ export const username = (options?: UsernameOptions | undefined) => {
|
||||
);
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
const username =
|
||||
typeof ctx.body.username === "string" &&
|
||||
options?.validationOrder?.username === "post-normalization"
|
||||
? normalizer(ctx.body.username)
|
||||
: ctx.body.username;
|
||||
const username = ctx.body.username;
|
||||
|
||||
if (username !== undefined && typeof username === "string") {
|
||||
const minUsernameLength = options?.minUsernameLength || 3;
|
||||
const maxUsernameLength = options?.maxUsernameLength || 30;
|
||||
if (username.length < minUsernameLength) {
|
||||
throw APIError.from(
|
||||
"BAD_REQUEST",
|
||||
ERROR_CODES.USERNAME_TOO_SHORT,
|
||||
);
|
||||
const validationError = await validateUsernameValue(username);
|
||||
if (validationError) {
|
||||
throw APIError.from("BAD_REQUEST", validationError);
|
||||
}
|
||||
|
||||
if (username.length > maxUsernameLength) {
|
||||
throw APIError.from(
|
||||
"BAD_REQUEST",
|
||||
ERROR_CODES.USERNAME_TOO_LONG,
|
||||
);
|
||||
}
|
||||
|
||||
const validator =
|
||||
options?.usernameValidator || defaultUsernameValidator;
|
||||
|
||||
const valid = await validator(username);
|
||||
if (!valid) {
|
||||
throw APIError.from(
|
||||
"BAD_REQUEST",
|
||||
ERROR_CODES.INVALID_USERNAME,
|
||||
);
|
||||
}
|
||||
const normalizedUsername = normalizer(ctx.body.username);
|
||||
const normalizedUsername = normalizer(username);
|
||||
const existingUser = await ctx.context.adapter.findOne<User>({
|
||||
model: "user",
|
||||
where: [
|
||||
@@ -715,9 +719,6 @@ export const username = (options?: UsernameOptions | undefined) => {
|
||||
if (ctx.body.username && !ctx.body.displayUsername) {
|
||||
ctx.body.displayUsername = ctx.body.username;
|
||||
}
|
||||
if (ctx.body.displayUsername && !ctx.body.username) {
|
||||
ctx.body.username = ctx.body.displayUsername;
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -276,7 +276,7 @@ describe("username", async () => {
|
||||
await client.signUp.email(
|
||||
{
|
||||
email: "display-test@email.com",
|
||||
displayUsername: "Test Username",
|
||||
displayUsername: "Test_Username",
|
||||
password: "test-password",
|
||||
name: "test-name",
|
||||
},
|
||||
@@ -292,8 +292,47 @@ describe("username", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(session?.user.username).toBe("test username");
|
||||
expect(session?.user.displayUsername).toBe("Test Username");
|
||||
expect(session?.user.username).toBe("test_username");
|
||||
expect(session?.user.displayUsername).toBe("Test_Username");
|
||||
});
|
||||
|
||||
it("should not store an invalid displayUsername-only value as username", async () => {
|
||||
const headers = new Headers();
|
||||
const res = await client.signUp.email(
|
||||
{
|
||||
email: "invalid-display-username@email.com",
|
||||
displayUsername: "Invalid Username",
|
||||
password: "test-password",
|
||||
name: "test-name",
|
||||
},
|
||||
{
|
||||
onSuccess: sessionSetter(headers),
|
||||
},
|
||||
);
|
||||
|
||||
expect(res.error).toBeNull();
|
||||
const session = await client.getSession({
|
||||
fetchOptions: {
|
||||
headers,
|
||||
throw: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(session?.user.username).toBeNull();
|
||||
expect(session?.user.displayUsername).toBe("Invalid Username");
|
||||
});
|
||||
|
||||
it("should not replace an explicit empty username with displayUsername", async () => {
|
||||
const res = await client.signUp.email({
|
||||
email: "empty-username@email.com",
|
||||
username: "",
|
||||
displayUsername: "valid_username",
|
||||
password: "test-password",
|
||||
name: "test-name",
|
||||
});
|
||||
|
||||
expect(res.error?.status).toBe(400);
|
||||
expect(res.error?.code).toBe(USERNAME_ERROR_CODES.USERNAME_TOO_SHORT.code);
|
||||
});
|
||||
|
||||
it("should preserve both username and displayUsername when both are provided", async () => {
|
||||
@@ -439,11 +478,14 @@ describe("username with displayUsername validation", async () => {
|
||||
name: "test-name",
|
||||
});
|
||||
expect(res.error).toBeNull();
|
||||
expect(res.data?.user.username).toBeNull();
|
||||
expect(res.data?.user.displayUsername).toBe("Valid_Display-123");
|
||||
});
|
||||
|
||||
it("should reject invalid displayUsername", async () => {
|
||||
const res = await client.signUp.email({
|
||||
email: "display-invalid@email.com",
|
||||
username: "invalid_display",
|
||||
displayUsername: "Invalid Display!",
|
||||
password: "test-password",
|
||||
name: "test-name",
|
||||
@@ -454,6 +496,19 @@ describe("username with displayUsername validation", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should not validate inferred displayUsername during sign-up", async () => {
|
||||
const res = await client.signUp.email({
|
||||
email: "inferred-display@email.com",
|
||||
username: "valid.username",
|
||||
password: "test-password",
|
||||
name: "test-name",
|
||||
});
|
||||
|
||||
expect(res.error).toBeNull();
|
||||
expect(res.data?.user.username).toBe("valid.username");
|
||||
expect(res.data?.user.displayUsername).toBe("valid.username");
|
||||
});
|
||||
|
||||
it("should update displayUsername with valid value", async () => {
|
||||
const headers = new Headers();
|
||||
await client.signUp.email(
|
||||
@@ -501,6 +556,7 @@ describe("username with displayUsername validation", async () => {
|
||||
await client.signUp.email(
|
||||
{
|
||||
email: "update-invalid@email.com",
|
||||
username: "valid_name",
|
||||
displayUsername: "Valid_Name",
|
||||
password: "test-password",
|
||||
name: "test-name",
|
||||
|
||||
Reference in New Issue
Block a user