nuxt-auth-utils 0.5.34 → 0.6.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.
Files changed (142) hide show
  1. package/README.md +118 -15
  2. package/dist/module.d.mts +3 -3
  3. package/dist/module.json +4 -1
  4. package/dist/module.mjs +18 -31
  5. package/dist/runtime/app/composables/session.js +3 -2
  6. package/dist/runtime/server/api/session.delete.d.ts +1 -1
  7. package/dist/runtime/server/api/session.delete.js +2 -2
  8. package/dist/runtime/server/api/session.get.d.ts +1 -1
  9. package/dist/runtime/server/api/session.get.js +6 -2
  10. package/dist/runtime/server/lib/atproto/bluesky.d.ts +3 -3
  11. package/dist/runtime/server/lib/atproto/bluesky.js +6 -6
  12. package/dist/runtime/server/lib/oauth/apple.d.ts +1 -1
  13. package/dist/runtime/server/lib/oauth/apple.js +3 -4
  14. package/dist/runtime/server/lib/oauth/atlassian.d.ts +1 -1
  15. package/dist/runtime/server/lib/oauth/atlassian.js +8 -8
  16. package/dist/runtime/server/lib/oauth/auth0.d.ts +1 -1
  17. package/dist/runtime/server/lib/oauth/auth0.js +4 -4
  18. package/dist/runtime/server/lib/oauth/authentik.d.ts +1 -1
  19. package/dist/runtime/server/lib/oauth/authentik.js +6 -6
  20. package/dist/runtime/server/lib/oauth/azureb2c.d.ts +1 -1
  21. package/dist/runtime/server/lib/oauth/azureb2c.js +5 -5
  22. package/dist/runtime/server/lib/oauth/battledotnet.d.ts +1 -1
  23. package/dist/runtime/server/lib/oauth/battledotnet.js +6 -6
  24. package/dist/runtime/server/lib/oauth/box.d.ts +1 -1
  25. package/dist/runtime/server/lib/oauth/box.js +5 -5
  26. package/dist/runtime/server/lib/oauth/cloudflare.d.ts +1 -1
  27. package/dist/runtime/server/lib/oauth/cloudflare.js +5 -5
  28. package/dist/runtime/server/lib/oauth/cognito.d.ts +1 -1
  29. package/dist/runtime/server/lib/oauth/cognito.js +4 -4
  30. package/dist/runtime/server/lib/oauth/discord.d.ts +1 -1
  31. package/dist/runtime/server/lib/oauth/discord.js +4 -4
  32. package/dist/runtime/server/lib/oauth/dropbox.d.ts +1 -1
  33. package/dist/runtime/server/lib/oauth/dropbox.js +5 -5
  34. package/dist/runtime/server/lib/oauth/facebook.d.ts +1 -1
  35. package/dist/runtime/server/lib/oauth/facebook.js +7 -7
  36. package/dist/runtime/server/lib/oauth/gitea.d.ts +1 -1
  37. package/dist/runtime/server/lib/oauth/gitea.js +5 -5
  38. package/dist/runtime/server/lib/oauth/github.d.ts +1 -1
  39. package/dist/runtime/server/lib/oauth/github.js +6 -6
  40. package/dist/runtime/server/lib/oauth/gitlab.d.ts +1 -1
  41. package/dist/runtime/server/lib/oauth/gitlab.js +5 -5
  42. package/dist/runtime/server/lib/oauth/google.d.ts +1 -1
  43. package/dist/runtime/server/lib/oauth/google.js +4 -4
  44. package/dist/runtime/server/lib/oauth/heroku.d.ts +1 -1
  45. package/dist/runtime/server/lib/oauth/heroku.js +5 -5
  46. package/dist/runtime/server/lib/oauth/hubspot.d.ts +1 -1
  47. package/dist/runtime/server/lib/oauth/hubspot.js +4 -4
  48. package/dist/runtime/server/lib/oauth/instagram.d.ts +1 -1
  49. package/dist/runtime/server/lib/oauth/instagram.js +6 -6
  50. package/dist/runtime/server/lib/oauth/keycloak.d.ts +1 -1
  51. package/dist/runtime/server/lib/oauth/keycloak.js +6 -6
  52. package/dist/runtime/server/lib/oauth/kick.d.ts +1 -1
  53. package/dist/runtime/server/lib/oauth/kick.js +5 -5
  54. package/dist/runtime/server/lib/oauth/line.d.ts +1 -1
  55. package/dist/runtime/server/lib/oauth/line.js +6 -6
  56. package/dist/runtime/server/lib/oauth/linear.d.ts +1 -1
  57. package/dist/runtime/server/lib/oauth/linear.js +6 -6
  58. package/dist/runtime/server/lib/oauth/linkedin.d.ts +1 -1
  59. package/dist/runtime/server/lib/oauth/linkedin.js +4 -4
  60. package/dist/runtime/server/lib/oauth/livechat.d.ts +1 -1
  61. package/dist/runtime/server/lib/oauth/livechat.js +4 -4
  62. package/dist/runtime/server/lib/oauth/mastodon.d.ts +1 -1
  63. package/dist/runtime/server/lib/oauth/mastodon.js +10 -11
  64. package/dist/runtime/server/lib/oauth/microsoft.d.ts +1 -1
  65. package/dist/runtime/server/lib/oauth/microsoft.js +5 -5
  66. package/dist/runtime/server/lib/oauth/oidc.d.ts +1 -1
  67. package/dist/runtime/server/lib/oauth/oidc.js +6 -6
  68. package/dist/runtime/server/lib/oauth/okta.d.ts +4 -4
  69. package/dist/runtime/server/lib/oauth/okta.js +8 -8
  70. package/dist/runtime/server/lib/oauth/ory.d.ts +1 -1
  71. package/dist/runtime/server/lib/oauth/ory.js +6 -6
  72. package/dist/runtime/server/lib/oauth/osu.d.ts +1 -1
  73. package/dist/runtime/server/lib/oauth/osu.js +4 -4
  74. package/dist/runtime/server/lib/oauth/paypal.d.ts +1 -1
  75. package/dist/runtime/server/lib/oauth/paypal.js +5 -5
  76. package/dist/runtime/server/lib/oauth/polar.d.ts +1 -1
  77. package/dist/runtime/server/lib/oauth/polar.js +5 -5
  78. package/dist/runtime/server/lib/oauth/riotgames.d.ts +1 -1
  79. package/dist/runtime/server/lib/oauth/riotgames.js +5 -5
  80. package/dist/runtime/server/lib/oauth/roblox.d.ts +1 -1
  81. package/dist/runtime/server/lib/oauth/roblox.js +4 -4
  82. package/dist/runtime/server/lib/oauth/salesforce.d.ts +1 -1
  83. package/dist/runtime/server/lib/oauth/salesforce.js +5 -5
  84. package/dist/runtime/server/lib/oauth/seznam.d.ts +1 -1
  85. package/dist/runtime/server/lib/oauth/seznam.js +4 -4
  86. package/dist/runtime/server/lib/oauth/shopifyCustomer.d.ts +1 -1
  87. package/dist/runtime/server/lib/oauth/shopifyCustomer.js +7 -7
  88. package/dist/runtime/server/lib/oauth/slack.d.ts +1 -1
  89. package/dist/runtime/server/lib/oauth/slack.js +5 -5
  90. package/dist/runtime/server/lib/oauth/spotify.d.ts +1 -1
  91. package/dist/runtime/server/lib/oauth/spotify.js +4 -4
  92. package/dist/runtime/server/lib/oauth/steam.d.ts +1 -1
  93. package/dist/runtime/server/lib/oauth/steam.js +9 -9
  94. package/dist/runtime/server/lib/oauth/strava.d.ts +1 -1
  95. package/dist/runtime/server/lib/oauth/strava.js +5 -5
  96. package/dist/runtime/server/lib/oauth/tiktok.d.ts +1 -1
  97. package/dist/runtime/server/lib/oauth/tiktok.js +5 -5
  98. package/dist/runtime/server/lib/oauth/twitch.d.ts +1 -1
  99. package/dist/runtime/server/lib/oauth/twitch.js +5 -5
  100. package/dist/runtime/server/lib/oauth/vk.d.ts +1 -1
  101. package/dist/runtime/server/lib/oauth/vk.js +5 -5
  102. package/dist/runtime/server/lib/oauth/wechat.d.ts +1 -1
  103. package/dist/runtime/server/lib/oauth/wechat.js +7 -7
  104. package/dist/runtime/server/lib/oauth/workos.d.ts +1 -1
  105. package/dist/runtime/server/lib/oauth/workos.js +3 -4
  106. package/dist/runtime/server/lib/oauth/x.d.ts +1 -1
  107. package/dist/runtime/server/lib/oauth/x.js +4 -4
  108. package/dist/runtime/server/lib/oauth/xsuaa.d.ts +1 -1
  109. package/dist/runtime/server/lib/oauth/xsuaa.js +4 -4
  110. package/dist/runtime/server/lib/oauth/yandex.d.ts +1 -1
  111. package/dist/runtime/server/lib/oauth/yandex.js +4 -4
  112. package/dist/runtime/server/lib/oauth/zendesk.d.ts +1 -1
  113. package/dist/runtime/server/lib/oauth/zendesk.js +4 -4
  114. package/dist/runtime/server/lib/oauth/zitadel.d.ts +1 -1
  115. package/dist/runtime/server/lib/oauth/zitadel.js +6 -6
  116. package/dist/runtime/server/lib/session.d.ts +11 -0
  117. package/dist/runtime/server/lib/session.js +33 -0
  118. package/dist/runtime/server/lib/utils.d.ts +9 -8
  119. package/dist/runtime/server/lib/utils.js +13 -11
  120. package/dist/runtime/server/lib/webauthn/authenticate.d.ts +1 -1
  121. package/dist/runtime/server/lib/webauthn/authenticate.js +8 -9
  122. package/dist/runtime/server/lib/webauthn/register.d.ts +2 -2
  123. package/dist/runtime/server/lib/webauthn/register.js +10 -11
  124. package/dist/runtime/server/plugins/oauth-hash.d.ts +2 -0
  125. package/dist/runtime/server/plugins/oauth-hash.js +8 -0
  126. package/dist/runtime/server/routes/atproto/client-metadata.json.get.d.ts +1 -1
  127. package/dist/runtime/server/routes/atproto/client-metadata.json.get.js +5 -5
  128. package/dist/runtime/server/utils/atproto.d.ts +2 -2
  129. package/dist/runtime/server/utils/atproto.js +3 -4
  130. package/dist/runtime/server/utils/password.js +1 -1
  131. package/dist/runtime/server/utils/session.d.ts +26 -17
  132. package/dist/runtime/server/utils/session.js +49 -29
  133. package/dist/runtime/types/event.d.ts +27 -3
  134. package/dist/runtime/types/index.d.ts +1 -1
  135. package/dist/runtime/types/oauth-config.d.ts +3 -3
  136. package/dist/runtime/types/session.d.ts +5 -2
  137. package/dist/runtime/types/webauthn.d.ts +14 -17
  138. package/package.json +10 -6
  139. package/dist/runtime/server/plugins/oauth.d.ts +0 -2
  140. package/dist/runtime/server/plugins/oauth.js +0 -16
  141. package/dist/runtime/server/plugins/ws.d.ts +0 -2
  142. package/dist/runtime/server/plugins/ws.js +0 -7
