nuxt-auth-utils 0.6.0 → 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.
package/README.md CHANGED
@@ -638,6 +638,7 @@ We leverage hooks to let you extend the session data with your own data or log w
638
638
  export default () => {
639
639
  // Called when the session is fetched during SSR for the Vue composable (/api/_auth/session)
640
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
641
642
  sessionHooks.hook('fetch', async (session, event) => {
642
643
  // extend User Session by calling your database
643
644
  // or
@@ -899,6 +900,34 @@ export default defineNuxtConfig({
899
900
 
900
901
  The browser then sends the session cookie to every subdomain of `example.com`, and each application can decrypt it.
901
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
+
902
931
  ## More
903
932
 
904
933
  - [nuxt-authorization](https://github.com/barbapapazes/nuxt-authorization): Authorization module for managing permissions inside a Nuxt app, compatible with `nuxt-auth-utils`
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.6.0"
6
6
  },
7
- "version": "0.6.0",
7
+ "version": "0.6.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/server").EventHandler<Promise<UserSession>>;
1
+ declare const _default: import("nuxt/server").EventHandler<Promise<any>>;
2
2
  export default _default;
@@ -1,6 +1,10 @@
1
- import { defineEventHandler } from "nuxt/server";
1
+ import { defineEventHandler, getCookie } from "nuxt/server";
2
2
  import { getUserSession, sessionHooks } from "../utils/session.js";
3
+ import { resolveSessionConfig } from "../lib/session.js";
3
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);
@@ -2,7 +2,7 @@ import { createError, defineEventHandler, deleteCookie, getCookie, getQuery, sen
2
2
  import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
4
  import { $fetch } from "ofetch";
5
- import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleInvalidState, handleState, isDevelopment, requestAccessToken } from "../utils.js";
5
+ import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleInvalidState, handleState, isOAuthCookieSecure, requestAccessToken } from "../utils.js";
6
6
  const FLOW_COOKIE_NAME = "nuxt-auth-mastodon-flow";
7
7
  const appRegistry = /* @__PURE__ */ new Map();
8
8
  const pendingRegistrations = /* @__PURE__ */ new Map();
@@ -114,7 +114,7 @@ export function defineOAuthMastodonEventHandler({ config: userConfig, onSuccess,
114
114
  const flowState2 = { instance: instance2, clientId: app.client_id, clientSecret: app.client_secret };
115
115
  setCookie(event, FLOW_COOKIE_NAME, btoa(JSON.stringify(flowState2)), {
116
116
  httpOnly: true,
117
- secure: !isDevelopment,
117
+ secure: isOAuthCookieSecure(),
118
118
  sameSite: "lax",
119
119
  maxAge: 60 * 10,
120
120
  path: "/"
@@ -12,10 +12,10 @@ export function defineOAuthSteamEventHandler({ config, onSuccess, onError }) {
12
12
  if (!config.apiKey) {
13
13
  return handleMissingConfiguration(event, "steam", ["apiKey"], onError);
14
14
  }
15
- const url = getRequestURL(event);
15
+ const url = getRequestURL(event, { xForwardedProto: true });
16
16
  const state = await handleState(event, { isCallback: Boolean(query["openid.claimed_id"]) });
17
17
  if (!query["openid.claimed_id"]) {
18
- const redirectURL = withQuery(config.redirectURL || getRequestURL(event).href, { state });
18
+ const redirectURL = withQuery(config.redirectURL || url.href, { state });
19
19
  const realm = url.port ? `${url.protocol}//${url.hostname}:${url.port}` : `${url.protocol}//${url.hostname}`;
20
20
  const steamOpenIdParams = {
21
21
  "openid.ns": "http://specs.openid.net/auth/2.0",
@@ -0,0 +1,11 @@
1
+ import type { SessionConfig } from 'nuxt/server';
2
+ /**
3
+ * Name of the cookie the user session is sealed into.
4
+ */
5
+ export declare const USER_SESSION_COOKIE_NAME = "nuxt-auth-session";
6
+ /**
7
+ * The session config of the user session: the runtime config merged under `config`, without `password`.
8
+ */
9
+ export declare function resolveSessionConfig(config?: Omit<SessionConfig, 'password'>): SessionConfig & {
10
+ name: string;
11
+ };
@@ -0,0 +1,33 @@
1
+ import { useRuntimeConfig } from "nuxt/server";
2
+ import { defu } from "defu";
3
+ export const USER_SESSION_COOKIE_NAME = "nuxt-auth-session";
4
+ const APP_SESSION_COOKIE_NAME = "nuxt-session";
5
+ export function resolveSessionConfig(config = {}) {
6
+ const runtimeConfig = useRuntimeConfig();
7
+ warnLegacySessionPassword(runtimeConfig);
8
+ const { password: _password, ...finalConfig } = defu(config, runtimeConfig.session, { name: USER_SESSION_COOKIE_NAME });
9
+ if (finalConfig.name === APP_SESSION_COOKIE_NAME) {
10
+ warnAppSessionName();
11
+ finalConfig.name = USER_SESSION_COOKIE_NAME;
12
+ }
13
+ if (finalConfig.cookie?.secure === "false") {
14
+ finalConfig.cookie = { ...finalConfig.cookie, secure: false };
15
+ }
16
+ return finalConfig;
17
+ }
18
+ let warnedAppSessionName = false;
19
+ function warnAppSessionName() {
20
+ if (warnedAppSessionName) return;
21
+ warnedAppSessionName = true;
22
+ console.warn(`[nuxt-auth-utils] The session name \`${APP_SESSION_COOKIE_NAME}\` is the cookie of \`useSession()\` from \`nuxt/server\`, using \`${USER_SESSION_COOKIE_NAME}\` instead. Set another \`runtimeConfig.session.name\` (\`NUXT_SESSION_NAME\`) to rename the user session cookie.`);
23
+ }
24
+ let checkedLegacySessionPassword = false;
25
+ function warnLegacySessionPassword(runtimeConfig) {
26
+ if (checkedLegacySessionPassword) return;
27
+ checkedLegacySessionPassword = true;
28
+ const envPrefix = runtimeConfig.nitro?.envPrefix || "NUXT_";
29
+ const envKey = `${envPrefix}SESSION_PASSWORD`;
30
+ if (globalThis.process?.env?.[envKey] || runtimeConfig.session?.password) {
31
+ console.warn(`[nuxt-auth-utils] \`${envKey}\` (\`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\`.`);
32
+ }
33
+ }
@@ -2,6 +2,7 @@ import type { RequestEvent } from 'nuxt/server';
2
2
  import * as jose from 'jose';
3
3
  import type { OAuthProvider, OnError } from '#auth-utils';
4
4
  export declare const isDevelopment: boolean;
5
+ export declare function isOAuthCookieSecure(): boolean;
5
6
  export declare function getOAuthRedirectURL(event: RequestEvent): string;
6
7
  /**
7
8
  * Request an access token body.
@@ -3,10 +3,14 @@ import { $fetch, FetchError } from "ofetch";
3
3
  import { snakeCase, upperFirst } from "scule";
4
4
  import * as jose from "jose";
5
5
  import { subtle, getRandomValues } from "uncrypto";
6
+ import { resolveSessionConfig } from "./session.js";
6
7
  export const isDevelopment = process.env.NODE_ENV === "development";
8
+ export function isOAuthCookieSecure() {
9
+ return !isDevelopment && resolveSessionConfig().cookie?.secure !== false;
10
+ }
7
11
  const OAUTH_COOKIE_MAX_AGE = 60 * 10;
8
12
  export function getOAuthRedirectURL(event) {
9
- const requestURL = getRequestURL(event);
13
+ const requestURL = getRequestURL(event, { xForwardedProto: true });
10
14
  return `${requestURL.protocol}//${requestURL.host}${requestURL.pathname}`;
11
15
  }
12
16
  export async function requestAccessToken(url, options) {
@@ -84,7 +88,7 @@ export async function handlePkceVerifier(event) {
84
88
  const verifier2 = encodeBase64Url(getRandomBytes());
85
89
  setCookie(event, "nuxt-auth-pkce", verifier2, {
86
90
  httpOnly: true,
87
- secure: !isDevelopment,
91
+ secure: isOAuthCookieSecure(),
88
92
  sameSite: "lax",
89
93
  maxAge: OAUTH_COOKIE_MAX_AGE,
90
94
  path: "/"
@@ -114,7 +118,7 @@ export async function handleState(event, options = {}) {
114
118
  const sameSite = options.sameSite || "lax";
115
119
  setCookie(event, "nuxt-auth-state", state, {
116
120
  httpOnly: true,
117
- secure: sameSite === "none" || !isDevelopment,
121
+ secure: sameSite === "none" || isOAuthCookieSecure(),
118
122
  sameSite,
119
123
  maxAge: OAUTH_COOKIE_MAX_AGE,
120
124
  path: "/"
@@ -134,7 +138,7 @@ export function handleNonce(event) {
134
138
  const nonce = encodeBase64Url(getRandomBytes());
135
139
  setCookie(event, "nuxt-auth-nonce", nonce, {
136
140
  httpOnly: true,
137
- secure: !isDevelopment,
141
+ secure: isOAuthCookieSecure(),
138
142
  sameSite: "lax",
139
143
  maxAge: OAUTH_COOKIE_MAX_AGE,
140
144
  path: "/"
@@ -13,7 +13,7 @@ export function defineWebAuthnAuthenticateEventHandler({
13
13
  onError
14
14
  }) {
15
15
  return defineEventHandler(async (event) => {
16
- const url = getRequestURL(event);
16
+ const url = getRequestURL(event, { xForwardedProto: true });
17
17
  const body = await readBody(event);
18
18
  const _config = defu(await getOptions?.(event, body) ?? {}, useRuntimeConfig().webauthn.authenticate, {
19
19
  rpID: url.hostname
@@ -13,7 +13,7 @@ export function defineWebAuthnRegisterEventHandler({
13
13
  onError
14
14
  }) {
15
15
  return defineEventHandler(async (event) => {
16
- const url = getRequestURL(event);
16
+ const url = getRequestURL(event, { xForwardedProto: true });
17
17
  const body = await readBody(event);
18
18
  if (body.verify === void 0 || !body.user?.userName)
19
19
  throw createError({
@@ -6,7 +6,7 @@ export function getAtprotoClientMetadata(event, provider, config) {
6
6
  const scopes = [.../* @__PURE__ */ new Set(["atproto", ...config?.scope ?? [], ...providerRuntimeConfig.scope ?? []])];
7
7
  const scope = scopes.join(" ");
8
8
  const grantTypes = [.../* @__PURE__ */ new Set(["authorization_code", ...providerRuntimeConfig.grantTypes ?? []])];
9
- const requestURL = getRequestURL(event);
9
+ const requestURL = getRequestURL(event, { xForwardedProto: true });
10
10
  const baseUrl = `${requestURL.protocol}//${requestURL.host}`;
11
11
  const redirectURL = new URL(
12
12
  config?.redirectUrl && baseUrl + config.redirectUrl || providerRuntimeConfig.redirectUris[0] && baseUrl + providerRuntimeConfig.redirectUris[0] || getOAuthRedirectURL(event)
@@ -1,10 +1,7 @@
1
1
  import type { RequestEvent, SessionConfig } from 'nuxt/server';
2
2
  import type { OmitWithIndexSignature } from '../../types/utils.js';
3
3
  import type { LegacyH3Event, SessionEvent, UserSession, UserSessionRequired } from '#auth-utils';
4
- /**
5
- * Name of the cookie the user session is sealed into.
6
- */
7
- export declare const USER_SESSION_COOKIE_NAME = "nuxt-auth-session";
4
+ export { USER_SESSION_COOKIE_NAME } from '../lib/session.js';
8
5
  type UserSessionData = OmitWithIndexSignature<UserSession, 'id'>;
9
6
  /**
10
7
  * Session options, the session is always sealed with a secret derived from `appSecret`.
@@ -66,4 +63,3 @@ export declare function requireUserSession(event: SessionEvent, opts?: {
66
63
  statusCode?: number;
67
64
  message?: string;
68
65
  }): Promise<UserSessionRequired>;
69
- export {};
@@ -1,7 +1,8 @@
1
- import { createError, defineEventHandler, useRuntimeConfig, useSession } from "nuxt/server";
1
+ import { createError, defineEventHandler, useSession } from "nuxt/server";
2
2
  import { defu } from "defu";
3
3
  import { createHooks } from "hookable";
4
- export const USER_SESSION_COOKIE_NAME = "nuxt-auth-session";
4
+ import { resolveSessionConfig } from "../lib/session.js";
5
+ export { USER_SESSION_COOKIE_NAME } from "../lib/session.js";
5
6
  export const sessionHooks = createHooks();
6
7
  export async function getUserSession(event) {
7
8
  const session = await _useSession(event);
@@ -51,25 +52,11 @@ async function _useSession(event, config = {}, write = false) {
51
52
  message: "[nuxt-auth-utils] The user session is read-only in WebSocket handlers: `setUserSession`, `replaceUserSession` and `clearUserSession` need the event of an HTTP request."
52
53
  });
53
54
  }
54
- const runtimeConfig = useRuntimeConfig();
55
- warnLegacySessionPassword(runtimeConfig);
56
- const { password: _password, ...finalConfig } = defu(config, runtimeConfig.session, { name: USER_SESSION_COOKIE_NAME });
57
- if (finalConfig.name === APP_SESSION_COOKIE_NAME) {
58
- warnAppSessionName();
59
- finalConfig.name = USER_SESSION_COOKIE_NAME;
60
- }
61
- return useSession(sessionEvent, finalConfig);
55
+ return useSession(sessionEvent, resolveSessionConfig(config));
62
56
  }
63
57
  function toJSONData(data) {
64
58
  return JSON.parse(JSON.stringify(data));
65
59
  }
66
- const APP_SESSION_COOKIE_NAME = "nuxt-session";
67
- let warnedAppSessionName = false;
68
- function warnAppSessionName() {
69
- if (warnedAppSessionName) return;
70
- warnedAppSessionName = true;
71
- console.warn(`[nuxt-auth-utils] The session name \`${APP_SESSION_COOKIE_NAME}\` is the cookie of \`useSession()\` from \`nuxt/server\`, using \`${USER_SESSION_COOKIE_NAME}\` instead. Set another \`runtimeConfig.session.name\` (\`NUXT_SESSION_NAME\`) to rename the user session cookie.`);
72
- }
73
60
  const toPortableEvent = defineEventHandler((event) => event);
74
61
  function toSessionEvent(event) {
75
62
  const input = event;
@@ -94,13 +81,3 @@ function toSessionEvent(event) {
94
81
  message: "[nuxt-auth-utils] The session utils need a request event, a WebSocket peer or a WebSocket upgrade request."
95
82
  });
96
83
  }
97
- let checkedLegacySessionPassword = false;
98
- function warnLegacySessionPassword(runtimeConfig) {
99
- if (checkedLegacySessionPassword) return;
100
- checkedLegacySessionPassword = true;
101
- const envPrefix = runtimeConfig.nitro?.envPrefix || "NUXT_";
102
- const envKey = `${envPrefix}SESSION_PASSWORD`;
103
- if (globalThis.process?.env?.[envKey] || runtimeConfig.session?.password) {
104
- console.warn(`[nuxt-auth-utils] \`${envKey}\` (\`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\`.`);
105
- }
106
- }
@@ -1,4 +1,5 @@
1
1
  import type { ComputedRef, Ref } from 'vue';
2
+ import type { OmitWithIndexSignature } from './utils.js';
2
3
  export interface User {
3
4
  }
4
5
  export interface SecureSessionData {
@@ -38,9 +39,11 @@ export interface UserSessionComposable {
38
39
  */
39
40
  user: ComputedRef<User | null>;
40
41
  /**
41
- * The session object.
42
+ * The session object, without `id` for a visitor who has no session yet.
42
43
  */
43
- session: Ref<UserSession | null>;
44
+ session: Ref<(OmitWithIndexSignature<UserSession, 'id'> & {
45
+ id?: string;
46
+ }) | null>;
44
47
  /**
45
48
  * Fetch the user session from the server.
46
49
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auth-utils",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
5
5
  "repository": {
6
6
  "type": "git",