nuxt-auth-utils 0.3.9 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +241 -5
- package/dist/module.d.mts +22 -0
- package/dist/module.d.ts +22 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +40 -2
- package/dist/runtime/app/composables/webauthn.d.ts +13 -0
- package/dist/runtime/app/composables/webauthn.js +62 -0
- package/dist/runtime/server/api/session.get.js +1 -1
- package/dist/runtime/server/lib/oauth/auth0.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/auth0.js +1 -1
- package/dist/runtime/server/lib/oauth/battledotnet.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/battledotnet.js +1 -1
- package/dist/runtime/server/lib/oauth/cognito.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/cognito.js +1 -1
- package/dist/runtime/server/lib/oauth/discord.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/discord.js +1 -1
- package/dist/runtime/server/lib/oauth/dropbox.d.ts +47 -0
- package/dist/runtime/server/lib/oauth/dropbox.js +72 -0
- package/dist/runtime/server/lib/oauth/facebook.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/facebook.js +1 -1
- package/dist/runtime/server/lib/oauth/github.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/github.js +1 -1
- package/dist/runtime/server/lib/oauth/gitlab.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/gitlab.js +1 -1
- package/dist/runtime/server/lib/oauth/google.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/google.js +2 -1
- package/dist/runtime/server/lib/oauth/instagram.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/instagram.js +1 -1
- package/dist/runtime/server/lib/oauth/keycloak.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/keycloak.js +1 -1
- package/dist/runtime/server/lib/oauth/linkedin.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/linkedin.js +1 -1
- package/dist/runtime/server/lib/oauth/microsoft.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/microsoft.js +1 -1
- package/dist/runtime/server/lib/oauth/paypal.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/paypal.js +1 -1
- package/dist/runtime/server/lib/oauth/spotify.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/spotify.js +1 -1
- package/dist/runtime/server/lib/oauth/steam.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/steam.js +11 -8
- package/dist/runtime/server/lib/oauth/tiktok.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/tiktok.js +1 -1
- package/dist/runtime/server/lib/oauth/twitch.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/twitch.js +1 -1
- package/dist/runtime/server/lib/oauth/vk.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/vk.js +1 -1
- package/dist/runtime/server/lib/oauth/x.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/x.js +1 -1
- package/dist/runtime/server/lib/oauth/xsuaa.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/xsuaa.js +1 -1
- package/dist/runtime/server/lib/oauth/yandex.d.ts +1 -1
- package/dist/runtime/server/lib/oauth/yandex.js +1 -1
- package/dist/runtime/server/lib/webauthn/authenticate.d.ts +5 -0
- package/dist/runtime/server/lib/webauthn/authenticate.js +73 -0
- package/dist/runtime/server/lib/webauthn/register.d.ts +7 -0
- package/dist/runtime/server/lib/webauthn/register.js +116 -0
- package/dist/runtime/server/utils/password.d.ts +23 -0
- package/dist/runtime/server/utils/password.js +18 -0
- package/dist/runtime/types/index.d.ts +1 -0
- package/dist/runtime/types/oauth-config.d.ts +1 -1
- package/dist/runtime/types/webauthn.d.ts +76 -0
- package/dist/runtime/types/webauthn.js +0 -0
- package/package.json +23 -9
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
Add Authentication to Nuxt applications with secured & sealed cookies sessions.
|
|
9
9
|
|
|
10
10
|
- [Release Notes](/CHANGELOG.md)
|
|
11
|
-
- [Demo](https://github.com/atinux/
|
|
11
|
+
- [Demo with OAuth](https://github.com/atinux/atidone)
|
|
12
|
+
- [Demo with Passkeys](https://github.com/atinux/todo-passkeys)
|
|
12
13
|
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/nuxt-auth-utils?file=playground%2Fapp.vue) -->
|
|
13
14
|
<!-- - [📖 Documentation](https://example.com) -->
|
|
14
15
|
|
|
@@ -16,6 +17,8 @@ Add Authentication to Nuxt applications with secured & sealed cookies sessions.
|
|
|
16
17
|
|
|
17
18
|
- [Hybrid Rendering](#hybrid-rendering) support (SSR / CSR / SWR / Prerendering)
|
|
18
19
|
- [20+ OAuth Providers](#supported-oauth-providers)
|
|
20
|
+
- [Password Hashing](#password-hashing)
|
|
21
|
+
- [WebAuthn (passkey)](#webauthn-passkey)
|
|
19
22
|
- [`useUserSession()` Vue composable](#vue-composable)
|
|
20
23
|
- [Tree-shakable server utils](#server-utils)
|
|
21
24
|
- [`<AuthState>` component](#authstate-component)
|
|
@@ -172,7 +175,7 @@ export {}
|
|
|
172
175
|
|
|
173
176
|
All handlers can be auto-imported and used in your server routes or API routes.
|
|
174
177
|
|
|
175
|
-
The pattern is `
|
|
178
|
+
The pattern is `defineOAuth<Provider>EventHandler({ onSuccess, config?, onError? })`, example: `defineOAuthGitHubEventHandler`.
|
|
176
179
|
|
|
177
180
|
The helper returns an event handler that automatically redirects to the provider authorization page and then calls `onSuccess` or `onError` depending on the result.
|
|
178
181
|
|
|
@@ -205,6 +208,7 @@ It can also be set using environment variables:
|
|
|
205
208
|
- AWS Cognito
|
|
206
209
|
- Battle.net
|
|
207
210
|
- Discord
|
|
211
|
+
- Dropbox
|
|
208
212
|
- Facebook
|
|
209
213
|
- GitHub
|
|
210
214
|
- GitLab
|
|
@@ -225,12 +229,12 @@ It can also be set using environment variables:
|
|
|
225
229
|
|
|
226
230
|
You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/).
|
|
227
231
|
|
|
228
|
-
|
|
232
|
+
#### Example
|
|
229
233
|
|
|
230
234
|
Example: `~/server/routes/auth/github.get.ts`
|
|
231
235
|
|
|
232
236
|
```ts
|
|
233
|
-
export default
|
|
237
|
+
export default defineOAuthGitHubEventHandler({
|
|
234
238
|
config: {
|
|
235
239
|
emailRequired: true
|
|
236
240
|
},
|
|
@@ -254,6 +258,238 @@ Make sure to set the callback URL in your OAuth app settings as `<your-domain>/a
|
|
|
254
258
|
|
|
255
259
|
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.
|
|
256
260
|
|
|
261
|
+
### Password Hashing
|
|
262
|
+
|
|
263
|
+
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.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const hashedPassword = await hashPassword('user_password')
|
|
267
|
+
|
|
268
|
+
if (await verifyPassword(hashedPassword, 'user_password')) {
|
|
269
|
+
// Password is valid
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
You can configure the scrypt options in your `nuxt.config.ts`:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
export default defineNuxtConfig({
|
|
277
|
+
modules: ['nuxt-auth-utils'],
|
|
278
|
+
auth: {
|
|
279
|
+
hash: {
|
|
280
|
+
scrypt: {
|
|
281
|
+
// See https://github.com/adonisjs/hash/blob/94637029cd526783ac0a763ec581306d98db2036/src/types.ts#L144
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### WebAuthn (passkey)
|
|
289
|
+
|
|
290
|
+
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.
|
|
291
|
+
|
|
292
|
+
To enable WebAuthn you need to:
|
|
293
|
+
|
|
294
|
+
1. Install the peer dependencies:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
npx nypm i @simplewebauthn/server @simplewebauthn/browser
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
2. Enable it in your `nuxt.config.ts`
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
export default defineNuxtConfig({
|
|
304
|
+
auth: {
|
|
305
|
+
webAuthn: true
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### Example
|
|
311
|
+
|
|
312
|
+
In this example we will implement the very basic steps to register and authenticate a credential.
|
|
313
|
+
|
|
314
|
+
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:
|
|
315
|
+
|
|
316
|
+
```sql
|
|
317
|
+
CREATE TABLE users (
|
|
318
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
319
|
+
email TEXT NOT NULL
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
323
|
+
userId INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
324
|
+
id TEXT UNIQUE NOT NULL,
|
|
325
|
+
publicKey TEXT NOT NULL,
|
|
326
|
+
counter INTEGER NOT NULL,
|
|
327
|
+
backedUp INTEGER NOT NULL,
|
|
328
|
+
transports TEXT NOT NULL,
|
|
329
|
+
PRIMARY KEY ("userId", "id")
|
|
330
|
+
);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
- 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.
|
|
334
|
+
- The `credentials` table stores:
|
|
335
|
+
- The `userId` from the `users` table.
|
|
336
|
+
- The credential `id` (as unique index)
|
|
337
|
+
- The credential `publicKey`
|
|
338
|
+
- 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.
|
|
339
|
+
- 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.
|
|
340
|
+
- 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.
|
|
341
|
+
|
|
342
|
+
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).
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
// server/api/webauthn/register.post.ts
|
|
346
|
+
import { z } from 'zod'
|
|
347
|
+
export default defineWebAuthnRegisterEventHandler({
|
|
348
|
+
// optional
|
|
349
|
+
validateUser: z.object({
|
|
350
|
+
// we want the userName to be a valid email
|
|
351
|
+
userName: z.string().email()
|
|
352
|
+
}).parse,
|
|
353
|
+
async onSuccess(event, { credential, user }) {
|
|
354
|
+
// The credential creation has been successful
|
|
355
|
+
// We need to create a user if it does not exist
|
|
356
|
+
const db = useDatabase()
|
|
357
|
+
|
|
358
|
+
// Get the user from the database
|
|
359
|
+
let dbUser = await db.sql`...`
|
|
360
|
+
if (!dbUser) {
|
|
361
|
+
// Store new user in database & its credentials
|
|
362
|
+
dbUser = await db.sql`...`
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// we now need to store the credential in our database and link it to the user
|
|
366
|
+
await db.sql`...`
|
|
367
|
+
|
|
368
|
+
// Set the user session
|
|
369
|
+
await setUserSession(event, {
|
|
370
|
+
user: {
|
|
371
|
+
id: dbUser.id
|
|
372
|
+
},
|
|
373
|
+
loggedInAt: Date.now(),
|
|
374
|
+
})
|
|
375
|
+
},
|
|
376
|
+
})
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
// server/api/webauthn/authenticate.post.ts
|
|
381
|
+
export default defineWebAuthnAuthenticateEventHandler({
|
|
382
|
+
// Optionally, we can prefetch the credentials if the user gives their userName during login
|
|
383
|
+
async allowCredentials(event, userName) {
|
|
384
|
+
const credentials = await useDatabase().sql`...`
|
|
385
|
+
// If no credentials are found, the authentication cannot be completed
|
|
386
|
+
if (!credentials.length)
|
|
387
|
+
throw createError({ statusCode: 400, message: 'User not found' })
|
|
388
|
+
|
|
389
|
+
// If user is found, only allow credentials that are registered
|
|
390
|
+
// The browser will automatically try to use the credential that it knows about
|
|
391
|
+
// Skipping the step for the user to select a credential for a better user experience
|
|
392
|
+
return credentials
|
|
393
|
+
// example: [{ id: '...' }]
|
|
394
|
+
},
|
|
395
|
+
async getCredential(event, credentialId) {
|
|
396
|
+
// Look for the credential in our database
|
|
397
|
+
const credential = await useDatabase().sql`...`
|
|
398
|
+
|
|
399
|
+
// If the credential is not found, there is no account to log in to
|
|
400
|
+
if (!credential)
|
|
401
|
+
throw createError({ statusCode: 400, message: 'Credential not found' })
|
|
402
|
+
|
|
403
|
+
return credential
|
|
404
|
+
},
|
|
405
|
+
async onSuccess(event, { credential, authenticationInfo }) {
|
|
406
|
+
// The credential authentication has been successful
|
|
407
|
+
// We can look it up in our database and get the corresponding user
|
|
408
|
+
const db = useDatabase()
|
|
409
|
+
const user = await db.sql`...`
|
|
410
|
+
|
|
411
|
+
// Update the counter in the database (authenticationInfo.newCounter)
|
|
412
|
+
await db.sql`...`
|
|
413
|
+
|
|
414
|
+
// Set the user session
|
|
415
|
+
await setUserSession(event, {
|
|
416
|
+
user: {
|
|
417
|
+
id: user.id
|
|
418
|
+
},
|
|
419
|
+
loggedInAt: Date.now(),
|
|
420
|
+
})
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
> [!IMPORTANT]
|
|
426
|
+
> Webauthn uses challenges to prevent replay attacks. By default, this module does not make use if this feature. If you want to use challenges (**which is highly recommended**), the `storeChallenge` and `getChallenge` functions are provided. An attempt ID is created and sent with each autentication request. You can use this ID to store the challenge in a database or KV store as shown in the example below.
|
|
427
|
+
|
|
428
|
+
> ```ts
|
|
429
|
+
> export default defineWebAuthnAuthenticateEventHandler({
|
|
430
|
+
> async storeChallenge(event, challenge, attemptId) {
|
|
431
|
+
> // Store the challenge in a KV store or DB
|
|
432
|
+
> await useStorage().setItem(`attempt:${attemptId}`, challenge)
|
|
433
|
+
> },
|
|
434
|
+
> async getChallenge(event, attemptId) {
|
|
435
|
+
> const challenge = await useStorage().getItem(`attempt:${attemptId}`)
|
|
436
|
+
>
|
|
437
|
+
> // Make sure to always remove the attempt because they are single use only!
|
|
438
|
+
> await useStorage().removeItem(`attempt:${attemptId}`)
|
|
439
|
+
>
|
|
440
|
+
> if (!challenge)
|
|
441
|
+
> throw createError({ statusCode: 400, message: 'Challenge expired' })
|
|
442
|
+
>
|
|
443
|
+
> return challenge
|
|
444
|
+
> },
|
|
445
|
+
> async onSuccess(event, { authenticator }) {
|
|
446
|
+
> // ...
|
|
447
|
+
> },
|
|
448
|
+
> })
|
|
449
|
+
> ```
|
|
450
|
+
|
|
451
|
+
On the frontend it is as simple as:
|
|
452
|
+
|
|
453
|
+
```vue
|
|
454
|
+
<script setup lang="ts">
|
|
455
|
+
const { register, authenticate } = useWebAuthn({
|
|
456
|
+
registerEndpoint: '/api/webauthn/register', // Default
|
|
457
|
+
authenticateEndpoint: '/api/webauthn/authenticate', // Default
|
|
458
|
+
})
|
|
459
|
+
const { fetch: fetchUserSession } = useUserSession()
|
|
460
|
+
|
|
461
|
+
const userName = ref('')
|
|
462
|
+
async function signUp() {
|
|
463
|
+
await register({ userName: userName.value })
|
|
464
|
+
.then(fetchUserSession) // refetch the user session
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function signIn() {
|
|
468
|
+
await authenticate(userName.value)
|
|
469
|
+
.then(fetchUserSession) // refetch the user session
|
|
470
|
+
}
|
|
471
|
+
</script>
|
|
472
|
+
|
|
473
|
+
<template>
|
|
474
|
+
<form @submit.prevent="signUp">
|
|
475
|
+
<input v-model="userName" placeholder="Email or username" />
|
|
476
|
+
<button type="submit">Sign up</button>
|
|
477
|
+
</form>
|
|
478
|
+
<form @submit.prevent="signIn">
|
|
479
|
+
<input v-model="userName" placeholder="Email or username" />
|
|
480
|
+
<button type="submit">Sign in</button>
|
|
481
|
+
</form>
|
|
482
|
+
</template>
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Take a look at the [`WebAuthnModal.vue`](https://github.com/atinux/nuxt-auth-utils/blob/main/playground/components/WebAuthnModal.vue) for a full example.
|
|
486
|
+
|
|
487
|
+
#### Demo
|
|
488
|
+
|
|
489
|
+
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).
|
|
490
|
+
|
|
491
|
+
The source code of the demo is available on https://github.com/atinux/todo-passkeys.
|
|
492
|
+
|
|
257
493
|
### Extend Session
|
|
258
494
|
|
|
259
495
|
We leverage hooks to let you extend the session data with your own data or log when the user clears the session.
|
|
@@ -413,7 +649,7 @@ npm run release
|
|
|
413
649
|
[npm-version-href]: https://npmjs.com/package/nuxt-auth-utils
|
|
414
650
|
|
|
415
651
|
[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-auth-utils.svg?style=flat&colorA=020420&colorB=00DC82
|
|
416
|
-
[npm-downloads-href]: https://
|
|
652
|
+
[npm-downloads-href]: https://npm.chart.dev/nuxt-auth-utils
|
|
417
653
|
|
|
418
654
|
[license-src]: https://img.shields.io/npm/l/nuxt-auth-utils.svg?style=flat&colorA=020420&colorB=00DC82
|
|
419
655
|
[license-href]: https://npmjs.com/package/nuxt-auth-utils
|
package/dist/module.d.mts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { ScryptConfig } from '@adonisjs/hash/types';
|
|
2
3
|
|
|
3
4
|
interface ModuleOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Enable WebAuthn (Passkeys)
|
|
7
|
+
* @default false
|
|
8
|
+
*/
|
|
9
|
+
webAuthn?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Hash options used for password hashing
|
|
12
|
+
*/
|
|
13
|
+
hash?: {
|
|
14
|
+
/**
|
|
15
|
+
* scrypt options used for password hashing
|
|
16
|
+
*/
|
|
17
|
+
scrypt?: ScryptConfig;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
declare module 'nuxt/schema' {
|
|
21
|
+
interface RuntimeConfig {
|
|
22
|
+
hash: {
|
|
23
|
+
scrypt: ScryptConfig;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
4
26
|
}
|
|
5
27
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
6
28
|
|
package/dist/module.d.ts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { ScryptConfig } from '@adonisjs/hash/types';
|
|
2
3
|
|
|
3
4
|
interface ModuleOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Enable WebAuthn (Passkeys)
|
|
7
|
+
* @default false
|
|
8
|
+
*/
|
|
9
|
+
webAuthn?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Hash options used for password hashing
|
|
12
|
+
*/
|
|
13
|
+
hash?: {
|
|
14
|
+
/**
|
|
15
|
+
* scrypt options used for password hashing
|
|
16
|
+
*/
|
|
17
|
+
scrypt?: ScryptConfig;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
declare module 'nuxt/schema' {
|
|
21
|
+
interface RuntimeConfig {
|
|
22
|
+
hash: {
|
|
23
|
+
scrypt: ScryptConfig;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
4
26
|
}
|
|
5
27
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
6
28
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir, addPlugin, addServerPlugin, addServerImportsDir, addServerHandler } from '@nuxt/kit';
|
|
2
|
+
import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir, addPlugin, addServerPlugin, addServerImportsDir, logger, addServerHandler } from '@nuxt/kit';
|
|
3
3
|
import { join } from 'pathe';
|
|
4
4
|
import { defu } from 'defu';
|
|
5
5
|
import { randomUUID } from 'uncrypto';
|
|
@@ -10,7 +10,12 @@ const module = defineNuxtModule({
|
|
|
10
10
|
configKey: "auth"
|
|
11
11
|
},
|
|
12
12
|
// Default configuration options of the Nuxt module
|
|
13
|
-
defaults: {
|
|
13
|
+
defaults: {
|
|
14
|
+
webAuthn: false,
|
|
15
|
+
hash: {
|
|
16
|
+
scrypt: {}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
14
19
|
async setup(options, nuxt) {
|
|
15
20
|
const resolver = createResolver(import.meta.url);
|
|
16
21
|
nuxt.options.alias["#auth-utils"] = resolver.resolve(
|
|
@@ -22,6 +27,22 @@ const module = defineNuxtModule({
|
|
|
22
27
|
addPlugin(resolver.resolve("./runtime/app/plugins/session.client"));
|
|
23
28
|
addServerPlugin(resolver.resolve("./runtime/server/plugins/oauth"));
|
|
24
29
|
addServerImportsDir(resolver.resolve("./runtime/server/lib/oauth"));
|
|
30
|
+
if (options.webAuthn) {
|
|
31
|
+
const missingDeps = [];
|
|
32
|
+
const peerDeps = ["@simplewebauthn/server", "@simplewebauthn/browser"];
|
|
33
|
+
for (const pkg of peerDeps) {
|
|
34
|
+
await import(pkg).catch(() => {
|
|
35
|
+
missingDeps.push(pkg);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (missingDeps.length > 0) {
|
|
39
|
+
logger.withTag("nuxt-auth-utils").error(`Missing dependencies for \`WebAuthn\`, please install with:
|
|
40
|
+
|
|
41
|
+
\`npx nypm i ${missingDeps.join(" ")}\``);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
addServerImportsDir(resolver.resolve("./runtime/server/lib/webauthn"));
|
|
45
|
+
}
|
|
25
46
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
26
47
|
addServerHandler({
|
|
27
48
|
handler: resolver.resolve("./runtime/server/api/session.delete"),
|
|
@@ -33,6 +54,11 @@ const module = defineNuxtModule({
|
|
|
33
54
|
route: "/api/_auth/session",
|
|
34
55
|
method: "get"
|
|
35
56
|
});
|
|
57
|
+
nuxt.options.nitro.unenv ||= {};
|
|
58
|
+
nuxt.options.nitro.unenv.external ||= [];
|
|
59
|
+
if (!nuxt.options.nitro.unenv.external.includes("node:crypto")) {
|
|
60
|
+
nuxt.options.nitro.unenv.external.push("node:crypto");
|
|
61
|
+
}
|
|
36
62
|
const runtimeConfig = nuxt.options.runtimeConfig;
|
|
37
63
|
const envSessionPassword = `${runtimeConfig.nitro?.envPrefix || "NUXT_"}SESSION_PASSWORD`;
|
|
38
64
|
runtimeConfig.session = defu(runtimeConfig.session, {
|
|
@@ -42,6 +68,9 @@ const module = defineNuxtModule({
|
|
|
42
68
|
sameSite: "lax"
|
|
43
69
|
}
|
|
44
70
|
});
|
|
71
|
+
runtimeConfig.hash = defu(runtimeConfig.hash, {
|
|
72
|
+
scrypt: options.hash?.scrypt
|
|
73
|
+
});
|
|
45
74
|
if (nuxt.options.dev && !runtimeConfig.session.password) {
|
|
46
75
|
runtimeConfig.session.password = randomUUID().replace(/-/g, "");
|
|
47
76
|
const envPath = join(nuxt.options.rootDir, ".env");
|
|
@@ -54,6 +83,10 @@ const module = defineNuxtModule({
|
|
|
54
83
|
);
|
|
55
84
|
}
|
|
56
85
|
}
|
|
86
|
+
runtimeConfig.webauthn = defu(runtimeConfig.webauthn, {
|
|
87
|
+
register: {},
|
|
88
|
+
authenticate: {}
|
|
89
|
+
});
|
|
57
90
|
runtimeConfig.oauth = defu(runtimeConfig.oauth, {});
|
|
58
91
|
runtimeConfig.oauth.github = defu(runtimeConfig.oauth.github, {
|
|
59
92
|
clientId: "",
|
|
@@ -171,6 +204,11 @@ const module = defineNuxtModule({
|
|
|
171
204
|
clientSecret: "",
|
|
172
205
|
redirectURL: ""
|
|
173
206
|
});
|
|
207
|
+
runtimeConfig.oauth.dropbox = defu(runtimeConfig.oauth.dropbox, {
|
|
208
|
+
clientId: "",
|
|
209
|
+
clientSecret: "",
|
|
210
|
+
redirectURL: ""
|
|
211
|
+
});
|
|
174
212
|
}
|
|
175
213
|
});
|
|
176
214
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WebAuthnComposable } from '#auth-utils';
|
|
2
|
+
export declare function useWebAuthn(options?: {
|
|
3
|
+
/**
|
|
4
|
+
* The endpoint to register a new credential.
|
|
5
|
+
* @default '/api/webauthn/register'
|
|
6
|
+
*/
|
|
7
|
+
registerEndpoint?: string;
|
|
8
|
+
/**
|
|
9
|
+
* The endpoint to authenticate a user.
|
|
10
|
+
* @default '/api/webauthn/authenticate'
|
|
11
|
+
*/
|
|
12
|
+
authenticateEndpoint?: string;
|
|
13
|
+
}): WebAuthnComposable;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
browserSupportsWebAuthn,
|
|
3
|
+
browserSupportsWebAuthnAutofill,
|
|
4
|
+
platformAuthenticatorIsAvailable,
|
|
5
|
+
startAuthentication,
|
|
6
|
+
startRegistration
|
|
7
|
+
} from "@simplewebauthn/browser";
|
|
8
|
+
import { ref, onMounted } from "#imports";
|
|
9
|
+
export function useWebAuthn(options = {}) {
|
|
10
|
+
const { registerEndpoint = "/api/webauthn/register", authenticateEndpoint = "/api/webauthn/authenticate" } = options;
|
|
11
|
+
async function register(user) {
|
|
12
|
+
const { creationOptions, attemptId } = await $fetch(registerEndpoint, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
body: {
|
|
15
|
+
user,
|
|
16
|
+
verify: false
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const attestationResponse = await startRegistration(creationOptions);
|
|
20
|
+
const verificationResponse = await $fetch(registerEndpoint, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
body: {
|
|
23
|
+
user,
|
|
24
|
+
attemptId,
|
|
25
|
+
response: attestationResponse,
|
|
26
|
+
verify: true
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return verificationResponse && verificationResponse.verified;
|
|
30
|
+
}
|
|
31
|
+
async function authenticate(userName) {
|
|
32
|
+
const { requestOptions, attemptId } = await $fetch(authenticateEndpoint, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
body: {
|
|
35
|
+
verify: false,
|
|
36
|
+
userName
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const assertionResponse = await startAuthentication(requestOptions);
|
|
40
|
+
const verificationResponse = await $fetch(authenticateEndpoint, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
body: {
|
|
43
|
+
attemptId,
|
|
44
|
+
userName,
|
|
45
|
+
response: assertionResponse,
|
|
46
|
+
verify: true
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return verificationResponse && verificationResponse.verified;
|
|
50
|
+
}
|
|
51
|
+
const isSupported = ref(false);
|
|
52
|
+
onMounted(() => {
|
|
53
|
+
isSupported.value = browserSupportsWebAuthn();
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
register,
|
|
57
|
+
authenticate,
|
|
58
|
+
isSupported,
|
|
59
|
+
isAutofillSupported: browserSupportsWebAuthnAutofill,
|
|
60
|
+
isPlatformAvailable: platformAuthenticatorIsAvailable
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -2,7 +2,7 @@ import { eventHandler } from "h3";
|
|
|
2
2
|
import { getUserSession, sessionHooks } from "../utils/session.js";
|
|
3
3
|
export default eventHandler(async (event) => {
|
|
4
4
|
const session = await getUserSession(event);
|
|
5
|
-
if (session.
|
|
5
|
+
if (Object.keys(session).length > 0) {
|
|
6
6
|
await sessionHooks.callHookParallel("fetch", session, event);
|
|
7
7
|
}
|
|
8
8
|
const { secure, ...data } = session;
|
|
@@ -57,4 +57,4 @@ export interface OAuthAuth0Config {
|
|
|
57
57
|
*/
|
|
58
58
|
redirectURL?: string;
|
|
59
59
|
}
|
|
60
|
-
export declare function
|
|
60
|
+
export declare function defineOAuthAuth0EventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthAuth0Config>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -3,7 +3,7 @@ import { withQuery } from "ufo";
|
|
|
3
3
|
import { defu } from "defu";
|
|
4
4
|
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
|
|
5
5
|
import { useRuntimeConfig } from "#imports";
|
|
6
|
-
export function
|
|
6
|
+
export function defineOAuthAuth0EventHandler({ config, onSuccess, onError }) {
|
|
7
7
|
return eventHandler(async (event) => {
|
|
8
8
|
config = defu(config, useRuntimeConfig(event).oauth?.auth0, {
|
|
9
9
|
authorizationParams: {}
|
|
@@ -45,4 +45,4 @@ export interface OAuthBattledotnetConfig {
|
|
|
45
45
|
*/
|
|
46
46
|
redirectURL?: string;
|
|
47
47
|
}
|
|
48
|
-
export declare function
|
|
48
|
+
export declare function defineOAuthBattledotnetEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthBattledotnetConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -4,7 +4,7 @@ import { defu } from "defu";
|
|
|
4
4
|
import { randomUUID } from "uncrypto";
|
|
5
5
|
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
|
|
6
6
|
import { useRuntimeConfig, createError } from "#imports";
|
|
7
|
-
export function
|
|
7
|
+
export function defineOAuthBattledotnetEventHandler({ config, onSuccess, onError }) {
|
|
8
8
|
return eventHandler(async (event) => {
|
|
9
9
|
config = defu(config, useRuntimeConfig(event).oauth?.battledotnet, {
|
|
10
10
|
authorizationURL: "https://oauth.battle.net/authorize",
|
|
@@ -41,4 +41,4 @@ export interface OAuthCognitoConfig {
|
|
|
41
41
|
*/
|
|
42
42
|
domain?: string;
|
|
43
43
|
}
|
|
44
|
-
export declare function
|
|
44
|
+
export declare function defineOAuthCognitoEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthCognitoConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -3,7 +3,7 @@ import { withQuery } from "ufo";
|
|
|
3
3
|
import { defu } from "defu";
|
|
4
4
|
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
|
|
5
5
|
import { useRuntimeConfig } from "#imports";
|
|
6
|
-
export function
|
|
6
|
+
export function defineOAuthCognitoEventHandler({ config, onSuccess, onError }) {
|
|
7
7
|
return eventHandler(async (event) => {
|
|
8
8
|
config = defu(config, useRuntimeConfig(event).oauth?.cognito, {
|
|
9
9
|
authorizationParams: {}
|
|
@@ -50,4 +50,4 @@ export interface OAuthDiscordConfig {
|
|
|
50
50
|
*/
|
|
51
51
|
redirectURL?: string;
|
|
52
52
|
}
|
|
53
|
-
export declare function
|
|
53
|
+
export declare function defineOAuthDiscordEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthDiscordConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -3,7 +3,7 @@ import { withQuery } from "ufo";
|
|
|
3
3
|
import { defu } from "defu";
|
|
4
4
|
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
|
|
5
5
|
import { useRuntimeConfig } from "#imports";
|
|
6
|
-
export function
|
|
6
|
+
export function defineOAuthDiscordEventHandler({ config, onSuccess, onError }) {
|
|
7
7
|
return eventHandler(async (event) => {
|
|
8
8
|
config = defu(config, useRuntimeConfig(event).oauth?.discord, {
|
|
9
9
|
authorizationURL: "https://discord.com/oauth2/authorize",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthDropboxConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Dropbox Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_DROPBOX_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Dropbox OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_DROPBOX_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Dropbox OAuth Scope
|
|
15
|
+
* @default []
|
|
16
|
+
* @see https://developers.dropbox.com/oauth-guide#dropbox-api-permissions
|
|
17
|
+
* @example ['email', 'profile']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Require email from user, adds the ['email'] scope if not present
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
emailRequired?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Dropbox OAuth Authorization URL
|
|
27
|
+
* @default 'https://www.dropbox.com/oauth2/authorize'
|
|
28
|
+
*/
|
|
29
|
+
authorizationURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Dropbox OAuth Token URL
|
|
32
|
+
* @default 'https://api.dropboxapi.com/oauth2/token'
|
|
33
|
+
*/
|
|
34
|
+
tokenURL?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
37
|
+
* @see https://www.dropbox.com/developers/documentation/http/documentation#authorization
|
|
38
|
+
* @example { locale: 'en-US' }
|
|
39
|
+
*/
|
|
40
|
+
authorizationParams?: Record<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
43
|
+
* @default process.env.NUXT_OAUTH_DROPBOX_REDIRECT_URL or current URL
|
|
44
|
+
*/
|
|
45
|
+
redirectURL?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function defineOAuthDropboxEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthDropboxConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|