@peterbud/nuxt-aegis 1.1.0-alpha
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 +166 -0
- package/dist/module.d.mts +6 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +354 -0
- package/dist/runtime/app/composables/useAuth.d.ts +85 -0
- package/dist/runtime/app/composables/useAuth.js +187 -0
- package/dist/runtime/app/middleware/auth-logged-in.d.ts +16 -0
- package/dist/runtime/app/middleware/auth-logged-in.js +25 -0
- package/dist/runtime/app/middleware/auth-logged-out.d.ts +20 -0
- package/dist/runtime/app/middleware/auth-logged-out.js +17 -0
- package/dist/runtime/app/pages/AuthCallback.d.vue.ts +3 -0
- package/dist/runtime/app/pages/AuthCallback.vue +92 -0
- package/dist/runtime/app/pages/AuthCallback.vue.d.ts +3 -0
- package/dist/runtime/app/plugins/api.client.d.ts +11 -0
- package/dist/runtime/app/plugins/api.client.js +92 -0
- package/dist/runtime/app/plugins/api.server.d.ts +13 -0
- package/dist/runtime/app/plugins/api.server.js +28 -0
- package/dist/runtime/app/plugins/ssr-state.server.d.ts +2 -0
- package/dist/runtime/app/plugins/ssr-state.server.js +13 -0
- package/dist/runtime/app/router.options.d.ts +12 -0
- package/dist/runtime/app/router.options.js +11 -0
- package/dist/runtime/app/utils/logger.d.ts +18 -0
- package/dist/runtime/app/utils/logger.js +48 -0
- package/dist/runtime/app/utils/redirectValidation.d.ts +18 -0
- package/dist/runtime/app/utils/redirectValidation.js +21 -0
- package/dist/runtime/app/utils/routeMatching.d.ts +13 -0
- package/dist/runtime/app/utils/routeMatching.js +10 -0
- package/dist/runtime/app/utils/tokenStore.d.ts +24 -0
- package/dist/runtime/app/utils/tokenStore.js +14 -0
- package/dist/runtime/app/utils/tokenUtils.d.ts +17 -0
- package/dist/runtime/app/utils/tokenUtils.js +4 -0
- package/dist/runtime/server/middleware/auth.d.ts +6 -0
- package/dist/runtime/server/middleware/auth.js +82 -0
- package/dist/runtime/server/plugins/ssr-auth.d.ts +7 -0
- package/dist/runtime/server/plugins/ssr-auth.js +82 -0
- package/dist/runtime/server/providers/auth0.d.ts +12 -0
- package/dist/runtime/server/providers/auth0.js +57 -0
- package/dist/runtime/server/providers/github.d.ts +12 -0
- package/dist/runtime/server/providers/github.js +44 -0
- package/dist/runtime/server/providers/google.d.ts +12 -0
- package/dist/runtime/server/providers/google.js +46 -0
- package/dist/runtime/server/providers/mock.d.ts +37 -0
- package/dist/runtime/server/providers/mock.js +129 -0
- package/dist/runtime/server/providers/oauthBase.d.ts +72 -0
- package/dist/runtime/server/providers/oauthBase.js +183 -0
- package/dist/runtime/server/routes/impersonate.post.d.ts +21 -0
- package/dist/runtime/server/routes/impersonate.post.js +68 -0
- package/dist/runtime/server/routes/logout.post.d.ts +9 -0
- package/dist/runtime/server/routes/logout.post.js +24 -0
- package/dist/runtime/server/routes/me.get.d.ts +6 -0
- package/dist/runtime/server/routes/me.get.js +11 -0
- package/dist/runtime/server/routes/mock/authorize.get.d.ts +29 -0
- package/dist/runtime/server/routes/mock/authorize.get.js +103 -0
- package/dist/runtime/server/routes/mock/token.post.d.ts +31 -0
- package/dist/runtime/server/routes/mock/token.post.js +88 -0
- package/dist/runtime/server/routes/mock/userinfo.get.d.ts +27 -0
- package/dist/runtime/server/routes/mock/userinfo.get.js +59 -0
- package/dist/runtime/server/routes/password/change.post.d.ts +4 -0
- package/dist/runtime/server/routes/password/change.post.js +108 -0
- package/dist/runtime/server/routes/password/login-verify.get.d.ts +2 -0
- package/dist/runtime/server/routes/password/login-verify.get.js +79 -0
- package/dist/runtime/server/routes/password/login.post.d.ts +4 -0
- package/dist/runtime/server/routes/password/login.post.js +66 -0
- package/dist/runtime/server/routes/password/register-verify.get.d.ts +2 -0
- package/dist/runtime/server/routes/password/register-verify.get.js +86 -0
- package/dist/runtime/server/routes/password/register.post.d.ts +4 -0
- package/dist/runtime/server/routes/password/register.post.js +87 -0
- package/dist/runtime/server/routes/password/reset-complete.post.d.ts +4 -0
- package/dist/runtime/server/routes/password/reset-complete.post.js +75 -0
- package/dist/runtime/server/routes/password/reset-request.post.d.ts +5 -0
- package/dist/runtime/server/routes/password/reset-request.post.js +52 -0
- package/dist/runtime/server/routes/password/reset-verify.get.d.ts +2 -0
- package/dist/runtime/server/routes/password/reset-verify.get.js +50 -0
- package/dist/runtime/server/routes/refresh.post.d.ts +8 -0
- package/dist/runtime/server/routes/refresh.post.js +102 -0
- package/dist/runtime/server/routes/token.post.d.ts +28 -0
- package/dist/runtime/server/routes/token.post.js +90 -0
- package/dist/runtime/server/routes/unimpersonate.post.d.ts +16 -0
- package/dist/runtime/server/routes/unimpersonate.post.js +65 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/auth.d.ts +94 -0
- package/dist/runtime/server/utils/auth.js +54 -0
- package/dist/runtime/server/utils/authCodeStore.d.ts +137 -0
- package/dist/runtime/server/utils/authCodeStore.js +123 -0
- package/dist/runtime/server/utils/cookies.d.ts +15 -0
- package/dist/runtime/server/utils/cookies.js +23 -0
- package/dist/runtime/server/utils/customClaims.d.ts +37 -0
- package/dist/runtime/server/utils/customClaims.js +45 -0
- package/dist/runtime/server/utils/handler.d.ts +77 -0
- package/dist/runtime/server/utils/handler.js +7 -0
- package/dist/runtime/server/utils/impersonation.d.ts +48 -0
- package/dist/runtime/server/utils/impersonation.js +259 -0
- package/dist/runtime/server/utils/jwt.d.ts +24 -0
- package/dist/runtime/server/utils/jwt.js +77 -0
- package/dist/runtime/server/utils/logger.d.ts +18 -0
- package/dist/runtime/server/utils/logger.js +49 -0
- package/dist/runtime/server/utils/magicCodeStore.d.ts +27 -0
- package/dist/runtime/server/utils/magicCodeStore.js +66 -0
- package/dist/runtime/server/utils/mockCodeStore.d.ts +89 -0
- package/dist/runtime/server/utils/mockCodeStore.js +71 -0
- package/dist/runtime/server/utils/password.d.ts +33 -0
- package/dist/runtime/server/utils/password.js +48 -0
- package/dist/runtime/server/utils/refreshToken.d.ts +74 -0
- package/dist/runtime/server/utils/refreshToken.js +108 -0
- package/dist/runtime/server/utils/resetSessionStore.d.ts +12 -0
- package/dist/runtime/server/utils/resetSessionStore.js +29 -0
- package/dist/runtime/tasks/cleanup/magic-codes.d.ts +10 -0
- package/dist/runtime/tasks/cleanup/magic-codes.js +79 -0
- package/dist/runtime/tasks/cleanup/refresh-tokens.d.ts +10 -0
- package/dist/runtime/tasks/cleanup/refresh-tokens.js +55 -0
- package/dist/runtime/tasks/cleanup/reset-sessions.d.ts +8 -0
- package/dist/runtime/tasks/cleanup/reset-sessions.js +45 -0
- package/dist/runtime/types/augmentation.d.ts +73 -0
- package/dist/runtime/types/augmentation.js +0 -0
- package/dist/runtime/types/authCode.d.ts +60 -0
- package/dist/runtime/types/authCode.js +0 -0
- package/dist/runtime/types/callbacks.d.ts +54 -0
- package/dist/runtime/types/callbacks.js +0 -0
- package/dist/runtime/types/config.d.ts +129 -0
- package/dist/runtime/types/config.js +0 -0
- package/dist/runtime/types/hooks.d.ts +118 -0
- package/dist/runtime/types/hooks.js +0 -0
- package/dist/runtime/types/index.d.ts +13 -0
- package/dist/runtime/types/index.js +1 -0
- package/dist/runtime/types/providers.d.ts +212 -0
- package/dist/runtime/types/providers.js +0 -0
- package/dist/runtime/types/refresh.d.ts +61 -0
- package/dist/runtime/types/refresh.js +0 -0
- package/dist/runtime/types/routes.d.ts +30 -0
- package/dist/runtime/types/routes.js +0 -0
- package/dist/runtime/types/token.d.ts +182 -0
- package/dist/runtime/types/token.js +0 -0
- package/dist/types.d.mts +7 -0
- package/package.json +80 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { useStorage } from "#imports";
|
|
3
|
+
import { createLogger } from "./logger.js";
|
|
4
|
+
const logger = createLogger("AuthCode");
|
|
5
|
+
export function generateAuthCode() {
|
|
6
|
+
const code = randomBytes(32).toString("base64url");
|
|
7
|
+
logger.security("Authorization code generated", {
|
|
8
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9
|
+
event: "CODE_GENERATED",
|
|
10
|
+
codePrefix: `${code.substring(0, 8)}...`
|
|
11
|
+
});
|
|
12
|
+
return code;
|
|
13
|
+
}
|
|
14
|
+
export async function storeAuthCode(code, providerUserInfo, providerTokens, provider, customClaims, expiresIn = 60, _event) {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const authCodeData = {
|
|
17
|
+
providerUserInfo,
|
|
18
|
+
providerTokens,
|
|
19
|
+
expiresAt: now + expiresIn * 1e3,
|
|
20
|
+
// CS-4: Set expiration timestamp
|
|
21
|
+
createdAt: now,
|
|
22
|
+
provider,
|
|
23
|
+
customClaims
|
|
24
|
+
// Store resolved custom claims
|
|
25
|
+
};
|
|
26
|
+
await useStorage("authCodeStore").setItem(code, authCodeData);
|
|
27
|
+
logger.security("Authorization code stored", {
|
|
28
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29
|
+
event: "CODE_STORED",
|
|
30
|
+
codePrefix: `${code.substring(0, 8)}...`,
|
|
31
|
+
expiresAt: new Date(authCodeData.expiresAt).toISOString(),
|
|
32
|
+
expiresInSeconds: expiresIn
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function validateAuthCode(code) {
|
|
36
|
+
const authCodeData = await useStorage("authCodeStore").getItem(code);
|
|
37
|
+
if (!authCodeData) {
|
|
38
|
+
logger.security("Authorization code not found", {
|
|
39
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
40
|
+
event: "CODE_NOT_FOUND",
|
|
41
|
+
codePrefix: `${code.substring(0, 8)}...`,
|
|
42
|
+
severity: "warning"
|
|
43
|
+
});
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (Date.now() > authCodeData.expiresAt) {
|
|
47
|
+
logger.security("Authorization code expired", {
|
|
48
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49
|
+
event: "CODE_EXPIRED",
|
|
50
|
+
codePrefix: `${code.substring(0, 8)}...`,
|
|
51
|
+
expiresAt: new Date(authCodeData.expiresAt).toISOString(),
|
|
52
|
+
severity: "warning"
|
|
53
|
+
});
|
|
54
|
+
await useStorage("authCodeStore").removeItem(code);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return authCodeData;
|
|
58
|
+
}
|
|
59
|
+
export async function retrieveAndDeleteAuthCode(code) {
|
|
60
|
+
const authCodeData = await validateAuthCode(code);
|
|
61
|
+
if (!authCodeData) {
|
|
62
|
+
logger.security("Invalid authorization code exchange attempt", {
|
|
63
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
64
|
+
event: "CODE_EXCHANGE_FAILED",
|
|
65
|
+
codePrefix: `${code.substring(0, 8)}...`,
|
|
66
|
+
reason: "invalid_or_expired",
|
|
67
|
+
severity: "warning"
|
|
68
|
+
});
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
await useStorage("authCodeStore").removeItem(code);
|
|
72
|
+
logger.security("Authorization code successfully exchanged", {
|
|
73
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
74
|
+
event: "CODE_EXCHANGE_SUCCESS",
|
|
75
|
+
codePrefix: `${code.substring(0, 8)}...`,
|
|
76
|
+
codeAge: Date.now() - authCodeData.createdAt
|
|
77
|
+
});
|
|
78
|
+
return authCodeData;
|
|
79
|
+
}
|
|
80
|
+
export async function cleanupExpiredAuthCodes() {
|
|
81
|
+
const storage = useStorage("authCodeStore");
|
|
82
|
+
const keys = await storage.getKeys();
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
let cleanedCount = 0;
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const authCodeData = await storage.getItem(key);
|
|
87
|
+
if (!authCodeData) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (now > authCodeData.expiresAt) {
|
|
91
|
+
await storage.removeItem(key);
|
|
92
|
+
cleanedCount++;
|
|
93
|
+
logger.debug("Cleaned up expired authorization code:", `${key.substring(0, 8)}...`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (cleanedCount > 0) {
|
|
97
|
+
logger.info(`Cleanup completed: ${cleanedCount} expired code(s) removed`);
|
|
98
|
+
}
|
|
99
|
+
return cleanedCount;
|
|
100
|
+
}
|
|
101
|
+
export async function getAuthCodeStats() {
|
|
102
|
+
const storage = useStorage("authCodeStore");
|
|
103
|
+
const keys = await storage.getKeys();
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
let validCount = 0;
|
|
106
|
+
let expiredCount = 0;
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
const authCodeData = await storage.getItem(key);
|
|
109
|
+
if (!authCodeData) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (now > authCodeData.expiresAt) {
|
|
113
|
+
expiredCount++;
|
|
114
|
+
} else {
|
|
115
|
+
validCount++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
total: keys.length,
|
|
120
|
+
valid: validCount,
|
|
121
|
+
expired: expiredCount
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { CookieConfig } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Set the authentication token as an HTTP-only cookie
|
|
5
|
+
* @param event - H3Event object
|
|
6
|
+
* @param token - JWT token to set as cookie
|
|
7
|
+
* @param cookieConfig - Optional cookie configuration
|
|
8
|
+
*/
|
|
9
|
+
export declare function setRefreshTokenCookie(event: H3Event, token: string, cookieConfig?: CookieConfig): void;
|
|
10
|
+
/**
|
|
11
|
+
* Clear the authentication token cookie
|
|
12
|
+
* @param event - H3Event object
|
|
13
|
+
* @param cookieConfig - Optional cookie configuration
|
|
14
|
+
*/
|
|
15
|
+
export declare function clearToken(event: H3Event, cookieConfig?: CookieConfig): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { setCookie, deleteCookie } from "h3";
|
|
2
|
+
export function setRefreshTokenCookie(event, token, cookieConfig) {
|
|
3
|
+
if (!token) {
|
|
4
|
+
throw new Error("Token is required");
|
|
5
|
+
}
|
|
6
|
+
const cookieName = cookieConfig?.cookieName || "nuxt-aegis-refresh";
|
|
7
|
+
const maxAge = cookieConfig?.maxAge || 604800;
|
|
8
|
+
setCookie(event, cookieName, token, {
|
|
9
|
+
httpOnly: cookieConfig?.httpOnly ?? true,
|
|
10
|
+
// SC-3: Set HttpOnly flag
|
|
11
|
+
secure: cookieConfig?.secure ?? process.env.NODE_ENV === "production",
|
|
12
|
+
// SC-4: Set Secure flag in production
|
|
13
|
+
sameSite: cookieConfig?.sameSite || "lax",
|
|
14
|
+
// SC-5: Set SameSite attribute
|
|
15
|
+
path: cookieConfig?.path || "/",
|
|
16
|
+
domain: cookieConfig?.domain,
|
|
17
|
+
maxAge
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function clearToken(event, cookieConfig) {
|
|
21
|
+
const cookieName = cookieConfig?.cookieName || "nuxt-aegis-refresh";
|
|
22
|
+
deleteCookie(event, cookieName);
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Claims Processing Utilities
|
|
3
|
+
* Handles processing of custom claims from static values or callback functions
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Process custom claims configuration
|
|
7
|
+
* Supports both static claim objects and callback functions (sync/async)
|
|
8
|
+
* The same function is used for both initial authentication and token refresh
|
|
9
|
+
*
|
|
10
|
+
* @param providerUserInfo - Complete OAuth provider user data
|
|
11
|
+
* @param customClaimsConfig - Custom claims configuration (static object or callback function)
|
|
12
|
+
* @returns Processed custom claims object
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Static claims
|
|
16
|
+
* const claims = await processCustomClaims(providerUserInfo, { role: 'admin', tier: 'premium' })
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Callback function
|
|
20
|
+
* const claims = await processCustomClaims(providerUserInfo, async (providerUserInfo) => {
|
|
21
|
+
* const dbUser = await fetchUserFromDB(providerUserInfo.email)
|
|
22
|
+
* return { role: dbUser.role, permissions: dbUser.permissions }
|
|
23
|
+
* })
|
|
24
|
+
*/
|
|
25
|
+
export declare function processCustomClaims(providerUserInfo: Record<string, unknown>, customClaimsConfig?: Record<string, unknown> | ((providerUserInfo: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>)): Promise<Record<string, unknown>>;
|
|
26
|
+
/**
|
|
27
|
+
* Validate that custom claims don't override reserved JWT claims
|
|
28
|
+
* @param claims - Custom claims to validate
|
|
29
|
+
* @returns Filtered claims with reserved claims removed
|
|
30
|
+
*/
|
|
31
|
+
export declare function filterReservedClaims(claims: Record<string, unknown>): Record<string, unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Validate that custom claim values are of supported types
|
|
34
|
+
* @param claims - Custom claims to validate
|
|
35
|
+
* @returns Filtered claims with only supported value types
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateClaimTypes(claims: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createLogger } from "./logger.js";
|
|
2
|
+
const logger = createLogger("CustomClaims");
|
|
3
|
+
export async function processCustomClaims(providerUserInfo, customClaimsConfig) {
|
|
4
|
+
if (!customClaimsConfig) {
|
|
5
|
+
return {};
|
|
6
|
+
}
|
|
7
|
+
if (typeof customClaimsConfig !== "function") {
|
|
8
|
+
return customClaimsConfig;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const result = customClaimsConfig(providerUserInfo);
|
|
12
|
+
const claims = result instanceof Promise ? await result : result;
|
|
13
|
+
if (typeof claims !== "object" || claims === null || Array.isArray(claims)) {
|
|
14
|
+
logger.warn("Custom claims callback must return an object");
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
return claims;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logger.error("Error processing custom claims:", error);
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function filterReservedClaims(claims) {
|
|
24
|
+
const reservedClaims = ["iss", "sub", "exp", "iat", "nbf", "jti", "aud"];
|
|
25
|
+
const filtered = {};
|
|
26
|
+
Object.entries(claims).forEach(([key, value]) => {
|
|
27
|
+
if (reservedClaims.includes(key)) {
|
|
28
|
+
logger.warn(`Cannot override reserved JWT claim: ${key}`);
|
|
29
|
+
} else {
|
|
30
|
+
filtered[key] = value;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return filtered;
|
|
34
|
+
}
|
|
35
|
+
export function validateClaimTypes(claims) {
|
|
36
|
+
const validated = {};
|
|
37
|
+
Object.entries(claims).forEach(([key, value]) => {
|
|
38
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || Array.isArray(value) || value === null) {
|
|
39
|
+
validated[key] = value;
|
|
40
|
+
} else {
|
|
41
|
+
logger.warn(`Custom claim "${key}" has unsupported type and will be ignored`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return validated;
|
|
45
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { UserInfoHookPayload } from '../../types/hooks.js';
|
|
3
|
+
import type { TokenPayload } from '../../types/token.js';
|
|
4
|
+
import type { PasswordUser } from '../../types/providers.js';
|
|
5
|
+
export interface AegisHandler {
|
|
6
|
+
/**
|
|
7
|
+
* Transform user data after fetching from OAuth provider.
|
|
8
|
+
* Replaces `nuxt-aegis:userInfo` hook.
|
|
9
|
+
* Return the modified user object to use it.
|
|
10
|
+
*/
|
|
11
|
+
onUserInfo?: (payload: UserInfoHookPayload) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Password authentication handler.
|
|
14
|
+
* Required if password provider is enabled.
|
|
15
|
+
*/
|
|
16
|
+
password?: {
|
|
17
|
+
/**
|
|
18
|
+
* Find a user by email.
|
|
19
|
+
* Used during login and registration checks.
|
|
20
|
+
* Return null if user is not found.
|
|
21
|
+
*/
|
|
22
|
+
findUser: (email: string) => Promise<PasswordUser | null> | PasswordUser | null;
|
|
23
|
+
/**
|
|
24
|
+
* Create or update a user.
|
|
25
|
+
* Called after successful registration or password change.
|
|
26
|
+
*/
|
|
27
|
+
upsertUser: (user: PasswordUser) => Promise<void> | void;
|
|
28
|
+
/**
|
|
29
|
+
* Send a verification code to the user.
|
|
30
|
+
* Called during registration, login, and password reset.
|
|
31
|
+
*/
|
|
32
|
+
sendVerificationCode: (email: string, code: string, action: 'register' | 'login' | 'reset') => Promise<void> | void;
|
|
33
|
+
/**
|
|
34
|
+
* Validate password strength.
|
|
35
|
+
* Override default validation logic.
|
|
36
|
+
* Return true if valid, or an array of error messages.
|
|
37
|
+
*/
|
|
38
|
+
validatePassword?: (password: string) => Promise<boolean | string[]> | boolean | string[];
|
|
39
|
+
/**
|
|
40
|
+
* Hash a password.
|
|
41
|
+
* Override default bcrypt hashing.
|
|
42
|
+
*/
|
|
43
|
+
hashPassword?: (password: string) => Promise<string> | string;
|
|
44
|
+
/**
|
|
45
|
+
* Verify a password against a hash.
|
|
46
|
+
* Override default bcrypt verification.
|
|
47
|
+
*/
|
|
48
|
+
verifyPassword?: (password: string, hash: string) => Promise<boolean> | boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Impersonation logic.
|
|
52
|
+
* Replaces `nuxt-aegis:impersonate:check` and `nuxt-aegis:impersonate:fetchTarget` hooks.
|
|
53
|
+
*/
|
|
54
|
+
impersonation?: {
|
|
55
|
+
/**
|
|
56
|
+
* Fetch the target user to be impersonated.
|
|
57
|
+
* Must return a user object that will be used to generate the JWT.
|
|
58
|
+
* Return null if user is not found.
|
|
59
|
+
*/
|
|
60
|
+
fetchTarget: (targetId: string, event: H3Event) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null;
|
|
61
|
+
/**
|
|
62
|
+
* Check if the requester is allowed to impersonate the target.
|
|
63
|
+
* If not defined, defaults to allowing if fetchTarget returns a user.
|
|
64
|
+
* You can throw an error here to provide a specific message.
|
|
65
|
+
*/
|
|
66
|
+
canImpersonate?: (requester: TokenPayload, targetId: string, event: H3Event) => Promise<boolean> | boolean;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Registers the Aegis handler configuration.
|
|
71
|
+
* Call this within a server plugin.
|
|
72
|
+
*/
|
|
73
|
+
export declare const defineAegisHandler: (handler: AegisHandler) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Internal: Retrieves the registered handler.
|
|
76
|
+
*/
|
|
77
|
+
export declare const useAegisHandler: () => AegisHandler | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { TokenPayload } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Check if the requester is allowed to impersonate other users
|
|
5
|
+
* @param requester - The user requesting impersonation
|
|
6
|
+
* @param targetUserId - The ID of the user to impersonate
|
|
7
|
+
* @param event - H3 event for context
|
|
8
|
+
* @throws 403 error if impersonation is not allowed
|
|
9
|
+
*/
|
|
10
|
+
export declare function checkImpersonationAllowed(requester: TokenPayload, targetUserId: string, event: H3Event): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Fetch target user data for impersonation
|
|
13
|
+
* @param requester - The user requesting impersonation
|
|
14
|
+
* @param targetUserId - The ID of the user to impersonate
|
|
15
|
+
* @param event - H3 event for context
|
|
16
|
+
* @returns User data object with sub, email, name, and custom claims
|
|
17
|
+
* @throws 404 error if user not found
|
|
18
|
+
* @throws 500 error if hook is not implemented
|
|
19
|
+
*/
|
|
20
|
+
export declare function fetchTargetUser(requester: TokenPayload, targetUserId: string, event: H3Event): Promise<Record<string, unknown>>;
|
|
21
|
+
/**
|
|
22
|
+
* Generate an impersonated JWT token
|
|
23
|
+
* @param requester - The user performing impersonation
|
|
24
|
+
* @param targetUserData - Target user data from database
|
|
25
|
+
* @param reason - Optional reason for impersonation
|
|
26
|
+
* @param _event - H3 event for context
|
|
27
|
+
* @returns JWT access token (no refresh token)
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateImpersonatedToken(requester: TokenPayload, targetUserData: Record<string, unknown>, reason: string | undefined, _event: H3Event): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Start impersonation session
|
|
32
|
+
* @param requester - The user requesting impersonation (must be admin)
|
|
33
|
+
* @param targetUserId - The ID of the user to impersonate
|
|
34
|
+
* @param reason - Optional reason for impersonation
|
|
35
|
+
* @param event - H3 event for context
|
|
36
|
+
* @returns Access token for impersonated session (no refresh token)
|
|
37
|
+
*/
|
|
38
|
+
export declare function startImpersonation(requester: TokenPayload, targetUserId: string, reason: string | undefined, event: H3Event): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* End impersonation and restore original user session
|
|
41
|
+
* @param currentToken - Current JWT token (must contain impersonation context)
|
|
42
|
+
* @param event - H3 event for context
|
|
43
|
+
* @returns Object with new access token and refresh token ID
|
|
44
|
+
*/
|
|
45
|
+
export declare function endImpersonation(currentToken: TokenPayload, event: H3Event): Promise<{
|
|
46
|
+
accessToken: string;
|
|
47
|
+
refreshTokenId: string;
|
|
48
|
+
}>;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { createError } from "h3";
|
|
2
|
+
import { generateToken } from "./jwt.js";
|
|
3
|
+
import { useRuntimeConfig, useNitroApp } from "#imports";
|
|
4
|
+
import { createLogger } from "./logger.js";
|
|
5
|
+
import { generateAndStoreRefreshToken } from "./refreshToken.js";
|
|
6
|
+
import { useAegisHandler } from "./handler.js";
|
|
7
|
+
const logger = createLogger("Impersonation");
|
|
8
|
+
function checkImpersonationEnabled() {
|
|
9
|
+
const config = useRuntimeConfig();
|
|
10
|
+
if (!config.nuxtAegis?.impersonation?.enabled) {
|
|
11
|
+
throw createError({
|
|
12
|
+
statusCode: 404,
|
|
13
|
+
message: "Impersonation feature is not enabled"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function getClientInfo(event) {
|
|
18
|
+
const headers = event.node.req.headers;
|
|
19
|
+
const ip = headers["x-forwarded-for"]?.split(",")[0]?.trim() || headers["x-real-ip"] || event.node.req.socket?.remoteAddress;
|
|
20
|
+
const userAgent = headers["user-agent"];
|
|
21
|
+
return { ip, userAgent };
|
|
22
|
+
}
|
|
23
|
+
export async function checkImpersonationAllowed(requester, targetUserId, event) {
|
|
24
|
+
checkImpersonationEnabled();
|
|
25
|
+
if (requester.impersonation) {
|
|
26
|
+
throw createError({
|
|
27
|
+
statusCode: 403,
|
|
28
|
+
message: "Cannot impersonate while already impersonating another user"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const handler = useAegisHandler();
|
|
32
|
+
if (handler?.impersonation?.canImpersonate) {
|
|
33
|
+
const allowed = await handler.impersonation.canImpersonate(requester, targetUserId, event);
|
|
34
|
+
if (!allowed) {
|
|
35
|
+
throw createError({
|
|
36
|
+
statusCode: 403,
|
|
37
|
+
message: "Insufficient permissions to impersonate users"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (requester.role !== "admin") {
|
|
43
|
+
throw createError({
|
|
44
|
+
statusCode: 403,
|
|
45
|
+
message: "Insufficient permissions to impersonate users"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function fetchTargetUser(requester, targetUserId, event) {
|
|
50
|
+
const handler = useAegisHandler();
|
|
51
|
+
if (!handler?.impersonation?.fetchTarget) {
|
|
52
|
+
throw createError({
|
|
53
|
+
statusCode: 500,
|
|
54
|
+
message: "Impersonation requires implementing fetchTarget handler."
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const targetUser = await handler.impersonation.fetchTarget(targetUserId, event);
|
|
59
|
+
if (!targetUser) {
|
|
60
|
+
throw createError({
|
|
61
|
+
statusCode: 404,
|
|
62
|
+
message: `Target user not found: ${targetUserId}`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return targetUser;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const err = error;
|
|
68
|
+
if (err.statusCode) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
throw createError({
|
|
72
|
+
statusCode: 500,
|
|
73
|
+
message: err.message || "Failed to fetch target user"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function generateImpersonatedToken(requester, targetUserData, reason, _event) {
|
|
78
|
+
const config = useRuntimeConfig();
|
|
79
|
+
const tokenConfig = config.nuxtAegis?.token;
|
|
80
|
+
const impersonationConfig = config.nuxtAegis?.impersonation;
|
|
81
|
+
if (!tokenConfig || !tokenConfig.secret) {
|
|
82
|
+
throw createError({
|
|
83
|
+
statusCode: 500,
|
|
84
|
+
message: "Token configuration is missing"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const originalClaims = {};
|
|
88
|
+
const standardTokenFields = ["sub", "id", "email", "name", "picture", "provider", "iat", "exp", "iss", "aud", "impersonation"];
|
|
89
|
+
for (const [key, value] of Object.entries(requester)) {
|
|
90
|
+
if (!standardTokenFields.includes(key)) {
|
|
91
|
+
originalClaims[key] = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const impersonationContext = {
|
|
95
|
+
originalUserId: requester.sub,
|
|
96
|
+
originalUserEmail: requester.email,
|
|
97
|
+
originalUserName: requester.name,
|
|
98
|
+
impersonatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
99
|
+
reason,
|
|
100
|
+
originalClaims
|
|
101
|
+
// Store all custom claims for restoration
|
|
102
|
+
};
|
|
103
|
+
const tokenPayload = {
|
|
104
|
+
sub: targetUserData.sub || targetUserData.id || targetUserData.email,
|
|
105
|
+
email: targetUserData.email,
|
|
106
|
+
name: targetUserData.name,
|
|
107
|
+
picture: targetUserData.picture,
|
|
108
|
+
provider: targetUserData.provider,
|
|
109
|
+
impersonation: impersonationContext
|
|
110
|
+
};
|
|
111
|
+
const standardFields = ["sub", "id", "email", "name", "picture", "provider", "iat", "exp", "iss", "aud"];
|
|
112
|
+
const customClaims = {};
|
|
113
|
+
for (const [key, value] of Object.entries(targetUserData)) {
|
|
114
|
+
if (!standardFields.includes(key)) {
|
|
115
|
+
customClaims[key] = value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const impersonationExpiration = impersonationConfig?.tokenExpiration || 900;
|
|
119
|
+
const modifiedTokenConfig = {
|
|
120
|
+
...tokenConfig,
|
|
121
|
+
expiresIn: impersonationExpiration
|
|
122
|
+
};
|
|
123
|
+
const accessToken = await generateToken(tokenPayload, modifiedTokenConfig, customClaims);
|
|
124
|
+
logger.security("Impersonated token generated", {
|
|
125
|
+
originalUser: requester.sub,
|
|
126
|
+
targetUser: tokenPayload.sub,
|
|
127
|
+
expiresIn: impersonationExpiration
|
|
128
|
+
});
|
|
129
|
+
return accessToken;
|
|
130
|
+
}
|
|
131
|
+
export async function startImpersonation(requester, targetUserId, reason, event) {
|
|
132
|
+
await checkImpersonationAllowed(requester, targetUserId, event);
|
|
133
|
+
const targetUserData = await fetchTargetUser(requester, targetUserId, event);
|
|
134
|
+
const accessToken = await generateImpersonatedToken(requester, targetUserData, reason, event);
|
|
135
|
+
const { ip, userAgent } = getClientInfo(event);
|
|
136
|
+
const targetPayload = {
|
|
137
|
+
sub: targetUserData.sub || targetUserData.id || targetUserData.email,
|
|
138
|
+
email: targetUserData.email,
|
|
139
|
+
name: targetUserData.name
|
|
140
|
+
};
|
|
141
|
+
const startPayload = {
|
|
142
|
+
requester,
|
|
143
|
+
targetUser: targetPayload,
|
|
144
|
+
reason,
|
|
145
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
146
|
+
ip: ip || "",
|
|
147
|
+
userAgent: userAgent || "",
|
|
148
|
+
event
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
const nitroApp = useNitroApp();
|
|
152
|
+
await nitroApp.hooks.callHook("nuxt-aegis:impersonate:start", startPayload);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.warn("Impersonation start hook failed (non-blocking)", error);
|
|
155
|
+
}
|
|
156
|
+
return accessToken;
|
|
157
|
+
}
|
|
158
|
+
export async function endImpersonation(currentToken, event) {
|
|
159
|
+
checkImpersonationEnabled();
|
|
160
|
+
if (!currentToken.impersonation) {
|
|
161
|
+
throw createError({
|
|
162
|
+
statusCode: 400,
|
|
163
|
+
message: "Current session is not impersonated"
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const impersonation = currentToken.impersonation;
|
|
167
|
+
let originalUserData = null;
|
|
168
|
+
try {
|
|
169
|
+
originalUserData = await fetchTargetUser(
|
|
170
|
+
currentToken,
|
|
171
|
+
// Pass current token as requester (for context)
|
|
172
|
+
impersonation.originalUserId,
|
|
173
|
+
event
|
|
174
|
+
);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const err = error;
|
|
177
|
+
if (err.statusCode === 404) {
|
|
178
|
+
logger.warn("Original user not found in database, using stored context", {
|
|
179
|
+
userId: impersonation.originalUserId
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const config = useRuntimeConfig();
|
|
186
|
+
const tokenConfig = config.nuxtAegis?.token;
|
|
187
|
+
if (!tokenConfig || !tokenConfig.secret) {
|
|
188
|
+
throw createError({
|
|
189
|
+
statusCode: 500,
|
|
190
|
+
message: "Token configuration is missing"
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const originalPayload = {
|
|
194
|
+
sub: originalUserData ? originalUserData.sub || originalUserData.id || originalUserData.email : impersonation.originalUserId,
|
|
195
|
+
email: originalUserData ? originalUserData.email : impersonation.originalUserEmail,
|
|
196
|
+
name: originalUserData ? originalUserData.name : impersonation.originalUserName,
|
|
197
|
+
picture: originalUserData?.picture,
|
|
198
|
+
provider: originalUserData?.provider || impersonation.originalClaims?.provider
|
|
199
|
+
};
|
|
200
|
+
const standardFields = ["sub", "id", "email", "name", "picture", "provider", "iat", "exp", "iss", "aud", "impersonation"];
|
|
201
|
+
const customClaims = {};
|
|
202
|
+
if (originalUserData) {
|
|
203
|
+
for (const [key, value] of Object.entries(originalUserData)) {
|
|
204
|
+
if (!standardFields.includes(key)) {
|
|
205
|
+
customClaims[key] = value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (impersonation.originalClaims) {
|
|
209
|
+
const filteredClaims = { ...impersonation.originalClaims };
|
|
210
|
+
delete filteredClaims.provider;
|
|
211
|
+
Object.assign(customClaims, filteredClaims);
|
|
212
|
+
}
|
|
213
|
+
const accessToken = await generateToken(originalPayload, tokenConfig, customClaims);
|
|
214
|
+
const refreshTokenConfig = config.nuxtAegis?.tokenRefresh;
|
|
215
|
+
if (!refreshTokenConfig) {
|
|
216
|
+
throw createError({
|
|
217
|
+
statusCode: 500,
|
|
218
|
+
message: "Token refresh configuration is missing"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const refreshTokenId = await generateAndStoreRefreshToken(
|
|
222
|
+
originalUserData || { sub: originalPayload.sub, email: originalPayload.email, name: originalPayload.name },
|
|
223
|
+
// Store user data (fresh or fallback)
|
|
224
|
+
"restored-session",
|
|
225
|
+
// Fake provider name for restored sessions
|
|
226
|
+
refreshTokenConfig,
|
|
227
|
+
void 0,
|
|
228
|
+
// No previous token
|
|
229
|
+
event
|
|
230
|
+
);
|
|
231
|
+
if (!refreshTokenId) {
|
|
232
|
+
throw createError({
|
|
233
|
+
statusCode: 500,
|
|
234
|
+
message: "Failed to generate refresh token"
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
logger.security("Impersonation ended, original session restored", {
|
|
238
|
+
originalUser: originalPayload.sub,
|
|
239
|
+
wasImpersonating: currentToken.sub
|
|
240
|
+
});
|
|
241
|
+
const { ip, userAgent } = getClientInfo(event);
|
|
242
|
+
const endPayload = {
|
|
243
|
+
restoredUser: originalPayload,
|
|
244
|
+
// Restored original user
|
|
245
|
+
impersonatedUser: currentToken,
|
|
246
|
+
// Current impersonated user
|
|
247
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
248
|
+
ip: ip || "",
|
|
249
|
+
userAgent: userAgent || "",
|
|
250
|
+
event
|
|
251
|
+
};
|
|
252
|
+
try {
|
|
253
|
+
const nitroApp = useNitroApp();
|
|
254
|
+
await nitroApp.hooks.callHook("nuxt-aegis:impersonate:end", endPayload);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
logger.warn("Impersonation end hook failed (non-blocking)", error);
|
|
257
|
+
}
|
|
258
|
+
return { accessToken, refreshTokenId };
|
|
259
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TokenConfig, TokenPayload } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a JWT token with the given payload and custom claims
|
|
4
|
+
* @param payload - Base token payload containing user information
|
|
5
|
+
* @param config - Token configuration including secret and expiration
|
|
6
|
+
* @param customClaims - Optional custom claims to add to the token
|
|
7
|
+
* @returns Signed JWT token
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateToken(payload: TokenPayload, config: TokenConfig, customClaims?: Record<string, unknown>): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Update an existing JWT token with additional claims
|
|
12
|
+
* @param token - Existing JWT token to update
|
|
13
|
+
* @param claims - Additional claims to merge into the token
|
|
14
|
+
* @param config - Token configuration
|
|
15
|
+
* @returns New JWT token with updated claims
|
|
16
|
+
*/
|
|
17
|
+
export declare function updateTokenWithClaims(token: string, claims: Record<string, unknown>, config: TokenConfig): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Verify and decode a JWT token
|
|
20
|
+
* @param token - JWT token to verify
|
|
21
|
+
* @param secret - Secret key used to sign the token
|
|
22
|
+
* @returns Decoded token payload or null if verification fails
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyToken(token: string, secret: string, checkExpiration?: boolean): Promise<TokenPayload | null>;
|