@peterbud/nuxt-aegis 1.1.0-alpha.3 → 1.1.0-alpha.4
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 +1 -1
- package/dist/module.mjs +8 -1
- package/dist/runtime/app/composables/useAuth.d.ts +4 -2
- package/dist/runtime/app/composables/useAuth.js +10 -2
- package/dist/runtime/server/routes/update-claims.post.d.ts +33 -0
- package/dist/runtime/server/routes/update-claims.post.js +66 -0
- package/dist/runtime/server/utils/recomputeClaims.d.ts +20 -0
- package/dist/runtime/server/utils/recomputeClaims.js +50 -0
- package/dist/runtime/types/refresh.d.ts +4 -0
- package/dist/runtime/types/routes.d.ts +2 -2
- package/package.json +11 -11
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -106,7 +106,7 @@ const module$1 = defineNuxtModule({
|
|
|
106
106
|
const resolver = createResolver(import.meta.url);
|
|
107
107
|
if (options.tokenRefresh?.encryption?.enabled) {
|
|
108
108
|
const encryptionKey = options.tokenRefresh.encryption.key;
|
|
109
|
-
if (!encryptionKey) {
|
|
109
|
+
if (!encryptionKey && nuxt.options._prepare !== true) {
|
|
110
110
|
logger.warn(
|
|
111
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."
|
|
112
112
|
);
|
|
@@ -147,6 +147,8 @@ const module$1 = defineNuxtModule({
|
|
|
147
147
|
{ name: "revokeRefreshToken", from: resolver.resolve("./runtime/server/utils/refreshToken") },
|
|
148
148
|
{ name: "deleteUserRefreshTokens", from: resolver.resolve("./runtime/server/utils/refreshToken") },
|
|
149
149
|
{ name: "hashRefreshToken", from: resolver.resolve("./runtime/server/utils/refreshToken") },
|
|
150
|
+
// Claims recomputation utilities (from recomputeClaims.ts)
|
|
151
|
+
{ name: "recomputeCustomClaims", from: resolver.resolve("./runtime/server/utils/recomputeClaims") },
|
|
150
152
|
// Cookie utilities (from cookies.ts)
|
|
151
153
|
{ name: "setRefreshTokenCookie", from: resolver.resolve("./runtime/server/utils/cookies") },
|
|
152
154
|
// Handler utilities (from handler.ts)
|
|
@@ -173,6 +175,11 @@ const module$1 = defineNuxtModule({
|
|
|
173
175
|
handler: resolver.resolve("./runtime/server/routes/refresh.post"),
|
|
174
176
|
method: "post"
|
|
175
177
|
});
|
|
178
|
+
addServerHandler({
|
|
179
|
+
route: `${runtimeConfig.public.nuxtAegis.authPath}/update-claims`,
|
|
180
|
+
handler: resolver.resolve("./runtime/server/routes/update-claims.post"),
|
|
181
|
+
method: "post"
|
|
182
|
+
});
|
|
176
183
|
if (options.impersonation?.enabled) {
|
|
177
184
|
addServerHandler({
|
|
178
185
|
route: `${runtimeConfig.public.nuxtAegis.authPath}/impersonate`,
|
|
@@ -26,7 +26,9 @@ interface UseAuthReturn<T extends BaseTokenClaims = BaseTokenClaims> {
|
|
|
26
26
|
/** Method to end the user session */
|
|
27
27
|
logout: (redirectTo?: string) => Promise<void>;
|
|
28
28
|
/** Method to refresh the authentication state */
|
|
29
|
-
refresh: (
|
|
29
|
+
refresh: (options?: {
|
|
30
|
+
updateClaims?: boolean;
|
|
31
|
+
}) => Promise<void>;
|
|
30
32
|
/** Method to impersonate another user (admin only) */
|
|
31
33
|
impersonate: (targetUserId: string, reason?: string) => Promise<void>;
|
|
32
34
|
/** Method to stop impersonation and restore original session */
|
|
@@ -58,7 +60,7 @@ interface UseAuthReturn<T extends BaseTokenClaims = BaseTokenClaims> {
|
|
|
58
60
|
* Methods:
|
|
59
61
|
* - login(provider) - Initiate OAuth flow
|
|
60
62
|
* - logout() - End user session
|
|
61
|
-
* - refresh() - Restore authentication state
|
|
63
|
+
* - refresh(options?) - Restore authentication state, optionally with updated claims
|
|
62
64
|
*
|
|
63
65
|
* @template T - Custom token payload type extending BaseTokenClaims
|
|
64
66
|
* @returns {UseAuthReturn<T>} Authentication state and methods
|
|
@@ -30,11 +30,18 @@ export function useAuth() {
|
|
|
30
30
|
const loginPath = publicConfig.nuxtAegis?.loginPath || authPath;
|
|
31
31
|
const logoutPath = publicConfig.nuxtAegis?.logoutPath;
|
|
32
32
|
const refreshPath = publicConfig.nuxtAegis?.refreshPath;
|
|
33
|
-
async function refresh() {
|
|
33
|
+
async function refresh(options) {
|
|
34
34
|
authState.value.isLoading = true;
|
|
35
35
|
authState.value.error = null;
|
|
36
|
-
logger.debug("Refreshing authentication state...");
|
|
36
|
+
logger.debug("Refreshing authentication state...", { updateClaims: options?.updateClaims });
|
|
37
37
|
try {
|
|
38
|
+
if (options?.updateClaims) {
|
|
39
|
+
logger.debug("Updating custom claims before refresh...");
|
|
40
|
+
await $fetch(`${authPath}/update-claims`, {
|
|
41
|
+
method: "POST"
|
|
42
|
+
});
|
|
43
|
+
logger.debug("Claims updated successfully in storage");
|
|
44
|
+
}
|
|
38
45
|
const response = await $fetch(`${refreshPath}`, {
|
|
39
46
|
method: "POST"
|
|
40
47
|
});
|
|
@@ -56,6 +63,7 @@ export function useAuth() {
|
|
|
56
63
|
authState.value.error = "Failed to refresh authentication";
|
|
57
64
|
clearAccessToken();
|
|
58
65
|
logger.error("Auth refresh failed:", error);
|
|
66
|
+
throw error;
|
|
59
67
|
} finally {
|
|
60
68
|
authState.value.isLoading = false;
|
|
61
69
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /auth/update-claims
|
|
3
|
+
*
|
|
4
|
+
* Recomputes custom JWT claims based on current user data.
|
|
5
|
+
* Useful when user data changes (role, permissions, etc.) and claims need updating
|
|
6
|
+
* without requiring the user to logout and login again.
|
|
7
|
+
*
|
|
8
|
+
* Process:
|
|
9
|
+
* 1. Validates refresh token from cookie
|
|
10
|
+
* 2. Verifies user owns the refresh token (authorization check)
|
|
11
|
+
* 3. Re-executes global handler's customClaims callback
|
|
12
|
+
* 4. Optionally re-executes onUserPersist for fresh DB data (if configured)
|
|
13
|
+
* 5. Updates stored refresh token data with new claims
|
|
14
|
+
* 6. User must call refresh() afterward to get a new JWT with updated claims
|
|
15
|
+
*
|
|
16
|
+
* Configuration:
|
|
17
|
+
* - Requires: tokenRefresh.enableClaimsUpdate = true (default)
|
|
18
|
+
* - Optional: tokenRefresh.recomputeOnUserPersist = true (fetch fresh DB data)
|
|
19
|
+
*
|
|
20
|
+
* Security:
|
|
21
|
+
* - Requires valid refresh token cookie
|
|
22
|
+
* - Users can only update their own claims
|
|
23
|
+
* - Claims are recomputed via the same handler used during initial auth
|
|
24
|
+
*
|
|
25
|
+
* @returns Success response with message
|
|
26
|
+
* @throws 401 if no refresh token or invalid token
|
|
27
|
+
* @throws 403 if feature is disabled
|
|
28
|
+
*/
|
|
29
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
30
|
+
success: boolean;
|
|
31
|
+
message: string;
|
|
32
|
+
}>>;
|
|
33
|
+
export default _default;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { defineEventHandler, getCookie, createError } from "h3";
|
|
2
|
+
import { hashRefreshToken, getRefreshTokenData, storeRefreshTokenData } from "../utils/refreshToken.js";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { createLogger } from "../utils/logger.js";
|
|
5
|
+
import { recomputeCustomClaims } from "../utils/recomputeClaims.js";
|
|
6
|
+
const logger = createLogger("UpdateClaims");
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const config = useRuntimeConfig(event);
|
|
9
|
+
const cookieConfig = config.nuxtAegis?.tokenRefresh?.cookie;
|
|
10
|
+
const tokenRefreshConfig = config.nuxtAegis?.tokenRefresh;
|
|
11
|
+
const enableClaimsUpdate = tokenRefreshConfig?.enableClaimsUpdate ?? true;
|
|
12
|
+
if (!enableClaimsUpdate) {
|
|
13
|
+
throw createError({
|
|
14
|
+
statusCode: 403,
|
|
15
|
+
statusMessage: "Forbidden",
|
|
16
|
+
message: "Claims update feature is disabled"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const cookieName = cookieConfig?.cookieName || "nuxt-aegis-refresh";
|
|
20
|
+
const refreshToken = getCookie(event, cookieName);
|
|
21
|
+
if (!refreshToken) {
|
|
22
|
+
throw createError({
|
|
23
|
+
statusCode: 401,
|
|
24
|
+
statusMessage: "Unauthorized",
|
|
25
|
+
message: "No refresh token found"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const hashedRefreshToken = hashRefreshToken(refreshToken);
|
|
30
|
+
const storedRefreshToken = await getRefreshTokenData(hashedRefreshToken, event);
|
|
31
|
+
const isRevoked = storedRefreshToken?.isRevoked || false;
|
|
32
|
+
const isExpired = storedRefreshToken?.expiresAt ? Date.now() > storedRefreshToken.expiresAt : true;
|
|
33
|
+
if (!storedRefreshToken || isRevoked || isExpired) {
|
|
34
|
+
throw createError({
|
|
35
|
+
statusCode: 401,
|
|
36
|
+
statusMessage: "Unauthorized",
|
|
37
|
+
message: "Invalid or expired refresh token"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
logger.debug(`Updating claims for user: ${storedRefreshToken.sub}`);
|
|
41
|
+
const newCustomClaims = await recomputeCustomClaims(storedRefreshToken, event);
|
|
42
|
+
await storeRefreshTokenData(
|
|
43
|
+
hashedRefreshToken,
|
|
44
|
+
{
|
|
45
|
+
...storedRefreshToken,
|
|
46
|
+
customClaims: newCustomClaims
|
|
47
|
+
},
|
|
48
|
+
event
|
|
49
|
+
);
|
|
50
|
+
logger.debug("Claims updated successfully. User should call refresh() to get new JWT.");
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
message: "Claims updated successfully. Call refresh() to receive a new access token with updated claims."
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error && typeof error === "object" && "statusCode" in error) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
logger.error("Claims update failed:", error);
|
|
60
|
+
throw createError({
|
|
61
|
+
statusCode: 500,
|
|
62
|
+
statusMessage: "Internal Server Error",
|
|
63
|
+
message: "Failed to update claims"
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { RefreshTokenData } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Recompute custom claims for a user based on current data
|
|
5
|
+
*
|
|
6
|
+
* Uses the global handler's customClaims callback to generate fresh claims.
|
|
7
|
+
* Optionally re-executes onUserPersist to fetch fresh database data.
|
|
8
|
+
*
|
|
9
|
+
* @param refreshTokenData - Stored refresh token data containing user info
|
|
10
|
+
* @param event - H3 event for context
|
|
11
|
+
* @returns Updated custom claims object
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const storedData = await getRefreshTokenData(hashedToken, event)
|
|
16
|
+
* const newClaims = await recomputeCustomClaims(storedData, event)
|
|
17
|
+
* // newClaims now contains fresh data from database
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function recomputeCustomClaims(refreshTokenData: RefreshTokenData, event: H3Event): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
import { useAegisHandler } from "./handler.js";
|
|
3
|
+
import { processCustomClaims } from "./customClaims.js";
|
|
4
|
+
import { createLogger } from "./logger.js";
|
|
5
|
+
const logger = createLogger("RecomputeClaims");
|
|
6
|
+
export async function recomputeCustomClaims(refreshTokenData, event) {
|
|
7
|
+
const config = useRuntimeConfig(event);
|
|
8
|
+
const tokenRefreshConfig = config.nuxtAegis?.tokenRefresh;
|
|
9
|
+
const handler = useAegisHandler();
|
|
10
|
+
if (!handler) {
|
|
11
|
+
logger.warn("No Aegis handler registered. Cannot recompute custom claims.");
|
|
12
|
+
return refreshTokenData.customClaims || {};
|
|
13
|
+
}
|
|
14
|
+
let userData = refreshTokenData.providerUserInfo;
|
|
15
|
+
const provider = refreshTokenData.provider;
|
|
16
|
+
if (tokenRefreshConfig?.recomputeOnUserPersist && handler.onUserPersist) {
|
|
17
|
+
logger.debug(`Re-executing onUserPersist for provider: ${provider}`);
|
|
18
|
+
const persistContext = {
|
|
19
|
+
provider,
|
|
20
|
+
event
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
const enrichedData = await handler.onUserPersist(userData, persistContext);
|
|
24
|
+
if (enrichedData) {
|
|
25
|
+
userData = {
|
|
26
|
+
...userData,
|
|
27
|
+
...enrichedData
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
logger.error("Error re-executing onUserPersist:", error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!handler.customClaims) {
|
|
35
|
+
logger.debug("No customClaims callback defined in handler. Returning existing claims.");
|
|
36
|
+
return refreshTokenData.customClaims || {};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
logger.debug(`Recomputing custom claims for user: ${refreshTokenData.sub}`);
|
|
40
|
+
const newClaims = await processCustomClaims(
|
|
41
|
+
userData,
|
|
42
|
+
handler.customClaims
|
|
43
|
+
);
|
|
44
|
+
logger.debug("Custom claims recomputed successfully");
|
|
45
|
+
return newClaims;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error("Error recomputing custom claims:", error);
|
|
48
|
+
return refreshTokenData.customClaims || {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -52,6 +52,10 @@ export interface TokenRefreshConfig {
|
|
|
52
52
|
automaticRefresh?: boolean;
|
|
53
53
|
/** Enable refresh token rotation on every refresh (default: true) */
|
|
54
54
|
rotationEnabled?: boolean;
|
|
55
|
+
/** Enable claims update endpoint and functionality (default: true) */
|
|
56
|
+
enableClaimsUpdate?: boolean;
|
|
57
|
+
/** Re-execute onUserPersist hook when updating claims for fresh database data (default: false) */
|
|
58
|
+
recomputeOnUserPersist?: boolean;
|
|
55
59
|
/** Refresh token cookie configuration */
|
|
56
60
|
cookie?: CookieConfig;
|
|
57
61
|
/** Encryption configuration for stored user data */
|
|
@@ -23,8 +23,8 @@ export interface ClientMiddlewareConfig {
|
|
|
23
23
|
global?: boolean;
|
|
24
24
|
/** Redirect destination for unauthenticated users (required when enabled) */
|
|
25
25
|
redirectTo: string;
|
|
26
|
-
/** Redirect destination for authenticated users on logged-out pages
|
|
27
|
-
loggedOutRedirectTo
|
|
26
|
+
/** Redirect destination for authenticated users on logged-out pages */
|
|
27
|
+
loggedOutRedirectTo?: string;
|
|
28
28
|
/** Array of route patterns excluded from authentication (glob patterns supported) */
|
|
29
29
|
publicRoutes?: string[];
|
|
30
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peterbud/nuxt-aegis",
|
|
3
|
-
"version": "1.1.0-alpha.
|
|
3
|
+
"version": "1.1.0-alpha.4",
|
|
4
4
|
"description": "Nuxt module for authentication with JWT token generation and session management.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "nuxt-module-build prepare && nuxt-module-build build",
|
|
41
41
|
"prepack": "nuxt-module-build build",
|
|
42
|
-
"dev": "pnpm run dev:prepare &&
|
|
43
|
-
"dev:build": "
|
|
44
|
-
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare &&
|
|
42
|
+
"dev": "pnpm run dev:prepare && nuxt dev playground",
|
|
43
|
+
"dev:build": "nuxt build playground",
|
|
44
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
45
45
|
"docs:dev": "pnpm --filter docs docs:dev",
|
|
46
46
|
"docs:build": "pnpm --filter docs docs:build",
|
|
47
47
|
"release": "pnpm run lint && pnpm run test && pnpm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
@@ -52,25 +52,25 @@
|
|
|
52
52
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@nuxt/kit": "^4.
|
|
55
|
+
"@nuxt/kit": "^4.3.0",
|
|
56
56
|
"consola": "^3.4.2",
|
|
57
57
|
"defu": "^6.1.4",
|
|
58
58
|
"jose": "^6.1.3",
|
|
59
|
-
"ufo": "^1.6.
|
|
59
|
+
"ufo": "^1.6.3"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@nuxt/devtools": "^3.1.1",
|
|
63
|
-
"@nuxt/eslint-config": "^1.
|
|
63
|
+
"@nuxt/eslint-config": "^1.13.0",
|
|
64
64
|
"@nuxt/module-builder": "^1.0.2",
|
|
65
|
-
"@nuxt/schema": "^4.
|
|
66
|
-
"@nuxt/test-utils": "^3.
|
|
65
|
+
"@nuxt/schema": "^4.3.0",
|
|
66
|
+
"@nuxt/test-utils": "^3.23.0",
|
|
67
67
|
"@types/node": "latest",
|
|
68
68
|
"changelogen": "^0.6.2",
|
|
69
69
|
"eslint": "^9.39.2",
|
|
70
|
-
"nuxt": "^4.
|
|
70
|
+
"nuxt": "^4.3.0",
|
|
71
71
|
"typescript": "~5.9.3",
|
|
72
72
|
"vitest": "^3.2.4",
|
|
73
|
-
"vue-tsc": "^3.2.
|
|
73
|
+
"vue-tsc": "^3.2.3"
|
|
74
74
|
},
|
|
75
75
|
"pnpm": {
|
|
76
76
|
"overrides": {
|