Repository: logto-io/logto
File: packages/core/src/routes/experience/verification-routes/web-authn-verification.ts (TypeScript)

Code before:
    router.post(
      `${experienceRoutes.verification}/sign-in-web-authn/authentication`,
      koaGuard({
        body: z.object({
          identifier: z.object({
            type: z.nativeEnum(SignInIdentifier),
            value: z.string(),
          }),
        }),
        response: z.object({
          verificationId: z.string(),
          authenticationOptions: webAuthnAuthenticationOptionsGuard,
        }),
        status: [200, 400, 404],
      }),
      koaExperienceVerificationsAuditLog({
        type: VerificationType.SignInWebAuthn,
        action: Action.Create,
      }),
      async (ctx, next) => {
        const { experienceInteraction, verificationAuditLog } = ctx;
        const { identifier } = ctx.guard.body;

        // Look up user by identifier to get their WebAuthn credentials
        const user = await findUserByIdentifier(queries.users, identifier);
        assertThat(user, new RequestError({ code: 'user.user_not_exist', status: 404 }));

        const { mfaVerifications = [] } = user;
        const { hostname } = ctx.URL;

        const authenticationOptions = await generateWebAuthnAuthenticationOptions({
          mfaVerifications,
          rpId: hostname,
          allowDiscoverable: false,
        });

        const webAuthnVerification = new SignInWebAuthnVerification(libraries, queries, {
          id: generateStandardId(),
          type: VerificationType.SignInWebAuthn,
          userId: user.id,
          verified: false,
          authenticationChallenge: authenticationOptions.challenge,
          authenticationRpId: authenticationOptions.rpId ?? hostname,
        });

        verificationAuditLog.append({
          payload: {
            verificationId: webAuthnVerification.id,
            identifier,
          },
        });

Review comment (security):
Per our existing product logic, we should not return a “user not found” error before the identity has been successfully validated. For example, we only return an “identity not found” error after the user has verified the email verification code.

Based on this assumption, the flow should store the user identifier when creating the WebAuthn authentication options. During the verification step, we then fetch the user using the stored identifier and compare it with the verified WebAuthn result.

Code after:
    router.post(
      `${experienceRoutes.verification}/sign-in-web-authn/authentication`,
      koaGuard({
        body: z.object({
          identifier: z.object({
            type: z.nativeEnum(SignInIdentifier),
            value: z.string(),
          }),
        }),
        response: z.object({
          verificationId: z.string(),
          authenticationOptions: webAuthnAuthenticationOptionsGuard,
        }),
        status: [200, 400, 404],
      }),
      koaExperienceVerificationsAuditLog({
        type: VerificationType.SignInWebAuthn,
        action: Action.Create,
      }),
      async (ctx, next) => {
        const { experienceInteraction, verificationAuditLog } = ctx;
        const { identifier } = ctx.guard.body;

        // Look up user by identifier to get their WebAuthn credentials
        const user = await findUserByIdentifier(queries.users, identifier);

        const { mfaVerifications = [] } = user ?? {};
        const { hostname } = ctx.URL;

        const authenticationOptions = await generateWebAuthnAuthenticationOptions({
          mfaVerifications,
          rpId: hostname,
          allowDiscoverable: false,
        });

        const webAuthnVerification = new SignInWebAuthnVerification(libraries, queries, {
          id: generateStandardId(),
          type: VerificationType.SignInWebAuthn,
          userId: user?.id,
          verified: false,
          authenticationChallenge: authenticationOptions.challenge,
          authenticationRpId: authenticationOptions.rpId ?? hostname,
        });

        verificationAuditLog.append({
          payload: {
            verificationId: webAuthnVerification.id,
            identifier,
          },
        });