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 +29 -0
- package/dist/module.json +1 -1
- package/dist/runtime/server/api/session.get.d.ts +1 -1
- package/dist/runtime/server/api/session.get.js +5 -1
- package/dist/runtime/server/lib/oauth/mastodon.js +2 -2
- package/dist/runtime/server/lib/oauth/steam.js +2 -2
- package/dist/runtime/server/lib/session.d.ts +11 -0
- package/dist/runtime/server/lib/session.js +33 -0
- package/dist/runtime/server/lib/utils.d.ts +1 -0
- package/dist/runtime/server/lib/utils.js +8 -4
- package/dist/runtime/server/lib/webauthn/authenticate.js +1 -1
- package/dist/runtime/server/lib/webauthn/register.js +1 -1
- package/dist/runtime/server/utils/atproto.js +1 -1
- package/dist/runtime/server/utils/session.d.ts +1 -5
- package/dist/runtime/server/utils/session.js +4 -27
- package/dist/runtime/types/session.d.ts +5 -2
- package/package.json +1 -1
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
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("nuxt/server").EventHandler<Promise<
|
|
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,
|
|
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:
|
|
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 ||
|
|
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:
|
|
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" ||
|
|
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:
|
|
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,
|
|
1
|
+
import { createError, defineEventHandler, useSession } from "nuxt/server";
|
|
2
2
|
import { defu } from "defu";
|
|
3
3
|
import { createHooks } from "hookable";
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
44
|
+
session: Ref<(OmitWithIndexSignature<UserSession, 'id'> & {
|
|
45
|
+
id?: string;
|
|
46
|
+
}) | null>;
|
|
44
47
|
/**
|
|
45
48
|
* Fetch the user session from the server.
|
|
46
49
|
*/
|