Skip to content

Commit 67fb446

Browse files
authored
fix: useWebAuthn composable registration & fix allowCredentials / 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
1 parent 404acc6 commit 67fb446

File tree

4 files changed

+50
-35
lines changed

4 files changed

+50
-35
lines changed

src/module.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
defineNuxtModule,
44
addPlugin,
55
createResolver,
6-
addImportsDir,
6+
addImports,
77
addServerHandler,
88
addServerPlugin,
99
addServerImportsDir,
@@ -60,9 +60,17 @@ export default defineNuxtModule<ModuleOptions>({
6060
'./runtime/types/index',
6161
)
6262

63+
const composables = [
64+
{ name: 'useUserSession', from: resolver.resolve('./runtime/app/composables/session') },
65+
]
66+
67+
if (options.webAuthn) {
68+
composables.push({ name: 'useWebAuthn', from: resolver.resolve('./runtime/app/composables/webauthn') })
69+
}
70+
6371
// App
6472
addComponentsDir({ path: resolver.resolve('./runtime/app/components') })
65-
addImportsDir(resolver.resolve('./runtime/app/composables'))
73+
addImports(composables)
6674
addPlugin(resolver.resolve('./runtime/app/plugins/session.server'))
6775
addPlugin(resolver.resolve('./runtime/app/plugins/session.client'))
6876
// Server

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

+7-16
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,11 @@ import { eventHandler, H3Error, createError, getRequestURL, readBody } from 'h3'
22
import type { GenerateAuthenticationOptionsOpts } from '@simplewebauthn/server'
33
import { generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server'
44
import defu from 'defu'
5-
import type { AuthenticationResponseJSON } from '@simplewebauthn/types'
65
import { getRandomValues } from 'uncrypto'
76
import { base64URLStringToBuffer, bufferToBase64URLString } from '@simplewebauthn/browser'
87
import { useRuntimeConfig } from '#imports'
98
import type { WebAuthnAuthenticateEventHandlerOptions, WebAuthnCredential } from '#auth-utils'
10-
11-
type AuthenticationBody = {
12-
verify: false
13-
userName?: string
14-
} | {
15-
verify: true
16-
attemptId: string
17-
userName?: string
18-
response: AuthenticationResponseJSON
19-
}
9+
import type { AuthenticationBody } from '~/src/runtime/types/webauthn'
2010

2111
export function defineWebAuthnAuthenticateEventHandler<T extends WebAuthnCredential>({
2212
storeChallenge,
@@ -30,20 +20,20 @@ export function defineWebAuthnAuthenticateEventHandler<T extends WebAuthnCredent
3020
return eventHandler(async (event) => {
3121
const url = getRequestURL(event)
3222
const body = await readBody<AuthenticationBody>(event)
33-
const _config = defu(await getOptions?.(event) ?? {}, useRuntimeConfig(event).webauthn.authenticate, {
23+
const _config = defu(await getOptions?.(event, body) ?? {}, useRuntimeConfig(event).webauthn.authenticate, {
3424
rpID: url.hostname,
3525
} satisfies GenerateAuthenticationOptionsOpts)
3626

37-
if (allowCredentials && body.userName) {
38-
_config.allowCredentials = await allowCredentials(event, body.userName)
39-
}
40-
4127
if (!storeChallenge) {
4228
_config.challenge = ''
4329
}
4430

4531
try {
4632
if (!body.verify) {
33+
if (allowCredentials && body.userName) {
34+
_config.allowCredentials = await allowCredentials(event, body.userName)
35+
}
36+
4737
const options = await generateAuthenticationOptions(_config as GenerateAuthenticationOptionsOpts)
4838
const attemptId = bufferToBase64URLString(getRandomValues(new Uint8Array(32)))
4939

@@ -71,6 +61,7 @@ export function defineWebAuthnAuthenticateEventHandler<T extends WebAuthnCredent
7161
expectedChallenge,
7262
expectedOrigin: url.origin,
7363
expectedRPID: url.hostname,
64+
requireUserVerification: false, // TODO: make configurable https://simplewebauthn.dev/docs/advanced/passkeys#verifyauthenticationresponse
7465
credential: {
7566
id: credential.id,
7667
publicKey: new Uint8Array(base64URLStringToBuffer(credential.publicKey)),

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

+7-12
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,18 @@ import type { ValidateFunction } from 'h3'
33
import type { GenerateRegistrationOptionsOpts } from '@simplewebauthn/server'
44
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'
55
import defu from 'defu'
6-
import type { RegistrationResponseJSON } from '@simplewebauthn/types'
76
import { bufferToBase64URLString } from '@simplewebauthn/browser'
87
import { getRandomValues } from 'uncrypto'
98
import { useRuntimeConfig } from '#imports'
109
import type { WebAuthnUser, WebAuthnRegisterEventHandlerOptions } from '#auth-utils'
11-
12-
type RegistrationBody<T extends WebAuthnUser> = {
13-
user: T
14-
verify: false
15-
} | {
16-
user: T
17-
verify: true
18-
attemptId: string
19-
response: RegistrationResponseJSON
20-
}
10+
import type { RegistrationBody } from '~/src/runtime/types/webauthn'
2111

2212
export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({
2313
storeChallenge,
2414
getChallenge,
2515
getOptions,
2616
validateUser,
17+
excludeCredentials,
2718
onSuccess,
2819
onError,
2920
}: WebAuthnRegisterEventHandlerOptions<T>) {
@@ -41,7 +32,7 @@ export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({
4132
user = await validateUserData(body.user, validateUser)
4233
}
4334

44-
const _config = defu(await getOptions?.(event) ?? {}, useRuntimeConfig(event).webauthn.register, {
35+
const _config = defu(await getOptions?.(event, body) ?? {}, useRuntimeConfig(event).webauthn.register, {
4536
rpID: url.hostname,
4637
rpName: url.hostname,
4738
userName: user.userName,
@@ -57,6 +48,10 @@ export function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({
5748

5849
try {
5950
if (!body.verify) {
51+
if (excludeCredentials) {
52+
_config.excludeCredentials = await excludeCredentials(event, user.userName)
53+
}
54+
6055
const options = await generateRegistrationOptions(_config as GenerateRegistrationOptionsOpts)
6156
const attemptId = bufferToBase64URLString(getRandomValues(new Uint8Array(32)))
6257

src/runtime/types/webauthn.ts

+26-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AuthenticatorTransportFuture } from '@simplewebauthn/types'
1+
import type { AuthenticationResponseJSON, AuthenticatorTransportFuture, RegistrationResponseJSON } from '@simplewebauthn/types'
22
import type { Ref } from 'vue'
33
import type { H3Event, H3Error, ValidateFunction } from 'h3'
44
import type {
@@ -23,7 +23,7 @@ export interface WebAuthnUser {
2323
[key: string]: unknown
2424
}
2525

26-
type AllowCredentials = NonNullable<GenerateAuthenticationOptionsOpts['allowCredentials']>
26+
type CredentialsList = NonNullable<GenerateAuthenticationOptionsOpts['allowCredentials']>
2727

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

41+
export type RegistrationBody<T extends WebAuthnUser> = {
42+
user: T
43+
verify: false
44+
} | {
45+
user: T
46+
verify: true
47+
attemptId: string
48+
response: RegistrationResponseJSON
49+
}
50+
4151
export type WebAuthnRegisterEventHandlerOptions<T extends WebAuthnUser> = WebAuthnEventHandlerBase<{
4252
user: T
4353
credential: WebAuthnCredential
4454
registrationInfo: Exclude<VerifiedRegistrationResponse['registrationInfo'], undefined>
4555
}> & {
46-
getOptions?: (event: H3Event) => GenerateRegistrationOptionsOpts | Promise<GenerateRegistrationOptionsOpts>
56+
getOptions?: (event: H3Event, body: RegistrationBody<T>) => Partial<GenerateRegistrationOptionsOpts> | Promise<Partial<GenerateRegistrationOptionsOpts>>
4757
validateUser?: ValidateFunction<T>
58+
excludeCredentials?: (event: H3Event, userName: string) => CredentialsList | Promise<CredentialsList>
59+
}
60+
61+
export type AuthenticationBody = {
62+
verify: false
63+
userName?: string
64+
} | {
65+
verify: true
66+
attemptId: string
67+
userName?: string
68+
response: AuthenticationResponseJSON
4869
}
4970

5071
export type WebAuthnAuthenticateEventHandlerOptions<T extends WebAuthnCredential> = WebAuthnEventHandlerBase<{
5172
credential: T
5273
authenticationInfo: Exclude<VerifiedAuthenticationResponse['authenticationInfo'], undefined>
5374
}> & {
54-
getOptions?: (event: H3Event) => Partial<GenerateAuthenticationOptionsOpts> | Promise<Partial<GenerateAuthenticationOptionsOpts>>
75+
getOptions?: (event: H3Event, body: AuthenticationBody) => Partial<GenerateAuthenticationOptionsOpts> | Promise<Partial<GenerateAuthenticationOptionsOpts>>
5576
getCredential: (event: H3Event, credentialID: string) => T | Promise<T>
56-
allowCredentials?: (event: H3Event, userName: string) => AllowCredentials | Promise<AllowCredentials>
77+
allowCredentials?: (event: H3Event, userName: string) => CredentialsList | Promise<CredentialsList>
5778
}
5879

5980
export interface WebAuthnComposable {

0 commit comments

Comments
 (0)