Skip to content

Commit 5392da9

Browse files
authored
feat(webauthn): add event to validateUser to track authenticated users (#287)
1 parent fc0d991 commit 5392da9

File tree

5 files changed

+49
-25
lines changed

5 files changed

+49
-25
lines changed

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,21 @@ The following code does not include the actual database queries, but shows the g
351351
import { z } from 'zod'
352352
export default defineWebAuthnRegisterEventHandler({
353353
// optional
354-
validateUser: z.object({
355-
// we want the userName to be a valid email
356-
userName: z.string().email()
357-
}).parse,
354+
async validateUser(userBody, event) {
355+
// bonus: check if the user is already authenticated to link a credential to his account
356+
// We first check if the user is already authenticated by getting the session
357+
// And verify that the email is the same as the one in session
358+
const session = await getUserSession(event)
359+
if (session.user?.email && session.user.email !== body.userName) {
360+
throw createError({ statusCode: 400, message: 'Email not matching curent session' })
361+
}
362+
363+
// If he registers a new account with credentials
364+
return z.object({
365+
// we want the userName to be a valid email
366+
userName: z.string().email()
367+
}).parse(userBody)
368+
},
358369
async onSuccess(event, { credential, user }) {
359370
// The credential creation has been successful
360371
// We need to create a user if it does not exist

playground/server/api/webauthn/register.post.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
import { z } from 'zod'
22

33
export default defineWebAuthnRegisterEventHandler({
4-
validateUser: z.object({
5-
userName: z.string().email().trim(),
6-
displayName: z.string().trim().optional(),
7-
company: z.string().trim().optional(),
8-
}).parse,
4+
async validateUser(userBody, event) {
5+
const session = await getUserSession(event)
6+
if (session.user?.email && session.user.email !== userBody.userName) {
7+
throw createError({ statusCode: 400, message: 'Email not matching curent session' })
8+
}
9+
10+
return z.object({
11+
userName: z.string().email().trim(),
12+
displayName: z.string().trim().optional(),
13+
company: z.string().trim().optional(),
14+
}).parse(userBody)
15+
},
916
async onSuccess(event, { credential, user }) {
1017
const db = useDatabase()
1118
try {
1219
await db.sql`BEGIN TRANSACTION`
13-
await db.sql`INSERT INTO users (email) VALUES (${user.userName})`
14-
const { rows: [dbUser] } = await db.sql`SELECT * FROM users WHERE email = ${user.userName}`
20+
let { rows: [dbUser] } = await db.sql`SELECT * FROM users WHERE email = ${user.userName}`
21+
if (!dbUser) {
22+
await db.sql`INSERT INTO users (email) VALUES (${user.userName})`
23+
dbUser = (await db.sql`SELECT * FROM users WHERE email = ${user.userName}`).rows?.[0]
24+
}
1525
await db.sql`
1626
INSERT INTO credentials (
1727
userId,

src/runtime/app/composables/webauthn.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import type { VerifiedAuthenticationResponse, VerifiedRegistrationResponse } from '@simplewebauthn/server'
99
import type { PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'
1010
import { ref, onMounted } from '#imports'
11-
import type { WebAuthnComposable } from '#auth-utils'
11+
import type { WebAuthnComposable, WebAuthnUser } from '#auth-utils'
1212

1313
interface RegistrationInitResponse {
1414
creationOptions: PublicKeyCredentialCreationOptionsJSON
@@ -43,7 +43,7 @@ export function useWebAuthn(options: {
4343
useBrowserAutofill = false,
4444
} = options
4545

46-
async function register(user: { userName: string, displayName?: string }) {
46+
async function register(user: WebAuthnUser) {
4747
const { creationOptions, attemptId } = await $fetch<RegistrationInitResponse>(registerEndpoint, {
4848
method: 'POST',
4949
body: {

src/runtime/server/lib/webauthn/register.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { eventHandler, H3Error, createError, getRequestURL, readBody } from 'h3'
2-
import type { ValidateFunction } from 'h3'
2+
import type { H3Event } from 'h3'
33
import type { GenerateRegistrationOptionsOpts } from '@simplewebauthn/server'
44
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'
55
import defu from 'defu'
66
import { bufferToBase64URLString } from '@simplewebauthn/browser'
77
import { getRandomValues } from 'uncrypto'
88
import { useRuntimeConfig } from '#imports'
99
import type { WebAuthnUser, WebAuthnRegisterEventHandlerOptions } from '#auth-utils'
10-
import type { RegistrationBody } from '~/src/runtime/types/webauthn'
10+
import type { RegistrationBody, ValidateUserFunction } from '~/src/runtime/types/webauthn'
1111

1212
export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({
1313
storeChallenge,
@@ -29,7 +29,7 @@ export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({
2929

3030
let user = body.user
3131
if (validateUser) {
32-
user = await validateUserData(body.user, validateUser)
32+
user = await validateUserData(body.user, event, validateUser)
3333
}
3434

3535
const _config = defu(await getOptions?.(event, body) ?? {}, useRuntimeConfig(event).webauthn.register, {
@@ -118,18 +118,19 @@ export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({
118118

119119
// Taken from h3
120120
export async function validateUserData<T>(
121-
data: unknown,
122-
fn: ValidateFunction<T>,
121+
userBody: WebAuthnUser,
122+
event: H3Event,
123+
fn: ValidateUserFunction<T>,
123124
): Promise<T> {
124125
try {
125-
const res = await fn(data)
126+
const res = await fn(userBody, event)
126127
if (res === false) {
127128
throw createUserValidationError()
128129
}
129130
if (res === true) {
130-
return data as T
131+
return userBody as T
131132
}
132-
return res ?? (data as T)
133+
return res ?? (userBody as T)
133134
}
134135
catch (error) {
135136
throw createUserValidationError(error as Error)
@@ -138,8 +139,8 @@ export async function validateUserData<T>(
138139
// eslint-disable-next-line @typescript-eslint/no-explicit-any
139140
function createUserValidationError(validateError?: any) {
140141
throw createError({
141-
status: 400,
142-
message: 'User Validation Error',
142+
status: validateError?.statusCode || 400,
143+
message: validateError?.message || 'User Validation Error',
143144
data: validateError,
144145
})
145146
}

src/runtime/types/webauthn.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AuthenticationResponseJSON, AuthenticatorTransportFuture, RegistrationResponseJSON } from '@simplewebauthn/types'
22
import type { Ref } from 'vue'
3-
import type { H3Event, H3Error, ValidateFunction } from 'h3'
3+
import type { H3Event, H3Error, ValidateResult } from 'h3'
44
import type {
55
GenerateAuthenticationOptionsOpts,
66
GenerateRegistrationOptionsOpts,
@@ -48,13 +48,15 @@ export type RegistrationBody<T extends WebAuthnUser> = {
4848
response: RegistrationResponseJSON
4949
}
5050

51+
export type ValidateUserFunction<T> = (userBody: WebAuthnUser, event: H3Event) => ValidateResult<T> | Promise<ValidateResult<T>>
52+
5153
export type WebAuthnRegisterEventHandlerOptions<T extends WebAuthnUser> = WebAuthnEventHandlerBase<{
5254
user: T
5355
credential: WebAuthnCredential
5456
registrationInfo: Exclude<VerifiedRegistrationResponse['registrationInfo'], undefined>
5557
}> & {
5658
getOptions?: (event: H3Event, body: RegistrationBody<T>) => Partial<GenerateRegistrationOptionsOpts> | Promise<Partial<GenerateRegistrationOptionsOpts>>
57-
validateUser?: ValidateFunction<T>
59+
validateUser?: ValidateUserFunction<T>
5860
excludeCredentials?: (event: H3Event, userName: string) => CredentialsList | Promise<CredentialsList>
5961
}
6062

0 commit comments

Comments
 (0)