feat: ✨ add login form and passkey register form
This commit is contained in:
parent
09bab92bca
commit
e590767b29
|
@ -34,6 +34,9 @@
|
|||
"@jotsr/smart-css-bundler": "jsr:@jotsr/smart-css-bundler@^0.3.0",
|
||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
|
||||
"@simplewebauthn/browser": "npm:@simplewebauthn/browser@^10.0.0",
|
||||
"@simplewebauthn/server": "npm:@simplewebauthn/server@^10.0.0",
|
||||
"@simplewebauthn/types": "npm:@simplewebauthn/types@^10.0.0",
|
||||
"@std/http": "jsr:@std/http@^0.224.4",
|
||||
"@univoq/": "https://deno.land/x/univoq@0.2.0/",
|
||||
"gfm": "https://deno.land/x/gfm@0.6.0/mod.ts",
|
||||
|
|
97
islands/LoginForm.tsx
Normal file
97
islands/LoginForm.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { startAuthentication } from '@simplewebauthn/browser'
|
||||
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'
|
||||
import { Button, Input } from 'univoq'
|
||||
import type {
|
||||
WebAuthnLoginFinishPayload,
|
||||
WebAuthnLoginStartPayload,
|
||||
} from '../routes/api/webauthn/login/[step].ts'
|
||||
import { requestApi } from '../src/utils.ts'
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<form onSubmit={connect} method='POST' action=''>
|
||||
<Input
|
||||
label='Email'
|
||||
name='email'
|
||||
autoComplete='email'
|
||||
type='email'
|
||||
required
|
||||
/>
|
||||
<Button label='Connection' variant='primary'>Connection</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
type LoginFormFields = {
|
||||
email: string
|
||||
}
|
||||
|
||||
async function connect(event: Event) {
|
||||
if (!(event instanceof SubmitEvent)) return false
|
||||
event.preventDefault()
|
||||
const form = event.target as HTMLFormElement
|
||||
const fields = formJSON<LoginFormFields>(form)
|
||||
|
||||
try {
|
||||
// Try PassKey connection
|
||||
if (!isWebAuthnSupported()) {
|
||||
throw new Error('WebAuthn is not supported by your browser')
|
||||
}
|
||||
|
||||
await webAuthnLogin(fields)
|
||||
|
||||
// Reload UI
|
||||
location.reload()
|
||||
} catch (error) {
|
||||
// Check is user stop passkey connection
|
||||
if (error instanceof Error && error.name === 'NotAllowedError') {
|
||||
form.reset()
|
||||
return
|
||||
}
|
||||
// Else use magic link
|
||||
await magicLinkLogin(fields)
|
||||
|
||||
// Reload UI
|
||||
location.reload()
|
||||
}
|
||||
|
||||
form.reset()
|
||||
}
|
||||
|
||||
async function magicLinkLogin(fields: LoginFormFields) {
|
||||
await requestApi('magiclink', 'POST', fields)
|
||||
console.log('Un lien de connection vous a été envoyé par mail !')
|
||||
}
|
||||
|
||||
async function webAuthnLogin(fields: LoginFormFields) {
|
||||
// Send WebAuthn authentication options
|
||||
const authenticationOptions = await requestApi<
|
||||
WebAuthnLoginStartPayload,
|
||||
PublicKeyCredentialRequestOptionsJSON
|
||||
>('webauthn/login/start', 'POST', fields)
|
||||
|
||||
// Pass the options to the authenticator and wait for a response
|
||||
const authentication = await startAuthentication(authenticationOptions)
|
||||
|
||||
// Verify authentication
|
||||
const verification = await requestApi<
|
||||
WebAuthnLoginFinishPayload,
|
||||
{ verified: boolean }
|
||||
>('webauthn/login/finish', 'POST', { ...fields, authentication })
|
||||
|
||||
// Show UI appropriate for the `verified` status
|
||||
if (verification.verified) {
|
||||
console.log('Success!')
|
||||
} else {
|
||||
console.error('PassKey was not verified!')
|
||||
}
|
||||
}
|
||||
|
||||
function isWebAuthnSupported(): boolean {
|
||||
return 'credentials' in navigator
|
||||
}
|
||||
|
||||
function formJSON<T extends Record<string, unknown>>(form: HTMLFormElement): T {
|
||||
const formData = new FormData(form)
|
||||
return Object.fromEntries(formData) as unknown as T
|
||||
}
|
94
islands/PassKeyRegister.tsx
Normal file
94
islands/PassKeyRegister.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import { startRegistration } from '@simplewebauthn/browser'
|
||||
import { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types'
|
||||
import { Button } from 'univoq'
|
||||
import type {
|
||||
WebAuthnRegisterFinishPayload,
|
||||
WebAuthnRegisterStartPayload,
|
||||
} from '../routes/api/webauthn/register/[step].ts'
|
||||
import { requestApi } from '../src/utils.ts'
|
||||
|
||||
function isWebAuthnSupported(): boolean {
|
||||
return 'credentials' in navigator
|
||||
}
|
||||
|
||||
function RegisterButton({ disabled }: { disabled?: boolean }) {
|
||||
return (
|
||||
<Button
|
||||
label='Enregistrer une PassKey'
|
||||
variant='primary'
|
||||
onClick={register}
|
||||
disabled={disabled}
|
||||
>
|
||||
Enregistrer une PassKey
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PassKeyRegister() {
|
||||
if (!isWebAuthnSupported()) {
|
||||
return (
|
||||
<label>
|
||||
<span>Erreur: WebAuthn n'est pas supporté par votre navigateur</span>
|
||||
<RegisterButton disabled />
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
<span>Enregistrer une PassKey pour cet appareil</span>
|
||||
<RegisterButton />
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
async function register() {
|
||||
try {
|
||||
await webAuthnRegister()
|
||||
} catch (cause) {
|
||||
console.error(
|
||||
new Error('passkey register failed', {
|
||||
cause,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function webAuthnRegister() {
|
||||
if (localStorage.getItem('webauthn-registered')) {
|
||||
return
|
||||
}
|
||||
|
||||
const registrationOptions = await requestApi<
|
||||
WebAuthnRegisterStartPayload,
|
||||
PublicKeyCredentialCreationOptionsJSON
|
||||
>('webauthn/register/start', 'POST')
|
||||
|
||||
// Pass the options to the authenticator and wait for a response
|
||||
const registration = await startRegistration(registrationOptions)
|
||||
.catch((error) => {
|
||||
// Some basic error handling
|
||||
if (error.name === 'InvalidStateError') {
|
||||
console.error(
|
||||
'Authenticator was probably already registered by user',
|
||||
)
|
||||
return null
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
if (registration === null) return
|
||||
|
||||
// Verify the registration
|
||||
try {
|
||||
await requestApi<
|
||||
WebAuthnRegisterFinishPayload,
|
||||
{ verified: boolean }
|
||||
>('webauthn/register/finish', 'POST', registration)
|
||||
console.log('Success!')
|
||||
localStorage.setItem('webauthn-registered', 'true')
|
||||
} catch (error) {
|
||||
console.error('Oh no, something went wrong! Response:', error)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue