refactor: reject all non uv passkeys
This commit is contained in:
parent
49e3977199
commit
111a5843ef
4 changed files with 30 additions and 17 deletions
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue