@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.
Files changed (134) hide show
  1. package/README.md +166 -0
  2. package/dist/module.d.mts +6 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +354 -0
  5. package/dist/runtime/app/composables/useAuth.d.ts +85 -0
  6. package/dist/runtime/app/composables/useAuth.js +187 -0
  7. package/dist/runtime/app/middleware/auth-logged-in.d.ts +16 -0
  8. package/dist/runtime/app/middleware/auth-logged-in.js +25 -0
  9. package/dist/runtime/app/middleware/auth-logged-out.d.ts +20 -0
  10. package/dist/runtime/app/middleware/auth-logged-out.js +17 -0
  11. package/dist/runtime/app/pages/AuthCallback.d.vue.ts +3 -0
  12. package/dist/runtime/app/pages/AuthCallback.vue +92 -0
  13. package/dist/runtime/app/pages/AuthCallback.vue.d.ts +3 -0
  14. package/dist/runtime/app/plugins/api.client.d.ts +11 -0
  15. package/dist/runtime/app/plugins/api.client.js +92 -0
  16. package/dist/runtime/app/plugins/api.server.d.ts +13 -0
  17. package/dist/runtime/app/plugins/api.server.js +28 -0
  18. package/dist/runtime/app/plugins/ssr-state.server.d.ts +2 -0
  19. package/dist/runtime/app/plugins/ssr-state.server.js +13 -0
  20. package/dist/runtime/app/router.options.d.ts +12 -0
  21. package/dist/runtime/app/router.options.js +11 -0
  22. package/dist/runtime/app/utils/logger.d.ts +18 -0
  23. package/dist/runtime/app/utils/logger.js +48 -0
  24. package/dist/runtime/app/utils/redirectValidation.d.ts +18 -0
  25. package/dist/runtime/app/utils/redirectValidation.js +21 -0
  26. package/dist/runtime/app/utils/routeMatching.d.ts +13 -0
  27. package/dist/runtime/app/utils/routeMatching.js +10 -0
  28. package/dist/runtime/app/utils/tokenStore.d.ts +24 -0
  29. package/dist/runtime/app/utils/tokenStore.js +14 -0
  30. package/dist/runtime/app/utils/tokenUtils.d.ts +17 -0
  31. package/dist/runtime/app/utils/tokenUtils.js +4 -0
  32. package/dist/runtime/server/middleware/auth.d.ts +6 -0
  33. package/dist/runtime/server/middleware/auth.js +82 -0
  34. package/dist/runtime/server/plugins/ssr-auth.d.ts +7 -0
  35. package/dist/runtime/server/plugins/ssr-auth.js +82 -0
  36. package/dist/runtime/server/providers/auth0.d.ts +12 -0
  37. package/dist/runtime/server/providers/auth0.js +57 -0
  38. package/dist/runtime/server/providers/github.d.ts +12 -0
  39. package/dist/runtime/server/providers/github.js +44 -0
  40. package/dist/runtime/server/providers/google.d.ts +12 -0
  41. package/dist/runtime/server/providers/google.js +46 -0
  42. package/dist/runtime/server/providers/mock.d.ts +37 -0
  43. package/dist/runtime/server/providers/mock.js +129 -0
  44. package/dist/runtime/server/providers/oauthBase.d.ts +72 -0
  45. package/dist/runtime/server/providers/oauthBase.js +183 -0
  46. package/dist/runtime/server/routes/impersonate.post.d.ts +21 -0
  47. package/dist/runtime/server/routes/impersonate.post.js +68 -0
  48. package/dist/runtime/server/routes/logout.post.d.ts +9 -0
  49. package/dist/runtime/server/routes/logout.post.js +24 -0
  50. package/dist/runtime/server/routes/me.get.d.ts +6 -0
  51. package/dist/runtime/server/routes/me.get.js +11 -0
  52. package/dist/runtime/server/routes/mock/authorize.get.d.ts +29 -0
  53. package/dist/runtime/server/routes/mock/authorize.get.js +103 -0
  54. package/dist/runtime/server/routes/mock/token.post.d.ts +31 -0
  55. package/dist/runtime/server/routes/mock/token.post.js +88 -0
  56. package/dist/runtime/server/routes/mock/userinfo.get.d.ts +27 -0
  57. package/dist/runtime/server/routes/mock/userinfo.get.js +59 -0
  58. package/dist/runtime/server/routes/password/change.post.d.ts +4 -0
  59. package/dist/runtime/server/routes/password/change.post.js +108 -0
  60. package/dist/runtime/server/routes/password/login-verify.get.d.ts +2 -0
  61. package/dist/runtime/server/routes/password/login-verify.get.js +79 -0
  62. package/dist/runtime/server/routes/password/login.post.d.ts +4 -0
  63. package/dist/runtime/server/routes/password/login.post.js +66 -0
  64. package/dist/runtime/server/routes/password/register-verify.get.d.ts +2 -0
  65. package/dist/runtime/server/routes/password/register-verify.get.js +86 -0
  66. package/dist/runtime/server/routes/password/register.post.d.ts +4 -0
  67. package/dist/runtime/server/routes/password/register.post.js +87 -0
  68. package/dist/runtime/server/routes/password/reset-complete.post.d.ts +4 -0
  69. package/dist/runtime/server/routes/password/reset-complete.post.js +75 -0
  70. package/dist/runtime/server/routes/password/reset-request.post.d.ts +5 -0
  71. package/dist/runtime/server/routes/password/reset-request.post.js +52 -0
  72. package/dist/runtime/server/routes/password/reset-verify.get.d.ts +2 -0
  73. package/dist/runtime/server/routes/password/reset-verify.get.js +50 -0
  74. package/dist/runtime/server/routes/refresh.post.d.ts +8 -0
  75. package/dist/runtime/server/routes/refresh.post.js +102 -0
  76. package/dist/runtime/server/routes/token.post.d.ts +28 -0
  77. package/dist/runtime/server/routes/token.post.js +90 -0
  78. package/dist/runtime/server/routes/unimpersonate.post.d.ts +16 -0
  79. package/dist/runtime/server/routes/unimpersonate.post.js +65 -0
  80. package/dist/runtime/server/tsconfig.json +3 -0
  81. package/dist/runtime/server/utils/auth.d.ts +94 -0
  82. package/dist/runtime/server/utils/auth.js +54 -0
  83. package/dist/runtime/server/utils/authCodeStore.d.ts +137 -0
  84. package/dist/runtime/server/utils/authCodeStore.js +123 -0
  85. package/dist/runtime/server/utils/cookies.d.ts +15 -0
  86. package/dist/runtime/server/utils/cookies.js +23 -0
  87. package/dist/runtime/server/utils/customClaims.d.ts +37 -0
  88. package/dist/runtime/server/utils/customClaims.js +45 -0
  89. package/dist/runtime/server/utils/handler.d.ts +77 -0
  90. package/dist/runtime/server/utils/handler.js +7 -0
  91. package/dist/runtime/server/utils/impersonation.d.ts +48 -0
  92. package/dist/runtime/server/utils/impersonation.js +259 -0
  93. package/dist/runtime/server/utils/jwt.d.ts +24 -0
  94. package/dist/runtime/server/utils/jwt.js +77 -0
  95. package/dist/runtime/server/utils/logger.d.ts +18 -0
  96. package/dist/runtime/server/utils/logger.js +49 -0
  97. package/dist/runtime/server/utils/magicCodeStore.d.ts +27 -0
  98. package/dist/runtime/server/utils/magicCodeStore.js +66 -0
  99. package/dist/runtime/server/utils/mockCodeStore.d.ts +89 -0
  100. package/dist/runtime/server/utils/mockCodeStore.js +71 -0
  101. package/dist/runtime/server/utils/password.d.ts +33 -0
  102. package/dist/runtime/server/utils/password.js +48 -0
  103. package/dist/runtime/server/utils/refreshToken.d.ts +74 -0
  104. package/dist/runtime/server/utils/refreshToken.js +108 -0
  105. package/dist/runtime/server/utils/resetSessionStore.d.ts +12 -0
  106. package/dist/runtime/server/utils/resetSessionStore.js +29 -0
  107. package/dist/runtime/tasks/cleanup/magic-codes.d.ts +10 -0
  108. package/dist/runtime/tasks/cleanup/magic-codes.js +79 -0
  109. package/dist/runtime/tasks/cleanup/refresh-tokens.d.ts +10 -0
  110. package/dist/runtime/tasks/cleanup/refresh-tokens.js +55 -0
  111. package/dist/runtime/tasks/cleanup/reset-sessions.d.ts +8 -0
  112. package/dist/runtime/tasks/cleanup/reset-sessions.js +45 -0
  113. package/dist/runtime/types/augmentation.d.ts +73 -0
  114. package/dist/runtime/types/augmentation.js +0 -0
  115. package/dist/runtime/types/authCode.d.ts +60 -0
  116. package/dist/runtime/types/authCode.js +0 -0
  117. package/dist/runtime/types/callbacks.d.ts +54 -0
  118. package/dist/runtime/types/callbacks.js +0 -0
  119. package/dist/runtime/types/config.d.ts +129 -0
  120. package/dist/runtime/types/config.js +0 -0
  121. package/dist/runtime/types/hooks.d.ts +118 -0
  122. package/dist/runtime/types/hooks.js +0 -0
  123. package/dist/runtime/types/index.d.ts +13 -0
  124. package/dist/runtime/types/index.js +1 -0
  125. package/dist/runtime/types/providers.d.ts +212 -0
  126. package/dist/runtime/types/providers.js +0 -0
  127. package/dist/runtime/types/refresh.d.ts +61 -0
  128. package/dist/runtime/types/refresh.js +0 -0
  129. package/dist/runtime/types/routes.d.ts +30 -0
  130. package/dist/runtime/types/routes.js +0 -0
  131. package/dist/runtime/types/token.d.ts +182 -0
  132. package/dist/runtime/types/token.js +0 -0
  133. package/dist/types.d.mts +7 -0
  134. 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,7 @@
1
+ let _handler = null;
2
+ export const defineAegisHandler = (handler) => {
3
+ _handler = handler;
4
+ };
5
+ export const useAegisHandler = () => {
6
+ return _handler;
7
+ };
@@ -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>;