mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-25 21:25:06 +02:00
renamed passkey to security key to stay aligned with the UI and other backend naming.
This commit is contained in:
parent
6ccc05b183
commit
5009906385
13 changed files with 158 additions and 118 deletions
|
@ -491,10 +491,24 @@ export const idpOrg = pgTable("idpOrg", {
|
|||
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", {
|
||||
sessionId: varchar("sessionId").primaryKey(),
|
||||
challenge: varchar("challenge").notNull(),
|
||||
passkeyName: varchar("passkeyName"),
|
||||
securityKeyName: varchar("securityKeyName"),
|
||||
userId: varchar("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
|
|
|
@ -7,12 +7,37 @@ import type { Database as BetterSqlite3Database } from "better-sqlite3";
|
|||
|
||||
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 () => {
|
||||
console.log("Running migrations...");
|
||||
try {
|
||||
// Initialize the database file with a valid SQLite header
|
||||
const sqlite = new Database(location) as BetterSqlite3Database;
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
|
||||
// Drop all existing tables first
|
||||
dropAllTables(sqlite);
|
||||
|
||||
// Run the migrations
|
||||
migrate(db as any, {
|
||||
|
|
|
@ -135,7 +135,7 @@ export const users = sqliteTable("user", {
|
|||
.default(false)
|
||||
});
|
||||
|
||||
export const passkeys = sqliteTable("webauthnCredentials", {
|
||||
export const securityKeys = sqliteTable("webauthnCredentials", {
|
||||
credentialId: text("credentialId").primaryKey(),
|
||||
userId: text("userId").notNull().references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
|
@ -151,7 +151,7 @@ export const passkeys = sqliteTable("webauthnCredentials", {
|
|||
export const webauthnChallenge = sqliteTable("webauthnChallenge", {
|
||||
sessionId: text("sessionId").primaryKey(),
|
||||
challenge: text("challenge").notNull(),
|
||||
passkeyName: text("passkeyName"),
|
||||
securityKeyName: text("securityKeyName"),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
|
|
|
@ -6,10 +6,10 @@ export * from "./requestTotpSecret";
|
|||
export * from "./disable2fa";
|
||||
export * from "./verifyEmail";
|
||||
export * from "./requestEmailVerificationCode";
|
||||
export * from "./changePassword";
|
||||
export * from "./requestPasswordReset";
|
||||
export * from "./resetPassword";
|
||||
export * from "./checkResourceSession";
|
||||
export * from "./requestPasswordReset";
|
||||
export * from "./setServerAdmin";
|
||||
export * from "./initialSetupComplete";
|
||||
export * from "./passkey";
|
||||
export * from "./changePassword";
|
||||
export * from "./checkResourceSession";
|
||||
export * from "./securityKey";
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
serializeSessionCookie
|
||||
} from "@server/auth/sessions/app";
|
||||
import { db } from "@server/db";
|
||||
import { users, passkeys } from "@server/db";
|
||||
import { users, securityKeys } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
@ -35,6 +35,7 @@ export type LoginBody = z.infer<typeof loginBodySchema>;
|
|||
export type LoginResponse = {
|
||||
codeRequested?: boolean;
|
||||
emailVerificationRequired?: boolean;
|
||||
useSecurityKey?: boolean;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
@ -91,15 +92,15 @@ export async function login(
|
|||
|
||||
const existingUser = existingUserRes[0];
|
||||
|
||||
// Check if user has passkeys registered
|
||||
const userPasskeys = await db
|
||||
// Check if user has security keys registered
|
||||
const userSecurityKeys = await db
|
||||
.select()
|
||||
.from(passkeys)
|
||||
.where(eq(passkeys.userId, existingUser.userId));
|
||||
.from(securityKeys)
|
||||
.where(eq(securityKeys.userId, existingUser.userId));
|
||||
|
||||
if (userPasskeys.length > 0) {
|
||||
return response<{ usePasskey: boolean }>(res, {
|
||||
data: { usePasskey: true },
|
||||
if (userSecurityKeys.length > 0) {
|
||||
return response<{ useSecurityKey: boolean }>(res, {
|
||||
data: { useSecurityKey: true },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Please use your security key to sign in",
|
||||
|
|
|
@ -4,7 +4,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
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 { response } from "@server/lib";
|
||||
import logger from "@server/logger";
|
||||
|
@ -55,14 +55,14 @@ setInterval(async () => {
|
|||
await db
|
||||
.delete(webauthnChallenge)
|
||||
.where(lt(webauthnChallenge.expiresAt, now));
|
||||
logger.debug("Cleaned up expired passkey challenges");
|
||||
logger.debug("Cleaned up expired security key challenges");
|
||||
} 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);
|
||||
|
||||
// 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
|
||||
|
||||
// Delete any existing challenge for this session
|
||||
|
@ -72,7 +72,7 @@ async function storeChallenge(sessionId: string, challenge: string, passkeyName?
|
|||
await db.insert(webauthnChallenge).values({
|
||||
sessionId,
|
||||
challenge,
|
||||
passkeyName,
|
||||
securityKeyName,
|
||||
userId,
|
||||
expiresAt
|
||||
});
|
||||
|
@ -102,7 +102,7 @@ async function clearChallenge(sessionId: string) {
|
|||
await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId));
|
||||
}
|
||||
|
||||
export const registerPasskeyBody = z.object({
|
||||
export const registerSecurityKeyBody = z.object({
|
||||
name: z.string().min(1),
|
||||
password: z.string().min(1)
|
||||
}).strict();
|
||||
|
@ -119,7 +119,7 @@ export const verifyAuthenticationBody = z.object({
|
|||
credential: z.any()
|
||||
}).strict();
|
||||
|
||||
export const deletePasskeyBody = z.object({
|
||||
export const deleteSecurityKeyBody = z.object({
|
||||
password: z.string().min(1)
|
||||
}).strict();
|
||||
|
||||
|
@ -128,7 +128,7 @@ export async function startRegistration(
|
|||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const parsedBody = registerPasskeyBody.safeParse(req.body);
|
||||
const parsedBody = registerSecurityKeyBody.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
|
@ -142,12 +142,12 @@ export async function startRegistration(
|
|||
const { name, password } = parsedBody.data;
|
||||
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) {
|
||||
return next(
|
||||
createHttpError(
|
||||
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
|
||||
const existingPasskeys = await db
|
||||
// Get existing security keys for user
|
||||
const existingSecurityKeys = await db
|
||||
.select()
|
||||
.from(passkeys)
|
||||
.where(eq(passkeys.userId, user.userId));
|
||||
.from(securityKeys)
|
||||
.where(eq(securityKeys.userId, user.userId));
|
||||
|
||||
const excludeCredentials = existingPasskeys.map(key => ({
|
||||
const excludeCredentials = existingSecurityKeys.map(key => ({
|
||||
id: Buffer.from(key.credentialId, 'base64').toString('base64url'),
|
||||
type: 'public-key' as const,
|
||||
transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransport[] : undefined
|
||||
|
@ -237,12 +237,12 @@ export async function verifyRegistration(
|
|||
const { credential } = parsedBody.data;
|
||||
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) {
|
||||
return next(
|
||||
createHttpError(
|
||||
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
|
||||
await db.insert(passkeys).values({
|
||||
// Store the security key in the database
|
||||
await db.insert(securityKeys).values({
|
||||
credentialId: Buffer.from(registrationInfo.credentialID).toString('base64'),
|
||||
userId: user.userId,
|
||||
publicKey: Buffer.from(registrationInfo.credentialPublicKey).toString('base64'),
|
||||
signCount: registrationInfo.counter || 0,
|
||||
transports: credential.response.transports ? JSON.stringify(credential.response.transports) : null,
|
||||
name: challengeData.passkeyName,
|
||||
name: challengeData.securityKeyName,
|
||||
lastUsed: new Date().toISOString(),
|
||||
dateCreated: new Date().toISOString()
|
||||
});
|
||||
|
@ -298,7 +298,7 @@ export async function verifyRegistration(
|
|||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Passkey registered successfully",
|
||||
message: "Security key registered successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -312,34 +312,34 @@ export async function verifyRegistration(
|
|||
}
|
||||
}
|
||||
|
||||
export async function listPasskeys(
|
||||
export async function listSecurityKeys(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
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) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Passkeys are only available for internal users"
|
||||
"Security keys are only available for internal users"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const userPasskeys = await db
|
||||
const userSecurityKeys = await db
|
||||
.select()
|
||||
.from(passkeys)
|
||||
.where(eq(passkeys.userId, user.userId));
|
||||
.from(securityKeys)
|
||||
.where(eq(securityKeys.userId, user.userId));
|
||||
|
||||
return response<typeof userPasskeys>(res, {
|
||||
data: userPasskeys,
|
||||
return response<typeof userSecurityKeys>(res, {
|
||||
data: userSecurityKeys,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Passkeys retrieved successfully",
|
||||
message: "Security keys retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -347,13 +347,13 @@ export async function listPasskeys(
|
|||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to retrieve passkeys"
|
||||
"Failed to retrieve security keys"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePasskey(
|
||||
export async function deleteSecurityKey(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
|
@ -362,7 +362,7 @@ export async function deletePasskey(
|
|||
const credentialId = decodeURIComponent(encodedCredentialId);
|
||||
const user = req.user as User;
|
||||
|
||||
const parsedBody = deletePasskeyBody.safeParse(req.body);
|
||||
const parsedBody = deleteSecurityKeyBody.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
|
@ -375,12 +375,12 @@ export async function deletePasskey(
|
|||
|
||||
const { password } = parsedBody.data;
|
||||
|
||||
// Only allow internal users to use passkeys
|
||||
// Only allow internal users to use security keys
|
||||
if (user.type !== UserType.Internal) {
|
||||
return next(
|
||||
createHttpError(
|
||||
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
|
||||
.delete(passkeys)
|
||||
.delete(securityKeys)
|
||||
.where(and(
|
||||
eq(passkeys.credentialId, credentialId),
|
||||
eq(passkeys.userId, user.userId)
|
||||
eq(securityKeys.credentialId, credentialId),
|
||||
eq(securityKeys.userId, user.userId)
|
||||
));
|
||||
|
||||
return response<null>(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Passkey deleted successfully",
|
||||
message: "Security key deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -422,7 +422,7 @@ export async function deletePasskey(
|
|||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to delete passkey"
|
||||
"Failed to delete security key"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -454,7 +454,7 @@ export async function startAuthentication(
|
|||
}> = [];
|
||||
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) {
|
||||
const [user] = await db
|
||||
.select()
|
||||
|
@ -473,27 +473,27 @@ export async function startAuthentication(
|
|||
|
||||
userId = user.userId;
|
||||
|
||||
const userPasskeys = await db
|
||||
const userSecurityKeys = await db
|
||||
.select()
|
||||
.from(passkeys)
|
||||
.where(eq(passkeys.userId, user.userId));
|
||||
.from(securityKeys)
|
||||
.where(eq(securityKeys.userId, user.userId));
|
||||
|
||||
if (userPasskeys.length === 0) {
|
||||
if (userSecurityKeys.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
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'),
|
||||
type: 'public-key' as const,
|
||||
transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransport[] : undefined
|
||||
}));
|
||||
} 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 = [];
|
||||
}
|
||||
|
||||
|
@ -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 [passkey] = await db
|
||||
const [securityKey] = await db
|
||||
.select()
|
||||
.from(passkeys)
|
||||
.where(eq(passkeys.credentialId, credentialId))
|
||||
.from(securityKeys)
|
||||
.where(eq(securityKeys.credentialId, credentialId))
|
||||
.limit(1);
|
||||
|
||||
if (!passkey) {
|
||||
if (!securityKey) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
|
@ -591,14 +591,14 @@ export async function verifyAuthentication(
|
|||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.userId, passkey.userId))
|
||||
.where(eq(users.userId, securityKey.userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user || user.type !== UserType.Internal) {
|
||||
return next(
|
||||
createHttpError(
|
||||
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,
|
||||
expectedRPID: rpID,
|
||||
authenticator: {
|
||||
credentialID: Buffer.from(passkey.credentialId, 'base64'),
|
||||
credentialPublicKey: Buffer.from(passkey.publicKey, 'base64'),
|
||||
counter: passkey.signCount,
|
||||
transports: passkey.transports ? JSON.parse(passkey.transports) as AuthenticatorTransport[] : undefined
|
||||
credentialID: Buffer.from(securityKey.credentialId, 'base64'),
|
||||
credentialPublicKey: Buffer.from(securityKey.publicKey, 'base64'),
|
||||
counter: securityKey.signCount,
|
||||
transports: securityKey.transports ? JSON.parse(securityKey.transports) as AuthenticatorTransport[] : undefined
|
||||
},
|
||||
requireUserVerification: false
|
||||
});
|
||||
|
@ -630,12 +630,12 @@ export async function verifyAuthentication(
|
|||
|
||||
// Update sign count
|
||||
await db
|
||||
.update(passkeys)
|
||||
.update(securityKeys)
|
||||
.set({
|
||||
signCount: authenticationInfo.newCounter,
|
||||
lastUsed: new Date().toISOString()
|
||||
})
|
||||
.where(eq(passkeys.credentialId, credentialId));
|
||||
.where(eq(securityKeys.credentialId, credentialId));
|
||||
|
||||
// Create session for the user
|
||||
const { createSession, generateSessionToken, serializeSessionCookie } = await import("@server/auth/sessions/app");
|
|
@ -789,35 +789,35 @@ authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback);
|
|||
authRouter.put("/set-server-admin", auth.setServerAdmin);
|
||||
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
|
||||
|
||||
// Passkey routes
|
||||
// Security Key routes
|
||||
authRouter.post(
|
||||
"/passkey/register/start",
|
||||
"/security-key/register/start",
|
||||
rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // Allow 5 passkey registrations per 15 minutes per IP
|
||||
keyGenerator: (req) => `passkeyRegister:${req.ip}:${req.user?.userId}`,
|
||||
max: 5, // Allow 5 security key registrations per 15 minutes per IP
|
||||
keyGenerator: (req) => `securityKeyRegister:${req.ip}:${req.user?.userId}`,
|
||||
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));
|
||||
}
|
||||
}),
|
||||
verifySessionUserMiddleware,
|
||||
auth.startRegistration
|
||||
);
|
||||
authRouter.post("/passkey/register/verify", verifySessionUserMiddleware, auth.verifyRegistration);
|
||||
authRouter.post("/security-key/register/verify", verifySessionUserMiddleware, auth.verifyRegistration);
|
||||
authRouter.post(
|
||||
"/passkey/authenticate/start",
|
||||
"/security-key/authenticate/start",
|
||||
rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
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) => {
|
||||
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));
|
||||
}
|
||||
}),
|
||||
auth.startAuthentication
|
||||
);
|
||||
authRouter.post("/passkey/authenticate/verify", auth.verifyAuthentication);
|
||||
authRouter.get("/passkey/list", verifySessionUserMiddleware, auth.listPasskeys);
|
||||
authRouter.delete("/passkey/:credentialId", verifySessionUserMiddleware, auth.deletePasskey);
|
||||
authRouter.post("/security-key/authenticate/verify", auth.verifyAuthentication);
|
||||
authRouter.get("/security-key/list", verifySessionUserMiddleware, auth.listSecurityKeys);
|
||||
authRouter.delete("/security-key/:credentialId", verifySessionUserMiddleware, auth.deleteSecurityKey);
|
||||
|
|
|
@ -8,7 +8,7 @@ export default async function migration() {
|
|||
|
||||
try {
|
||||
db.transaction((trx) => {
|
||||
trx.run(sql`CREATE TABLE 'passkey' (
|
||||
trx.run(sql`CREATE TABLE 'securityKey' (
|
||||
'credentialId' text PRIMARY KEY NOT NULL,
|
||||
'userId' text NOT NULL,
|
||||
'publicKey' text NOT NULL,
|
||||
|
|
|
@ -14,7 +14,7 @@ export default async function migration() {
|
|||
db.pragma("foreign_keys = OFF");
|
||||
db.transaction(() => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS passkey (
|
||||
CREATE TABLE IF NOT EXISTS securityKey (
|
||||
credentialId TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
publicKey TEXT NOT NULL,
|
||||
|
@ -28,9 +28,9 @@ export default async function migration() {
|
|||
`);
|
||||
})(); // executes the transaction immediately
|
||||
db.pragma("foreign_keys = ON");
|
||||
console.log(`Created passkey table`);
|
||||
console.log(`Created securityKey table`);
|
||||
} catch (e) {
|
||||
console.error("Unable to create passkey table");
|
||||
console.error("Unable to create securityKey table");
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -14,22 +14,22 @@ export default async function migration() {
|
|||
db.pragma("foreign_keys = OFF");
|
||||
db.transaction(() => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS passkeyChallenge (
|
||||
CREATE TABLE IF NOT EXISTS securityKeyChallenge (
|
||||
sessionId TEXT PRIMARY KEY,
|
||||
challenge TEXT NOT NULL,
|
||||
passkeyName TEXT,
|
||||
securityKeyName TEXT,
|
||||
userId TEXT,
|
||||
expiresAt INTEGER NOT NULL,
|
||||
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
|
||||
db.pragma("foreign_keys = ON");
|
||||
console.log(`Created passkeyChallenge table`);
|
||||
console.log(`Created securityKeyChallenge table`);
|
||||
} catch (e) {
|
||||
console.error("Unable to create passkeyChallenge table");
|
||||
console.error("Unable to create securityKeyChallenge table");
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -6,21 +6,21 @@ export default async function migrate() {
|
|||
|
||||
// Rename the table
|
||||
await db.run(`
|
||||
ALTER TABLE passkeyChallenge RENAME TO webauthnChallenge;
|
||||
ALTER TABLE securityKeyChallenge RENAME TO webauthnChallenge;
|
||||
`);
|
||||
console.log("Successfully renamed table");
|
||||
|
||||
// Rename the index
|
||||
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);
|
||||
`);
|
||||
console.log("Successfully updated index");
|
||||
|
||||
console.log(`Renamed passkeyChallenge table to webauthnChallenge`);
|
||||
console.log(`Renamed securityKeyChallenge table to webauthnChallenge`);
|
||||
return true;
|
||||
} 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);
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import { useState } from "react";
|
|||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
|
@ -13,15 +13,15 @@ import {
|
|||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
} from "@app/components/ui/form";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
} from "@app/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { LoginResponse } from "@server/routers/auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
@ -120,8 +120,8 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
|||
setError(null);
|
||||
const data = res.data.data;
|
||||
|
||||
if (data?.usePasskey) {
|
||||
await initiateSecurityKeyAuth();
|
||||
if (data?.useSecurityKey) {
|
||||
setShowSecurityKeyPrompt(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -197,7 +197,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
|||
const email = form.getValues().email;
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
|
@ -216,7 +216,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
|||
|
||||
// Verify authentication
|
||||
const verifyRes = await api.post(
|
||||
"/auth/passkey/authenticate/verify",
|
||||
"/auth/security-key/authenticate/verify",
|
||||
{ credential },
|
||||
{
|
||||
headers: {
|
||||
|
@ -355,9 +355,9 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
|||
pattern={
|
||||
REGEXP_ONLY_DIGITS_AND_CHARS
|
||||
}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
if (e.length === 6) {
|
||||
onChange={(value: string) => {
|
||||
field.onChange(value);
|
||||
if (value.length === 6) {
|
||||
mfaForm.handleSubmit(onSubmit)();
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -108,7 +108,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
|
|||
|
||||
const loadSecurityKeys = async () => {
|
||||
try {
|
||||
const response = await api.get("/auth/passkey/list");
|
||||
const response = await api.get("/auth/security-key/list");
|
||||
setSecurityKeys(response.data.data);
|
||||
} catch (error) {
|
||||
toast({
|
||||
|
@ -132,7 +132,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
|
|||
}
|
||||
|
||||
setIsRegistering(true);
|
||||
const startRes = await api.post("/auth/passkey/register/start", {
|
||||
const startRes = await api.post("/auth/security-key/register/start", {
|
||||
name: values.name,
|
||||
password: values.password,
|
||||
});
|
||||
|
@ -152,7 +152,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
|
|||
try {
|
||||
const credential = await startRegistration(options);
|
||||
|
||||
await api.post("/auth/passkey/register/verify", {
|
||||
await api.post("/auth/security-key/register/verify", {
|
||||
credential,
|
||||
});
|
||||
|
||||
|
@ -217,7 +217,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
|
|||
|
||||
try {
|
||||
const encodedCredentialId = encodeURIComponent(selectedSecurityKey.credentialId);
|
||||
await api.delete(`/auth/passkey/${encodedCredentialId}`, {
|
||||
await api.delete(`/auth/security-key/${encodedCredentialId}`, {
|
||||
data: {
|
||||
password: values.password,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue