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 50 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3632b0d
feat: add passkey specific webauthn authentication support
Gerbuuun Aug 27, 2024
f701e10
feat: playground passkey implementation
Gerbuuun Aug 27, 2024
71e04d2
feat: initial docs
Gerbuuun Aug 27, 2024
61287d4
fix: composable type and availability functions
Gerbuuun Aug 27, 2024
59b7fca
fix: types and webauthn config functions
Gerbuuun Aug 27, 2024
57d0488
fix: auto import
Gerbuuun Aug 27, 2024
dcc2e00
fix: composable jsdoc
Gerbuuun Aug 27, 2024
def9bc1
feat: handle attempts internally and change config to respective opti…
Gerbuuun Aug 27, 2024
12fd116
chore: update README.md
Gerbuuun Aug 27, 2024
96fb37c
fix: make sure attempt is always removed from storage!
Gerbuuun Aug 27, 2024
16f9d9e
chore: make playground implementation more consistent
Gerbuuun Aug 27, 2024
ddd9329
refactor: use 'webauthn' and 'credential' terms instead of 'passkey'
Gerbuuun Aug 27, 2024
4554c68
refactor: use body instead of query param for `attemptId`
Gerbuuun Aug 27, 2024
37224a4
Merge remote-tracking branch 'origin/main' into feat/simplewebauthn-p…
Gerbuuun Aug 29, 2024
e46e1ec
chore: rename passkey terms
Gerbuuun Aug 29, 2024
f4ba1c6
chore: improvements
atinux Sep 2, 2024
2a3962c
up
atinux Sep 2, 2024
a8dc0ee
lint fix
atinux Sep 2, 2024
4249310
feat: use session to store challenge by default
Gerbuuun Sep 2, 2024
3ae788d
feat: base64 encode publicKey by default
Gerbuuun Sep 3, 2024
6f24e68
chore: types cleanup and typo fixes
Gerbuuun Sep 4, 2024
812e1f0
feat: improve example and documentation
Gerbuuun Sep 4, 2024
0197284
chore: proofread readme
Gerbuuun Sep 4, 2024
bc0769d
fix: typo
Gerbuuun Sep 4, 2024
70a0f60
docs: add frontend example
Gerbuuun Sep 4, 2024
f2b9b59
docs: fix typo
ipanamski Sep 2, 2024
d8bf231
refactor: request token
Barbapapazes Sep 2, 2024
4e5d0f2
feat: add tiktok provider
ahmedrangel Sep 4, 2024
9218da2
chore: update deps
atinux Sep 4, 2024
f57f8a4
chore(release): v0.3.6
atinux Sep 4, 2024
8a7b2ff
fix: paypal tokens request requires encoded `redirect_uri`
Yizack Sep 6, 2024
f9b8f0d
chore: update deps
atinux Sep 6, 2024
4cec38a
chore(release): v0.3.7
atinux Sep 6, 2024
5557bc4
docs: add note about cookie size
atinux Sep 11, 2024
7d0337d
feat: add Gitlab provider
blumgart Sep 11, 2024
d09037d
docs: Add note to readme about session API route
rudokemper Sep 11, 2024
2c311dd
feat: add instagram provider
sandros94 Sep 11, 2024
cfa03ab
chore: add emailRequired for testing Gitlab
atinux Sep 11, 2024
6aaa94c
feat: add vk provider
blumgart Sep 11, 2024
0396904
fix: ensure plugin declaration files are emitted (#170)
danielroe Sep 11, 2024
242c24b
feat: add support for private data & config argument (#171)
atinux Sep 11, 2024
7b128b0
chore: up
atinux Sep 11, 2024
441c405
chore(release): v0.3.8
atinux Sep 11, 2024
c244471
fix: UserSession secure type augmentation (#181)
IsraelOrtuno Sep 19, 2024
06f7f93
chore: update deps
atinux Sep 19, 2024
1037f43
chore(release): v0.3.9
atinux Sep 19, 2024
78079eb
feat: add Dropbox as supported oauth provider (#183)
Yizack Sep 23, 2024
9560e60
fix(steam): improve open id validation (#184)
ahmedrangel Sep 23, 2024
3bd9c3b
feat!: call `fetch` hook if session is not empty instead of user defi…
atinux Sep 25, 2024
1c8d72a
feat!: rename `oauth<Provider>EventHandler` to`defineOAuth<Provider>E…
atinux Sep 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,100 @@
# Changelog


## v0.3.9

[compare changes](https://github.com/Atinux/nuxt-auth-utils/compare/v0.3.8...v0.3.9)

### 🩹 Fixes

- UserSession secure type augmentation ([#181](https://github.com/Atinux/nuxt-auth-utils/pull/181))

### 🏡 Chore

- Update deps ([4a0e1e9](https://github.com/Atinux/nuxt-auth-utils/commit/4a0e1e9))

### ❤️ Contributors

- Sébastien Chopin ([@atinux](http://github.com/atinux))
- Israel Ortuño <ai.ortuno@gmail.com>

## v0.3.8

[compare changes](https://github.com/Atinux/nuxt-auth-utils/compare/v0.3.7...v0.3.8)

### 🚀 Enhancements

- Add Gitlab provider ([fec746f](https://github.com/Atinux/nuxt-auth-utils/commit/fec746f))
- Add instagram provider ([3bd553c](https://github.com/Atinux/nuxt-auth-utils/commit/3bd553c))
- Add vk provider ([6581f12](https://github.com/Atinux/nuxt-auth-utils/commit/6581f12))
- Add support for private data & config argument ([#171](https://github.com/Atinux/nuxt-auth-utils/pull/171))

### 🩹 Fixes

- Ensure plugin declaration files are emitted ([#170](https://github.com/Atinux/nuxt-auth-utils/pull/170))

### 📖 Documentation

- Add note about cookie size ([a725436](https://github.com/Atinux/nuxt-auth-utils/commit/a725436))
- Add note to readme about session API route ([ddf38c1](https://github.com/Atinux/nuxt-auth-utils/commit/ddf38c1))

### 🏡 Chore

- Add emailRequired for testing Gitlab ([408b580](https://github.com/Atinux/nuxt-auth-utils/commit/408b580))
- Up ([bd37690](https://github.com/Atinux/nuxt-auth-utils/commit/bd37690))

### ❤️ Contributors

- Sébastien Chopin ([@atinux](http://github.com/atinux))
- Daniel Roe ([@danielroe](http://github.com/danielroe))
- Alex Blumgart <dev.blumgart@yandex.ru>
- Sandro Circi ([@sandros94](http://github.com/sandros94))
- Rudo Kemper ([@rudokemper](http://github.com/rudokemper))

## v0.3.7

[compare changes](https://github.com/Atinux/nuxt-auth-utils/compare/v0.3.6...v0.3.7)

### 🩹 Fixes

- Paypal tokens request requires encoded `redirect_uri` ([8bf3b0b](https://github.com/Atinux/nuxt-auth-utils/commit/8bf3b0b))

### 🏡 Chore

- Update deps ([50aba8d](https://github.com/Atinux/nuxt-auth-utils/commit/50aba8d))

### ❤️ Contributors

- Sébastien Chopin ([@atinux](http://github.com/atinux))
- Yizack Rangel ([@Yizack](http://github.com/Yizack))

## v0.3.6

[compare changes](https://github.com/Atinux/nuxt-auth-utils/compare/v0.3.5...v0.3.6)

### 🚀 Enhancements

- Add tiktok provider ([c1b1f44](https://github.com/Atinux/nuxt-auth-utils/commit/c1b1f44))

### 💅 Refactors

- Request token ([925f688](https://github.com/Atinux/nuxt-auth-utils/commit/925f688))

### 📖 Documentation

- Fix typo ([8d3af7e](https://github.com/Atinux/nuxt-auth-utils/commit/8d3af7e))

### 🏡 Chore

- Update deps ([c4189b2](https://github.com/Atinux/nuxt-auth-utils/commit/c4189b2))

### ❤️ Contributors

- Sébastien Chopin ([@atinux](http://github.com/atinux))
- Ahmed Rangel ([@ahmedrangel](http://github.com/ahmedrangel))
- Estéban <e.soubiran25@gmail.com>
- Ivailo Panamski <ipanamski@gmail.com>

## v0.3.5

[compare changes](https://github.com/Atinux/nuxt-auth-utils/compare/v0.3.4...v0.3.5)
Expand Down
206 changes: 196 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Add Authentication to Nuxt applications with secured & sealed cookies sessions.
## Features

- [Hybrid Rendering](#hybrid-rendering) support (SSR / CSR / SWR / Prerendering)
- [15+ OAuth Providers](#supported-oauth-providers)
- [Vue composable](#vue-composable)
- [Server utils](#server-utils)
- [20+ OAuth Providers](#supported-oauth-providers)
- [`useUserSession()` Vue composable](#vue-composable)
- [Tree-shakable server utils](#server-utils)
- [`<AuthState>` component](#authstate-component)
- [Extendable with hooks](#extend-session)

Expand Down Expand Up @@ -105,6 +105,9 @@ interface UserSessionComposable {
}
```

> [!IMPORTANT]
> Nuxt Auth Utils uses the `/api/_auth/session` route for session management. Ensure your API route middleware doesn't interfere with this path.

## Server Utils

The following helpers are auto-imported in your `server/` directory.
Expand All @@ -114,13 +117,18 @@ The following helpers are auto-imported in your `server/` directory.
```ts
// Set a user session, note that this data is encrypted in the cookie but can be decrypted with an API call
// Only store the data that allow you to recognize a user, but do not store sensitive data
// Merges new data with existing data using defu()
// Merges new data with existing data using unjs/defu library
await setUserSession(event, {
// User data
user: {
// ... user data
login: 'atinux'
},
// Private data accessible on server/ routes
secure: {
apiToken: '1234567890'
},
// Any extra fields for the session data
loggedInAt: new Date()
// Any extra fields
})

// Replace a user session. Same behaviour as setUserSession, except it does not merge data with existing data
Expand Down Expand Up @@ -148,16 +156,23 @@ declare module '#auth-utils' {
interface UserSession {
// Add your own fields
}

interface SecureSessionData {
// Add your own fields
}
}

export {}
```

> [!IMPORTANT]
> Since we encrypt and store session data in cookies, we're constrained by the 4096-byte cookie size limit. Store only essential information.

### OAuth Event Handlers

All handlers can be auto-imported and used in your server routes or API routes.

The pattern is `oauth<Provider>EventHandler({ onSuccess, config?, onError? })`, example: `oauthGitHubEventHandler`.
The pattern is `defineOAuth<Provider>EventHandler({ onSuccess, config?, onError? })`, example: `defineOAuthGitHubEventHandler`.

The helper returns an event handler that automatically redirects to the provider authorization page and then calls `onSuccess` or `onError` depending on the result.

Expand Down Expand Up @@ -190,28 +205,33 @@ It can also be set using environment variables:
- AWS Cognito
- Battle.net
- Discord
- Dropbox
- Facebook
- GitHub
- GitLab
- Google
- Instagram
- Keycloak
- LinkedIn
- Microsoft
- PayPal
- Spotify
- Steam
- TikTok
- Twitch
- VK
- X (Twitter)
- XSUAA
- Yandex

You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/).

### Example
#### Example

Example: `~/server/routes/auth/github.get.ts`

```ts
export default oauthGitHubEventHandler({
export default defineOAuthGitHubEventHandler({
config: {
emailRequired: true
},
Expand All @@ -235,6 +255,164 @@ Make sure to set the callback URL in your OAuth app settings as `<your-domain>/a

If the redirect URL mismatch in production, this means that the module cannot guess the right redirect URL. You can set the `NUXT_OAUTH_<PROVIDER>_REDIRECT_URL` env variable to overwrite the default one.

### Webauthn (passkey)

WebAuthn (Web Authentication) is a web standard that enhances security by replacing passwords with passkeys using public key cryptography. Users can authenticate with biometric data (like fingerprints or facial recognition) or physical devices (like USB keys), reducing the risk of phishing and password breaches. This approach offers a more secure and user-friendly authentication method, supported by major browsers and platforms.

#### Example

In this example we will implement the very basic steps to register and authenticate a credential.
The full code can be found in the [playground](https://github.com/Atinux/nuxt-auth-utils/blob/main/playground/server/api/webauthn). The example uses a SQLite database with the following minimal tables:

```sql
CREATE TABLE users (
user_name TEXT UNIQUE NOT NULL
);
CREATE TABLE credentials (
user_name TEXT NOT NULL,
credential_id TEXT NOT NULL,
credential_public_key TEXT NOT NULL,
counter INTEGER NOT NULL,
backed_up INTEGER NOT NULL,
transports TEXT NOT NULL
);
```

- For the `users` table it is important to have a unique identifier such as a username or email. When creating a new credential, this identifier is required and stored with the passkey on the user's device, password manager, or authenticator.
- The `credentials` table stores:
- The credential `ID` (potentially as primary key)
- The credential `public key`
- A `counter`. Each time a credential is used, the counter is incremented. We can use this value to perform extra security checks. More about `counter` can be read [here](https://simplewebauthn.dev/docs/packages/server#3-post-registration-responsibilities). For this example, we won't be using the counter. But you should update the counter in your database with the new value.
- A `backed up` flag. Normally, credentials are stored on the generating device. When you use a password manager or authenticator, the credential is "backed up" because it can be used on multiple devices. See [this section](https://arc.net/l/quote/ugaemxot) for more details.
- The credential `transports`. It is an array of strings that indicate how the credential communicates with the client. It is used to show the correct UI for the user to utilize the credential. Again, see [this section](https://arc.net/l/quote/ycxtiorp) for more details.

The following code does not include the actual database queries, but shows the general steps to follow. The full example can be found in the playground: [registration](https://github.com/Atinux/nuxt-auth-utils/blob/main/playground/server/api/webauthn/register.post.ts), [authentication](https://github.com/Atinux/nuxt-auth-utils/blob/main/playground/server/api/webauthn/login.post.ts).
```ts
export default defineCredentialRegistrationEventHandler({
async onSuccess(event, { authenticator, userName }) {
// The credential creation has been successful
// We need to create a user if it does not exist

// Get the user from the database
const user = await useDatabase().sql`...`
if (!user) {
// Store new user in database
await useDatabase().sql`...`
}

// we now need to store the credential in our database and link it to the user
await useDatabase().sql`...`

// Set the user session
await setUserSession(event, {
user: {
webauthn: user.userName,
},
loggedInAt: Date.now(),
})
},
})
```

```ts
export default defineCredentialAuthenticationEventHandler({
async getCredential(event, credentialId) {
// Look for the credential in our database
const credential = await useDatabase().sql`...`

// If the credential is not found, there is no account to log in to
if (!credential)
throw createError({ statusCode: 400, message: 'Credential not found' })

return credential
},
async onSuccess(event, { authenticator, authenticationInfo }) {
// The credential authentication has been successful
// We can look it up in our database and get the corresponding user
const user = await useDatabase().sql`...`

// Update the counter in the database (authenticationInfo.newCounter)
await useDatabase().sql`...`

// Set the user session
await setUserSession(event, {
user: {
webauthn: user.userName,
},
loggedInAt: Date.now(),
})
},

// Optionally, we can prefetch the credentials if the user gives their username during login
async authenticationOptions(event) {
const body = await readBody(event)
// If no userName is provided, no credentials can be returned
if (!body.userName || body.userName === '')
return {}

const credentials = await useDatabase().sql`...`

// If no credentials are found, the authentication cannot be completed
if (!credentials.length)
throw createError({ statusCode: 400, message: 'User not found' })

// If user is found, only allow credentials that are registered
// The browser will automatically try to use the credential that it knows about
// Skipping the step for the user to select a credential for a better user experience
return {
allowCredentials: credentials,
}
},
})
```

> [!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.
> ```ts
> 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: 'Challenge expired' })
>
> return challenge
> },
> async onSuccess(event, { authenticator }) {
> // ...
> },
> })
> ```

On the frontend it is as simple as:
```vue
<script setup lang="ts">
const { register, authenticate } = useWebauthn({
registrationEndpoint: '/api/webauthn/register',
authenticationEndpoint: '/api/webauthn/login',
})

const userName = ref('')
async function submit() {
await register(userName.value)
}
</script>

<template>
<form @submit.prevent="submit">
<input v-model="userName" />
<button type="submit">Register</button>
</form>
</template>
```

### Extend Session

We leverage hooks to let you extend the session data with your own data or log when the user clears the session.
Expand All @@ -250,7 +428,7 @@ export default defineNitroPlugin(() => {
// throw createError({ ... }) if session is invalid for example
})

// Called when we call useServerSession().clear() or clearUserSession(event)
// Called when we call useUserSession().clear() or clearUserSession(event)
sessionHooks.hook('clear', async (session, event) => {
// Log that user logged out
})
Expand Down Expand Up @@ -349,6 +527,14 @@ Our defaults are:
}
```

You can also overwrite the session config by passing it as 3rd argument of the `setUserSession` and `replaceUserSession` functions:

```ts
await setUserSession(event, { ... } , {
maxAge: 60 * 60 * 24 * 7 // 1 week
})
```

Checkout the [`SessionConfig`](https://github.com/unjs/h3/blob/c04c458810e34eb15c1647e1369e7d7ef19f567d/src/utils/session.ts#L20) for all options.

## More
Expand Down
Loading