package/README.md CHANGED
@@ -29,6 +29,8 @@ It has few dependencies (only from [UnJS](https://github.com/unjs)), run on mult
29
29
 
30
30
  ## Requirements
31
31
 
32
+ This module requires Nuxt 4.6 or later. If you upgrade from v0.5, read [Migrate from v0.5](#migrate-from-v05).
33
+
32
34
  This module only works with a Nuxt server running as it uses server API routes (`nuxt build`).
33
35
 
34
36
  This means that you cannot use this module with `nuxt generate`.
@@ -43,14 +45,19 @@ You can anyway use [Hybrid Rendering](#hybrid-rendering) to pre-render pages of
43
45
  npx nuxi@latest module add auth-utils
44
46
  ```
45
47
 
46
- 2. Add a `NUXT_SESSION_PASSWORD` env variable with at least 32 characters in the `.env`.
48
+ 2. Set the `NUXT_APP_SECRET` env variable in production, with at least 32 characters.
49
+
50
+ ```bash
51
+ # Generate a secret
52
+ openssl rand -base64 32
53
+ ```
47
54
 
48
55
  ```bash
49
56
  # .env
50
- NUXT_SESSION_PASSWORD=password-with-at-least-32-characters
57
+ NUXT_APP_SECRET=<the-generated-secret>
51
58
  ```
52
59
 
53
- Nuxt Auth Utils generates one for you when running Nuxt in development the first time if no `NUXT_SESSION_PASSWORD` is set.
60
+ Nuxt Auth Utils seals the session cookie with a secret that it derives from the [Nuxt `appSecret`](https://nuxt.com/blog/v4-6). In development, Nuxt generates an `appSecret` for you if none is set. Builds never generate one, so you must set `NUXT_APP_SECRET` in production.
54
61
 
55
62
  3. That's it! You can now add authentication to your Nuxt app ✨
56
63
 
@@ -122,6 +129,19 @@ interface UserSessionComposable {
122
129
 
123
130
  The following helpers are auto-imported in your `server/` directory.
124
131
 
132
+ They accept the event of a handler defined with `defineEventHandler` from `nuxt/server`. On Nuxt 4, they also accept the event of a handler defined with the auto-imported `defineEventHandler` from h3.
133
+
134
+ ```ts
135
+ // server/api/me.get.ts
136
+ import { defineEventHandler } from 'nuxt/server'
137
+
138
+ export default defineEventHandler(async (event) => {
139
+ const { user } = await requireUserSession(event)
140
+
141
+ return user
142
+ })
143
+ ```
144
+
125
145
  ### Session Management
126
146
 
127
147
  ```ts
@@ -152,6 +172,9 @@ await clearUserSession(event)
152
172
 
153
173
  // Require a user session (send back 401 if no `user` key in session)
154
174
  const session = await requireUserSession(event)
175
+
176
+ // Customize the error status and message
177
+ const session = await requireUserSession(event, { status: 403, message: 'Forbidden' })
155
178
  ```
156
179
 
157
180
  You can define the type for your user session by creating a type declaration file (for example, `auth.d.ts`) in your project to augment the `UserSession` type:
@@ -271,6 +294,8 @@ You can add your favorite provider by creating a new file in [src/runtime/server
271
294
  Example: `~/server/routes/auth/github.get.ts`
272
295
 
273
296
  ```ts
297
+ import { sendRedirect } from 'nuxt/server'
298
+
274
299
  export default defineOAuthGitHubEventHandler({
275
300
  config: {
276
301
  emailRequired: true
@@ -291,6 +316,8 @@ export default defineOAuthGitHubEventHandler({
291
316
  })
292
317
  ```
293
318
 
319
+ The `event` given to `onSuccess` and `onError` is a `RequestEvent` from `nuxt/server`. Import the helpers you use from `nuxt/server`, for example `sendRedirect`. On Nuxt 4, the auto-imported h3 helpers also work, but TypeScript reports a type error.
320
+
294
321
  Make sure to set the callback URL in your OAuth app settings as `<your-domain>/auth/github`.
295
322
 
296
323
  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.
@@ -450,7 +477,7 @@ export default defineWebAuthnRegisterEventHandler({
450
477
  // And verify that the email is the same as the one in session
451
478
  const session = await getUserSession(event)
452
479
  if (session.user?.email && session.user.email !== userBody.userName) {
453
- throw createError({ statusCode: 400, message: 'Email not matching current session' })
480
+ throw createError({ status: 400, message: 'Email not matching current session' })
454
481
  }
455
482
 
456
483
  // If he registers a new account with credentials
@@ -495,7 +522,7 @@ export default defineWebAuthnAuthenticateEventHandler({
495
522
  const credentials = await useDatabase().sql`...`
496
523
  // If no credentials are found, the authentication cannot be completed
497
524
  if (!credentials.length)
498
- throw createError({ statusCode: 400, message: 'User not found' })
525
+ throw createError({ status: 400, message: 'User not found' })
499
526
 
500
527
  // If user is found, only allow credentials that are registered
501
528
  // The browser will automatically try to use the credential that it knows about
@@ -509,7 +536,7 @@ export default defineWebAuthnAuthenticateEventHandler({
509
536
 
510
537
  // If the credential is not found, there is no account to log in to
511
538
  if (!credential)
512
- throw createError({ statusCode: 400, message: 'Credential not found' })
539
+ throw createError({ status: 400, message: 'Credential not found' })
513
540
 
514
541
  return credential
515
542
  },
@@ -549,7 +576,7 @@ export default defineWebAuthnAuthenticateEventHandler({
549
576
  > await useStorage().removeItem(`attempt:${attemptId}`)
550
577
  >
551
578
  > if (!challenge)
552
- > throw createError({ statusCode: 400, message: 'Challenge expired' })
579
+ > throw createError({ status: 400, message: 'Challenge expired' })
553
580
  >
554
581
  > return challenge
555
582
  > },
@@ -607,9 +634,11 @@ We leverage hooks to let you extend the session data with your own data or log w
607
634
 
608
635
  ```ts
609
636
  // server/plugins/session.ts
610
- export default defineNitroPlugin(() => {
637
+ // A server plugin is a function, so this file runs on Nuxt 4 (Nitro 2) and Nuxt 5 (Nitro 3)
638
+ export default () => {
611
639
  // Called when the session is fetched during SSR for the Vue composable (/api/_auth/session)
612
640
  // Or when we call useUserSession().fetch()
641
+ // Not called for a visitor without a session cookie: they get an empty session and no cookie is set
613
642
  sessionHooks.hook('fetch', async (session, event) => {
614
643
  // extend User Session by calling your database
615
644
  // or
@@ -620,7 +649,7 @@ export default defineNitroPlugin(() => {
620
649
  sessionHooks.hook('clear', async (session, event) => {
621
650
  // Log that user logged out
622
651
  })
623
- })
652
+ }
624
653
  ```
625
654
 
626
655
  ## Server-Side Rendering
@@ -728,10 +757,13 @@ export default defineNuxtConfig({
728
757
  })
729
758
  ```
730
759
 
731
- You can use the `requireUserSession` function in the `upgrade` function to check if the user is authenticated before upgrading the WebSocket connection.
760
+ You can use the `requireUserSession` function in the `upgrade` function to check if the user is authenticated before upgrading the WebSocket connection. In WebSocket handlers, the session is read-only: `setUserSession`, `replaceUserSession` and `clearUserSession` throw an error when given a WebSocket peer or upgrade request.
732
761
 
733
762
  ```ts
734
763
  // server/routes/ws.ts
764
+ // `defineWebSocketHandler` is not auto-imported on Nuxt 5, `h3` works on Nuxt 4 and Nuxt 5
765
+ import { defineWebSocketHandler } from 'h3'
766
+
735
767
  export default defineWebSocketHandler({
736
768
  async upgrade(request) {
737
769
  // Make sure the user is authenticated before upgrading the WebSocket connection
@@ -773,7 +805,7 @@ onMounted(open)
773
805
 
774
806
  ## Configuration
775
807
 
776
- We leverage `runtimeConfig.session` to give the defaults option to [h3 `useSession`](https://h3.unjs.io/examples/handle-session).
808
+ Nuxt Auth Utils stores the user session with `useSession` from `nuxt/server`. It uses `runtimeConfig.session` for the default options.
777
809
 
778
810
  You can overwrite the options in your `nuxt.config.ts`:
779
811
 
@@ -792,14 +824,21 @@ Our defaults are:
792
824
 
793
825
  ```ts
794
826
  {
795
- name: 'nuxt-session',
796
- password: process.env.NUXT_SESSION_PASSWORD || '',
827
+ name: 'nuxt-auth-session',
797
828
  cookie: {
798
829
  sameSite: 'lax'
799
830
  }
800
831
  }
801
832
  ```
802
833
 
834
+ The available options are:
835
+
836
+ - `name`: the name of the session cookie.
837
+ - `maxAge`: the lifetime of the cookie and of the sealed session, in seconds.
838
+ - `cookie`: the cookie options, merged over `httpOnly: true`, `secure: true`, `sameSite: 'lax'`, and `path: '/'`.
839
+
840
+ The session is sealed with a secret that Nuxt derives from `appSecret`. The module ignores `runtimeConfig.session.password`.
841
+
803
842
  You can also overwrite the session config by passing it as 3rd argument of the `setUserSession` and `replaceUserSession` functions:
804
843
 
805
844
  ```ts
@@ -808,11 +847,41 @@ await setUserSession(event, { ... } , {
808
847
  })
809
848
  ```
810
849
 
811
- Checkout the [`SessionConfig`](https://github.com/unjs/h3/blob/c04c458810e34eb15c1647e1369e7d7ef19f567d/src/utils/session.ts#L20) for all options.
850
+ ## Migrate from v0.5
851
+
852
+ Nuxt Auth Utils v0.6 moves to `nuxt/server`, the server API of Nuxt 4.6. The same server code runs on Nuxt 4 and Nuxt 5. This release has breaking changes.
853
+
854
+ > [!WARNING]
855
+ > All users are logged out once after the upgrade. The session cookie has a new name and a new format.
856
+
857
+ To migrate your app, follow these steps:
858
+
859
+ 1. Upgrade Nuxt to 4.6 or later.
860
+ 2. Generate a secret with `openssl rand -base64 32`.
861
+ 3. Set the secret as `NUXT_APP_SECRET` in your production environment.
862
+ 4. On Cloudflare Workers, set `compatibility_date` to `2025-04-01` or later, or add the `nodejs_compat_populate_process_env` compatibility flag. Otherwise the module can't read `NUXT_APP_SECRET` and the `NUXT_OAUTH_*` variables from your Worker variables and secrets.
863
+ 5. Remove `NUXT_SESSION_PASSWORD` from your environment and from your `.env` file. The module logs a warning while it is set.
864
+ 6. Change `statusCode` to `status` in `createError()` calls.
865
+ 7. In `onSuccess` and `onError` callbacks, import helpers such as `sendRedirect` from `nuxt/server`.
866
+
867
+ The other changes in v0.6 are:
868
+
869
+ - The session cookie is `nuxt-auth-session`. It was `nuxt-session`, which is now the default name of `useSession` from `nuxt/server`.
870
+ - The session can't be read from the `x-nuxt-session-session` request header anymore.
871
+ - The `sessionHeader`, `seal`, `crypto`, `generateId`, and `password` session options are removed.
872
+ - The `event` in the OAuth and WebAuthn callbacks is a `RequestEvent` from `nuxt/server`, and `error` is a `NuxtError`.
873
+ - In WebSocket handlers, `setUserSession`, `replaceUserSession` and `clearUserSession` throw an error, as the session cookie can't be sent back to the client.
874
+ - `requireUserSession` accepts a `status` option. The `statusCode` option still works, but it's deprecated.
875
+ - Nuxt throws a 500 error if a session cookie is larger than 4096 bytes.
876
+ - The session `maxAge` counts from the last write: each `setUserSession` call extends the cookie. In v0.5, it counted from when the session was created.
877
+ - The session can't be named `nuxt-session`, the cookie of `useSession()` from `nuxt/server`. The module warns and uses `nuxt-auth-session` instead.
878
+ - WebAuthn requires SimpleWebAuthn v13, with `@simplewebauthn/server` 13.3.2 or later. Import its JSON types from `@simplewebauthn/server` or `@simplewebauthn/browser` instead of `@simplewebauthn/types`.
812
879
 
813
880
  ## Shared session between microservices and subdomains
814
881
 
815
- To share a session between multiple applications, use the same `NUXT_SESSION_PASSWORD` and session `name` in each of them.
882
+ To share a session between multiple applications, use the same `NUXT_APP_SECRET` and session `name` in each of them.
883
+
884
+ Any application with the same `NUXT_APP_SECRET` can also derive the other keys Nuxt derives from it, so share it only between applications you trust equally.
816
885
 
817
886
  If the applications run on different subdomains, also set the session cookie `domain` to the parent domain in each `nuxt.config.ts`:
818
887
 
@@ -831,6 +900,34 @@ export default defineNuxtConfig({
831
900
 
832
901
  The browser then sends the session cookie to every subdomain of `example.com`, and each application can decrypt it.
833
902
 
903
+ ## Serve over plain HTTP
904
+
905
+ The OAuth event handlers set short-lived cookies for the state, nonce and PKCE verifier. They are `Secure` except in development. Browsers ignore `Secure` cookies set over plain HTTP, except on `localhost`, so logging in to a built app served over plain HTTP, for example in an end-to-end test stack, fails with `state mismatch`.
906
+
907
+ When `runtimeConfig.session.cookie.secure` is `false`, both the session cookie and these cookies are set without `Secure`. Nuxt only overrides declared options from environment variables, so declare it in your `nuxt.config.ts` to switch it at runtime:
908
+
909
+ ```ts
910
+ export default defineNuxtConfig({
911
+ modules: ['nuxt-auth-utils'],
912
+ runtimeConfig: {
913
+ session: {
914
+ cookie: {
915
+ secure: true
916
+ }
917
+ }
918
+ }
919
+ })
920
+ ```
921
+
922
+ Then start the built app with `NUXT_SESSION_COOKIE_SECURE=false`:
923
+
924
+ ```bash
925
+ NUXT_SESSION_COOKIE_SECURE=false node .output/server/index.mjs
926
+ ```
927
+
928
+ > [!WARNING]
929
+ > Only do this when the app is served over plain HTTP: without `Secure`, the cookies can be read or overwritten on the network. A proxy that terminates TLS is not a reason to disable it, as `Secure` applies to the browser's connection.
930
+
834
931
  ## More
835
932
 
836
933
  - [nuxt-authorization](https://github.com/barbapapazes/nuxt-authorization): Authorization module for managing permissions inside a Nuxt app, compatible with `nuxt-auth-utils`
@@ -847,6 +944,12 @@ pnpm run dev:prepare
847
944
  # Develop with the playground
848
945
  pnpm run dev
849
946
 
947
+ # Develop with the playground on the Nuxt 5 nightly (Nitro 3), run `pnpm install` to go back to the lockfile
948
+ pnpm run dev:v5
949
+
950
+ # Run the tests on the Nuxt 5 nightly
951
+ pnpm run install:nuxt5 && pnpm run dev:prepare && pnpm run test
952
+
850
953
  # Build the playground
851
954
  pnpm run dev:build
852
955
 
package/dist/module.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ScryptConfig } from '@adonisjs/hash/types';
2
- import { SessionConfig } from 'h3';
2
+ import { SessionConfig } from 'nuxt/server';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -33,9 +33,9 @@ declare module 'nuxt/schema' {
33
33
  scrypt: ScryptConfig;
34
34
  };
35
35
  /**
36
- * Session configuration
36
+ * Session configuration, the session is sealed with a secret derived from `appSecret` (`NUXT_APP_SECRET`)
37
37
  */
38
- session: SessionConfig;
38
+ session: Omit<SessionConfig, 'password'>;
39
39
  }
40
40
  interface PublicRuntimeConfig {
41
41
  auth: {
package/dist/module.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "auth-utils",
3
3
  "configKey": "auth",
4
- "version": "0.5.34",
4
+ "compatibility": {
5
+ "nuxt": ">=4.6.0"
6
+ },
7
+ "version": "0.6.1",
5
8
  "builder": {
6
9
  "@nuxt/module-builder": "1.0.3",
7
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,14 +1,14 @@
1
- import { defineNuxtModule, createResolver, addComponentsDir, addImports, addPlugin, addServerPlugin, addServerImportsDir, logger, addServerHandler } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addComponentsDir, addImports, addPlugin, addServerPlugin, addServerImportsDir, logger, addServerHandler, getNitroVersion } from '@nuxt/kit';
2
2
  import { defu } from 'defu';
3
- import { readFile, writeFile } from 'node:fs/promises';
4
- import { join } from 'pathe';
5
- import { randomUUID } from 'uncrypto';
6
3
  import { atprotoProviders, atprotoProviderDefaultClientMetadata, getClientMetadataFilename } from '../dist/runtime/utils/atproto.js';
7
4
 
8
5
  const module$1 = defineNuxtModule({
9
6
  meta: {
10
7
  name: "auth-utils",
11
- configKey: "auth"
8
+ configKey: "auth",
9
+ compatibility: {
10
+ nuxt: ">=4.6.0"
11
+ }
12
12
  },
13
13
  // Default configuration options of the Nuxt module
14
14
  defaults: {
@@ -36,11 +36,8 @@ const module$1 = defineNuxtModule({
36
36
  addPlugin(resolver.resolve("./runtime/app/plugins/session.server"));
37
37
  addPlugin(resolver.resolve("./runtime/app/plugins/session.client"));
38
38
  }
39
- addServerPlugin(resolver.resolve("./runtime/server/plugins/oauth"));
39
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/oauth-hash"));
40
40
  addServerImportsDir(resolver.resolve("./runtime/server/lib/oauth"));
41
- if (nuxt.options.nitro?.experimental?.websocket) {
42
- addServerPlugin(resolver.resolve("./runtime/server/plugins/ws"));
43
- }
44
41
  if (options.webAuthn) {
45
42
  const missingDeps = [];
46
43
  const peerDeps = ["@simplewebauthn/server", "@simplewebauthn/browser"];
@@ -68,16 +65,22 @@ const module$1 = defineNuxtModule({
68
65
  route: "/api/_auth/session",
69
66
  method: "get"
70
67
  });
71
- nuxt.options.nitro.unenv ||= {};
72
- nuxt.options.nitro.unenv.external ||= [];
73
- if (!nuxt.options.nitro.unenv.external.includes("node:crypto")) {
74
- nuxt.options.nitro.unenv.external.push("node:crypto");
68
+ if (getNitroVersion(nuxt) === 2) {
69
+ const nitroOptions = nuxt.options.nitro;
70
+ nitroOptions.unenv ||= {};
71
+ nitroOptions.unenv.external ||= [];
72
+ if (!nitroOptions.unenv.external.includes("node:crypto")) {
73
+ nitroOptions.unenv.external.push("node:crypto");
74
+ }
75
75
  }
76
76
  const runtimeConfig = nuxt.options.runtimeConfig;
77
77
  const envSessionPassword = `${runtimeConfig.nitro?.envPrefix || "NUXT_"}SESSION_PASSWORD`;
78
+ if (process.env[envSessionPassword] || runtimeConfig.session?.password) {
79
+ logger.withTag("nuxt-auth-utils").warn(`\`${envSessionPassword}\` (\`runtimeConfig.session.password\`) is not used anymore. Set \`NUXT_APP_SECRET\` instead, for example with \`openssl rand -base64 32\`. Sessions are now sealed with a secret derived from \`appSecret\`, so users are logged out once.`);
80
+ }
78
81
  runtimeConfig.session = defu(runtimeConfig.session, {
79
- name: "nuxt-session",
80
- password: "",
82
+ // Declared so `NUXT_SESSION_NAME` can override it at runtime
83
+ name: "nuxt-auth-session",
81
84
  cookie: {
82
85
  sameSite: "lax"
83
86
  }
@@ -85,22 +88,6 @@ const module$1 = defineNuxtModule({
85
88
  runtimeConfig.hash = defu(runtimeConfig.hash, {
86
89
  scrypt: options.hash?.scrypt
87
90
  });
88
- if (nuxt.options.dev && !process.env[envSessionPassword]) {
89
- if (nuxt.options.runtimeConfig.session.password) {
90
- process.env[envSessionPassword] = nuxt.options.runtimeConfig.session.password;
91
- } else {
92
- const password = process.env[envSessionPassword] = randomUUID().replace(/-/g, "");
93
- const envPath = join(nuxt.options.rootDir, ".env");
94
- const envContent = await readFile(envPath, "utf-8").catch(() => "");
95
- if (!envContent.includes(envSessionPassword)) {
96
- await writeFile(
97
- envPath,
98
- `${envContent ? envContent + "\n" : envContent}${envSessionPassword}=${password}`,
99
- "utf-8"
100
- );
101
- }
102
- }
103
- }
104
91
  runtimeConfig.public.auth = defu(runtimeConfig.public.auth, {
105
92
  loadStrategy: options.loadStrategy ?? "server-first"
106
93
  });
@@ -1,4 +1,3 @@
1
- import { appendResponseHeader } from "h3";
2
1
  import { useState, computed, useRequestFetch, useRequestEvent } from "#imports";
3
2
  export function useUserSession() {
4
3
  const serverEvent = import.meta.server ? useRequestEvent() : null;
@@ -10,8 +9,10 @@ export function useUserSession() {
10
9
  method: "DELETE",
11
10
  onResponse({ response: { headers } }) {
12
11
  if (import.meta.server && serverEvent) {
12
+ const res = serverEvent.res;
13
13
  for (const setCookie of headers.getSetCookie()) {
14
- appendResponseHeader(serverEvent, "Set-Cookie", setCookie);
14
+ if (res.headers) res.headers.append("set-cookie", setCookie);
15
+ else res.appendHeader?.("set-cookie", setCookie);
15
16
  }
16
17
  }
17
18
  }
@@ -1,4 +1,4 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
1
+ declare const _default: import("nuxt/server").EventHandler<Promise<{
2
2
  loggedOut: boolean;
3
3
  }>>;
4
4
  export default _default;
@@ -1,6 +1,6 @@
1
- import { eventHandler } from "h3";
1
+ import { defineEventHandler } from "nuxt/server";
2
2
  import { clearUserSession } from "../utils/session.js";
3
- export default eventHandler(async (event) => {
3
+ export default defineEventHandler(async (event) => {
4
4
  await clearUserSession(event);
5
5
  return { loggedOut: true };
6
6
  });
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<UserSession>>;
1
+ declare const _default: import("nuxt/server").EventHandler<Promise<any>>;
2
2
  export default _default;
@@ -1,6 +1,10 @@
1
- import { eventHandler } from "h3";
1
+ import { defineEventHandler, getCookie } from "nuxt/server";
2
2
  import { getUserSession, sessionHooks } from "../utils/session.js";
3
- export default eventHandler(async (event) => {
3
+ import { resolveSessionConfig } from "../lib/session.js";
4
+ export default defineEventHandler(async (event) => {
5
+ if (!getCookie(event, resolveSessionConfig().name)) {
6
+ return {};
7
+ }
4
8
  const session = await getUserSession(event);
5
9
  if (Object.keys(session).length > 0) {
6
10
  await sessionHooks.callHookParallel("fetch", session, event);
@@ -1,4 +1,4 @@
1
- import type { H3Event } from 'h3';
1
+ import type { RequestEvent } from 'nuxt/server';
2
2
  import type { NodeSavedSession, NodeSavedSessionStore, NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node';
3
3
  import type { AppBskyActorDefs } from '@atproto/api';
4
4
  import type { OAuthConfig } from '#auth-utils';
@@ -22,11 +22,11 @@ type BlueSkyTokens = NodeSavedSession['tokenSet'];
22
22
  export declare function defineOAuthBlueskyEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthBlueskyConfig, {
23
23
  user: BlueSkyUser;
24
24
  tokens: BlueSkyTokens;
25
- }>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
25
+ }>): import("nuxt/server").EventHandler<Promise<any>>;
26
26
  export declare class StateStore implements NodeSavedStateStore {
27
27
  private event;
28
28
  private readonly stateKey;
29
- constructor(event: H3Event);
29
+ constructor(event: RequestEvent);
30
30
  get(): Promise<NodeSavedState | undefined>;
31
31
  set(key: string, val: NodeSavedState): Promise<void>;
32
32
  del(): Promise<void>;
@@ -1,9 +1,9 @@
1
- import { createError, eventHandler, getQuery, sendRedirect, getCookie, setCookie, deleteCookie } from "h3";
1
+ import { defineEventHandler, createError, deleteCookie, getCookie, getQuery, sendRedirect, setCookie } from "nuxt/server";
2
2
  import { NodeOAuthClient, OAuthCallbackError, OAuthResolverError, OAuthResponseError } from "@atproto/oauth-client-node";
3
3
  import { Agent } from "@atproto/api";
4
4
  import { getAtprotoClientMetadata } from "../../utils/atproto.js";
5
5
  export function defineOAuthBlueskyEventHandler({ config, onSuccess, onError }) {
6
- return eventHandler(async (event) => {
6
+ return defineEventHandler(async (event) => {
7
7
  const clientMetadata = getAtprotoClientMetadata(event, "bluesky", config);
8
8
  const scopes = clientMetadata.scope?.split(" ") ?? [];
9
9
  const sessionStore = new SessionStore();
@@ -18,7 +18,7 @@ export function defineOAuthBlueskyEventHandler({ config, onSuccess, onError }) {
18
18
  try {
19
19
  const handle = query.handle?.toString();
20
20
  if (!handle) throw createError({
21
- statusCode: 400,
21
+ status: 400,
22
22
  message: "Query parameter `handle` empty or missing. Please provide a valid Bluesky handle."
23
23
  });
24
24
  const url = await client.authorize(handle, { scope: clientMetadata.scope });
@@ -28,13 +28,13 @@ export function defineOAuthBlueskyEventHandler({ config, onSuccess, onError }) {
28
28
  switch (true) {
29
29
  case err instanceof OAuthResponseError:
30
30
  return createError({
31
- statusCode: 500,
31
+ status: 500,
32
32
  message: `Bluesky login failed: ${err.errorDescription || "Unknown error"}`,
33
33
  data: err.payload
34
34
  });
35
35
  case err instanceof OAuthResolverError:
36
36
  return createError({
37
- statusCode: 400,
37
+ status: 400,
38
38
  message: `Bluesky login failed: ${err.message || "Unknown error"}`
39
39
  });
40
40
  default:
@@ -57,7 +57,7 @@ export function defineOAuthBlueskyEventHandler({ config, onSuccess, onError }) {
57
57
  } catch (err) {
58
58
  if (!(err instanceof OAuthCallbackError)) throw err;
59
59
  const error = createError({
60
- statusCode: 500,
60
+ status: 500,
61
61
  message: `Bluesky login failed: ${err.message || "Unknown error"}`
62
62
  });
63
63
  if (!onError) throw error;
@@ -74,4 +74,4 @@ export declare function defineOAuthAppleEventHandler({ config, onSuccess, onErro
74
74
  user: OAuthAppleUser;
75
75
  payload: OAuthAppleTokens;
76
76
  tokens: any;
77
- }>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
77
+ }>): import("nuxt/server").EventHandler<Promise<any>>;
@@ -1,16 +1,15 @@
1
- import { eventHandler, getRequestHeader, readBody, sendRedirect } from "h3";
1
+ import { defineEventHandler, getRequestHeader, readBody, sendRedirect, useRuntimeConfig } from "nuxt/server";
2
2
  import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
4
  import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, handleInvalidState, handleState, requestAccessToken, signJwt, verifyJwt } from "../utils.js";
5
- import { useRuntimeConfig } from "#imports";
6
5
  export function defineOAuthAppleEventHandler({
7
6
  config,
8
7
  onSuccess,
9
8
  onError
10
9
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
10
  }) {
12
- return eventHandler(async (event) => {
13
- config = defu(config, useRuntimeConfig(event).oauth?.apple, {
11
+ return defineEventHandler(async (event) => {
12
+ config = defu(config, useRuntimeConfig().oauth?.apple, {
14
13
  authorizationURL: config?.authorizationURL || "https://appleid.apple.com/auth/authorize",
15
14
  authorizationParams: {}
16
15
  });
@@ -91,5 +91,5 @@ export interface OAuthAtlassianConfig {
91
91
  export declare function defineOAuthAtlassianEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthAtlassianConfig, {
92
92
  user: AtlassianUser;
93
93
  tokens: AtlassianTokens;
94
- }>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
94
+ }>): import("nuxt/server").EventHandler<Promise<any>>;
95
95
  export {};
@@ -1,14 +1,14 @@
1
- import { eventHandler, getQuery, sendRedirect } from "h3";
1
+ import { defineEventHandler, createError, getQuery, sendRedirect, useRuntimeConfig } from "nuxt/server";
2
+ import { $fetch } from "ofetch";
2
3
  import { withQuery } from "ufo";
3
4
  import { defu } from "defu";
4
5
  import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, handleInvalidState, handleState, requestAccessToken } from "../utils.js";
5
- import { useRuntimeConfig, createError } from "#imports";
6
6
  export function defineOAuthAtlassianEventHandler({
7
7
  config,
8
8
  onSuccess,
9
9
  onError
10
10
  }) {
11
- return eventHandler(async (event) => {
11
+ return defineEventHandler(async (event) => {
12
12
  config = defu(config, useRuntimeConfig().oauth?.atlassian, {
13
13
  authorizationURL: "https://auth.atlassian.com/authorize",
14
14
  tokenURL: "https://auth.atlassian.com/oauth/token",
@@ -45,7 +45,7 @@ export function defineOAuthAtlassianEventHandler({
45
45
  }
46
46
  if (query.error) {
47
47
  const error = createError({
48
- statusCode: 401,
48
+ status: 401,
49
49
  message: `Atlassian login failed: ${query.error || "Unknown error"}`,
50
50
  data: query
51
51
  });
@@ -78,8 +78,8 @@ export function defineOAuthAtlassianEventHandler({
78
78
  });
79
79
  if (user.account_status === "inactive") {
80
80
  const error = createError({
81
- statusCode: 403,
82
- statusMessage: "Atlassian account is inactive",
81
+ status: 403,
82
+ message: "Atlassian account is inactive",
83
83
  data: { accountStatus: user.account_status }
84
84
  });
85
85
  if (!onError) throw error;
@@ -87,8 +87,8 @@ export function defineOAuthAtlassianEventHandler({
87
87
  }
88
88
  if (!user.email_verified) {
89
89
  const error = createError({
90
- statusCode: 400,
91
- statusMessage: "Email address is not verified",
90
+ status: 400,
91
+ message: "Email address is not verified",
92
92
  data: { email: user.email }
93
93
  });
94
94
  if (!onError) throw error;
@@ -57,4 +57,4 @@ export interface OAuthAuth0Config {
57
57
  */
58
58
  redirectURL?: string;
59
59
  }
60
- export declare function defineOAuthAuth0EventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthAuth0Config>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
60
+ export declare function defineOAuthAuth0EventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthAuth0Config>): import("nuxt/server").EventHandler<Promise<any>>;
@@ -1,11 +1,11 @@
1
- import { eventHandler, getQuery, sendRedirect } from "h3";
1
+ import { defineEventHandler, getQuery, sendRedirect, useRuntimeConfig } from "nuxt/server";
2
+ import { $fetch } from "ofetch";
2
3
  import { withQuery } from "ufo";
3
4
  import { defu } from "defu";
4
5
  import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, handleInvalidState, handleState, requestAccessToken } from "../utils.js";
5
- import { useRuntimeConfig } from "#imports";
6
6
  export function defineOAuthAuth0EventHandler({ config, onSuccess, onError }) {
7
- return eventHandler(async (event) => {
8
- config = defu(config, useRuntimeConfig(event).oauth?.auth0, {
7
+ return defineEventHandler(async (event) => {
8
+ config = defu(config, useRuntimeConfig().oauth?.auth0, {
9
9
  authorizationParams: {}
10
10
  });
11
11
  if (!config.clientId || !config.clientSecret || !config.domain) {
@@ -27,4 +27,4 @@ export interface OAuthAuthentikConfig {
27
27
  */
28
28
  scope?: string[];
29
29
  }
30
- export declare function defineOAuthAuthentikEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthAuthentikConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
30
+ export declare function defineOAuthAuthentikEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthAuthentikConfig>): import("nuxt/server").EventHandler<Promise<any>>;