104 lines
3.7 KiB
TypeScript
104 lines
3.7 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,
|
|
getClientAddress,
|
|
}) => {
|
|
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, {
|
|
request,
|
|
getClientAddress,
|
|
failureReason: 'account_restricted',
|
|
credentialId: String(passkey.id),
|
|
});
|
|
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, {
|
|
request,
|
|
getClientAddress,
|
|
failureReason: 'verification_failed',
|
|
credentialId: String(passkey.id),
|
|
});
|
|
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));
|
|
const sessionId = await createSession(cookies, String(passkey.user_id));
|
|
await recordLogin(String(passkey.user_id), 'passkey', true, {
|
|
request,
|
|
getClientAddress,
|
|
credentialId: String(passkey.id),
|
|
sessionId,
|
|
});
|
|
return json({ verified: true, redirectTo: '/account' });
|
|
};
|