From 9abb21971ca1d4ef91d7193d3367b37b5e68741e Mon Sep 17 00:00:00 2001 From: Gerben Mulder Date: Wed, 30 Oct 2024 17:20:05 +0100 Subject: [PATCH 1/4] fix: do not register composable when not using webauthn --- src/module.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/module.ts b/src/module.ts index b5103e66..3c39067c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3,7 +3,7 @@ import { defineNuxtModule, addPlugin, createResolver, - addImportsDir, + addImports, addServerHandler, addServerPlugin, addServerImportsDir, @@ -60,9 +60,17 @@ export default defineNuxtModule({ './runtime/types/index', ) + const composables = [ + { name: 'useUserSession', from: resolver.resolve('./runtime/app/composables/oauth') }, + ] + + if (options.webAuthn) { + composables.push({ name: 'useWebAuthn', from: resolver.resolve('./runtime/app/composables/webauthn') }) + } + // App addComponentsDir({ path: resolver.resolve('./runtime/app/components') }) - addImportsDir(resolver.resolve('./runtime/app/composables')) + addImports(composables) addPlugin(resolver.resolve('./runtime/app/plugins/session.server')) addPlugin(resolver.resolve('./runtime/app/plugins/session.client')) // Server From 426a34bed555dc7e92471d01b4433f7b264eae53 Mon Sep 17 00:00:00 2001 From: Gerben Mulder Date: Wed, 30 Oct 2024 17:20:55 +0100 Subject: [PATCH 2/4] fix: credential registration option type --- src/runtime/types/webauthn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/types/webauthn.ts b/src/runtime/types/webauthn.ts index 6de5bdda..3b1d363e 100644 --- a/src/runtime/types/webauthn.ts +++ b/src/runtime/types/webauthn.ts @@ -43,7 +43,7 @@ export type WebAuthnRegisterEventHandlerOptions = WebAut credential: WebAuthnCredential registrationInfo: Exclude }> & { - getOptions?: (event: H3Event) => GenerateRegistrationOptionsOpts | Promise + getOptions?: (event: H3Event) => Partial | Promise> validateUser?: ValidateFunction } From 1c7eb02c0a982b12a925657b47138eb362a56187 Mon Sep 17 00:00:00 2001 From: Gerben Mulder Date: Wed, 30 Oct 2024 17:37:37 +0100 Subject: [PATCH 3/4] fix: file name --- src/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/module.ts b/src/module.ts index 3c39067c..66a3967c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -61,7 +61,7 @@ export default defineNuxtModule({ ) const composables = [ - { name: 'useUserSession', from: resolver.resolve('./runtime/app/composables/oauth') }, + { name: 'useUserSession', from: resolver.resolve('./runtime/app/composables/session') }, ] if (options.webAuthn) { From f79e6090e16d19126a5a1dd1cbfe6d124830ccac Mon Sep 17 00:00:00 2001 From: Gerben Mulder Date: Wed, 30 Oct 2024 19:37:17 +0100 Subject: [PATCH 4/4] fix: only run allowCredentials on first request & add excludeCredentials function --- .../server/lib/webauthn/authenticate.ts | 23 +++++--------- src/runtime/server/lib/webauthn/register.ts | 19 +++++------- src/runtime/types/webauthn.ts | 31 ++++++++++++++++--- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/runtime/server/lib/webauthn/authenticate.ts b/src/runtime/server/lib/webauthn/authenticate.ts index c8ba0c9b..c3453495 100644 --- a/src/runtime/server/lib/webauthn/authenticate.ts +++ b/src/runtime/server/lib/webauthn/authenticate.ts @@ -2,21 +2,11 @@ import { eventHandler, H3Error, createError, getRequestURL, readBody } from 'h3' import type { GenerateAuthenticationOptionsOpts } from '@simplewebauthn/server' import { generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server' import defu from 'defu' -import type { AuthenticationResponseJSON } from '@simplewebauthn/types' import { getRandomValues } from 'uncrypto' import { base64URLStringToBuffer, bufferToBase64URLString } from '@simplewebauthn/browser' import { useRuntimeConfig } from '#imports' import type { WebAuthnAuthenticateEventHandlerOptions, WebAuthnCredential } from '#auth-utils' - -type AuthenticationBody = { - verify: false - userName?: string -} | { - verify: true - attemptId: string - userName?: string - response: AuthenticationResponseJSON -} +import type { AuthenticationBody } from '~/src/runtime/types/webauthn' export function defineWebAuthnAuthenticateEventHandler({ storeChallenge, @@ -30,20 +20,20 @@ export function defineWebAuthnAuthenticateEventHandler { const url = getRequestURL(event) const body = await readBody(event) - const _config = defu(await getOptions?.(event) ?? {}, useRuntimeConfig(event).webauthn.authenticate, { + const _config = defu(await getOptions?.(event, body) ?? {}, useRuntimeConfig(event).webauthn.authenticate, { rpID: url.hostname, } satisfies GenerateAuthenticationOptionsOpts) - if (allowCredentials && body.userName) { - _config.allowCredentials = await allowCredentials(event, body.userName) - } - if (!storeChallenge) { _config.challenge = '' } try { if (!body.verify) { + if (allowCredentials && body.userName) { + _config.allowCredentials = await allowCredentials(event, body.userName) + } + const options = await generateAuthenticationOptions(_config as GenerateAuthenticationOptionsOpts) const attemptId = bufferToBase64URLString(getRandomValues(new Uint8Array(32))) @@ -71,6 +61,7 @@ export function defineWebAuthnAuthenticateEventHandler = { - user: T - verify: false -} | { - user: T - verify: true - attemptId: string - response: RegistrationResponseJSON -} +import type { RegistrationBody } from '~/src/runtime/types/webauthn' export function defineWebAuthnRegisterEventHandler({ storeChallenge, getChallenge, getOptions, validateUser, + excludeCredentials, onSuccess, onError, }: WebAuthnRegisterEventHandlerOptions) { @@ -41,7 +32,7 @@ export function defineWebAuthnRegisterEventHandler({ user = await validateUserData(body.user, validateUser) } - const _config = defu(await getOptions?.(event) ?? {}, useRuntimeConfig(event).webauthn.register, { + const _config = defu(await getOptions?.(event, body) ?? {}, useRuntimeConfig(event).webauthn.register, { rpID: url.hostname, rpName: url.hostname, userName: user.userName, @@ -57,6 +48,10 @@ export function defineWebAuthnRegisterEventHandler({ try { if (!body.verify) { + if (excludeCredentials) { + _config.excludeCredentials = await excludeCredentials(event, user.userName) + } + const options = await generateRegistrationOptions(_config as GenerateRegistrationOptionsOpts) const attemptId = bufferToBase64URLString(getRandomValues(new Uint8Array(32))) diff --git a/src/runtime/types/webauthn.ts b/src/runtime/types/webauthn.ts index 3b1d363e..c746a085 100644 --- a/src/runtime/types/webauthn.ts +++ b/src/runtime/types/webauthn.ts @@ -1,4 +1,4 @@ -import type { AuthenticatorTransportFuture } from '@simplewebauthn/types' +import type { AuthenticationResponseJSON, AuthenticatorTransportFuture, RegistrationResponseJSON } from '@simplewebauthn/types' import type { Ref } from 'vue' import type { H3Event, H3Error, ValidateFunction } from 'h3' import type { @@ -23,7 +23,7 @@ export interface WebAuthnUser { [key: string]: unknown } -type AllowCredentials = NonNullable +type CredentialsList = NonNullable // Using a discriminated union makes it such that you can only define both storeChallenge and getChallenge or neither type WebAuthnEventHandlerBase> = { @@ -38,22 +38,43 @@ type WebAuthnEventHandlerBase> = { onError?: (event: H3Event, error: H3Error) => void | Promise } +export type RegistrationBody = { + user: T + verify: false +} | { + user: T + verify: true + attemptId: string + response: RegistrationResponseJSON +} + export type WebAuthnRegisterEventHandlerOptions = WebAuthnEventHandlerBase<{ user: T credential: WebAuthnCredential registrationInfo: Exclude }> & { - getOptions?: (event: H3Event) => Partial | Promise> + getOptions?: (event: H3Event, body: RegistrationBody) => Partial | Promise> validateUser?: ValidateFunction + excludeCredentials?: (event: H3Event, userName: string) => CredentialsList | Promise +} + +export type AuthenticationBody = { + verify: false + userName?: string +} | { + verify: true + attemptId: string + userName?: string + response: AuthenticationResponseJSON } export type WebAuthnAuthenticateEventHandlerOptions = WebAuthnEventHandlerBase<{ credential: T authenticationInfo: Exclude }> & { - getOptions?: (event: H3Event) => Partial | Promise> + getOptions?: (event: H3Event, body: AuthenticationBody) => Partial | Promise> getCredential: (event: H3Event, credentialID: string) => T | Promise - allowCredentials?: (event: H3Event, userName: string) => AllowCredentials | Promise + allowCredentials?: (event: H3Event, userName: string) => CredentialsList | Promise } export interface WebAuthnComposable {