diff --git a/README.md b/README.md index 8ccd6bb..f50d2f0 100644 --- a/README.md +++ b/README.md @@ -422,7 +422,7 @@ export default defineWebAuthnAuthenticateEventHandler({ ``` > [!IMPORTANT] -> By default, the webauthn event handlers will store the challenge in a short lived, encrypted session cookie. This is not recommended for applications that require strong security guarantees. On a secure connection (https) it is highly unlikely for this to cause problems. However, if the connection is not secure, there is a possibility of a man-in-the-middle attack. To prevent this, you should use a database or KV store to store the challenge instead. For this the `storeChallenge` and `getChallenge` functions are provided. +> Webauthn uses challenges to prevent replay attacks. By default, this module does not make use if this feature. If you want to use challenges, the `storeChallenge` and `getChallenge` functions are provided. An attempt ID is created and sent with each autentication request. You can use this ID to store the challenge in a database or KV store as shown in the example below. > ```ts > export default defineWebAuthnAuthenticateEventHandler({ diff --git a/src/runtime/server/lib/webauthn/authenticate.ts b/src/runtime/server/lib/webauthn/authenticate.ts index 9377a79..bfad52e 100644 --- a/src/runtime/server/lib/webauthn/authenticate.ts +++ b/src/runtime/server/lib/webauthn/authenticate.ts @@ -5,7 +5,6 @@ import defu from 'defu' import type { AuthenticationResponseJSON } from '@simplewebauthn/types' import { getRandomValues } from 'uncrypto' import { base64URLStringToBuffer, bufferToBase64URLString } from '@simplewebauthn/browser' -import { storeChallengeAsSession, getChallengeFromSession } from './utils' import { useRuntimeConfig } from '#imports' import type { WebAuthnAuthenticateEventHandlerOptions, WebAuthnCredential } from '#auth-utils' @@ -39,15 +38,18 @@ export function defineWebAuthnAuthenticateEventHandler({ }, } satisfies GenerateRegistrationOptionsOpts) + if (!storeChallenge) { + _config.challenge = '' + } + try { if (!body.verify) { const options = await generateRegistrationOptions(_config as GenerateRegistrationOptionsOpts) const attemptId = bufferToBase64URLString(getRandomValues(new Uint8Array(32))) // If the developer has stricter storage requirements, they can implement their own storeChallenge function to store the options in a database or KV store - if (storeChallenge) - await storeChallenge?.(event, options.challenge, attemptId) - else - await storeChallengeAsSession(event, options.challenge, attemptId) + if (storeChallenge) { + await storeChallenge(event, options.challenge, attemptId) + } return { creationOptions: options, @@ -76,11 +78,10 @@ export function defineWebAuthnRegisterEventHandler({ }) } - let expectedChallenge: string - if (getChallenge) + let expectedChallenge = '' + if (getChallenge) { expectedChallenge = await getChallenge(event, body.attemptId) - else - expectedChallenge = await getChallengeFromSession(event, body.attemptId) + } const verification = await verifyRegistrationResponse({ response: body.response, diff --git a/src/runtime/server/lib/webauthn/utils.ts b/src/runtime/server/lib/webauthn/utils.ts deleted file mode 100644 index b6bac50..0000000 --- a/src/runtime/server/lib/webauthn/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { type H3Event, useSession, createError } from 'h3' -import { useRuntimeConfig } from '#imports' - -interface ChallengeSession { - data: { - challenge: string - challengeId: string - } - expires: number -} - -function useChallengeSession(event: H3Event) { - return useSession(event, { - name: 'webauthn', - password: useRuntimeConfig(event).session.password, - cookie: { - httpOnly: true, - secure: true, - maxAge: 180, // 3 minutes - }, - }) -} - -export async function storeChallengeAsSession(event: H3Event, challenge: string, challengeId: string) { - const challengeSession = await useChallengeSession(event) - await challengeSession.update({ - data: { - challenge, - challengeId, - }, - expires: Date.now() + 180 * 1000, // 3 minutes - }) -} - -export async function getChallengeFromSession(event: H3Event, challengeId: string) { - const challengeSession = await useChallengeSession(event) - const challenge = challengeSession.data - await challengeSession.clear() - - if (challenge.expires < Date.now()) - throw createError({ statusCode: 400, message: 'Challenge expired' }) - if (challenge.data.challengeId !== challengeId) - throw createError({ statusCode: 400, message: 'Challenge id mismatch' }) - - return challenge.data.challenge -}