2024-07-01 13:11:20 +02:00
|
|
|
import { SessionHandlers } from ':src/session/mod.ts'
|
|
|
|
import { respondApi } from ':src/utils.ts'
|
2024-06-19 16:09:45 +02:00
|
|
|
import {
|
|
|
|
generateRegistrationOptions,
|
|
|
|
verifyRegistrationResponse,
|
|
|
|
} from '@simplewebauthn/server'
|
|
|
|
import type {
|
|
|
|
PublicKeyCredentialCreationOptionsJSON,
|
|
|
|
RegistrationResponseJSON,
|
|
|
|
} from '@simplewebauthn/types'
|
|
|
|
|
|
|
|
//TODO improve workspace imports
|
2024-07-01 13:11:20 +02:00
|
|
|
import { db } from ':src/db/mod.ts'
|
|
|
|
import { getRelyingParty } from ':src/webauthn/mod.ts'
|
2024-07-16 16:34:43 +02:00
|
|
|
import {
|
|
|
|
Credential,
|
|
|
|
Passkey,
|
|
|
|
Ref,
|
|
|
|
User,
|
|
|
|
} from '@cohabit/resources-manager/models'
|
2024-06-19 16:09:45 +02:00
|
|
|
import { encodeBase64 } from '@std/encoding'
|
|
|
|
|
|
|
|
type Params = { step: 'start' | 'finish' }
|
|
|
|
|
|
|
|
export type WebAuthnRegisterStartPayload = { name: string }
|
|
|
|
|
|
|
|
export type WebAuthnRegisterFinishPayload = RegistrationResponseJSON
|
|
|
|
|
|
|
|
export const handler: SessionHandlers = {
|
|
|
|
async POST(req, ctx) {
|
|
|
|
const relyingParty = getRelyingParty(ctx.url)
|
|
|
|
|
|
|
|
const { step } = ctx.params as Params
|
|
|
|
const user = ctx.state.session.get<User>('user')
|
|
|
|
|
|
|
|
if (user === undefined) {
|
|
|
|
return respondApi(
|
|
|
|
'error',
|
|
|
|
new Error('no logged user in current session'),
|
|
|
|
401,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (step === 'start') {
|
|
|
|
const { name } = await req.json() as WebAuthnRegisterStartPayload
|
|
|
|
|
|
|
|
// Get user credentials
|
|
|
|
// Ensure latest user datas
|
2024-07-16 15:29:30 +02:00
|
|
|
const dbUser = await db.resource.user.get(user)
|
2024-06-19 16:09:45 +02:00
|
|
|
// Resolve refs to credentials
|
|
|
|
const resolver = Ref.dbResolver(db)
|
|
|
|
const credentials = await Promise.all(dbUser.credentials.map(resolver))
|
|
|
|
const excludeCredentials = credentials
|
|
|
|
.filter((credential): credential is Credential<'passkey'> =>
|
|
|
|
credential.category === 'passkey'
|
|
|
|
)
|
|
|
|
.map((credential) => credential.store)
|
|
|
|
|
|
|
|
const options = await generateRegistrationOptions({
|
|
|
|
rpName: relyingParty.name,
|
2024-06-24 16:02:53 +02:00
|
|
|
rpID: relyingParty.origin,
|
2024-06-19 16:09:45 +02:00
|
|
|
userName: user.login,
|
|
|
|
attestationType: 'none',
|
|
|
|
excludeCredentials,
|
|
|
|
authenticatorSelection: {
|
|
|
|
residentKey: 'preferred',
|
|
|
|
userVerification: 'preferred',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
ctx.state.session.flash('webauthn-registration', { name, options })
|
|
|
|
|
|
|
|
return respondApi('success', options)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (step === 'finish') {
|
|
|
|
const registration = await req.json() as WebAuthnRegisterFinishPayload
|
|
|
|
|
|
|
|
const { name, options } = ctx.state.session.get<
|
|
|
|
{ name: string; options: PublicKeyCredentialCreationOptionsJSON }
|
|
|
|
>('webauthn-registration')!
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (options === undefined) {
|
|
|
|
throw new Error(`no registration found for ${user.mail}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const verification = await verifyRegistrationResponse({
|
|
|
|
response: registration,
|
|
|
|
expectedChallenge: options.challenge,
|
|
|
|
expectedOrigin: relyingParty.origin,
|
|
|
|
expectedRPID: relyingParty.id,
|
|
|
|
requireUserVerification: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
const { registrationInfo } = verification
|
|
|
|
|
|
|
|
if (registrationInfo === undefined) {
|
|
|
|
throw new Error('no registration info found from verification')
|
|
|
|
}
|
|
|
|
|
|
|
|
const {
|
|
|
|
credentialID,
|
|
|
|
credentialPublicKey,
|
|
|
|
counter,
|
|
|
|
credentialDeviceType,
|
|
|
|
credentialBackedUp,
|
|
|
|
} = registrationInfo
|
|
|
|
|
|
|
|
// Create new Passkey
|
|
|
|
const store: Passkey = {
|
|
|
|
user: user.uuid,
|
|
|
|
webAuthnUserID: options.user.id,
|
|
|
|
id: credentialID,
|
|
|
|
publicKey: encodeBase64(credentialPublicKey),
|
|
|
|
counter,
|
|
|
|
deviceType: credentialDeviceType,
|
|
|
|
backedUp: credentialBackedUp,
|
|
|
|
transports: registration.response.transports,
|
|
|
|
}
|
|
|
|
|
|
|
|
// create and save new Credentials
|
|
|
|
const credential = Credential.load({ name, category: 'passkey', store })
|
2024-07-16 15:29:30 +02:00
|
|
|
await db.resource.credential.set([credential])
|
2024-06-19 16:09:45 +02:00
|
|
|
|
|
|
|
// Update user credentials
|
|
|
|
// Ensure latest user datas
|
2024-07-16 15:29:30 +02:00
|
|
|
const dbUser = await db.resource.user.get(user)
|
2024-06-19 16:09:45 +02:00
|
|
|
// Append new credentials
|
|
|
|
const credentials = [...dbUser.credentials, credential.toRef()]
|
|
|
|
const updatedUser = user.update({ credentials })
|
|
|
|
// Save user to db
|
2024-07-16 15:29:30 +02:00
|
|
|
await db.resource.user.set([updatedUser])
|
2024-06-19 16:09:45 +02:00
|
|
|
// Update session
|
|
|
|
ctx.state.session.set('user', updatedUser)
|
|
|
|
|
|
|
|
const { verified } = verification
|
|
|
|
|
|
|
|
return respondApi('success', { verified })
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error)
|
|
|
|
return respondApi('error', error, 400)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return respondApi('error', new Error('unknown step'), 400)
|
|
|
|
},
|
|
|
|
}
|