atinux / nuxt-auth-utils

Add Authentication to Nuxt applications with secured & sealed cookies sessions.
MIT License
980 stars 91 forks source link

How do we link a passkey to an existing user? Since there's only register and authenticate methods #272

Closed fayazara closed 1 week ago

fayazara commented 2 weeks ago

I see that we have defineWebAuthnRegisterEventHandler but this is always expecting a userName and when we want to just link an credential to a existing user, its not possible.

export default defineWebAuthnRegisterEventHandler({
  async onSuccess(event, { credential }) {
    const { user } = await requireUserSession(event);
    // we have the users email here
    return user;
  },
});

The above endpoint breaks - it throws

{
    "url": "/api/auth/link-passkey",
    "statusCode": 400,
    "statusMessage": "",
    "message": "Invalid request, missing userName or verify property",
    "stack": "<pre><span class=\"stack internal\">at createError (./node_modules/.pnpm/h3@1.13.0/node_modules/h3/dist/index.mjs:78:15)</span>\n<span class=\"stack internal\">at Object.handler (./node_modules/.pnpm/nuxt-auth-utils@0.5.2_@simplewebauthn+browser@11.0.0_@simplewebauthn+server@11.0.0_encoding@0_53tvg2bimanm6byvroxwbmjc6q/node_modules/nuxt-auth-utils/dist/runtime/server/lib/webauthn/register.js:20:13)</span>\n<span class=\"stack internal\">at process.processTicksAndRejections (node:internal/process/task_queues:105:5)</span>\n<span class=\"stack internal\">at async ./node_modules/.pnpm/h3@1.13.0/node_modules/h3/dist/index.mjs:1978:19</span>\n<span class=\"stack internal\">at async Object.callAsync (./node_modules/.pnpm/unctx@2.3.1_webpack-sources@3.2.3/node_modules/unctx/dist/index.mjs:72:16)</span>\n<span class=\"stack internal\">at async Object.callAsync (./node_modules/.pnpm/unctx@2.3.1_webpack-sources@3.2.3/node_modules/unctx/dist/index.mjs:72:16)</span>\n<span class=\"stack internal\">at async Server.toNodeHandle (./node_modules/.pnpm/h3@1.13.0/node_modules/h3/dist/index.mjs:2270:7)</span></pre>"
}

@Gerbuuun is this handled somewhere that I am missing in the docs or can we not do this currently?

I can contribute, just want to make sure if this is not implemented already

fayazara commented 2 weeks ago

I was thinking if something like below would make sense

get the userName from user session if it exists or check the body later, I am aware the user might not always have user.email this is just a POC

More detailed version here CleanShot 2024-11-09 at 14 53 08@2x

fayazara commented 2 weeks ago

Another workaround would be passing an email to the body, but this would be quite error prone - any user can hit this endpoint and pass in any email and there's no way for me to validate it in the backend, the email should be picked from the logged in users session

Gerbuuun commented 2 weeks ago

There is the validateUser function. You can check if the user is logged in with that one. Something like this?

export default defineWebAuthnRegisterEventHandler({
  ...,
  async validateUser(body) {
    const { user } = await requireUserSession(event);
    return user.email === body.userName;
  },
  ...,
});
fayazara commented 2 weeks ago

Doesn't work, I still get the same error, this is my endpoint

import { z } from "zod";
import { userActions } from "~~/server/services/db/UserActions";

/**
 * This endpoint is used to connect a passkey to an existing user.
 */
export default defineWebAuthnRegisterEventHandler({
  async validateUser(event) {
    const { user } = await requireUserSession(event);
    console.log(user.email, body.userName);
    return user.email === body.userName;
  },

  async onSuccess(event, { credential, user }) {
    // const { user } = await requireUserSession(event);
    // const record = await userActions.linkPasskeyToUser(
    //   user.id,
    //   passkeyName,
    //   credential,
    // );
    console.log(user);
    return { credential };
  },
});

I believe its because the EventHandler immidiately checks for the body CleanShot 2024-11-10 at 08 22 03@2x

atinux commented 1 week ago

What about?

// api route to link a passkey to a authenticated user
export default defineWebAuthnRegisterEventHandler({
  // ...,
  async validateUser(body) {
    const { user } = await requireUserSession(event);
    return user.email === body.userName;
  },
  // ...,
});

Then in your dashboard, once logged in, you send the user.email in the session:

<script setup lang="ts">
const { register, authenticate } = useWebAuthn({
  registerEndpoint: '/api/me/passkeys/register',
})
const { user } = useUserSession()

async function registerPasskey() {
  await register({ userName: user.value.email })
    .then(fetchUserSession) // refetch the user session
}
</script>
fayazara commented 1 week ago

No, this still doesn't work it throws - message : "User Validation Error" with 400 Bad response

// app/components/App/Settings/PasskeyManager.vue
<template>
  <UButton
    icon="i-ph-fingerprint"
    label="Link Passkey"
    size="lg"
    color="black"
    @click="linkPasskey"
  />
</template>

<script setup>
const { register } = useWebAuthn({
  registerEndpoint: "/api/auth/webauthn/link-passkey",
});
const { user } = useUserSession();
async function linkPasskey() {
  await register({ userName: user.value.email, displayName: user.value.name });
}
</script>
// server/api/auth/webauthn/link-passkey.js
import { z } from "zod";
export default defineWebAuthnRegisterEventHandler({
  async validateUser(body) {
    const { user } = await requireUserSession(event);
    return user.email === body.userName;
  },
  async onSuccess(event, { credential, user }) {
    console.log(credential, user);
    return "Success";
  },
});

Coming from https://github.com/atinux/nuxt-auth-utils/blob/21bff83df9a0f7b812821914d579a2b10821d46b/src/runtime/server/lib/webauthn/register.ts#L139-L145

Trying to figure out whats happening because it's not logging any errors just throws an error