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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from

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 defineCredentialRegistrationEventHandler({
  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 registrationOptions(event) {
    // Return your own registration options
  },
})
export default defineCredentialAuthenticationEventHandler({
  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 authenticationOptions(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 defineCredentialAuthenticationEventHandler({
  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 registrationOptions/authenticationOptions 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({
  registrationEndpoint: '/api/auth/passkey/register',
  authenticationEndpoint: '/api/auth/passkey/login',
})
const userName = ref('')

async function submit() {
  await register(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.

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
2 participants