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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 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
6286703
Merge branch 'main' into pr/149
atinux Sep 25, 2024
d2054a1
up
atinux Sep 25, 2024
d9d9400
lint fix
atinux Sep 25, 2024
54d9b70
up
atinux Sep 25, 2024
6cd7040
type error
atinux Sep 25, 2024
a77a7ca
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 25, 2024
bbf367c
fix all types
atinux Sep 25, 2024
84cfa0a
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 25, 2024
3c46d45
chore: use logger
atinux Sep 25, 2024
74ec35a
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 25, 2024
38b74cc
Update autofix.yml
atinux Sep 25, 2024
0b43a42
rename to useWebAuthn
atinux Sep 25, 2024
e117cf6
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 25, 2024
d77b6c6
update readme
atinux Sep 25, 2024
977470b
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 25, 2024
1e9c628
feat: allow for extra data fields to be included in the registration …
Gerbuuun Sep 27, 2024
63c4514
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 27, 2024
652cdf4
fix: component name
Gerbuuun Sep 28, 2024
f8f2dcd
Update autofix.yml
atinux Sep 30, 2024
309abd4
chore: update
atinux Sep 30, 2024
09dafe4
up
atinux Sep 30, 2024
56a7301
chore: fix types
atinux Sep 30, 2024
43ffd40
add validateUser method
atinux Sep 30, 2024
32c2983
chore: small update
atinux Sep 30, 2024
0ce4015
add allowCredentials and improve validateUser
atinux Sep 30, 2024
f500bf2
lint
atinux Sep 30, 2024
aa24feb
feat: infer registration body and credential data
Gerbuuun Sep 30, 2024
f18c4e3
chore: remove unnecessary generic param
Gerbuuun Sep 30, 2024
616de6b
chore: add demo
atinux Sep 30, 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
12 changes: 9 additions & 3 deletions .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
Expand All @@ -26,4 +26,10 @@ jobs:
- name: Lint (code)
run: pnpm lint:fix

- uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc
- name: prepare
run: pnpm dev:prepare

- name: Release PR version
run: pnpm dlx pkg-pr-new publish

- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
212 changes: 209 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Add Authentication to Nuxt applications with secured & sealed cookies sessions.

