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
No known key found for this signature in database
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": case "SSO_LOGIN_FAILED":
return "SSO authentication failed. Please try again."; return "SSO authentication failed. Please try again.";
case PASSKEY_LOGIN_FAILED_ERROR: 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": case "ERROR_INVALID_RP_ID":
return "You can only sign in with a passkey on the domain set by the BASE_URL environment variable"; 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(), MIGRATIONS_PATH: z.string().optional(),
APP_VERSION: z.string().default("dev"), APP_VERSION: z.string().default("dev"),
TRUSTED_ORIGINS: z.string().optional(), TRUSTED_ORIGINS: z.string().optional(),
PORTLESS_URL: z.string().optional(),
PORTLESS_TAILSCALE_URL: z.string().optional(),
TRUST_PROXY: z.string().default("false"), TRUST_PROXY: z.string().default("false"),
DISABLE_RATE_LIMITING: 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()), 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(), PROVISIONING_PATH: z.string().optional(),
}) })
.transform((s, ctx) => { .transform((s, ctx) => {
const baseUrl = unquote(s.BASE_URL); let baseUrl = unquote(s.BASE_URL);
const trustedOrigins = s.TRUSTED_ORIGINS?.split(",").map(unquote).filter(Boolean).concat(baseUrl) ?? [baseUrl]; 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 webhookAllowedOrigins = s.WEBHOOK_ALLOWED_ORIGINS?.split(",").map(unquote).filter(Boolean) ?? [];
const authOrigins = [baseUrl, ...trustedOrigins]; const authOrigins = [baseUrl, ...uniqueTrustedOrigins];
const { allowedHosts, invalidOrigins } = buildAllowedHosts(authOrigins); const { allowedHosts, invalidOrigins } = buildAllowedHosts(authOrigins);
let appSecret = s.APP_SECRET; let appSecret = s.APP_SECRET;
@ -108,7 +124,7 @@ const envSchema = z
port: s.PORT, port: s.PORT,
migrationsPath: s.MIGRATIONS_PATH, migrationsPath: s.MIGRATIONS_PATH,
appVersion: s.APP_VERSION, appVersion: s.APP_VERSION,
trustedOrigins: trustedOrigins, trustedOrigins: uniqueTrustedOrigins,
trustProxy: s.TRUST_PROXY === "true", trustProxy: s.TRUST_PROXY === "true",
appSecret: appSecret ?? "", appSecret: appSecret ?? "",
baseUrl, baseUrl,

View file

@ -174,23 +174,20 @@ export const auth = betterAuth({
passkey({ passkey({
rpID: new URL(config.baseUrl).hostname, rpID: new URL(config.baseUrl).hostname,
rpName: "Zerobyte", rpName: "Zerobyte",
authenticatorSelection: {
userVerification: "required",
residentKey: "required",
},
authentication: { authentication: {
afterVerification: async ({ verification, clientData }) => { afterVerification: async ({ verification }) => {
if (verification.authenticationInfo.userVerified) { if (verification.authenticationInfo.userVerified) {
return; return;
} }
const passkeyRecord = await db.query.passkey.findFirst({ throw new APIError("UNAUTHORIZED", {
where: { credentialID: clientData.id }, message:
with: { usersTable: { columns: { twoFactorEnabled: true } } }, "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: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: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", "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", "gen:migrations": "dotenv -e .env.local -- drizzle-kit generate",
"staged": "vp staged", "staged": "vp staged",
"studio": "drizzle-kit studio", "studio": "drizzle-kit studio",