@peterbud/nuxt-aegis 1.1.0-alpha.2 → 1.1.0-alpha.3

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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-aegis",
3
3
  "configKey": "nuxtAegis",
4
- "version": "1.1.0-alpha.2",
4
+ "version": "1.1.0-alpha.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -21,6 +21,8 @@ const module$1 = defineNuxtModule({
21
21
  tokenRefresh: {
22
22
  enabled: true,
23
23
  automaticRefresh: true,
24
+ rotationEnabled: true,
25
+ // Enable refresh token rotation by default for security
24
26
  cookie: {
25
27
  cookieName: "nuxt-aegis-refresh",
26
28
  maxAge: 60 * 60 * 24 * 7,
@@ -95,18 +97,18 @@ const module$1 = defineNuxtModule({
95
97
  nuxtAegis: options
96
98
  });
97
99
  const runtimeConfig = nuxt.options.runtimeConfig;
100
+ const logger = useLogger("nuxt-aegis");
98
101
  if (options.enableSSR && nuxt.options.ssr === false) {
99
- const logger2 = useLogger("nuxt-aegis");
100
- logger2.warn(
102
+ logger.warn(
101
103
  "nuxtAegis.enableSSR is true but Nuxt SSR is disabled. SSR authentication will not work. Set ssr: true in nuxt.config.ts or disable enableSSR."
102
104
  );
103
105
  }
104
106
  const resolver = createResolver(import.meta.url);
105
107
  if (options.tokenRefresh?.encryption?.enabled) {
106
- const encryptionKey = options.tokenRefresh.encryption.key || process.env.NUXT_AEGIS_ENCRYPTION_KEY;
108
+ const encryptionKey = options.tokenRefresh.encryption.key;
107
109
  if (!encryptionKey) {
108
- throw new Error(
109
- "[Nuxt Aegis] Encryption is enabled but no encryption key is configured. Please set tokenRefresh.encryption.key in nuxt.config.ts or NUXT_AEGIS_ENCRYPTION_KEY environment variable."
110
+ logger.warn(
111
+ "[Nuxt Aegis] Encryption is enabled but no encryption key is configured. The application will fail at runtime if encryption is attempted. Please set tokenRefresh.encryption.key in nuxt.config.ts or in the appropriate environment variable."
110
112
  );
111
113
  }
112
114
  if (!runtimeConfig.nuxtAegis) {
@@ -211,7 +213,6 @@ const module$1 = defineNuxtModule({
211
213
  addPlugin(resolver.resolve("./runtime/app/plugins/api.server"));
212
214
  addPlugin(resolver.resolve("./runtime/app/plugins/ssr-state.server"));
213
215
  }
214
- const logger = useLogger("nuxt-aegis");
215
216
  if (options.clientMiddleware?.enabled) {
216
217
  const cm = options.clientMiddleware;
217
218
  if (cm.global) {
@@ -1,5 +1,5 @@
1
1
  import { useRuntimeConfig, navigateTo, useState, computed } from "#imports";
2
- import { clearAccessToken } from "../utils/tokenStore.js";
2
+ import { clearAccessToken, setAccessToken, getAccessToken } from "../utils/tokenStore.js";
3
3
  import { createLogger } from "../utils/logger.js";
4
4
  import { validateRedirectPath } from "../utils/redirectValidation.js";
5
5
  import { filterTimeSensitiveClaims } from "../utils/tokenUtils.js";
@@ -39,7 +39,6 @@ export function useAuth() {
39
39
  method: "POST"
40
40
  });
41
41
  if (response?.accessToken) {
42
- const { setAccessToken } = await import("../utils/tokenStore.js");
43
42
  setAccessToken(response.accessToken);
44
43
  const tokenParts = response.accessToken.split(".");
45
44
  if (tokenParts[1]) {
@@ -95,7 +94,6 @@ export function useAuth() {
95
94
  authState.value.error = null;
96
95
  authState.value.isLoading = true;
97
96
  logger.debug("Starting impersonation...", { targetUserId });
98
- const { getAccessToken } = await import("../utils/tokenStore.js");
99
97
  const currentToken = getAccessToken();
100
98
  if (!currentToken) {
101
99
  throw new Error("Must be authenticated to impersonate");
@@ -111,7 +109,6 @@ export function useAuth() {
111
109
  }
112
110
  });
113
111
  if (response?.accessToken) {
114
- const { setAccessToken } = await import("../utils/tokenStore.js");
115
112
  setAccessToken(response.accessToken);
116
113
  const tokenParts = response.accessToken.split(".");
117
114
  if (tokenParts[1]) {
@@ -138,7 +135,6 @@ export function useAuth() {
138
135
  authState.value.error = null;
139
136
  authState.value.isLoading = true;
140
137
  logger.debug("Stopping impersonation...");
141
- const { getAccessToken } = await import("../utils/tokenStore.js");
142
138
  const currentToken = getAccessToken();
143
139
  if (!currentToken) {
144
140
  throw new Error("No active session");
@@ -150,7 +146,6 @@ export function useAuth() {
150
146
  }
151
147
  });
152
148
  if (response?.accessToken) {
153
- const { setAccessToken } = await import("../utils/tokenStore.js");
154
149
  setAccessToken(response.accessToken);
155
150
  const tokenParts = response.accessToken.split(".");
156
151
  if (tokenParts[1]) {
@@ -5,88 +5,91 @@ import { isRouteMatch } from "../utils/routeMatching.js";
5
5
  import { createLogger } from "../utils/logger.js";
6
6
  import { validateRedirectPath } from "../utils/redirectValidation.js";
7
7
  const logger = createLogger("API:Client");
8
- export default defineNuxtPlugin(async (nuxtApp) => {
9
- let isRefreshing = false;
10
- let refreshPromise = null;
11
- let isInitialized = false;
12
- const autoRefreshEnabled = nuxtApp.$config.public.nuxtAegis.tokenRefresh.automaticRefresh ?? true;
13
- async function attemptTokenRefresh() {
14
- if (isRefreshing) return refreshPromise;
15
- logger.debug("Attempting token refresh...");
16
- isRefreshing = true;
17
- refreshPromise = nuxtApp.runWithContext(async () => {
18
- const auth = useAuth();
19
- try {
20
- await auth.refresh();
21
- return getAccessToken();
22
- } catch (error) {
23
- logger.error("Token refresh failed:", error);
24
- return null;
25
- } finally {
26
- isRefreshing = false;
27
- refreshPromise = null;
28
- }
29
- });
30
- return refreshPromise;
31
- }
32
- const api = $fetch.create({
33
- onRequest({ options }) {
34
- const token = getAccessToken();
35
- if (token) {
36
- options.headers.set("Authorization", `Bearer ${token}`);
37
- }
38
- },
39
- async onResponseError({ options, response }) {
40
- if (response.status === 401 && autoRefreshEnabled) {
41
- const newToken = await attemptTokenRefresh();
42
- if (newToken) {
43
- options.headers.set("Authorization", `Bearer ${newToken}`);
44
- return;
8
+ export default defineNuxtPlugin({
9
+ name: "nuxt-aegis-api-client",
10
+ async setup(nuxtApp) {
11
+ let isRefreshing = false;
12
+ let refreshPromise = null;
13
+ let isInitialized = false;
14
+ const autoRefreshEnabled = nuxtApp.$config.public.nuxtAegis.tokenRefresh.automaticRefresh ?? true;
15
+ async function attemptTokenRefresh() {
16
+ if (isRefreshing) return refreshPromise;
17
+ logger.debug("Attempting token refresh...");
18
+ isRefreshing = true;
19
+ refreshPromise = nuxtApp.runWithContext(async () => {
20
+ const auth = useAuth();
21
+ try {
22
+ await auth.refresh();
23
+ return getAccessToken();
24
+ } catch (error) {
25
+ logger.error("Token refresh failed:", error);
26
+ return null;
27
+ } finally {
28
+ isRefreshing = false;
29
+ refreshPromise = null;
45
30
  }
46
- clearAccessToken();
47
- const config = useRuntimeConfig();
48
- const errorUrl = validateRedirectPath(config.public.nuxtAegis.redirect?.error || "/");
49
- await nuxtApp.runWithContext(() => navigateTo(`${errorUrl}?error=token_refresh_failed&error_description=${encodeURIComponent("Session expired. Please log in again.")}`));
50
- }
51
- },
52
- retry: autoRefreshEnabled ? 1 : 0,
53
- retryStatusCodes: [401]
54
- });
55
- const result = {
56
- provide: {
57
- api
31
+ });
32
+ return refreshPromise;
58
33
  }
59
- };
60
- if (!isInitialized && autoRefreshEnabled) {
61
- isInitialized = true;
62
- logger.debug("Initializing auth state on startup...");
63
- nuxtApp.hook("app:mounted", async () => {
64
- await nuxtApp.runWithContext(async () => {
65
- const callbackPath = nuxtApp.$config.public.nuxtAegis.callbackPath;
66
- if (typeof window !== "undefined" && window.location.pathname === callbackPath) {
67
- logger.debug("On auth callback page, skipping refresh");
68
- return;
34
+ const api = $fetch.create({
35
+ onRequest({ options }) {
36
+ const token = getAccessToken();
37
+ if (token) {
38
+ options.headers.set("Authorization", `Bearer ${token}`);
69
39
  }
70
- if (typeof window !== "undefined") {
71
- const publicRoutes = nuxtApp.$config.public.nuxtAegis?.clientMiddleware?.publicRoutes || [];
72
- const currentPath = window.location.pathname;
73
- if (isRouteMatch(currentPath, publicRoutes)) {
74
- logger.debug("On public route, skipping refresh on startup");
40
+ },
41
+ async onResponseError({ options, response }) {
42
+ if (response.status === 401 && autoRefreshEnabled) {
43
+ const newToken = await attemptTokenRefresh();
44
+ if (newToken) {
45
+ options.headers.set("Authorization", `Bearer ${newToken}`);
75
46
  return;
76
47
  }
48
+ clearAccessToken();
49
+ const config = useRuntimeConfig();
50
+ const errorUrl = validateRedirectPath(config.public.nuxtAegis.redirect?.error || "/");
51
+ await nuxtApp.runWithContext(() => navigateTo(`${errorUrl}?error=token_refresh_failed&error_description=${encodeURIComponent("Session expired. Please log in again.")}`));
77
52
  }
78
- const currentToken = getAccessToken();
79
- if (currentToken) {
80
- logger.debug("Access token already present, skipping refresh on startup");
81
- return;
82
- }
83
- try {
84
- await useAuth().refresh();
85
- } catch {
86
- logger.debug("No valid refresh token found on startup");
87
- }
88
- });
53
+ },
54
+ retry: autoRefreshEnabled ? 1 : 0,
55
+ retryStatusCodes: [401]
89
56
  });
57
+ const result = {
58
+ provide: {
59
+ api
60
+ }
61
+ };
62
+ if (!isInitialized && autoRefreshEnabled) {
63
+ isInitialized = true;
64
+ logger.debug("Initializing auth state on startup...");
65
+ nuxtApp.hook("app:mounted", async () => {
66
+ await nuxtApp.runWithContext(async () => {
67
+ const callbackPath = nuxtApp.$config.public.nuxtAegis.callbackPath;
68
+ if (typeof window !== "undefined" && window.location.pathname === callbackPath) {
69
+ logger.debug("On auth callback page, skipping refresh");
70
+ return;
71
+ }
72
+ if (typeof window !== "undefined") {
73
+ const publicRoutes = nuxtApp.$config.public.nuxtAegis?.clientMiddleware?.publicRoutes || [];
74
+ const currentPath = window.location.pathname;
75
+ if (isRouteMatch(currentPath, publicRoutes)) {
76
+ logger.debug("On public route, skipping refresh on startup");
77
+ return;
78
+ }
79
+ }
80
+ const currentToken = getAccessToken();
81
+ if (currentToken) {
82
+ logger.debug("Access token already present, skipping refresh on startup");
83
+ return;
84
+ }
85
+ try {
86
+ await useAuth().refresh();
87
+ } catch {
88
+ logger.debug("No valid refresh token found on startup");
89
+ }
90
+ });
91
+ });
92
+ }
93
+ return result;
90
94
  }
91
- return result;
92
95
  });
@@ -1,6 +1,6 @@
1
1
  import { defineNuxtPlugin, useRequestEvent } from "#app";
2
2
  export default defineNuxtPlugin({
3
- name: "api-server",
3
+ name: "nuxt-aegis-api-server",
4
4
  enforce: "pre",
5
5
  async setup() {
6
6
  const api = $fetch.create({
@@ -1,13 +1,16 @@
1
1
  import { defineNuxtPlugin, useState, useRequestEvent } from "#app";
2
2
  import { filterTimeSensitiveClaims } from "../utils/tokenUtils.js";
3
- export default defineNuxtPlugin(() => {
4
- const event = useRequestEvent();
5
- if (event?.context.user) {
6
- const user = event.context.user;
7
- useState("auth-state", () => ({
8
- user: filterTimeSensitiveClaims(user),
9
- isLoading: false,
10
- error: null
11
- }));
3
+ export default defineNuxtPlugin({
4
+ name: "nuxt-aegis-ssr-state",
5
+ async setup() {
6
+ const event = useRequestEvent();
7
+ if (event?.context.user) {
8
+ const user = event.context.user;
9
+ useState("auth-state", () => ({
10
+ user: filterTimeSensitiveClaims(user),
11
+ isLoading: false,
12
+ error: null
13
+ }));
14
+ }
12
15
  }
13
16
  });
@@ -18,21 +18,20 @@ export default defineEventHandler(async (event) => {
18
18
  const routeRules = await getRouteRules(event);
19
19
  const authConfig = routeRules.nuxtAegis?.auth;
20
20
  const shouldProtect = authConfig === true || authConfig === "required" || authConfig === "protected";
21
- const shouldSkip = authConfig === false || authConfig === "public" || authConfig === "skip";
22
- if (!shouldProtect || shouldSkip) {
23
- return;
24
- }
25
21
  let token;
26
22
  const authHeader = getHeader(event, "authorization");
27
23
  if (authHeader?.startsWith("Bearer ")) {
28
24
  token = authHeader.substring(7);
29
25
  }
30
26
  if (!token) {
31
- throw createError({
32
- statusCode: 401,
33
- statusMessage: "Unauthorized",
34
- message: "Authentication required"
35
- });
27
+ if (shouldProtect) {
28
+ throw createError({
29
+ statusCode: 401,
30
+ statusMessage: "Unauthorized",
31
+ message: "Authentication required"
32
+ });
33
+ }
34
+ return;
36
35
  }
37
36
  if (!tokenConfig || !tokenConfig.secret) {
38
37
  logger.error("Token configuration is missing");
@@ -45,29 +44,38 @@ export default defineEventHandler(async (event) => {
45
44
  const payload = await verifyToken(token, tokenConfig.secret);
46
45
  if (!payload) {
47
46
  logger.debug("Token verification failed for path:", requestURL.pathname);
48
- throw createError({
49
- statusCode: 401,
50
- statusMessage: "Unauthorized",
51
- message: "Invalid or expired token"
52
- });
47
+ if (shouldProtect) {
48
+ throw createError({
49
+ statusCode: 401,
50
+ statusMessage: "Unauthorized",
51
+ message: "Invalid or expired token"
52
+ });
53
+ }
54
+ return;
53
55
  }
54
56
  if (tokenConfig.issuer && payload.iss !== tokenConfig.issuer) {
55
57
  logger.debug("Token issuer mismatch. Expected:", tokenConfig.issuer, "Got:", payload.iss);
56
- throw createError({
57
- statusCode: 401,
58
- statusMessage: "Unauthorized",
59
- message: "Invalid token issuer"
60
- });
58
+ if (shouldProtect) {
59
+ throw createError({
60
+ statusCode: 401,
61
+ statusMessage: "Unauthorized",
62
+ message: "Invalid token issuer"
63
+ });
64
+ }
65
+ return;
61
66
  }
62
67
  if (tokenConfig.audience && payload.aud) {
63
68
  const audienceMatch = Array.isArray(payload.aud) ? payload.aud.includes(tokenConfig.audience) : payload.aud === tokenConfig.audience;
64
69
  if (!audienceMatch) {
65
70
  logger.debug("Token audience mismatch. Expected:", tokenConfig.audience, "Got:", payload.aud);
66
- throw createError({
67
- statusCode: 401,
68
- statusMessage: "Unauthorized",
69
- message: "Invalid token audience"
70
- });
71
+ if (shouldProtect) {
72
+ throw createError({
73
+ statusCode: 401,
74
+ statusMessage: "Unauthorized",
75
+ message: "Invalid token audience"
76
+ });
77
+ }
78
+ return;
71
79
  }
72
80
  }
73
81
  const { iat, exp, iss, aud, ...userData } = payload;
@@ -8,6 +8,8 @@ import {
8
8
  } from "../utils/refreshToken.js";
9
9
  import { setRefreshTokenCookie } from "../utils/cookies.js";
10
10
  import { useRuntimeConfig } from "#imports";
11
+ import { createLogger } from "../utils/logger.js";
12
+ const logger = createLogger("Refresh");
11
13
  export default defineEventHandler(async (event) => {
12
14
  const config = useRuntimeConfig(event);
13
15
  const cookieConfig = config.nuxtAegis?.tokenRefresh?.cookie;
@@ -65,22 +67,29 @@ export default defineEventHandler(async (event) => {
65
67
  // Include provider name in JWT payload
66
68
  };
67
69
  const newToken = await generateToken(payload, tokenConfig, customClaims);
68
- const newRefreshToken = await generateAndStoreRefreshToken(
69
- providerUserInfo,
70
- // RS-2: Store complete OAuth provider user data
71
- provider,
72
- // Store provider name
73
- tokenRefreshConfig,
74
- hashedRefreshToken,
75
- // Pass previous token hash for rotation tracking
76
- customClaims,
77
- // Preserve custom claims in new refresh token
78
- event
79
- );
80
- if (newRefreshToken) {
81
- setRefreshTokenCookie(event, newRefreshToken, cookieConfig);
70
+ const rotationEnabled = tokenRefreshConfig?.rotationEnabled ?? true;
71
+ if (rotationEnabled) {
72
+ logger.debug("Rotating refresh token for user:", payload.sub);
73
+ const newRefreshToken = await generateAndStoreRefreshToken(
74
+ providerUserInfo,
75
+ // RS-2: Store complete OAuth provider user data
76
+ provider,
77
+ // Store provider name
78
+ tokenRefreshConfig,
79
+ hashedRefreshToken,
80
+ // Pass previous token hash for rotation tracking
81
+ customClaims,
82
+ // Preserve custom claims in new refresh token
83
+ event
84
+ );
85
+ if (newRefreshToken) {
86
+ setRefreshTokenCookie(event, newRefreshToken, cookieConfig);
87
+ }
88
+ await revokeRefreshToken(hashedRefreshToken, event);
89
+ } else {
90
+ logger.debug("Reusing existing refresh token for user:", payload.sub);
91
+ setRefreshTokenCookie(event, refreshToken, cookieConfig);
82
92
  }
83
- await revokeRefreshToken(hashedRefreshToken, event);
84
93
  return {
85
94
  success: true,
86
95
  message: "Token refreshed successfully",
@@ -50,6 +50,8 @@ export interface TokenRefreshConfig {
50
50
  enabled?: boolean;
51
51
  /** Automatically refresh tokens in the background (default: true) */
52
52
  automaticRefresh?: boolean;
53
+ /** Enable refresh token rotation on every refresh (default: true) */
54
+ rotationEnabled?: boolean;
53
55
  /** Refresh token cookie configuration */
54
56
  cookie?: CookieConfig;
55
57
  /** Encryption configuration for stored user data */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterbud/nuxt-aegis",
3
- "version": "1.1.0-alpha.2",
3
+ "version": "1.1.0-alpha.3",
4
4
  "description": "Nuxt module for authentication with JWT token generation and session management.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -52,7 +52,7 @@
52
52
  "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
53
53
  },
54
54
  "dependencies": {
55
- "@nuxt/kit": "^4.2.1",
55
+ "@nuxt/kit": "^4.2.2",
56
56
  "consola": "^3.4.2",
57
57
  "defu": "^6.1.4",
58
58
  "jose": "^6.1.3",
@@ -60,17 +60,17 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@nuxt/devtools": "^3.1.1",
63
- "@nuxt/eslint-config": "^1.11.0",
63
+ "@nuxt/eslint-config": "^1.12.1",
64
64
  "@nuxt/module-builder": "^1.0.2",
65
- "@nuxt/schema": "^4.2.1",
66
- "@nuxt/test-utils": "^3.21.0",
65
+ "@nuxt/schema": "^4.2.2",
66
+ "@nuxt/test-utils": "^3.22.0",
67
67
  "@types/node": "latest",
68
68
  "changelogen": "^0.6.2",
69
- "eslint": "^9.39.1",
70
- "nuxt": "^4.2.1",
69
+ "eslint": "^9.39.2",
70
+ "nuxt": "^4.2.2",
71
71
  "typescript": "~5.9.3",
72
72
  "vitest": "^3.2.4",
73
- "vue-tsc": "^3.1.5"
73
+ "vue-tsc": "^3.2.1"
74
74
  },
75
75
  "pnpm": {
76
76
  "overrides": {