refactor: reject all non uv passkeys

This commit is contained in:
Nicolas Meienberger
2026-06-03 17:03:00 +02:00
parent 49e3977199
commit 111a5843ef
4 changed files with 30 additions and 17 deletions

View File

@@ -25,7 +25,7 @@ export function getLoginErrorDescription(errorCode: LoginErrorCode): string {
case "SSO_LOGIN_FAILED":
return "SSO authentication failed. Please try again.";
case PASSKEY_LOGIN_FAILED_ERROR:
return "Passkey sign-in failed. If 2FA is enabled, use a passkey protected by a PIN, biometrics, or screen lock, or sign in with your password and authenticator code.";
return "Passkey sign-in failed. The passkey didn't verify your identity with a PIN, biometrics, or screen lock. Please use a verified passkey or sign in with your password.";
case "ERROR_INVALID_RP_ID":
return "You can only sign in with a passkey on the domain set by the BASE_URL environment variable";
}

View File

@@ -18,6 +18,8 @@ const envSchema = z
MIGRATIONS_PATH: z.string().optional(),
APP_VERSION: z.string().default("dev"),
TRUSTED_ORIGINS: z.string().optional(),
PORTLESS_URL: z.string().optional(),
PORTLESS_TAILSCALE_URL: z.string().optional(),
TRUST_PROXY: z.string().default("false"),
DISABLE_RATE_LIMITING: z.string().default("false"),
APP_SECRET: z.preprocess((value) => (value === "" ? undefined : value), z.string().min(32).max(256).optional()),
@@ -29,10 +31,24 @@ const envSchema = z
PROVISIONING_PATH: z.string().optional(),
})
.transform((s, ctx) => {
const baseUrl = unquote(s.BASE_URL);
const trustedOrigins = s.TRUSTED_ORIGINS?.split(",").map(unquote).filter(Boolean).concat(baseUrl) ?? [baseUrl];
let baseUrl = unquote(s.BASE_URL);
const trustedOrigins = s.TRUSTED_ORIGINS?.split(",").map(unquote).filter(Boolean) ?? [];
if (s.NODE_ENV === "development") {
if (s.PORTLESS_URL) {
trustedOrigins.push(unquote(s.PORTLESS_URL));
}
if (s.PORTLESS_TAILSCALE_URL) {
baseUrl = unquote(s.PORTLESS_TAILSCALE_URL);
trustedOrigins.push(baseUrl);
}
}
trustedOrigins.push(baseUrl);
const uniqueTrustedOrigins = Array.from(new Set(trustedOrigins));
const webhookAllowedOrigins = s.WEBHOOK_ALLOWED_ORIGINS?.split(",").map(unquote).filter(Boolean) ?? [];
const authOrigins = [baseUrl, ...trustedOrigins];
const authOrigins = [baseUrl, ...uniqueTrustedOrigins];
const { allowedHosts, invalidOrigins } = buildAllowedHosts(authOrigins);
let appSecret = s.APP_SECRET;
@@ -108,7 +124,7 @@ const envSchema = z
port: s.PORT,
migrationsPath: s.MIGRATIONS_PATH,
appVersion: s.APP_VERSION,
trustedOrigins: trustedOrigins,
trustedOrigins: uniqueTrustedOrigins,
trustProxy: s.TRUST_PROXY === "true",
appSecret: appSecret ?? "",
baseUrl,

View File

@@ -174,23 +174,20 @@ export const auth = betterAuth({
passkey({
rpID: new URL(config.baseUrl).hostname,
rpName: "Zerobyte",
authenticatorSelection: {
userVerification: "required",
residentKey: "required",
},
authentication: {
afterVerification: async ({ verification, clientData }) => {
afterVerification: async ({ verification }) => {
if (verification.authenticationInfo.userVerified) {
return;
}
const passkeyRecord = await db.query.passkey.findFirst({
where: { credentialID: clientData.id },
with: { usersTable: { columns: { twoFactorEnabled: true } } },
throw new APIError("UNAUTHORIZED", {
message:
"Your passkey was accepted, but it did not confirm your identity with a PIN, biometrics, or screen lock. Please use a verified passkey or sign in with your password.",
});
if (passkeyRecord?.usersTable?.twoFactorEnabled) {
throw new APIError("UNAUTHORIZED", {
message:
"Your passkey was accepted, but it did not confirm your identity with a PIN, biometrics, or screen lock. Because 2FA is enabled on this account, please use a verified passkey or sign in with your password and authenticator code.",
});
}
},
},
}),

View File

@@ -25,7 +25,7 @@
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
"start:e2e": "docker compose down && mkdir -p playwright/data playwright/temp playwright/tinyauth/app-data playwright/tinyauth/caddy-data && rm -rf playwright/data/* playwright/.auth/user.json playwright/restic.pass playwright/temp/* playwright/tinyauth/app-data/* && docker compose up --build zerobyte-e2e",
"start:distroless": "docker compose down && docker compose up --build zerobyte-distroless",
"gen:api-client": "dotenv -e .env.local -- openapi-ts",
"gen:api-client": "NODE_TLS_REJECT_UNAUTHORIZED=0 dotenv -e .env.local -- openapi-ts",
"gen:migrations": "dotenv -e .env.local -- drizzle-kit generate",
"staged": "vp staged",
"studio": "drizzle-kit studio",