renamed passkey to security key to stay aligned with the UI and other backend naming.

This commit is contained in:
Adrian Astles 2025-07-05 21:51:31 +08:00
parent 6ccc05b183
commit 5009906385
13 changed files with 158 additions and 118 deletions

View file

@ -491,10 +491,24 @@ export const idpOrg = pgTable("idpOrg", {
orgMapping: varchar("orgMapping") orgMapping: varchar("orgMapping")
}); });
export const securityKeys = pgTable("webauthnCredentials", {
credentialId: varchar("credentialId").primaryKey(),
userId: varchar("userId").notNull().references(() => users.userId, {
onDelete: "cascade"
}),
publicKey: varchar("publicKey").notNull(),
signCount: integer("signCount").notNull(),
transports: varchar("transports"),
name: varchar("name"),
lastUsed: varchar("lastUsed").notNull(),
dateCreated: varchar("dateCreated").notNull(),
securityKeyName: varchar("securityKeyName")
});
export const webauthnChallenge = pgTable("webauthnChallenge", { export const webauthnChallenge = pgTable("webauthnChallenge", {
sessionId: varchar("sessionId").primaryKey(), sessionId: varchar("sessionId").primaryKey(),
challenge: varchar("challenge").notNull(), challenge: varchar("challenge").notNull(),
passkeyName: varchar("passkeyName"), securityKeyName: varchar("securityKeyName"),
userId: varchar("userId").references(() => users.userId, { userId: varchar("userId").references(() => users.userId, {
onDelete: "cascade" onDelete: "cascade"
}), }),

View file

@ -7,12 +7,37 @@ import type { Database as BetterSqlite3Database } from "better-sqlite3";
const migrationsFolder = path.join("server/migrations"); const migrationsFolder = path.join("server/migrations");
const dropAllTables = (sqlite: BetterSqlite3Database) => {
console.log("Dropping all existing tables...");
// Disable foreign key checks
sqlite.pragma('foreign_keys = OFF');
// Get all tables
const tables = sqlite.prepare(`
SELECT name FROM sqlite_master
WHERE type='table'
AND name NOT LIKE 'sqlite_%'
`).all() as { name: string }[];
// Drop each table
for (const table of tables) {
console.log(`Dropping table: ${table.name}`);
sqlite.prepare(`DROP TABLE IF EXISTS "${table.name}"`).run();
}
// Re-enable foreign key checks
sqlite.pragma('foreign_keys = ON');
};
const runMigrations = async () => { const runMigrations = async () => {
console.log("Running migrations..."); console.log("Running migrations...");
try { try {
// Initialize the database file with a valid SQLite header // Initialize the database file with a valid SQLite header
const sqlite = new Database(location) as BetterSqlite3Database; const sqlite = new Database(location) as BetterSqlite3Database;
sqlite.pragma('foreign_keys = ON');
// Drop all existing tables first
dropAllTables(sqlite);
// Run the migrations // Run the migrations
migrate(db as any, { migrate(db as any, {

View file

@ -135,7 +135,7 @@ export const users = sqliteTable("user", {
.default(false) .default(false)
}); });
export const passkeys = sqliteTable("webauthnCredentials", { export const securityKeys = sqliteTable("webauthnCredentials", {
credentialId: text("credentialId").primaryKey(), credentialId: text("credentialId").primaryKey(),
userId: text("userId").notNull().references(() => users.userId, { userId: text("userId").notNull().references(() => users.userId, {
onDelete: "cascade" onDelete: "cascade"
@ -151,7 +151,7 @@ export const passkeys = sqliteTable("webauthnCredentials", {
export const webauthnChallenge = sqliteTable("webauthnChallenge", { export const webauthnChallenge = sqliteTable("webauthnChallenge", {
sessionId: text("sessionId").primaryKey(), sessionId: text("sessionId").primaryKey(),
challenge: text("challenge").notNull(), challenge: text("challenge").notNull(),
passkeyName: text("passkeyName"), securityKeyName: text("securityKeyName"),
userId: text("userId").references(() => users.userId, { userId: text("userId").references(() => users.userId, {
onDelete: "cascade" onDelete: "cascade"
}), }),

View file

@ -6,10 +6,10 @@ export * from "./requestTotpSecret";
export * from "./disable2fa"; export * from "./disable2fa";
export * from "./verifyEmail"; export * from "./verifyEmail";
export * from "./requestEmailVerificationCode"; export * from "./requestEmailVerificationCode";
export * from "./changePassword";
export * from "./requestPasswordReset";
export * from "./resetPassword"; export * from "./resetPassword";
export * from "./checkResourceSession"; export * from "./requestPasswordReset";
export * from "./setServerAdmin"; export * from "./setServerAdmin";
export * from "./initialSetupComplete"; export * from "./initialSetupComplete";
export * from "./passkey"; export * from "./changePassword";
export * from "./checkResourceSession";
export * from "./securityKey";

View file

@ -4,7 +4,7 @@ import {
serializeSessionCookie serializeSessionCookie
} from "@server/auth/sessions/app"; } from "@server/auth/sessions/app";
import { db } from "@server/db"; import { db } from "@server/db";
import { users, passkeys } from "@server/db"; import { users, securityKeys } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
@ -35,6 +35,7 @@ export type LoginBody = z.infer<typeof loginBodySchema>;
export type LoginResponse = { export type LoginResponse = {
codeRequested?: boolean; codeRequested?: boolean;
emailVerificationRequired?: boolean; emailVerificationRequired?: boolean;
useSecurityKey?: boolean;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@ -91,15 +92,15 @@ export async function login(
const existingUser = existingUserRes[0]; const existingUser = existingUserRes[0];
// Check if user has passkeys registered // Check if user has security keys registered
const userPasskeys = await db const userSecurityKeys = await db
.select() .select()
.from(passkeys) .from(securityKeys)
.where(eq(passkeys.userId, existingUser.userId)); .where(eq(securityKeys.userId, existingUser.userId));
if (userPasskeys.length > 0) { if (userSecurityKeys.length > 0) {
return response<{ usePasskey: boolean }>(res, { return response<{ useSecurityKey: boolean }>(res, {
data: { usePasskey: true }, data: { useSecurityKey: true },
success: true, success: true,
error: false, error: false,
message: "Please use your security key to sign in", message: "Please use your security key to sign in",

View file

@ -4,7 +4,7 @@ import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { User, passkeys, users, webauthnChallenge } from "@server/db"; import { User, securityKeys, users, webauthnChallenge } from "@server/db";
import { eq, and, lt } from "drizzle-orm"; import { eq, and, lt } from "drizzle-orm";
import { response } from "@server/lib"; import { response } from "@server/lib";
import logger from "@server/logger"; import logger from "@server/logger";
@ -55,14 +55,14 @@ setInterval(async () => {
await db await db
.delete(webauthnChallenge) .delete(webauthnChallenge)
.where(lt(webauthnChallenge.expiresAt, now)); .where(lt(webauthnChallenge.expiresAt, now));
logger.debug("Cleaned up expired passkey challenges"); logger.debug("Cleaned up expired security key challenges");
} catch (error) { } catch (error) {
logger.error("Failed to clean up expired passkey challenges", error); logger.error("Failed to clean up expired security key challenges", error);
} }
}, 5 * 60 * 1000); }, 5 * 60 * 1000);
// Helper functions for challenge management // Helper functions for challenge management
async function storeChallenge(sessionId: string, challenge: string, passkeyName?: string, userId?: string) { async function storeChallenge(sessionId: string, challenge: string, securityKeyName?: string, userId?: string) {
const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes
// Delete any existing challenge for this session // Delete any existing challenge for this session
@ -72,7 +72,7 @@ async function storeChallenge(sessionId: string, challenge: string, passkeyName?
await db.insert(webauthnChallenge).values({ await db.insert(webauthnChallenge).values({
sessionId, sessionId,
challenge, challenge,
passkeyName, securityKeyName,
userId, userId,
expiresAt expiresAt
}); });
@ -102,7 +102,7 @@ async function clearChallenge(sessionId: string) {
await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId)); await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId));
} }
export const registerPasskeyBody = z.object({ export const registerSecurityKeyBody = z.object({
name: z.string().min(1), name: z.string().min(1),
password: z.string().min(1) password: z.string().min(1)
}).strict(); }).strict();
@ -119,7 +119,7 @@ export const verifyAuthenticationBody = z.object({
credential: z.any() credential: z.any()
}).strict(); }).strict();
export const deletePasskeyBody = z.object({ export const deleteSecurityKeyBody = z.object({
password: z.string().min(1) password: z.string().min(1)
}).strict(); }).strict();
@ -128,7 +128,7 @@ export async function startRegistration(
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
const parsedBody = registerPasskeyBody.safeParse(req.body); const parsedBody = registerSecurityKeyBody.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
@ -142,12 +142,12 @@ export async function startRegistration(
const { name, password } = parsedBody.data; const { name, password } = parsedBody.data;
const user = req.user as User; const user = req.user as User;
// Only allow internal users to use passkeys // Only allow internal users to use security keys
if (user.type !== UserType.Internal) { if (user.type !== UserType.Internal) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"Passkeys are only available for internal users" "Security keys are only available for internal users"
) )
); );
} }
@ -170,13 +170,13 @@ export async function startRegistration(
}); });
} }
// Get existing passkeys for user // Get existing security keys for user
const existingPasskeys = await db const existingSecurityKeys = await db
.select() .select()
.from(passkeys) .from(securityKeys)
.where(eq(passkeys.userId, user.userId)); .where(eq(securityKeys.userId, user.userId));
const excludeCredentials = existingPasskeys.map(key => ({ const excludeCredentials = existingSecurityKeys.map(key => ({
id: Buffer.from(key.credentialId, 'base64').toString('base64url'), id: Buffer.from(key.credentialId, 'base64').toString('base64url'),
type: 'public-key' as const, type: 'public-key' as const,
transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransport[] : undefined transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransport[] : undefined
@ -237,12 +237,12 @@ export async function verifyRegistration(
const { credential } = parsedBody.data; const { credential } = parsedBody.data;
const user = req.user as User; const user = req.user as User;
// Only allow internal users to use passkeys // Only allow internal users to use security keys
if (user.type !== UserType.Internal) { if (user.type !== UserType.Internal) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"Passkeys are only available for internal users" "Security keys are only available for internal users"
) )
); );
} }
@ -279,14 +279,14 @@ export async function verifyRegistration(
); );
} }
// Store the passkey in the database // Store the security key in the database
await db.insert(passkeys).values({ await db.insert(securityKeys).values({
credentialId: Buffer.from(registrationInfo.credentialID).toString('base64'), credentialId: Buffer.from(registrationInfo.credentialID).toString('base64'),
userId: user.userId, userId: user.userId,
publicKey: Buffer.from(registrationInfo.credentialPublicKey).toString('base64'), publicKey: Buffer.from(registrationInfo.credentialPublicKey).toString('base64'),
signCount: registrationInfo.counter || 0, signCount: registrationInfo.counter || 0,
transports: credential.response.transports ? JSON.stringify(credential.response.transports) : null, transports: credential.response.transports ? JSON.stringify(credential.response.transports) : null,
name: challengeData.passkeyName, name: challengeData.securityKeyName,
lastUsed: new Date().toISOString(), lastUsed: new Date().toISOString(),
dateCreated: new Date().toISOString() dateCreated: new Date().toISOString()
}); });
@ -298,7 +298,7 @@ export async function verifyRegistration(
data: null, data: null,
success: true, success: true,
error: false, error: false,
message: "Passkey registered successfully", message: "Security key registered successfully",
status: HttpCode.OK status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
@ -312,34 +312,34 @@ export async function verifyRegistration(
} }
} }
export async function listPasskeys( export async function listSecurityKeys(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
const user = req.user as User; const user = req.user as User;
// Only allow internal users to use passkeys // Only allow internal users to use security keys
if (user.type !== UserType.Internal) { if (user.type !== UserType.Internal) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"Passkeys are only available for internal users" "Security keys are only available for internal users"
) )
); );
} }
try { try {
const userPasskeys = await db const userSecurityKeys = await db
.select() .select()
.from(passkeys) .from(securityKeys)
.where(eq(passkeys.userId, user.userId)); .where(eq(securityKeys.userId, user.userId));
return response<typeof userPasskeys>(res, { return response<typeof userSecurityKeys>(res, {
data: userPasskeys, data: userSecurityKeys,
success: true, success: true,
error: false, error: false,
message: "Passkeys retrieved successfully", message: "Security keys retrieved successfully",
status: HttpCode.OK status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
@ -347,13 +347,13 @@ export async function listPasskeys(
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
"Failed to retrieve passkeys" "Failed to retrieve security keys"
) )
); );
} }
} }
export async function deletePasskey( export async function deleteSecurityKey(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@ -362,7 +362,7 @@ export async function deletePasskey(
const credentialId = decodeURIComponent(encodedCredentialId); const credentialId = decodeURIComponent(encodedCredentialId);
const user = req.user as User; const user = req.user as User;
const parsedBody = deletePasskeyBody.safeParse(req.body); const parsedBody = deleteSecurityKeyBody.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
@ -375,12 +375,12 @@ export async function deletePasskey(
const { password } = parsedBody.data; const { password } = parsedBody.data;
// Only allow internal users to use passkeys // Only allow internal users to use security keys
if (user.type !== UserType.Internal) { if (user.type !== UserType.Internal) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"Passkeys are only available for internal users" "Security keys are only available for internal users"
) )
); );
} }
@ -404,17 +404,17 @@ export async function deletePasskey(
} }
await db await db
.delete(passkeys) .delete(securityKeys)
.where(and( .where(and(
eq(passkeys.credentialId, credentialId), eq(securityKeys.credentialId, credentialId),
eq(passkeys.userId, user.userId) eq(securityKeys.userId, user.userId)
)); ));
return response<null>(res, { return response<null>(res, {
data: null, data: null,
success: true, success: true,
error: false, error: false,
message: "Passkey deleted successfully", message: "Security key deleted successfully",
status: HttpCode.OK status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
@ -422,7 +422,7 @@ export async function deletePasskey(
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
"Failed to delete passkey" "Failed to delete security key"
) )
); );
} }
@ -454,7 +454,7 @@ export async function startAuthentication(
}> = []; }> = [];
let userId; let userId;
// If email is provided, get passkeys for that specific user // If email is provided, get security keys for that specific user
if (email) { if (email) {
const [user] = await db const [user] = await db
.select() .select()
@ -473,27 +473,27 @@ export async function startAuthentication(
userId = user.userId; userId = user.userId;
const userPasskeys = await db const userSecurityKeys = await db
.select() .select()
.from(passkeys) .from(securityKeys)
.where(eq(passkeys.userId, user.userId)); .where(eq(securityKeys.userId, user.userId));
if (userPasskeys.length === 0) { if (userSecurityKeys.length === 0) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"No passkeys registered for this user" "No security keys registered for this user"
) )
); );
} }
allowCredentials = userPasskeys.map(key => ({ allowCredentials = userSecurityKeys.map(key => ({
id: Buffer.from(key.credentialId, 'base64'), id: Buffer.from(key.credentialId, 'base64'),
type: 'public-key' as const, type: 'public-key' as const,
transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransport[] : undefined transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransport[] : undefined
})); }));
} else { } else {
// If no email provided, allow any passkey (for resident key authentication) // If no email provided, allow any security key (for resident key authentication)
allowCredentials = []; allowCredentials = [];
} }
@ -570,15 +570,15 @@ export async function verifyAuthentication(
); );
} }
// Find the passkey in database // Find the security key in database
const credentialId = Buffer.from(credential.id, 'base64').toString('base64'); const credentialId = Buffer.from(credential.id, 'base64').toString('base64');
const [passkey] = await db const [securityKey] = await db
.select() .select()
.from(passkeys) .from(securityKeys)
.where(eq(passkeys.credentialId, credentialId)) .where(eq(securityKeys.credentialId, credentialId))
.limit(1); .limit(1);
if (!passkey) { if (!securityKey) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -591,14 +591,14 @@ export async function verifyAuthentication(
const [user] = await db const [user] = await db
.select() .select()
.from(users) .from(users)
.where(eq(users.userId, passkey.userId)) .where(eq(users.userId, securityKey.userId))
.limit(1); .limit(1);
if (!user || user.type !== UserType.Internal) { if (!user || user.type !== UserType.Internal) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"User not found or not authorized for passkey authentication" "User not found or not authorized for security key authentication"
) )
); );
} }
@ -609,10 +609,10 @@ export async function verifyAuthentication(
expectedOrigin: origin, expectedOrigin: origin,
expectedRPID: rpID, expectedRPID: rpID,
authenticator: { authenticator: {
credentialID: Buffer.from(passkey.credentialId, 'base64'), credentialID: Buffer.from(securityKey.credentialId, 'base64'),
credentialPublicKey: Buffer.from(passkey.publicKey, 'base64'), credentialPublicKey: Buffer.from(securityKey.publicKey, 'base64'),
counter: passkey.signCount, counter: securityKey.signCount,
transports: passkey.transports ? JSON.parse(passkey.transports) as AuthenticatorTransport[] : undefined transports: securityKey.transports ? JSON.parse(securityKey.transports) as AuthenticatorTransport[] : undefined
}, },
requireUserVerification: false requireUserVerification: false
}); });
@ -630,12 +630,12 @@ export async function verifyAuthentication(
// Update sign count // Update sign count
await db await db
.update(passkeys) .update(securityKeys)
.set({ .set({
signCount: authenticationInfo.newCounter, signCount: authenticationInfo.newCounter,
lastUsed: new Date().toISOString() lastUsed: new Date().toISOString()
}) })
.where(eq(passkeys.credentialId, credentialId)); .where(eq(securityKeys.credentialId, credentialId));
// Create session for the user // Create session for the user
const { createSession, generateSessionToken, serializeSessionCookie } = await import("@server/auth/sessions/app"); const { createSession, generateSessionToken, serializeSessionCookie } = await import("@server/auth/sessions/app");

View file

@ -789,35 +789,35 @@ authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback);
authRouter.put("/set-server-admin", auth.setServerAdmin); authRouter.put("/set-server-admin", auth.setServerAdmin);
authRouter.get("/initial-setup-complete", auth.initialSetupComplete); authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
// Passkey routes // Security Key routes
authRouter.post( authRouter.post(
"/passkey/register/start", "/security-key/register/start",
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Allow 5 passkey registrations per 15 minutes per IP max: 5, // Allow 5 security key registrations per 15 minutes per IP
keyGenerator: (req) => `passkeyRegister:${req.ip}:${req.user?.userId}`, keyGenerator: (req) => `securityKeyRegister:${req.ip}:${req.user?.userId}`,
handler: (req, res, next) => { handler: (req, res, next) => {
const message = `You can only register ${5} passkeys every ${15} minutes. Please try again later.`; const message = `You can only register ${5} security keys every ${15} minutes. Please try again later.`;
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
} }
}), }),
verifySessionUserMiddleware, verifySessionUserMiddleware,
auth.startRegistration auth.startRegistration
); );
authRouter.post("/passkey/register/verify", verifySessionUserMiddleware, auth.verifyRegistration); authRouter.post("/security-key/register/verify", verifySessionUserMiddleware, auth.verifyRegistration);
authRouter.post( authRouter.post(
"/passkey/authenticate/start", "/security-key/authenticate/start",
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Allow 10 authentication attempts per 15 minutes per IP max: 10, // Allow 10 authentication attempts per 15 minutes per IP
keyGenerator: (req) => `passkeyAuth:${req.ip}`, keyGenerator: (req) => `securityKeyAuth:${req.ip}`,
handler: (req, res, next) => { handler: (req, res, next) => {
const message = `You can only attempt passkey authentication ${10} times every ${15} minutes. Please try again later.`; const message = `You can only attempt security key authentication ${10} times every ${15} minutes. Please try again later.`;
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
} }
}), }),
auth.startAuthentication auth.startAuthentication
); );
authRouter.post("/passkey/authenticate/verify", auth.verifyAuthentication); authRouter.post("/security-key/authenticate/verify", auth.verifyAuthentication);
authRouter.get("/passkey/list", verifySessionUserMiddleware, auth.listPasskeys); authRouter.get("/security-key/list", verifySessionUserMiddleware, auth.listSecurityKeys);
authRouter.delete("/passkey/:credentialId", verifySessionUserMiddleware, auth.deletePasskey); authRouter.delete("/security-key/:credentialId", verifySessionUserMiddleware, auth.deleteSecurityKey);

