Files
shd.build/src/routes/auth/passkey/authenticate/verify/+server.ts
T
jb dd84dff055
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s
feat: add community publishing and news syndication
2026-08-01 21:42:09 -04:00

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' });
};