@peterbud/nuxt-aegis 1.1.0-alpha.4 → 1.1.0-alpha.6
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 +4 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +6 -1
- package/dist/runtime/app/composables/useAuth.d.ts +1 -1
- package/dist/runtime/app/composables/useAuth.js +2 -2
- package/dist/runtime/server/middleware/auth.js +1 -1
- package/dist/runtime/server/routes/unimpersonate.post.js +1 -1
- package/dist/runtime/server/utils/impersonation.d.ts +2 -1
- package/dist/runtime/server/utils/impersonation.js +20 -6
- package/dist/runtime/types/config.d.ts +2 -0
- package/dist/runtime/types/token.d.ts +4 -2
- package/package.json +14 -19
package/README.md
CHANGED
|
@@ -61,12 +61,12 @@ export default defineNuxtConfig({
|
|
|
61
61
|
|
|
62
62
|
nuxtAegis: {
|
|
63
63
|
token: {
|
|
64
|
-
secret: process.env.
|
|
64
|
+
secret: process.env.NUXT_NUXT_AEGIS_TOKEN_SECRET!,
|
|
65
65
|
},
|
|
66
66
|
providers: {
|
|
67
67
|
google: {
|
|
68
|
-
clientId: process.env.
|
|
69
|
-
clientSecret: process.env.
|
|
68
|
+
clientId: process.env.NUXT_NUXT_AEGIS_PROVIDERS_GOOGLE_CLIENT_ID!,
|
|
69
|
+
clientSecret: process.env.NUXT_NUXT_AEGIS_PROVIDERS_GOOGLE_CLIENT_SECRET!,
|
|
70
70
|
},
|
|
71
71
|
},
|
|
72
72
|
},
|
|
@@ -114,6 +114,7 @@ const { user, isLoggedIn, login, logout } = useAuth()
|
|
|
114
114
|
|
|
115
115
|
Ready to dive deeper? Check out the full documentation:
|
|
116
116
|
|
|
117
|
+
- **[Documentation Home](https://peterbud.github.io/nuxt-aegis/)** - Main documentation site.
|
|
117
118
|
- **[Getting Started](https://peterbud.github.io/nuxt-aegis/getting-started/installation)** - Installation and setup guides.
|
|
118
119
|
- **[Architecture](https://peterbud.github.io/nuxt-aegis/architecture/)** - Overview of the system architecture.
|
|
119
120
|
- **[Providers](https://peterbud.github.io/nuxt-aegis/providers/)** - Configure Google, GitHub, Auth0, Password, and Mock providers.
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -74,6 +74,11 @@ const module$1 = defineNuxtModule({
|
|
|
74
74
|
logging: {
|
|
75
75
|
level: "info",
|
|
76
76
|
security: false
|
|
77
|
+
},
|
|
78
|
+
impersonation: {
|
|
79
|
+
enabled: false,
|
|
80
|
+
tokenExpiration: 900,
|
|
81
|
+
originalUserLookupClaim: "sub"
|
|
77
82
|
}
|
|
78
83
|
},
|
|
79
84
|
setup(options, nuxt) {
|
|
@@ -225,7 +230,7 @@ const module$1 = defineNuxtModule({
|
|
|
225
230
|
if (cm.global) {
|
|
226
231
|
const userPublicRoutes = cm.publicRoutes || [];
|
|
227
232
|
const redirectRoutes = [cm.redirectTo, cm.loggedOutRedirectTo];
|
|
228
|
-
const allPublicRoutes = [
|
|
233
|
+
const allPublicRoutes = [...new Set([...userPublicRoutes, ...redirectRoutes].filter((route) => Boolean(route)))];
|
|
229
234
|
options.clientMiddleware.publicRoutes = allPublicRoutes;
|
|
230
235
|
if (allPublicRoutes.length === 0) {
|
|
231
236
|
throw new Error(
|
|
@@ -17,7 +17,7 @@ interface UseAuthReturn<T extends BaseTokenClaims = BaseTokenClaims> {
|
|
|
17
17
|
isImpersonating: ComputedRef<boolean>;
|
|
18
18
|
/** Reactive property containing original user data when impersonating */
|
|
19
19
|
originalUser: ComputedRef<{
|
|
20
|
-
|
|
20
|
+
originalUserSub: string;
|
|
21
21
|
originalUserEmail?: string;
|
|
22
22
|
originalUserName?: string;
|
|
23
23
|
} | null>;
|
|
@@ -19,7 +19,7 @@ export function useAuth() {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
return {
|
|
22
|
-
|
|
22
|
+
originalUserSub: impersonation.originalUserSub,
|
|
23
23
|
originalUserEmail: impersonation.originalUserEmail,
|
|
24
24
|
originalUserName: impersonation.originalUserName
|
|
25
25
|
};
|
|
@@ -125,7 +125,7 @@ export function useAuth() {
|
|
|
125
125
|
authState.value.error = null;
|
|
126
126
|
logger.debug("Impersonation started successfully", {
|
|
127
127
|
targetUser: payload.sub,
|
|
128
|
-
originalUser: payload.impersonation?.
|
|
128
|
+
originalUser: payload.impersonation?.originalUserSub
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -82,7 +82,7 @@ export default defineEventHandler(async (event) => {
|
|
|
82
82
|
event.context.user = userData;
|
|
83
83
|
if (payload.impersonation) {
|
|
84
84
|
event.context.originalUser = {
|
|
85
|
-
sub: payload.impersonation.
|
|
85
|
+
sub: payload.impersonation.originalUserSub,
|
|
86
86
|
email: payload.impersonation.originalUserEmail,
|
|
87
87
|
name: payload.impersonation.originalUserName
|
|
88
88
|
};
|
|
@@ -47,7 +47,7 @@ export default defineEventHandler(async (event) => {
|
|
|
47
47
|
domain: cookieConfig?.domain
|
|
48
48
|
});
|
|
49
49
|
logger.security("Impersonation ended", {
|
|
50
|
-
originalUser: currentToken.impersonation?.
|
|
50
|
+
originalUser: currentToken.impersonation?.originalUserSub,
|
|
51
51
|
wasImpersonating: currentToken.sub
|
|
52
52
|
});
|
|
53
53
|
return { accessToken };
|
|
@@ -23,10 +23,11 @@ export declare function fetchTargetUser(requester: BaseTokenClaims, targetUserId
|
|
|
23
23
|
* @param requester - The user performing impersonation
|
|
24
24
|
* @param targetUserData - Target user data from database
|
|
25
25
|
* @param reason - Optional reason for impersonation
|
|
26
|
+
* @param originalUserLookupId - Original user lookup ID used for restoration
|
|
26
27
|
* @param _event - H3 event for context
|
|
27
28
|
* @returns JWT access token (no refresh token)
|
|
28
29
|
*/
|
|
29
|
-
export declare function generateImpersonatedToken(requester: BaseTokenClaims, targetUserData: Record<string, unknown>, reason: string | undefined, _event: H3Event): Promise<string>;
|
|
30
|
+
export declare function generateImpersonatedToken(requester: BaseTokenClaims, targetUserData: Record<string, unknown>, reason: string | undefined, originalUserLookupId: string, _event: H3Event): Promise<string>;
|
|
30
31
|
/**
|
|
31
32
|
* Start impersonation session
|
|
32
33
|
* @param requester - The user requesting impersonation (must be admin)
|
|
@@ -21,6 +21,18 @@ function getClientInfo(event) {
|
|
|
21
21
|
const userAgent = headers["user-agent"];
|
|
22
22
|
return { ip, userAgent };
|
|
23
23
|
}
|
|
24
|
+
function resolveOriginalUserLookupId(requester) {
|
|
25
|
+
const config = useRuntimeConfig();
|
|
26
|
+
const lookupClaim = config.nuxtAegis?.impersonation?.originalUserLookupClaim || "sub";
|
|
27
|
+
const lookupValue = requester[lookupClaim];
|
|
28
|
+
if (typeof lookupValue !== "string" || lookupValue.length === 0) {
|
|
29
|
+
throw createError({
|
|
30
|
+
statusCode: 500,
|
|
31
|
+
message: `Impersonation originalUserLookupClaim "${lookupClaim}" must resolve to a non-empty string on the requester token`
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return lookupValue;
|
|
35
|
+
}
|
|
24
36
|
export async function checkImpersonationAllowed(requester, targetUserId, event) {
|
|
25
37
|
checkImpersonationEnabled();
|
|
26
38
|
if (requester.impersonation) {
|
|
@@ -75,7 +87,7 @@ export async function fetchTargetUser(requester, targetUserId, event) {
|
|
|
75
87
|
});
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
|
-
export async function generateImpersonatedToken(requester, targetUserData, reason, _event) {
|
|
90
|
+
export async function generateImpersonatedToken(requester, targetUserData, reason, originalUserLookupId, _event) {
|
|
79
91
|
const config = useRuntimeConfig();
|
|
80
92
|
const tokenConfig = config.nuxtAegis?.token;
|
|
81
93
|
const impersonationConfig = config.nuxtAegis?.impersonation;
|
|
@@ -93,7 +105,8 @@ export async function generateImpersonatedToken(requester, targetUserData, reaso
|
|
|
93
105
|
}
|
|
94
106
|
}
|
|
95
107
|
const impersonationContext = {
|
|
96
|
-
|
|
108
|
+
originalUserSub: requester.sub,
|
|
109
|
+
originalUserLookupId,
|
|
97
110
|
originalUserEmail: requester.email,
|
|
98
111
|
originalUserName: requester.name,
|
|
99
112
|
impersonatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -132,7 +145,8 @@ export async function generateImpersonatedToken(requester, targetUserData, reaso
|
|
|
132
145
|
export async function startImpersonation(requester, targetUserId, reason, event) {
|
|
133
146
|
await checkImpersonationAllowed(requester, targetUserId, event);
|
|
134
147
|
const targetUserData = await fetchTargetUser(requester, targetUserId, event);
|
|
135
|
-
const
|
|
148
|
+
const originalUserLookupId = resolveOriginalUserLookupId(requester);
|
|
149
|
+
const accessToken = await generateImpersonatedToken(requester, targetUserData, reason, originalUserLookupId, event);
|
|
136
150
|
const { ip, userAgent } = getClientInfo(event);
|
|
137
151
|
const targetPayload = {
|
|
138
152
|
sub: targetUserData.sub || targetUserData.id || targetUserData.email,
|
|
@@ -170,14 +184,14 @@ export async function endImpersonation(currentToken, event) {
|
|
|
170
184
|
originalUserData = await fetchTargetUser(
|
|
171
185
|
currentToken,
|
|
172
186
|
// Pass current token as requester (for context)
|
|
173
|
-
impersonation.
|
|
187
|
+
impersonation.originalUserLookupId,
|
|
174
188
|
event
|
|
175
189
|
);
|
|
176
190
|
} catch (error) {
|
|
177
191
|
const err = error;
|
|
178
192
|
if (err.statusCode === 404) {
|
|
179
193
|
logger.warn("Original user not found in database, using stored context", {
|
|
180
|
-
|
|
194
|
+
userLookupId: impersonation.originalUserLookupId
|
|
181
195
|
});
|
|
182
196
|
} else {
|
|
183
197
|
throw error;
|
|
@@ -192,7 +206,7 @@ export async function endImpersonation(currentToken, event) {
|
|
|
192
206
|
});
|
|
193
207
|
}
|
|
194
208
|
const originalPayload = {
|
|
195
|
-
sub: originalUserData ? originalUserData.sub || originalUserData.id || originalUserData.email : impersonation.
|
|
209
|
+
sub: originalUserData ? originalUserData.sub || originalUserData.id || originalUserData.email : impersonation.originalUserSub,
|
|
196
210
|
email: originalUserData ? originalUserData.email : impersonation.originalUserEmail,
|
|
197
211
|
name: originalUserData ? originalUserData.name : impersonation.originalUserName,
|
|
198
212
|
picture: originalUserData?.picture,
|
|
@@ -51,6 +51,8 @@ export interface ImpersonationConfig {
|
|
|
51
51
|
enabled?: boolean;
|
|
52
52
|
/** Token expiration time for impersonated sessions in seconds (default: 900 = 15 minutes) */
|
|
53
53
|
tokenExpiration?: number;
|
|
54
|
+
/** Token claim name used to resolve the original user when ending impersonation (default: 'sub') */
|
|
55
|
+
originalUserLookupClaim?: string;
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* Runtime config for Nuxt Aegis
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* Contains essential information about the original user when impersonating
|
|
8
8
|
*/
|
|
9
9
|
export interface ImpersonationContext {
|
|
10
|
-
/** Original user
|
|
11
|
-
|
|
10
|
+
/** Original user subject (`sub`) who is performing the impersonation */
|
|
11
|
+
originalUserSub: string;
|
|
12
|
+
/** Original user lookup ID used to refetch the user when ending impersonation */
|
|
13
|
+
originalUserLookupId: string;
|
|
12
14
|
/** Original user email */
|
|
13
15
|
originalUserEmail?: string;
|
|
14
16
|
/** Original user name */
|
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.6",
|
|
4
4
|
"description": "Nuxt module for authentication with JWT token generation and session management.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -52,30 +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.4.6",
|
|
56
56
|
"consola": "^3.4.2",
|
|
57
|
-
"defu": "^6.1.
|
|
58
|
-
"jose": "^6.
|
|
59
|
-
"ufo": "^1.6.
|
|
57
|
+
"defu": "^6.1.7",
|
|
58
|
+
"jose": "^6.2.3",
|
|
59
|
+
"ufo": "^1.6.4"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@nuxt/devtools": "^3.
|
|
63
|
-
"@nuxt/eslint-config": "^1.
|
|
62
|
+
"@nuxt/devtools": "^3.2.4",
|
|
63
|
+
"@nuxt/eslint-config": "^1.15.2",
|
|
64
64
|
"@nuxt/module-builder": "^1.0.2",
|
|
65
|
-
"@nuxt/schema": "^4.
|
|
66
|
-
"@nuxt/test-utils": "^
|
|
65
|
+
"@nuxt/schema": "^4.4.6",
|
|
66
|
+
"@nuxt/test-utils": "^4.0.3",
|
|
67
67
|
"@types/node": "latest",
|
|
68
68
|
"changelogen": "^0.6.2",
|
|
69
|
-
"eslint": "^
|
|
70
|
-
"nuxt": "^4.
|
|
71
|
-
"typescript": "~
|
|
72
|
-
"vitest": "^
|
|
73
|
-
"vue-tsc": "^3.
|
|
74
|
-
},
|
|
75
|
-
"pnpm": {
|
|
76
|
-
"overrides": {
|
|
77
|
-
"vue-router": "4.4.5"
|
|
78
|
-
}
|
|
69
|
+
"eslint": "^10.4.0",
|
|
70
|
+
"nuxt": "^4.4.6",
|
|
71
|
+
"typescript": "~6.0.3",
|
|
72
|
+
"vitest": "^4.1.7",
|
|
73
|
+
"vue-tsc": "^3.3.1"
|
|
79
74
|
},
|
|
80
75
|
"packageManager": "pnpm@10.17.1"
|
|
81
76
|
}
|