Files
shd.build/src/routes/auth/passkey/authenticate/verify/+server.ts
T

58 lines
3.1 KiB
TypeScript

import { ensureBootstrapAdmin } from '$lib/server/auth/bootstrap';
import { consumeChallenge } from '$lib/server/auth/passkeys';
import { createSession, recordLogin } from '$lib/server/auth/session';
import { getDatabase } from '$lib/server/database';
import { verifyAuthenticationResponse, type AuthenticationResponseJSON, type AuthenticatorTransportFuture } from '@simplewebauthn/server';
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
function bytes(value: unknown): Uint8Array<ArrayBuffer> {
if (value instanceof Uint8Array) {
const copy = new Uint8Array(value.byteLength);
copy.set(value);
return copy;
}
if (value instanceof ArrayBuffer) return new Uint8Array(value.slice(0));
if (Array.isArray(value)) return new Uint8Array(value);
throw new Error('Invalid passkey public key.');
}
export const POST: RequestHandler = async ({ cookies, request }) => {
const body = await request.json() as { challengeId?: string; response?: AuthenticationResponseJSON };
if (!body.challengeId || !body.response) error(400, 'Incomplete passkey authentication.');
const database = await getDatabase();
const challenge = await consumeChallenge(database, body.challengeId, 'authentication');
if (!challenge) error(400, 'The passkey request expired. Try again.');
const passkey = await (await database.prepare(`SELECT passkeys.*,user_status.blocked_at,user_status.banned_until,deleted_accounts.deleted_at
FROM passkeys JOIN users ON users.id=passkeys.user_id
LEFT JOIN user_status ON user_status.user_id=users.id
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id WHERE passkeys.id=?`)).get(body.response.id);
if (!passkey) error(400, 'This passkey is not registered with SHD.build.');
const unavailable = passkey.deleted_at || passkey.blocked_at ||
(typeof passkey.banned_until === 'string' && new Date(passkey.banned_until) > new Date());
if (unavailable) {
await recordLogin(String(passkey.user_id), 'passkey', false);
error(403, 'This account is not permitted to sign in.');
}
const transports = passkey.transports ? JSON.parse(String(passkey.transports)) as AuthenticatorTransportFuture[] : undefined;
const verification = await verifyAuthenticationResponse({
response: body.response,
expectedChallenge: String(challenge.challenge),
expectedOrigin: String(challenge.origin),
expectedRPID: String(challenge.rp_id),
credential: { id: String(passkey.id), publicKey: bytes(passkey.public_key), counter: Number(passkey.counter), transports },
requireUserVerification: true
});
if (!verification.verified) {
await recordLogin(String(passkey.user_id), 'passkey', false);
error(400, 'Passkey authentication could not be verified.');
}
const now = new Date().toISOString();
await (await database.prepare('UPDATE passkeys SET counter=?,last_used_at=? WHERE id=?'))
.run(verification.authenticationInfo.newCounter, now, passkey.id);
await ensureBootstrapAdmin(database, String(passkey.user_id));
await createSession(cookies, String(passkey.user_id));
await recordLogin(String(passkey.user_id), 'passkey', true);
return json({ verified: true, redirectTo: '/account' });
};