Files
bewcloud/islands/auth/PasswordlessPasskeyLogin.tsx
0xGingi 455a7201e9 Add Optional 2FA Support (#61)
* Add TOTP MFA Support

* Add Passkey MFA Support

It's not impossible I missed some minor cleanup, but most things make sense and there isn't a lot of obvious duplication anymore.

---------

Co-authored-by: Bruno Bernardino <me@brunobernardino.com>
2025-05-29 17:30:28 +01:00

117 lines
3.3 KiB
TypeScript

import { useSignal } from '@preact/signals';
import { startAuthentication } from '@simplewebauthn/browser';
import {
RequestBody as PasskeyBeginRequestBody,
ResponseBody as PasskeyBeginResponseBody,
} from '/routes/api/auth/multi-factor/passkey/begin.ts';
import {
RequestBody as PasskeyVerifyRequestBody,
ResponseBody as PasskeyVerifyResponseBody,
} from '/routes/api/auth/multi-factor/passkey/verify.ts';
interface PasswordlessPasskeyLoginProps {
email?: string;
redirectUrl?: string;
}
export default function PasswordlessPasskeyLogin({ email: providedEmail, redirectUrl }: PasswordlessPasskeyLoginProps) {
const isLoading = useSignal(false);
const email = useSignal<string | null>(providedEmail || null);
const error = useSignal<string | null>(null);
const handlePasswordlessLogin = async () => {
if (isLoading.value) {
return;
}
if (!email.value) {
const promptEmail = prompt('Please enter your email');
if (!promptEmail) {
throw new Error('Email is required to login with Passkey');
}
email.value = promptEmail;
}
isLoading.value = true;
error.value = null;
try {
const beginRequestBody: PasskeyBeginRequestBody = {
email: email.value,
};
const beginResponse = await fetch('/api/auth/multi-factor/passkey/begin', {
method: 'POST',
body: JSON.stringify(beginRequestBody),
});
if (!beginResponse.ok) {
throw new Error(
`Failed to begin passwordless login! ${beginResponse.statusText} ${await beginResponse.text()}`,
);
}
const beginData = await beginResponse.json() as PasskeyBeginResponseBody;
if (!beginData.success) {
throw new Error(beginData.error || 'Failed to begin passwordless login');
}
const authenticationResponse = await startAuthentication({
optionsJSON: beginData.options!,
});
const verifyRequestBody: PasskeyVerifyRequestBody = {
email: email.value,
challenge: beginData.sessionData!.challenge,
authenticationResponse,
redirectUrl: redirectUrl || '/',
};
const verifyResponse = await fetch('/api/auth/multi-factor/passkey/verify', {
method: 'POST',
body: JSON.stringify(verifyRequestBody),
});
if (verifyResponse.ok) {
window.location.href = redirectUrl || '/';
return;
}
const verifyData = await verifyResponse.json() as PasskeyVerifyResponseBody;
throw new Error(
verifyData.error || `Authentication failed! ${verifyResponse.statusText} ${await verifyResponse.text()}`,
);
} catch (handleError) {
console.error('Passwordless passkey login failed:', handleError);
error.value = (handleError as Error).message;
} finally {
isLoading.value = false;
}
};
return (
<section class='space-y-4'>
<section class='flex justify-center mt-2 mb-4'>
<button
type='button'
onClick={handlePasswordlessLogin}
class='button-secondary'
>
{isLoading.value ? 'Authenticating...' : 'Login with Passkey'}
</button>
</section>
{error.value
? (
<section class='notification-error'>
<p>{error.value}</p>
</section>
)
: null}
</section>
);
}