58 lines
3.1 KiB
TypeScript
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' });
|
|
};
|