View file

@ -8,7 +8,7 @@ export default async function migration() {
try { try {
db.transaction((trx) => { db.transaction((trx) => {
trx.run(sql`CREATE TABLE 'passkey' ( trx.run(sql`CREATE TABLE 'securityKey' (
'credentialId' text PRIMARY KEY NOT NULL, 'credentialId' text PRIMARY KEY NOT NULL,
'userId' text NOT NULL, 'userId' text NOT NULL,
'publicKey' text NOT NULL, 'publicKey' text NOT NULL,

View file

@ -14,7 +14,7 @@ export default async function migration() {
db.pragma("foreign_keys = OFF"); db.pragma("foreign_keys = OFF");
db.transaction(() => { db.transaction(() => {
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS passkey ( CREATE TABLE IF NOT EXISTS securityKey (
credentialId TEXT PRIMARY KEY, credentialId TEXT PRIMARY KEY,
userId TEXT NOT NULL, userId TEXT NOT NULL,
publicKey TEXT NOT NULL, publicKey TEXT NOT NULL,
@ -28,9 +28,9 @@ export default async function migration() {
`); `);
})(); // executes the transaction immediately })(); // executes the transaction immediately
db.pragma("foreign_keys = ON"); db.pragma("foreign_keys = ON");
console.log(`Created passkey table`); console.log(`Created securityKey table`);
} catch (e) { } catch (e) {
console.error("Unable to create passkey table"); console.error("Unable to create securityKey table");
console.error(e); console.error(e);
throw e; throw e;
} }

View file

@ -14,22 +14,22 @@ export default async function migration() {
db.pragma("foreign_keys = OFF"); db.pragma("foreign_keys = OFF");
db.transaction(() => { db.transaction(() => {
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS passkeyChallenge ( CREATE TABLE IF NOT EXISTS securityKeyChallenge (
sessionId TEXT PRIMARY KEY, sessionId TEXT PRIMARY KEY,
challenge TEXT NOT NULL, challenge TEXT NOT NULL,
passkeyName TEXT, securityKeyName TEXT,
userId TEXT, userId TEXT,
expiresAt INTEGER NOT NULL, expiresAt INTEGER NOT NULL,
FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE
); );
CREATE INDEX IF NOT EXISTS idx_passkeyChallenge_expiresAt ON passkeyChallenge(expiresAt); CREATE INDEX IF NOT EXISTS idx_securityKeyChallenge_expiresAt ON securityKeyChallenge(expiresAt);
`); `);
})(); // executes the transaction immediately })(); // executes the transaction immediately
db.pragma("foreign_keys = ON"); db.pragma("foreign_keys = ON");
console.log(`Created passkeyChallenge table`); console.log(`Created securityKeyChallenge table`);
} catch (e) { } catch (e) {
console.error("Unable to create passkeyChallenge table"); console.error("Unable to create securityKeyChallenge table");
console.error(e); console.error(e);
throw e; throw e;
} }

View file

@ -6,21 +6,21 @@ export default async function migrate() {
// Rename the table // Rename the table
await db.run(` await db.run(`
ALTER TABLE passkeyChallenge RENAME TO webauthnChallenge; ALTER TABLE securityKeyChallenge RENAME TO webauthnChallenge;
`); `);
console.log("Successfully renamed table"); console.log("Successfully renamed table");
// Rename the index // Rename the index
await db.run(` await db.run(`
DROP INDEX IF EXISTS idx_passkeyChallenge_expiresAt; DROP INDEX IF EXISTS idx_securityKeyChallenge_expiresAt;
CREATE INDEX IF NOT EXISTS idx_webauthnChallenge_expiresAt ON webauthnChallenge(expiresAt); CREATE INDEX IF NOT EXISTS idx_webauthnChallenge_expiresAt ON webauthnChallenge(expiresAt);
`); `);
console.log("Successfully updated index"); console.log("Successfully updated index");
console.log(`Renamed passkeyChallenge table to webauthnChallenge`); console.log(`Renamed securityKeyChallenge table to webauthnChallenge`);
return true; return true;
} catch (error: any) { } catch (error: any) {
console.error("Unable to rename passkeyChallenge table:", error); console.error("Unable to rename securityKeyChallenge table:", error);
console.error("Error details:", error.message); console.error("Error details:", error.message);
return false; return false;
} }

View file

@ -4,8 +4,8 @@ import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod"; import * as z from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@app/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@app/components/ui/input";
import { import {
Form, Form,
FormControl, FormControl,
@ -13,15 +13,15 @@ import {
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage FormMessage
} from "@/components/ui/form"; } from "@app/components/ui/form";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from "@/components/ui/card"; } from "@app/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@app/components/ui/alert";
import { LoginResponse } from "@server/routers/auth"; import { LoginResponse } from "@server/routers/auth";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
@ -120,8 +120,8 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
setError(null); setError(null);
const data = res.data.data; const data = res.data.data;
if (data?.usePasskey) { if (data?.useSecurityKey) {
await initiateSecurityKeyAuth(); setShowSecurityKeyPrompt(true);
return; return;
} }
@ -197,7 +197,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
const email = form.getValues().email; const email = form.getValues().email;
// Start WebAuthn authentication // Start WebAuthn authentication
const startRes = await api.post("/auth/passkey/authenticate/start", { const startRes = await api.post("/auth/security-key/authenticate/start", {
email: email || undefined email: email || undefined
}); });
@ -216,7 +216,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
// Verify authentication // Verify authentication
const verifyRes = await api.post( const verifyRes = await api.post(
"/auth/passkey/authenticate/verify", "/auth/security-key/authenticate/verify",
{ credential }, { credential },
{ {
headers: { headers: {
@ -355,9 +355,9 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
pattern={ pattern={
REGEXP_ONLY_DIGITS_AND_CHARS REGEXP_ONLY_DIGITS_AND_CHARS
} }
onChange={(e) => { onChange={(value: string) => {
field.onChange(e); field.onChange(value);
if (e.length === 6) { if (value.length === 6) {
mfaForm.handleSubmit(onSubmit)(); mfaForm.handleSubmit(onSubmit)();
} }
}} }}

View file

@ -108,7 +108,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
const loadSecurityKeys = async () => { const loadSecurityKeys = async () => {
try { try {
const response = await api.get("/auth/passkey/list"); const response = await api.get("/auth/security-key/list");
setSecurityKeys(response.data.data); setSecurityKeys(response.data.data);
} catch (error) { } catch (error) {
toast({ toast({
@ -132,7 +132,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
} }
setIsRegistering(true); setIsRegistering(true);
const startRes = await api.post("/auth/passkey/register/start", { const startRes = await api.post("/auth/security-key/register/start", {
name: values.name, name: values.name,
password: values.password, password: values.password,
}); });
@ -152,7 +152,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
try { try {
const credential = await startRegistration(options); const credential = await startRegistration(options);
await api.post("/auth/passkey/register/verify", { await api.post("/auth/security-key/register/verify", {
credential, credential,
}); });
@ -217,7 +217,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
try { try {
const encodedCredentialId = encodeURIComponent(selectedSecurityKey.credentialId); const encodedCredentialId = encodeURIComponent(selectedSecurityKey.credentialId);
await api.delete(`/auth/passkey/${encodedCredentialId}`, { await api.delete(`/auth/security-key/${encodedCredentialId}`, {
data: { data: {
password: values.password, password: values.password,
} }