- [Hybrid Rendering](#hybrid-rendering) support (SSR / CSR / SWR / Prerendering)
- [20+ OAuth Providers](#supported-oauth-providers)
- [Password hasing](#password-hashing)
- [WebAuthn (passkey)](#webauthn-passkey)
- [`useUserSession()` Vue composable](#vue-composable)
- [Tree-shakable server utils](#server-utils)
- [`<AuthState>` component](#authstate-component)
Expand Down Expand Up @@ -226,7 +228,7 @@ It can also be set using environment variables:

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`

Expand Down Expand Up @@ -255,9 +257,9 @@ 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.

### Password Utils
### Password Hashing

Nuxt Auth Utils provides a `hashPassword` and `verifyPassword` function to hash and verify passwords by using [scrypt](https://en.wikipedia.org/wiki/Scrypt) as it is supported in many JS runtime.
Nuxt Auth Utils provides password hashing utilities like `hashPassword` and `verifyPassword` to hash and verify passwords by using [scrypt](https://en.wikipedia.org/wiki/Scrypt) as it is supported in many JS runtime.

```ts
const hashedPassword = await hashPassword('user_password')
Expand All @@ -282,6 +284,210 @@ export default defineNuxtConfig({
})
```

### 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.

To enable WebAuthn you need to:

1. Install the peer dependencies:

```bash
npx nypm i @simplewebauthn/server @simplewebauthn/browser
```

2. Enable it in your `nuxt.config.ts`

```ts
export default defineNuxtConfig({
auth: {
webAuthn: true
}
})
```

#### 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 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS credentials (
userId INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
id TEXT UNIQUE NOT NULL,
publicKey TEXT NOT NULL,
counter INTEGER NOT NULL,
backedUp INTEGER NOT NULL,
transports TEXT NOT NULL,
PRIMARY KEY ("userId", "id")
);
```

- For the `users` table it is important to have a unique identifier such as a username or email (here we use 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 `userId` from the `users` table.
- The credential `id` (as unique index)
- The credential `publicKey`
- 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 `backedUp` 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/authenticate.post.ts) and the [database setup](https://github.com/atinux/nuxt-auth-utils/blob/main/playground/server/plugins/database.ts).

```ts
// server/api/webauthn/register.post.ts
import { z } from 'zod'
export default defineWebAuthnRegisterEventHandler({
// optional
validateUser: z.object({
// we want the userName to be a valid email
userName: z.string().email()
}).parse,
async onSuccess(event, { credential, user }) {
// The credential creation has been successful
// We need to create a user if it does not exist
const db = useDatabase()

// Get the user from the database
let dbUser = await db.sql`...`
if (!dbUser) {
// Store new user in database & its credentials
dbUser = await db.sql`...`
}

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

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

```ts
// server/api/webauthn/authenticate.post.ts
export default defineWebAuthnAuthenticateEventHandler({
// Optionally, we can prefetch the credentials if the user gives their userName during login
async allowCredentials(event, userName) {
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 credentials
// example: [{ id: '...' }]
},
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, { credential, authenticationInfo }) {
// The credential authentication has been successful
// We can look it up in our database and get the corresponding user
const db = useDatabase()
const user = await db.sql`...`

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

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

> [!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 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: '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({
registerEndpoint: '/api/webauthn/register', // Default
authenticateEndpoint: '/api/webauthn/authenticate', // Default
})
const { fetch: fetchUserSession } = useUserSession()

const userName = ref('')
async function signUp() {
await register({ userName: userName.value })
.then(fetchUserSession) // refetch the user session
}

async function signIn() {
await authenticate(userName.value)
.then(fetchUserSession) // refetch the user session
}
</script>

<template>
<form @submit.prevent="signUp">
<input v-model="userName" placeholder="Email or username" />
<button type="submit">Sign up</button>
</form>
<form @submit.prevent="signIn">
<input v-model="userName" placeholder="Email or username" />
<button type="submit">Sign in</button>
</form>
</template>
```

Take a look at the [`WebAuthnModal.vue`](https://github.com/atinux/nuxt-auth-utils/blob/main/playground/components/WebAuthnModal.vue) for a full example.

#### Demo

A full demo can be found on https://todo-passkeys.nuxt.dev using [Drizzle ORM](https://orm.drizzle.team/) and [NuxtHub](https://hub.nuxt.com).

The source code of the demo is available on https://github.com/atinux/todo-passkeys.

### Extend Session

Expand Down
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "nuxt-auth-utils",
"version": "0.3.9",
"description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
"repository": "Atinux/nuxt-auth-utils",
"repository": "atinux/nuxt-auth-utils",
"license": "MIT",
"type": "module",
"packageManager": "[email protected]",
Expand Down Expand Up @@ -33,7 +33,7 @@
},
"dependencies": {
"@adonisjs/hash": "^9.0.5",
"@nuxt/kit": "^3.13.0",
"@nuxt/kit": "^3.13.2",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"ofetch": "^1.3.4",
Expand All @@ -42,6 +42,18 @@
"scule": "^1.3.0",
"uncrypto": "^0.1.3"
},
"peerDependencies": {
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.1"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
"optional": true
},
"@simplewebauthn/server": {
"optional": true
}
},
"devDependencies": {
"@iconify-json/simple-icons": "^1.2.3",
"@nuxt/devtools": "latest",
Expand All @@ -51,6 +63,7 @@
"@nuxt/test-utils": "^3.14.2",
"@nuxt/ui": "^2.18.5",
"@nuxt/ui-pro": "^1.4.2",
"@simplewebauthn/types": "^10.0.0",
"changelogen": "^0.5.7",
"eslint": "^9.10.0",
"nuxt": "^3.13.2",
Expand Down
Loading