Files
drop/server/api/v1/auth/passkey/finish.post.ts
Husky f04daf0388 Add ODIC Back-Channel Logout (#304)
* prevent returning expired sessions

* add issuer to ODIC creds

* get id token in ODIC

* make session signin return session

* working backchannel logout?

* require https for ODIC provider

* handle wellknown not being https

* find session api progress

* fix windows build

* return session token on session

* switch OIDC to #searchSessions

* update pnpm

* switch to using message on error obj

* move odic callback

* fix type errors

* redirect old oidc callback

* make redirect url a URL

* remove scheduled task downloadCleanup

* fix session search for oidc

* fix signin result

* cleanup code

* ignore data dir

* fix lint error
2026-01-20 09:50:04 +11:00

108 lines
3.1 KiB
TypeScript

import { verifyAuthenticationResponse } from "@simplewebauthn/server";
import { MFAMec } from "~/prisma/client/enums";
import { dropDecodeArrayBase64 } from "~/server/internal/auth/totp";
import type { WebAuthNv1Credentials } from "~/server/internal/auth/webauthn";
import { getRpId } from "~/server/internal/auth/webauthn";
import { systemConfig } from "~/server/internal/config/sys-conf";
import prisma from "~/server/internal/db/database";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => {
const body = await readBody(h3);
const credentialId = body?.id;
if (!credentialId || typeof credentialId !== "string")
throw createError({
statusCode: 400,
message: "Missing credential id in body.",
});
const optionsRaw = await sessionHandler.getSessionDataKey<string>(
h3,
"webauthn/options",
);
if (!optionsRaw)
throw createError({
statusCode: 400,
message: "WebAuthn setup not started for this session.",
});
const options = JSON.parse(optionsRaw);
await sessionHandler.deleteSessionDataKey(h3, "webauthn/challenge");
// See WebAuthNv1Credentials for schema
const mfaMec = await prisma.linkedMFAMec.findFirst({
where: {
credentials: {
path: ["passkeys"],
array_contains: [
{
id: credentialId,
},
],
},
},
});
if (!mfaMec)
throw createError({ statusCode: 404, message: "Passkey not found" });
const passkeys = (mfaMec.credentials as unknown as WebAuthNv1Credentials)
.passkeys;
const passkeyIndex = passkeys.findIndex((v) => v.id === credentialId);
const passkey = passkeys[passkeyIndex]; // Exists guarantee by database
const rpID = await getRpId();
const externalUrl = await systemConfig.getExternalUrl();
const url = new URL(externalUrl);
let verification;
try {
verification = await verifyAuthenticationResponse({
response: body,
expectedChallenge: options.challenge,
expectedOrigin: url.origin,
expectedRPID: rpID,
credential: {
id: passkey.id,
publicKey: Buffer.from(dropDecodeArrayBase64(passkey.publicKey)),
counter: passkey.counter,
transports: passkey.transports ?? [],
},
});
} catch (error) {
throw createError({
statusCode: 400,
message: (error as string)?.toString(),
});
}
const { verified } = verification;
if (!verified)
throw createError({ statusCode: 403, message: "Invalid passkey." });
const { authenticationInfo } = verification;
const { newCounter } = authenticationInfo;
passkeys[passkeyIndex].counter = newCounter;
(mfaMec.credentials as unknown as WebAuthNv1Credentials).passkeys = passkeys;
// Safe because we query it before
// eslint-disable-next-line drop/no-prisma-delete
await prisma.linkedMFAMec.update({
where: {
userId_mec: {
userId: mfaMec.userId,
mec: MFAMec.WebAuthn,
},
},
data: {
credentials: mfaMec.credentials!,
},
});
await sessionHandler.signin(h3, mfaMec.userId, {
rememberMe: true,
});
await sessionHandler.mfa(h3, 10);
return {};
});