Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: webauthn (passkey) support #149

Merged
merged 79 commits into from
Sep 30, 2024

Conversation

Gerbuuun
Copy link
Contributor

@Gerbuuun Gerbuuun commented Aug 27, 2024

resolves #119

This makes use of the @simplewebauthn packages documented here
It is open source but doesn't accept contributions. The maintainers are actively trying to support new runtimes which is exactly what we need for the Nuxt ecosystem

I based the interface on the oauth event handlers already present in this package.

export default defineWebAuthnRegisterEventHandler({
  async onSuccess(event, { authenticator, userName }) {
    // The credential creation has been successful
    // Create user, store credential, etc, etc...
    // Set the user session
  },
  async onError(event, error) {
    // Handle error
  },
  async getOptions(event) {
    // Return your own registration options
  },
})
export default defineWebAuthnAuthenticateEventHandler({
  async getCredential(event, credentialId) {
    // Look up the credential in the DB
    // Return credential (or handle credential not found)
  },
  async onSuccess(event, { authenticator, userName }) {
    // The credential authentication has been successful
    // fetch user, update credential, etc, etc...
    // Set the user session
  },
  async onError(event, error) {
    // Handle error
  },
  async getOptions(event) {
    // Return your own authentication options
    // e.g. allowedCredentials or requireUserVerification
  },
})

By default the challenge is stored in an encrypted cookie because it is not known beforehand what kind of storage is available. In secure connections this works but for better security guarantees the developer can choose to store the challenge server side. (I'd say highly recommended)

export default defineWebAuthnAuthenticateEventHandler({
  async storeChallenge(event, challenge, attemptId) {
    // Store the challenge in a KV store or DB
    await useStorage().setItem(`attempt:${attemptId}`, challenge)
  },
  async getChallenge(event, attemptId) {
    const challenge = await useStorage().getItem(`attempt:${attemptId}`)

    // Make sure to always remove the attempt because they are single use only!
    await useStorage().removeItem(`attempt:${attemptId}`)

    if (!challenge)
      throw createError({ statusCode: 400, message: 'Authentication attempt expired' })

    return challenge
  },
  // ... other functions
})

A getOptions function where the developer can customize the respective options as much as they want. This allows for multiple webauthn implementations in a single project (for example one for passkeys and one for standard physical keys like Yubikey with more strict requirements)

There is a guide for FIDO conformance here https://simplewebauthn.dev/docs/advanced/fido-conformance (not sure if fully possible with my current implementation)

On the frontend it is as simple as:

<script setup lang="ts">
const { register, authenticate } = useWebauthn()
const userName = ref('')

async function submit() {
  await register({ userName: userName.value })
}
</script>

@Gerbuuun

This comment was marked as resolved.

@Gerbuuun
Copy link
Contributor Author

Ok, I have implemented the changes I proposed. I'm a lot more satisfied with how it looks now.
Let me know your opinions.

Gerbuuun and others added 16 commits August 27, 2024 18:22
Change useServerSession() to useUserSession()
* refactor: request token

* chore: fix import

* up

* up

* Merge branch 'main' into refactor/request-token

* [autofix.ci] apply automated fixes

* chore: fix types issue

* chore: lint

---------

Co-authored-by: Sébastien Chopin <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* feat: add tiktok provider

* docs: add tiktok

* feat: add tiktok .env example

* chore: remove console logs

* [autofix.ci] apply automated fixes

* chore: remove unused authorizationParams

* chore: use new utils

* fix: extends from RequestAccesTokenBody interface

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Copy link

pkg-pr-new bot commented Sep 25, 2024

Open in Stackblitz

pnpm add https://pkg.pr.new/atinux/nuxt-auth-utils@149

commit: 616de6b

@atinux atinux merged commit a90b173 into atinux:main Sep 30, 2024
4 checks passed
@sandros94
Copy link
Contributor

I was experimenting with this and I've noticed that regardless if registration's onSuccess returns an error or not (for example in storing user or credential information into the database) the device still stores the credential, potentially causing a soft-lock state.

Is this correct? Should I open up an issue?
(I'm only copy-pasting the code examples from this repo and from nuxt-todo-passkey's registration, while slightly, intentionally, breaking things like comment the db write operations)

@Gerbuuun
Copy link
Contributor Author

Yes, the credential creation was successful. That's why the onSuccess is called. If you then fail to properly store it, those credentials still exist. You have to manually remove them from your device or password manager.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Passkey integration