fix: handle usernames case-insensitively in auth and oidc (#197)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import argon2 from "argon2";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { db } from "../db/client.js";
|
||||
@@ -129,7 +129,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
const { username, password } = parsed.data;
|
||||
|
||||
// Check if username already exists
|
||||
const [existingUser] = await db.select().from(users).where(eq(users.username, username));
|
||||
const [existingUser] = await db.select().from(users).where(sql`lower(${users.username}) = lower(${username})`);
|
||||
if (existingUser) {
|
||||
return reply.status(409).send({ error: "Username already taken", code: "USERNAME_EXISTS" });
|
||||
}
|
||||
@@ -190,7 +190,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
const { username, password, rememberMe } = parsed.data;
|
||||
|
||||
// Find user by username
|
||||
const [user] = await db.select().from(users).where(eq(users.username, username));
|
||||
const [user] = await db.select().from(users).where(sql`lower(${users.username}) = lower(${username})`);
|
||||
|
||||
// Generic error to prevent user enumeration
|
||||
const invalidCredentialsError = () =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import type { FastifyInstance, FastifyReply } from "fastify";
|
||||
import * as client from "openid-client";
|
||||
import { db } from "../db/client.js";
|
||||
@@ -234,7 +234,7 @@ async function findOrCreateOIDCUser(
|
||||
}
|
||||
|
||||
// Check if username already exists (potential collision)
|
||||
const [existingByUsername] = await db.select().from(users).where(eq(users.username, username));
|
||||
const [existingByUsername] = await db.select().from(users).where(sql`lower(${users.username}) = lower(${username})`);
|
||||
|
||||
if (existingByUsername) {
|
||||
// Username collision! Check if it's a local user without OIDC linked
|
||||
|
||||
@@ -194,6 +194,29 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
expect(response.json().code).toBe("USERNAME_EXISTS");
|
||||
});
|
||||
|
||||
it("should reject duplicate username regardless of case", async () => {
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: "/auth/register",
|
||||
payload: {
|
||||
username: "CaseUser",
|
||||
password: "TestPassword123",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/auth/register",
|
||||
payload: {
|
||||
username: "caseuser",
|
||||
password: "AnotherPassword123",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(response.json().code).toBe("USERNAME_EXISTS");
|
||||
});
|
||||
|
||||
it("should reject short password", async () => {
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
@@ -275,6 +298,21 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
expect(cookies.find((c: any) => c.name === "refresh_token")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should login case-insensitively with different username casing", async () => {
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/auth/login",
|
||||
payload: {
|
||||
username: "LOGINUSER",
|
||||
password: "TestPassword123",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json().ok).toBe(true);
|
||||
expect(response.json().user.username).toBe("loginuser");
|
||||
});
|
||||
|
||||
it("should reject invalid password", async () => {
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user