Skip to content

Commit

Permalink
fix: useWebAuthn composable registration & fix allowCredentials /…
Browse files Browse the repository at this point in the history
… `excludeCredentials` option (#266)

* fix: do not register composable when not using webauthn

* fix: credential registration option type

* fix: file name

* fix: only run allowCredentials on first request & add excludeCredentials function
  • Loading branch information
Gerbuuun authored Oct 31, 2024
1 parent 404acc6 commit 67fb446
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 35 deletions.
12 changes: 10 additions & 2 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
defineNuxtModule,
addPlugin,
createResolver,
addImportsDir,
addImports,
addServerHandler,
addServerPlugin,
addServerImportsDir,
Expand Down Expand Up @@ -60,9 +60,17 @@ export default defineNuxtModule<ModuleOptions>({
'./runtime/types/index',
)

const composables = [
{ name: 'useUserSession', from: resolver.resolve('./runtime/app/composables/session') },
]

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
Expand Down
23 changes: 7 additions & 16 deletions src/runtime/server/lib/webauthn/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends WebAuthnCredential>({
storeChallenge,
Expand All @@ -30,20 +20,20 @@ export function defineWebAuthnAuthenticateEventHandler<T extends WebAuthnCredent
return eventHandler(async (event) => {
const url = getRequestURL(event)
const body = await readBody<AuthenticationBody>(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)))

Expand Down Expand Up @@ -71,6 +61,7 @@ export function defineWebAuthnAuthenticateEventHandler<T extends WebAuthnCredent
expectedChallenge,
expectedOrigin: url.origin,
expectedRPID: url.hostname,
requireUserVerification: false, // TODO: make configurable https://simplewebauthn.dev/docs/advanced/passkeys#verifyauthenticationresponse
credential: {
id: credential.id,
publicKey: new Uint8Array(base64URLStringToBuffer(credential.publicKey)),
Expand Down
19 changes: 7 additions & 12 deletions src/runtime/server/lib/webauthn/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,18 @@ import type { ValidateFunction } from 'h3'
import type { GenerateRegistrationOptionsOpts } from '@simplewebauthn/server'
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'
import defu from 'defu'
import type { RegistrationResponseJSON } from '@simplewebauthn/types'
import { bufferToBase64URLString } from '@simplewebauthn/browser'
import { getRandomValues } from 'uncrypto'
import { useRuntimeConfig } from '#imports'
import type { WebAuthnUser, WebAuthnRegisterEventHandlerOptions } from '#auth-utils'

type RegistrationBody<T extends WebAuthnUser> = {
user: T
verify: false
} | {
user: T
verify: true
attemptId: string
response: RegistrationResponseJSON
}
import type { RegistrationBody } from '~/src/runtime/types/webauthn'

export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({
storeChallenge,
getChallenge,
getOptions,
validateUser,
excludeCredentials,
onSuccess,
onError,
}: WebAuthnRegisterEventHandlerOptions<T>) {
Expand All @@ -41,7 +32,7 @@ export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({
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,
Expand All @@ -57,6 +48,10 @@ export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({

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)))

Expand Down
31 changes: 26 additions & 5 deletions src/runtime/types/webauthn.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -23,7 +23,7 @@ export interface WebAuthnUser {
[key: string]: unknown
}

type AllowCredentials = NonNullable<GenerateAuthenticationOptionsOpts['allowCredentials']>
type CredentialsList = NonNullable<GenerateAuthenticationOptionsOpts['allowCredentials']>

// Using a discriminated union makes it such that you can only define both storeChallenge and getChallenge or neither
type WebAuthnEventHandlerBase<T extends Record<PropertyKey, unknown>> = {
Expand All @@ -38,22 +38,43 @@ type WebAuthnEventHandlerBase<T extends Record<PropertyKey, unknown>> = {
onError?: (event: H3Event, error: H3Error) => void | Promise<void>
}

export type RegistrationBody<T extends WebAuthnUser> = {
user: T
verify: false
} | {
user: T
verify: true
attemptId: string
response: RegistrationResponseJSON
}

export type WebAuthnRegisterEventHandlerOptions<T extends WebAuthnUser> = WebAuthnEventHandlerBase<{
user: T
credential: WebAuthnCredential
registrationInfo: Exclude<VerifiedRegistrationResponse['registrationInfo'], undefined>
}> & {
getOptions?: (event: H3Event) => GenerateRegistrationOptionsOpts | Promise<GenerateRegistrationOptionsOpts>
getOptions?: (event: H3Event, body: RegistrationBody<T>) => Partial<GenerateRegistrationOptionsOpts> | Promise<Partial<GenerateRegistrationOptionsOpts>>
validateUser?: ValidateFunction<T>
excludeCredentials?: (event: H3Event, userName: string) => CredentialsList | Promise<CredentialsList>
}

export type AuthenticationBody = {
verify: false
userName?: string
} | {
verify: true
attemptId: string
userName?: string
response: AuthenticationResponseJSON
}

export type WebAuthnAuthenticateEventHandlerOptions<T extends WebAuthnCredential> = WebAuthnEventHandlerBase<{
credential: T
authenticationInfo: Exclude<VerifiedAuthenticationResponse['authenticationInfo'], undefined>
}> & {
getOptions?: (event: H3Event) => Partial<GenerateAuthenticationOptionsOpts> | Promise<Partial<GenerateAuthenticationOptionsOpts>>
getOptions?: (event: H3Event, body: AuthenticationBody) => Partial<GenerateAuthenticationOptionsOpts> | Promise<Partial<GenerateAuthenticationOptionsOpts>>
getCredential: (event: H3Event, credentialID: string) => T | Promise<T>
allowCredentials?: (event: H3Event, userName: string) => AllowCredentials | Promise<AllowCredentials>
allowCredentials?: (event: H3Event, userName: string) => CredentialsList | Promise<CredentialsList>
}

export interface WebAuthnComposable {
Expand Down

0 comments on commit 67fb446

Please sign in to comment.