From 111a5843ef6eaff1c8f3ba60f6dc841b80c2ee08 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 3 Jun 2026 17:03:00 +0200 Subject: [PATCH] refactor: reject all non uv passkeys --- app/lib/sso-errors.ts | 2 +- app/server/core/config.ts | 24 ++++++++++++++++++++---- app/server/lib/auth.ts | 19 ++++++++----------- package.json | 2 +- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/lib/sso-errors.ts b/app/lib/sso-errors.ts index 8c0f83eb..9ed1363b 100644 --- a/app/lib/sso-errors.ts +++ b/app/lib/sso-errors.ts @@ -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"; } diff --git a/app/server/core/config.ts b/app/server/core/config.ts index bc9ab7d8..53a354b4 100644 --- a/app/server/core/config.ts +++ b/app/server/core/config.ts @@ -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, diff --git a/app/server/lib/auth.ts b/app/server/lib/auth.ts index 48cbca27..c1acae3b 100644 --- a/app/server/lib/auth.ts +++ b/app/server/lib/auth.ts @@ -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.", - }); - } }, }, }), diff --git a/package.json b/package.json index df604f50..c8935e28 100644 --- a/package.json +++ b/package.json @@ -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",