Fixed Origin mismatch for passkeys
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m9s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m9s
This commit is contained in:
@@ -2,4 +2,5 @@ DATA_DIR=./data
|
|||||||
ROOT_DIR=./data
|
ROOT_DIR=./data
|
||||||
APP_PORT=4321
|
APP_PORT=4321
|
||||||
IMAGE=git.atri.dad/atash/chronus:latest
|
IMAGE=git.atri.dad/atash/chronus:latest
|
||||||
JWT_SECRET=some-secret
|
JWT_SECRET=some-secret
|
||||||
|
ORIGIN=https://chronus.example.com
|
||||||
@@ -9,6 +9,7 @@ services:
|
|||||||
- PORT=4321
|
- PORT=4321
|
||||||
- DATA_DIR=/app/data
|
- DATA_DIR=/app/data
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- ORIGIN=${ORIGIN}
|
||||||
volumes:
|
volumes:
|
||||||
- ${ROOT_DIR}:/app/data
|
- ${ROOT_DIR}:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -55,6 +55,22 @@ export async function getUserFromToken(token: string) {
|
|||||||
return user ?? null;
|
return user ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the public origin and hostname from the ORIGIN environment variable.
|
||||||
|
* This is required for WebAuthn/passkey rpID to match the browser's origin.
|
||||||
|
*/
|
||||||
|
export function getOrigin(): { hostname: string; origin: string } {
|
||||||
|
const origin = process.env.ORIGIN;
|
||||||
|
if (!origin) {
|
||||||
|
throw new Error("ORIGIN environment variable is not set");
|
||||||
|
}
|
||||||
|
const url = new URL(origin);
|
||||||
|
return {
|
||||||
|
hostname: url.hostname,
|
||||||
|
origin: url.origin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function hashPassword(password: string) {
|
export async function hashPassword(password: string) {
|
||||||
return await bcrypt.hash(password, 10);
|
return await bcrypt.hash(password, 10);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { verifyAuthenticationResponse } from "@simplewebauthn/server";
|
|||||||
import { db } from "../../../../../db";
|
import { db } from "../../../../../db";
|
||||||
import { users, passkeys, passkeyChallenges } from "../../../../../db/schema";
|
import { users, passkeys, passkeyChallenges } from "../../../../../db/schema";
|
||||||
import { eq, and, gt } from "drizzle-orm";
|
import { eq, and, gt } from "drizzle-orm";
|
||||||
import { setAuthCookie } from "../../../../../lib/auth";
|
import { setAuthCookie, getOrigin } from "../../../../../lib/auth";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request, cookies }) => {
|
export const POST: APIRoute = async ({ request, cookies }) => {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@@ -50,11 +50,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|||||||
|
|
||||||
let verification;
|
let verification;
|
||||||
try {
|
try {
|
||||||
|
const { origin, hostname } = getOrigin();
|
||||||
verification = await verifyAuthenticationResponse({
|
verification = await verifyAuthenticationResponse({
|
||||||
response: body,
|
response: body,
|
||||||
expectedChallenge: challenge as string,
|
expectedChallenge: challenge as string,
|
||||||
expectedOrigin: new URL(request.url).origin,
|
expectedOrigin: origin,
|
||||||
expectedRPID: new URL(request.url).hostname,
|
expectedRPID: hostname,
|
||||||
credential: {
|
credential: {
|
||||||
id: passkey.id,
|
id: passkey.id,
|
||||||
publicKey: new Uint8Array(Buffer.from(passkey.publicKey, "base64")),
|
publicKey: new Uint8Array(Buffer.from(passkey.publicKey, "base64")),
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ import { generateAuthenticationOptions } from "@simplewebauthn/server";
|
|||||||
import { db } from "../../../../../db";
|
import { db } from "../../../../../db";
|
||||||
import { passkeyChallenges } from "../../../../../db/schema";
|
import { passkeyChallenges } from "../../../../../db/schema";
|
||||||
import { lte } from "drizzle-orm";
|
import { lte } from "drizzle-orm";
|
||||||
|
import { getOrigin } from "../../../../../lib/auth";
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
await db
|
await db
|
||||||
.delete(passkeyChallenges)
|
.delete(passkeyChallenges)
|
||||||
.where(lte(passkeyChallenges.expiresAt, new Date()));
|
.where(lte(passkeyChallenges.expiresAt, new Date()));
|
||||||
|
|
||||||
|
const { hostname } = getOrigin();
|
||||||
|
|
||||||
const options = await generateAuthenticationOptions({
|
const options = await generateAuthenticationOptions({
|
||||||
rpID: new URL(request.url).hostname,
|
rpID: hostname,
|
||||||
userVerification: "preferred",
|
userVerification: "preferred",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { verifyRegistrationResponse } from "@simplewebauthn/server";
|
|||||||
import { db } from "../../../../../db";
|
import { db } from "../../../../../db";
|
||||||
import { passkeys, passkeyChallenges } from "../../../../../db/schema";
|
import { passkeys, passkeyChallenges } from "../../../../../db/schema";
|
||||||
import { eq, and, gt } from "drizzle-orm";
|
import { eq, and, gt } from "drizzle-orm";
|
||||||
|
import { getOrigin } from "../../../../../lib/auth";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request, locals }) => {
|
export const POST: APIRoute = async ({ request, locals }) => {
|
||||||
const user = locals.user;
|
const user = locals.user;
|
||||||
@@ -41,11 +42,12 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
|
|
||||||
let verification;
|
let verification;
|
||||||
try {
|
try {
|
||||||
|
const { origin, hostname } = getOrigin();
|
||||||
verification = await verifyRegistrationResponse({
|
verification = await verifyRegistrationResponse({
|
||||||
response: body,
|
response: body,
|
||||||
expectedChallenge: challenge,
|
expectedChallenge: challenge,
|
||||||
expectedOrigin: new URL(request.url).origin,
|
expectedOrigin: origin,
|
||||||
expectedRPID: new URL(request.url).hostname,
|
expectedRPID: hostname,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Passkey registration verification failed:", error);
|
console.error("Passkey registration verification failed:", error);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { generateRegistrationOptions } from "@simplewebauthn/server";
|
|||||||
import { db } from "../../../../../db";
|
import { db } from "../../../../../db";
|
||||||
import { passkeys, passkeyChallenges } from "../../../../../db/schema";
|
import { passkeys, passkeyChallenges } from "../../../../../db/schema";
|
||||||
import { eq, lte } from "drizzle-orm";
|
import { eq, lte } from "drizzle-orm";
|
||||||
|
import { getOrigin } from "../../../../../lib/auth";
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request, locals }) => {
|
export const GET: APIRoute = async ({ request, locals }) => {
|
||||||
const user = locals.user;
|
const user = locals.user;
|
||||||
@@ -21,9 +22,11 @@ export const GET: APIRoute = async ({ request, locals }) => {
|
|||||||
where: eq(passkeys.userId, user.id),
|
where: eq(passkeys.userId, user.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { hostname } = getOrigin();
|
||||||
|
|
||||||
const options = await generateRegistrationOptions({
|
const options = await generateRegistrationOptions({
|
||||||
rpName: "Chronus",
|
rpName: "Chronus",
|
||||||
rpID: new URL(request.url).hostname,
|
rpID: hostname,
|
||||||
userName: user.email,
|
userName: user.email,
|
||||||
attestationType: "none",
|
attestationType: "none",
|
||||||
excludeCredentials: userPasskeys.map((passkey) => ({
|
excludeCredentials: userPasskeys.map((passkey) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user