@peterbud/nuxt-aegis 1.1.0-alpha.1 → 1.1.0-alpha.2
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 +43 -1
- package/dist/runtime/app/composables/useAuth.d.ts +7 -7
- package/dist/runtime/app/utils/tokenUtils.d.ts +2 -2
- package/dist/runtime/app/utils/tokenUtils.js +1 -1
- package/dist/runtime/server/plugins/ssr-auth.js +1 -9
- package/dist/runtime/server/providers/oauthBase.js +13 -3
- package/dist/runtime/server/routes/me.get.d.ts +1 -1
- package/dist/runtime/server/routes/password/change.post.js +17 -4
- package/dist/runtime/server/routes/password/register-verify.get.js +15 -7
- package/dist/runtime/server/routes/password/reset-complete.post.js +17 -4
- package/dist/runtime/server/routes/refresh.post.js +3 -9
- package/dist/runtime/server/utils/auth.d.ts +9 -9
- package/dist/runtime/server/utils/auth.js +2 -0
- package/dist/runtime/server/utils/handler.d.ts +71 -8
- package/dist/runtime/server/utils/impersonation.d.ts +6 -6
- package/dist/runtime/server/utils/impersonation.js +4 -1
- package/dist/runtime/server/utils/jwt.d.ts +3 -3
- package/dist/runtime/server/utils/refreshToken.d.ts +2 -1
- package/dist/runtime/server/utils/refreshToken.js +4 -2
- package/dist/runtime/types/augmentation.d.ts +2 -2
- package/dist/runtime/types/hooks.d.ts +7 -7
- package/dist/runtime/types/index.d.ts +1 -1
- package/dist/runtime/types/token.d.ts +36 -20
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -316,7 +316,15 @@ const module$1 = defineNuxtModule({
|
|
|
316
316
|
addTypeTemplate({
|
|
317
317
|
filename: "types/nuxt-aegis-nitro.d.ts",
|
|
318
318
|
getContents: () => {
|
|
319
|
-
return `import type {
|
|
319
|
+
return `import type {
|
|
320
|
+
NitroAegisAuth,
|
|
321
|
+
UserInfoHookPayload,
|
|
322
|
+
SuccessHookPayload,
|
|
323
|
+
ImpersonateCheckPayload,
|
|
324
|
+
ImpersonateFetchTargetPayload,
|
|
325
|
+
ImpersonateStartPayload,
|
|
326
|
+
ImpersonateEndPayload
|
|
327
|
+
} from ${JSON.stringify(typesPath)}
|
|
320
328
|
|
|
321
329
|
type NuxtAegisRouteRules = {
|
|
322
330
|
/**
|
|
@@ -335,6 +343,36 @@ declare module 'nitropack/types' {
|
|
|
335
343
|
interface NitroRouteConfig {
|
|
336
344
|
nuxtAegis?: NuxtAegisRouteRules
|
|
337
345
|
}
|
|
346
|
+
interface NitroRuntimeHooks {
|
|
347
|
+
/**
|
|
348
|
+
* Hook called after fetching user info from the provider, before storing it.
|
|
349
|
+
* Use this to transform or validate the OAuth provider response.
|
|
350
|
+
*/
|
|
351
|
+
'nuxt-aegis:userInfo': (payload: UserInfoHookPayload) => Promise<void> | void
|
|
352
|
+
/**
|
|
353
|
+
* Hook called after successful authentication.
|
|
354
|
+
* Use this for logging, analytics, or database operations.
|
|
355
|
+
*/
|
|
356
|
+
'nuxt-aegis:success': (payload: SuccessHookPayload) => Promise<void> | void
|
|
357
|
+
/**
|
|
358
|
+
* Hook called to determine if a user is allowed to impersonate others.
|
|
359
|
+
* Return true to allow, false to deny.
|
|
360
|
+
*/
|
|
361
|
+
'nuxt-aegis:impersonate:check': (payload: ImpersonateCheckPayload) => Promise<boolean> | boolean
|
|
362
|
+
/**
|
|
363
|
+
* Hook called to fetch the target user's data for impersonation.
|
|
364
|
+
* Must return the target user's data or null if not found.
|
|
365
|
+
*/
|
|
366
|
+
'nuxt-aegis:impersonate:fetchTarget': (payload: ImpersonateFetchTargetPayload) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null
|
|
367
|
+
/**
|
|
368
|
+
* Hook called after impersonation starts successfully (fire-and-forget for audit logging).
|
|
369
|
+
*/
|
|
370
|
+
'nuxt-aegis:impersonate:start': (payload: ImpersonateStartPayload) => Promise<void> | void
|
|
371
|
+
/**
|
|
372
|
+
* Hook called after impersonation ends successfully (fire-and-forget for audit logging).
|
|
373
|
+
*/
|
|
374
|
+
'nuxt-aegis:impersonate:end': (payload: ImpersonateEndPayload) => Promise<void> | void
|
|
375
|
+
}
|
|
338
376
|
}
|
|
339
377
|
|
|
340
378
|
declare module 'nitropack' {
|
|
@@ -348,6 +386,10 @@ declare module 'nitropack' {
|
|
|
348
386
|
|
|
349
387
|
export {}`;
|
|
350
388
|
}
|
|
389
|
+
}, {
|
|
390
|
+
nuxt: true,
|
|
391
|
+
nitro: true,
|
|
392
|
+
node: true
|
|
351
393
|
});
|
|
352
394
|
}
|
|
353
395
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ComputedRef } from 'vue';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BaseTokenClaims } from '../../types/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Return type for the useAuth composable
|
|
5
|
-
* @template T - Token payload type extending
|
|
5
|
+
* @template T - Token payload type extending BaseTokenClaims (defaults to BaseTokenClaims)
|
|
6
6
|
*/
|
|
7
|
-
interface UseAuthReturn<T extends
|
|
7
|
+
interface UseAuthReturn<T extends BaseTokenClaims = BaseTokenClaims> {
|
|
8
8
|
/** Reactive property indicating whether a user is logged in */
|
|
9
9
|
isLoggedIn: ComputedRef<boolean>;
|
|
10
10
|
/** Reactive property indicating the authentication state is being initialized */
|
|
@@ -60,7 +60,7 @@ interface UseAuthReturn<T extends TokenPayload = TokenPayload> {
|
|
|
60
60
|
* - logout() - End user session
|
|
61
61
|
* - refresh() - Restore authentication state
|
|
62
62
|
*
|
|
63
|
-
* @template T - Custom token payload type extending
|
|
63
|
+
* @template T - Custom token payload type extending BaseTokenClaims
|
|
64
64
|
* @returns {UseAuthReturn<T>} Authentication state and methods
|
|
65
65
|
*
|
|
66
66
|
* @example
|
|
@@ -71,15 +71,15 @@ interface UseAuthReturn<T extends TokenPayload = TokenPayload> {
|
|
|
71
71
|
* // With custom claims
|
|
72
72
|
* import type { CustomTokenClaims } from '#nuxt-aegis'
|
|
73
73
|
*
|
|
74
|
-
* type
|
|
74
|
+
* type AppTokenClaims = CustomTokenClaims<{
|
|
75
75
|
* role: string
|
|
76
76
|
* permissions: string[]
|
|
77
77
|
* organizationId: string
|
|
78
78
|
* }>
|
|
79
79
|
*
|
|
80
|
-
* const { user, login, logout } = useAuth<
|
|
80
|
+
* const { user, login, logout } = useAuth<AppTokenClaims>()
|
|
81
81
|
* // user.value?.role is now type-safe
|
|
82
82
|
* ```
|
|
83
83
|
*/
|
|
84
|
-
export declare function useAuth<T extends
|
|
84
|
+
export declare function useAuth<T extends BaseTokenClaims = BaseTokenClaims>(): UseAuthReturn<T>;
|
|
85
85
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BaseTokenClaims } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Filter out time-sensitive JWT metadata claims that cause hydration mismatches
|
|
4
4
|
*
|
|
@@ -14,4 +14,4 @@ import type { TokenPayload } from '../../types/index.js';
|
|
|
14
14
|
* @param user - Token payload with all claims
|
|
15
15
|
* @returns Token payload with only stable user data
|
|
16
16
|
*/
|
|
17
|
-
export declare function filterTimeSensitiveClaims(user:
|
|
17
|
+
export declare function filterTimeSensitiveClaims(user: BaseTokenClaims): BaseTokenClaims;
|
|
@@ -2,7 +2,6 @@ import { defineNitroPlugin } from "nitropack/runtime";
|
|
|
2
2
|
import { getCookie } from "h3";
|
|
3
3
|
import { hashRefreshToken, getRefreshTokenData } from "../utils/refreshToken.js";
|
|
4
4
|
import { generateToken } from "../utils/jwt.js";
|
|
5
|
-
import { processCustomClaims } from "../utils/customClaims.js";
|
|
6
5
|
import { useRuntimeConfig } from "#imports";
|
|
7
6
|
import { createLogger } from "../utils/logger.js";
|
|
8
7
|
const logger = createLogger("SSR:Auth");
|
|
@@ -54,14 +53,7 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
54
53
|
picture: providerUserInfo.picture,
|
|
55
54
|
provider
|
|
56
55
|
};
|
|
57
|
-
|
|
58
|
-
const providerConfig = config.nuxtAegis?.providers?.[provider];
|
|
59
|
-
if (providerConfig && "customClaims" in providerConfig && providerConfig.customClaims) {
|
|
60
|
-
customClaims = await processCustomClaims(
|
|
61
|
-
providerUserInfo,
|
|
62
|
-
providerConfig.customClaims
|
|
63
|
-
);
|
|
64
|
-
}
|
|
56
|
+
const customClaims = storedRefreshToken.customClaims || {};
|
|
65
57
|
const ssrTokenExpiry = tokenRefreshConfig?.ssrTokenExpiry || "5m";
|
|
66
58
|
const ssrAccessToken = await generateToken(
|
|
67
59
|
userPayload,
|
|
@@ -95,20 +95,28 @@ export function defineOAuthEventHandler(implementation, {
|
|
|
95
95
|
if (_onUserInfo) {
|
|
96
96
|
providerUserInfo = await _onUserInfo(providerUserInfo, tokens, event);
|
|
97
97
|
} else {
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
98
|
+
const handler2 = useAegisHandler();
|
|
99
|
+
if (handler2?.onUserInfo) {
|
|
100
100
|
const hookPayload = {
|
|
101
101
|
providerUserInfo,
|
|
102
102
|
tokens,
|
|
103
103
|
provider: implementation.runtimeConfigKey,
|
|
104
104
|
event
|
|
105
105
|
};
|
|
106
|
-
const transformedUser = await
|
|
106
|
+
const transformedUser = await handler2.onUserInfo(hookPayload);
|
|
107
107
|
if (transformedUser) {
|
|
108
108
|
providerUserInfo = transformedUser;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
const handler = useAegisHandler();
|
|
113
|
+
if (handler?.onUserPersist) {
|
|
114
|
+
const enrichedData = await handler.onUserPersist(providerUserInfo, {
|
|
115
|
+
provider: implementation.runtimeConfigKey,
|
|
116
|
+
event
|
|
117
|
+
});
|
|
118
|
+
providerUserInfo = { ...providerUserInfo, ...enrichedData };
|
|
119
|
+
}
|
|
112
120
|
let resolvedCustomClaims;
|
|
113
121
|
if (_customClaims) {
|
|
114
122
|
if (typeof _customClaims === "function") {
|
|
@@ -116,6 +124,8 @@ export function defineOAuthEventHandler(implementation, {
|
|
|
116
124
|
} else {
|
|
117
125
|
resolvedCustomClaims = _customClaims;
|
|
118
126
|
}
|
|
127
|
+
} else if (handler?.customClaims) {
|
|
128
|
+
resolvedCustomClaims = await handler.customClaims(providerUserInfo);
|
|
119
129
|
}
|
|
120
130
|
if (_onSuccess) {
|
|
121
131
|
await _onSuccess({
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* GET /api/user/me
|
|
3
3
|
* Returns the current authenticated user's information
|
|
4
4
|
*/
|
|
5
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../types/index.js").
|
|
5
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../types/index.js").BaseTokenClaims>>;
|
|
6
6
|
export default _default;
|
|
@@ -93,10 +93,23 @@ export default defineEventHandler(async (event) => {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
const newHashedPassword = handler.password.hashPassword ? await handler.password.hashPassword(newPassword) : await hashPassword(newPassword);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
if (handler.onUserPersist) {
|
|
97
|
+
await handler.onUserPersist(
|
|
98
|
+
{
|
|
99
|
+
...user,
|
|
100
|
+
hashedPassword: newHashedPassword
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
provider: "password",
|
|
104
|
+
event
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
throw createError({
|
|
109
|
+
statusCode: 500,
|
|
110
|
+
message: "onUserPersist handler is required for password authentication"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
100
113
|
const refreshCookieName = config.nuxtAegis?.tokenRefresh?.cookie?.cookieName || "nuxt-aegis-refresh";
|
|
101
114
|
const currentRefreshToken = getCookie(event, refreshCookieName);
|
|
102
115
|
let currentTokenHash;
|
|
@@ -50,19 +50,27 @@ export default defineEventHandler(async (event) => {
|
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
try {
|
|
53
|
-
|
|
53
|
+
let userData = {
|
|
54
54
|
email,
|
|
55
55
|
hashedPassword
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
};
|
|
57
|
+
if (handler.onUserPersist) {
|
|
58
|
+
const enrichedData = await handler.onUserPersist(userData, {
|
|
59
|
+
provider: "password",
|
|
60
|
+
event
|
|
61
|
+
});
|
|
62
|
+
userData = { ...userData, ...enrichedData };
|
|
63
|
+
} else {
|
|
64
|
+
throw createError({
|
|
65
|
+
statusCode: 500,
|
|
66
|
+
message: "onUserPersist handler is required for password authentication"
|
|
67
|
+
});
|
|
60
68
|
}
|
|
61
69
|
await retrieveAndDeleteMagicCode(code);
|
|
62
70
|
const providerUserInfo = {
|
|
63
|
-
sub:
|
|
71
|
+
sub: userData.id || userData.email,
|
|
64
72
|
provider: "password",
|
|
65
|
-
...
|
|
73
|
+
...userData
|
|
66
74
|
};
|
|
67
75
|
const authCode = generateAuthCode();
|
|
68
76
|
await storeAuthCode(
|
|
@@ -60,10 +60,23 @@ export default defineEventHandler(async (event) => {
|
|
|
60
60
|
message: "User not found"
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
if (handler.onUserPersist) {
|
|
64
|
+
await handler.onUserPersist(
|
|
65
|
+
{
|
|
66
|
+
...user,
|
|
67
|
+
hashedPassword
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
provider: "password",
|
|
71
|
+
event
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
throw createError({
|
|
76
|
+
statusCode: 500,
|
|
77
|
+
message: "onUserPersist handler is required for password authentication"
|
|
78
|
+
});
|
|
79
|
+
}
|
|
67
80
|
return { success: true };
|
|
68
81
|
} catch (error) {
|
|
69
82
|
logger.error("Password reset failed", error);
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
revokeRefreshToken
|
|
8
8
|
} from "../utils/refreshToken.js";
|
|
9
9
|
import { setRefreshTokenCookie } from "../utils/cookies.js";
|
|
10
|
-
import { processCustomClaims } from "../utils/customClaims.js";
|
|
11
10
|
import { useRuntimeConfig } from "#imports";
|
|
12
11
|
export default defineEventHandler(async (event) => {
|
|
13
12
|
const config = useRuntimeConfig(event);
|
|
@@ -56,14 +55,7 @@ export default defineEventHandler(async (event) => {
|
|
|
56
55
|
}
|
|
57
56
|
const providerUserInfo = storedRefreshToken.providerUserInfo;
|
|
58
57
|
const provider = storedRefreshToken.provider;
|
|
59
|
-
|
|
60
|
-
const providerConfig = config.nuxtAegis?.providers?.[provider];
|
|
61
|
-
if (providerConfig && "customClaims" in providerConfig) {
|
|
62
|
-
const customClaimsConfig = providerConfig.customClaims;
|
|
63
|
-
if (customClaimsConfig) {
|
|
64
|
-
customClaims = await processCustomClaims(providerUserInfo, customClaimsConfig);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
58
|
+
const customClaims = storedRefreshToken.customClaims || {};
|
|
67
59
|
const payload = {
|
|
68
60
|
sub: String(providerUserInfo.sub || providerUserInfo.email || providerUserInfo.id || ""),
|
|
69
61
|
email: providerUserInfo.email,
|
|
@@ -81,6 +73,8 @@ export default defineEventHandler(async (event) => {
|
|
|
81
73
|
tokenRefreshConfig,
|
|
82
74
|
hashedRefreshToken,
|
|
83
75
|
// Pass previous token hash for rotation tracking
|
|
76
|
+
customClaims,
|
|
77
|
+
// Preserve custom claims in new refresh token
|
|
84
78
|
event
|
|
85
79
|
);
|
|
86
80
|
if (newRefreshToken) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BaseTokenClaims } from '../../types/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Generate authentication tokens from user data with optional custom claims
|
|
5
5
|
* This is the recommended way to generate tokens after successful OAuth authentication
|
|
@@ -42,19 +42,19 @@ export declare function generateAuthTokens(event: H3Event, providerUserInfo: Rec
|
|
|
42
42
|
* @example
|
|
43
43
|
* ```typescript
|
|
44
44
|
* // With custom claims typing
|
|
45
|
-
*
|
|
45
|
+
* type AppTokenClaims = CustomTokenClaims<{
|
|
46
46
|
* role: string
|
|
47
47
|
* permissions: string[]
|
|
48
|
-
* }
|
|
48
|
+
* }>
|
|
49
49
|
*
|
|
50
50
|
* export default defineEventHandler((event) => {
|
|
51
|
-
* const authedEvent = requireAuth<
|
|
51
|
+
* const authedEvent = requireAuth<AppTokenClaims>(event)
|
|
52
52
|
* // TypeScript knows about role and permissions
|
|
53
53
|
* return { role: authedEvent.context.user.role }
|
|
54
54
|
* })
|
|
55
55
|
* ```
|
|
56
56
|
*/
|
|
57
|
-
export declare function requireAuth<T extends
|
|
57
|
+
export declare function requireAuth<T extends BaseTokenClaims = BaseTokenClaims>(event: H3Event): H3Event & {
|
|
58
58
|
context: {
|
|
59
59
|
user: T;
|
|
60
60
|
};
|
|
@@ -79,16 +79,16 @@ export declare function requireAuth<T extends TokenPayload = TokenPayload>(event
|
|
|
79
79
|
* @example
|
|
80
80
|
* ```typescript
|
|
81
81
|
* // With custom claims typing
|
|
82
|
-
*
|
|
82
|
+
* export type AppTokenClaims = CustomTokenClaims<{
|
|
83
83
|
* role: string
|
|
84
84
|
* permissions: string[]
|
|
85
|
-
* }
|
|
85
|
+
* }>
|
|
86
86
|
*
|
|
87
87
|
* export default defineEventHandler((event) => {
|
|
88
|
-
* const user = getAuthUser<
|
|
88
|
+
* const user = getAuthUser<AppTokenClaims>(event)
|
|
89
89
|
* // TypeScript knows about role and permissions
|
|
90
90
|
* return { role: user.role, permissions: user.permissions }
|
|
91
91
|
* })
|
|
92
92
|
* ```
|
|
93
93
|
*/
|
|
94
|
-
export declare function getAuthUser<T extends
|
|
94
|
+
export declare function getAuthUser<T extends BaseTokenClaims = BaseTokenClaims>(event: H3Event): T | null;
|
|
@@ -25,6 +25,8 @@ export async function generateAuthTokens(event, providerUserInfo, provider, cust
|
|
|
25
25
|
tokenRefreshConfig,
|
|
26
26
|
void 0,
|
|
27
27
|
// No previous token hash for initial auth
|
|
28
|
+
customClaims,
|
|
29
|
+
// Store resolved custom claims for consistent refresh
|
|
28
30
|
event
|
|
29
31
|
);
|
|
30
32
|
return {
|
|
@@ -1,14 +1,82 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
2
|
import type { UserInfoHookPayload } from '../../types/hooks.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { BaseTokenClaims } from '../../types/token.js';
|
|
4
4
|
import type { PasswordUser } from '../../types/providers.js';
|
|
5
|
+
/**
|
|
6
|
+
* Context passed to onUserPersist handler
|
|
7
|
+
*/
|
|
8
|
+
export interface UserPersistContext {
|
|
9
|
+
/** Provider name (e.g., 'google', 'github', 'password') */
|
|
10
|
+
provider: string;
|
|
11
|
+
/** H3 event for server context access */
|
|
12
|
+
event: H3Event;
|
|
13
|
+
}
|
|
5
14
|
export interface AegisHandler {
|
|
6
15
|
/**
|
|
7
16
|
* Transform user data after fetching from OAuth provider.
|
|
8
|
-
* Replaces `nuxt-aegis:userInfo` hook.
|
|
9
17
|
* Return the modified user object to use it.
|
|
10
18
|
*/
|
|
11
19
|
onUserInfo?: (payload: UserInfoHookPayload) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Persist user data to your database and return enriched user information.
|
|
22
|
+
*
|
|
23
|
+
* Called for:
|
|
24
|
+
* - OAuth authentication: After provider data transformation, before JWT generation
|
|
25
|
+
* - Password authentication: After registration, password change, or reset
|
|
26
|
+
*
|
|
27
|
+
* The returned object is merged into the user data and used for JWT claims.
|
|
28
|
+
*
|
|
29
|
+
* @param user - User data to persist (provider-specific fields vary)
|
|
30
|
+
* @param context - Context with provider name and H3 event
|
|
31
|
+
* @returns Enriched user object with database fields (e.g., userId, role, permissions)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* onUserPersist: async (user, { provider, event }) => {
|
|
36
|
+
* // For password provider, user includes hashedPassword
|
|
37
|
+
* if (provider === 'password') {
|
|
38
|
+
* const dbUser = await db.users.upsert({
|
|
39
|
+
* where: { email: user.email },
|
|
40
|
+
* update: { hashedPassword: user.hashedPassword },
|
|
41
|
+
* create: { email: user.email, hashedPassword: user.hashedPassword },
|
|
42
|
+
* })
|
|
43
|
+
* return { userId: dbUser.id, role: dbUser.role }
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* // For OAuth providers, link or create user
|
|
47
|
+
* const dbUser = await db.users.upsert({
|
|
48
|
+
* where: { email: user.email },
|
|
49
|
+
* update: { lastLogin: new Date() },
|
|
50
|
+
* create: { email: user.email, name: user.name, picture: user.picture },
|
|
51
|
+
* })
|
|
52
|
+
* return { userId: dbUser.id, role: dbUser.role, permissions: dbUser.permissions }
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
onUserPersist?: (user: Record<string, unknown>, context: UserPersistContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Generate custom claims for JWT tokens.
|
|
59
|
+
*
|
|
60
|
+
* Called after onUserPersist, receives the merged user data.
|
|
61
|
+
* This is the recommended location for adding database-driven claims.
|
|
62
|
+
*
|
|
63
|
+
* Provider-level customClaims (defined in route handlers) take precedence over this.
|
|
64
|
+
*
|
|
65
|
+
* @param user - Complete user object including data from onUserPersist
|
|
66
|
+
* @returns Custom claims to add to the JWT
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* customClaims: async (user) => {
|
|
71
|
+
* return {
|
|
72
|
+
* role: user.role,
|
|
73
|
+
* permissions: user.permissions,
|
|
74
|
+
* organizationId: user.organizationId,
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
customClaims?: (user: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
12
80
|
/**
|
|
13
81
|
* Password authentication handler.
|
|
14
82
|
* Required if password provider is enabled.
|
|
@@ -20,11 +88,6 @@ export interface AegisHandler {
|
|
|
20
88
|
* Return null if user is not found.
|
|
21
89
|
*/
|
|
22
90
|
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
91
|
/**
|
|
29
92
|
* Send a verification code to the user.
|
|
30
93
|
* Called during registration, login, and password reset.
|
|
@@ -63,7 +126,7 @@ export interface AegisHandler {
|
|
|
63
126
|
* If not defined, defaults to allowing if fetchTarget returns a user.
|
|
64
127
|
* You can throw an error here to provide a specific message.
|
|
65
128
|
*/
|
|
66
|
-
canImpersonate?: (requester:
|
|
129
|
+
canImpersonate?: (requester: BaseTokenClaims, targetId: string, event: H3Event) => Promise<boolean> | boolean;
|
|
67
130
|
};
|
|
68
131
|
}
|
|
69
132
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BaseTokenClaims } from '../../types/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Check if the requester is allowed to impersonate other users
|
|
5
5
|
* @param requester - The user requesting impersonation
|
|
@@ -7,7 +7,7 @@ import type { TokenPayload } from '../../types/index.js';
|
|
|
7
7
|
* @param event - H3 event for context
|
|
8
8
|
* @throws 403 error if impersonation is not allowed
|
|
9
9
|
*/
|
|
10
|
-
export declare function checkImpersonationAllowed(requester:
|
|
10
|
+
export declare function checkImpersonationAllowed(requester: BaseTokenClaims, targetUserId: string, event: H3Event): Promise<void>;
|
|
11
11
|
/**
|
|
12
12
|
* Fetch target user data for impersonation
|
|
13
13
|
* @param requester - The user requesting impersonation
|
|
@@ -17,7 +17,7 @@ export declare function checkImpersonationAllowed(requester: TokenPayload, targe
|
|
|
17
17
|
* @throws 404 error if user not found
|
|
18
18
|
* @throws 500 error if hook is not implemented
|
|
19
19
|
*/
|
|
20
|
-
export declare function fetchTargetUser(requester:
|
|
20
|
+
export declare function fetchTargetUser(requester: BaseTokenClaims, targetUserId: string, event: H3Event): Promise<Record<string, unknown>>;
|
|
21
21
|
/**
|
|
22
22
|
* Generate an impersonated JWT token
|
|
23
23
|
* @param requester - The user performing impersonation
|
|
@@ -26,7 +26,7 @@ export declare function fetchTargetUser(requester: TokenPayload, targetUserId: s
|
|
|
26
26
|
* @param _event - H3 event for context
|
|
27
27
|
* @returns JWT access token (no refresh token)
|
|
28
28
|
*/
|
|
29
|
-
export declare function generateImpersonatedToken(requester:
|
|
29
|
+
export declare function generateImpersonatedToken(requester: BaseTokenClaims, targetUserData: Record<string, unknown>, reason: string | undefined, _event: H3Event): Promise<string>;
|
|
30
30
|
/**
|
|
31
31
|
* Start impersonation session
|
|
32
32
|
* @param requester - The user requesting impersonation (must be admin)
|
|
@@ -35,14 +35,14 @@ export declare function generateImpersonatedToken(requester: TokenPayload, targe
|
|
|
35
35
|
* @param event - H3 event for context
|
|
36
36
|
* @returns Access token for impersonated session (no refresh token)
|
|
37
37
|
*/
|
|
38
|
-
export declare function startImpersonation(requester:
|
|
38
|
+
export declare function startImpersonation(requester: BaseTokenClaims, targetUserId: string, reason: string | undefined, event: H3Event): Promise<string>;
|
|
39
39
|
/**
|
|
40
40
|
* End impersonation and restore original user session
|
|
41
41
|
* @param currentToken - Current JWT token (must contain impersonation context)
|
|
42
42
|
* @param event - H3 event for context
|
|
43
43
|
* @returns Object with new access token and refresh token ID
|
|
44
44
|
*/
|
|
45
|
-
export declare function endImpersonation(currentToken:
|
|
45
|
+
export declare function endImpersonation(currentToken: BaseTokenClaims, event: H3Event): Promise<{
|
|
46
46
|
accessToken: string;
|
|
47
47
|
refreshTokenId: string;
|
|
48
48
|
}>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createError } from "h3";
|
|
2
2
|
import { generateToken } from "./jwt.js";
|
|
3
|
-
import { useRuntimeConfig
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { useNitroApp } from "nitropack/runtime";
|
|
4
5
|
import { createLogger } from "./logger.js";
|
|
5
6
|
import { generateAndStoreRefreshToken } from "./refreshToken.js";
|
|
6
7
|
import { useAegisHandler } from "./handler.js";
|
|
@@ -226,6 +227,8 @@ export async function endImpersonation(currentToken, event) {
|
|
|
226
227
|
refreshTokenConfig,
|
|
227
228
|
void 0,
|
|
228
229
|
// No previous token
|
|
230
|
+
customClaims,
|
|
231
|
+
// Store custom claims for restored session
|
|
229
232
|
event
|
|
230
233
|
);
|
|
231
234
|
if (!refreshTokenId) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TokenConfig,
|
|
1
|
+
import type { TokenConfig, BaseTokenClaims } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Generate a JWT token with the given payload and custom claims
|
|
4
4
|
* @param payload - Base token payload containing user information
|
|
@@ -6,7 +6,7 @@ import type { TokenConfig, TokenPayload } from '../../types/index.js';
|
|
|
6
6
|
* @param customClaims - Optional custom claims to add to the token
|
|
7
7
|
* @returns Signed JWT token
|
|
8
8
|
*/
|
|
9
|
-
export declare function generateToken(payload:
|
|
9
|
+
export declare function generateToken(payload: BaseTokenClaims, config: TokenConfig, customClaims?: Record<string, unknown>): Promise<string>;
|
|
10
10
|
/**
|
|
11
11
|
* Update an existing JWT token with additional claims
|
|
12
12
|
* @param token - Existing JWT token to update
|
|
@@ -21,4 +21,4 @@ export declare function updateTokenWithClaims(token: string, claims: Record<stri
|
|
|
21
21
|
* @param secret - Secret key used to sign the token
|
|
22
22
|
* @returns Decoded token payload or null if verification fails
|
|
23
23
|
*/
|
|
24
|
-
export declare function verifyToken(token: string, secret: string, checkExpiration?: boolean): Promise<
|
|
24
|
+
export declare function verifyToken(token: string, secret: string, checkExpiration?: boolean): Promise<BaseTokenClaims | null>;
|
|
@@ -60,10 +60,11 @@ export declare function revokeRefreshToken(tokenHash: string, event?: H3Event):
|
|
|
60
60
|
* @param provider - Provider name (e.g., 'google', 'github', 'microsoft', 'auth0')
|
|
61
61
|
* @param config - Token refresh configuration
|
|
62
62
|
* @param previousTokenHash - Hash of previous refresh token for rotation tracking
|
|
63
|
+
* @param customClaims - Resolved custom claims from initial authentication
|
|
63
64
|
* @param event - H3Event for Nitro storage access
|
|
64
65
|
* @returns The generated refresh token string
|
|
65
66
|
*/
|
|
66
|
-
export declare function generateAndStoreRefreshToken(providerUserInfo: Record<string, unknown>, provider: string, config: TokenRefreshConfig, previousTokenHash?: string, event?: H3Event): Promise<string | undefined>;
|
|
67
|
+
export declare function generateAndStoreRefreshToken(providerUserInfo: Record<string, unknown>, provider: string, config: TokenRefreshConfig, previousTokenHash?: string, customClaims?: Record<string, unknown>, event?: H3Event): Promise<string | undefined>;
|
|
67
68
|
/**
|
|
68
69
|
* Delete all refresh tokens for a specific user
|
|
69
70
|
* Used during password change or account deletion
|
|
@@ -77,7 +77,7 @@ export async function revokeRefreshToken(tokenHash, event) {
|
|
|
77
77
|
data.isRevoked = true;
|
|
78
78
|
await storeRefreshTokenData(tokenHash, data, event);
|
|
79
79
|
}
|
|
80
|
-
export async function generateAndStoreRefreshToken(providerUserInfo, provider, config, previousTokenHash, event) {
|
|
80
|
+
export async function generateAndStoreRefreshToken(providerUserInfo, provider, config, previousTokenHash, customClaims, event) {
|
|
81
81
|
const refreshToken = randomBytes(32).toString("base64url");
|
|
82
82
|
const expiresIn = config.cookie?.maxAge || 604800;
|
|
83
83
|
await storeRefreshTokenData(hashRefreshToken(refreshToken), {
|
|
@@ -87,8 +87,10 @@ export async function generateAndStoreRefreshToken(providerUserInfo, provider, c
|
|
|
87
87
|
previousTokenHash,
|
|
88
88
|
providerUserInfo,
|
|
89
89
|
// Store complete OAuth provider user data
|
|
90
|
-
provider
|
|
90
|
+
provider,
|
|
91
91
|
// Store provider name for custom claims refresh
|
|
92
|
+
customClaims
|
|
93
|
+
// Store resolved custom claims for consistent refresh
|
|
92
94
|
}, event);
|
|
93
95
|
return refreshToken;
|
|
94
96
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NuxtAegisRuntimeConfig, RedirectConfig, LoggingConfig } from './config.js';
|
|
2
2
|
import type { TokenRefreshConfig } from './refresh.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { BaseTokenClaims } from './token.js';
|
|
4
4
|
import type { ClientMiddlewareConfig } from './routes.js';
|
|
5
5
|
import type { SuccessHookPayload } from './hooks.js';
|
|
6
6
|
/**
|
|
@@ -41,7 +41,7 @@ declare module 'h3' {
|
|
|
41
41
|
* Authenticated user data from JWT token
|
|
42
42
|
* Available when request is authenticated via the auth middleware
|
|
43
43
|
*/
|
|
44
|
-
user?:
|
|
44
|
+
user?: BaseTokenClaims;
|
|
45
45
|
/**
|
|
46
46
|
* Original user data before impersonation
|
|
47
47
|
* Available when impersonation is active
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BaseTokenClaims } from './token.js';
|
|
3
3
|
/**
|
|
4
4
|
* Nitro hook type definitions for Nuxt Aegis
|
|
5
5
|
* These hooks allow users to customize authentication behavior via server plugins
|
|
@@ -52,7 +52,7 @@ export interface SuccessHookPayload {
|
|
|
52
52
|
*/
|
|
53
53
|
export interface ImpersonateCheckPayload {
|
|
54
54
|
/** JWT payload of the user requesting impersonation */
|
|
55
|
-
requester:
|
|
55
|
+
requester: BaseTokenClaims;
|
|
56
56
|
/** Target user ID to impersonate */
|
|
57
57
|
targetUserId: string;
|
|
58
58
|
/** Optional reason for impersonation (for audit) */
|
|
@@ -72,7 +72,7 @@ export interface ImpersonateCheckPayload {
|
|
|
72
72
|
*/
|
|
73
73
|
export interface ImpersonateFetchTargetPayload {
|
|
74
74
|
/** JWT payload of the user requesting impersonation */
|
|
75
|
-
requester:
|
|
75
|
+
requester: BaseTokenClaims;
|
|
76
76
|
/** Target user ID to impersonate */
|
|
77
77
|
targetUserId: string;
|
|
78
78
|
/** H3 event for server context access */
|
|
@@ -84,9 +84,9 @@ export interface ImpersonateFetchTargetPayload {
|
|
|
84
84
|
*/
|
|
85
85
|
export interface ImpersonateStartPayload {
|
|
86
86
|
/** JWT payload of the user who initiated impersonation */
|
|
87
|
-
requester:
|
|
87
|
+
requester: BaseTokenClaims;
|
|
88
88
|
/** JWT payload of the impersonated user */
|
|
89
|
-
targetUser:
|
|
89
|
+
targetUser: BaseTokenClaims;
|
|
90
90
|
/** Reason for impersonation */
|
|
91
91
|
reason?: string;
|
|
92
92
|
/** H3 event for server context access */
|
|
@@ -104,9 +104,9 @@ export interface ImpersonateStartPayload {
|
|
|
104
104
|
*/
|
|
105
105
|
export interface ImpersonateEndPayload {
|
|
106
106
|
/** JWT payload of the restored original user */
|
|
107
|
-
restoredUser:
|
|
107
|
+
restoredUser: BaseTokenClaims;
|
|
108
108
|
/** JWT payload of the user who was being impersonated */
|
|
109
|
-
impersonatedUser:
|
|
109
|
+
impersonatedUser: BaseTokenClaims;
|
|
110
110
|
/** H3 event for server context access */
|
|
111
111
|
event: H3Event;
|
|
112
112
|
/** Client IP address (for audit) */
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Main barrel file for all type exports
|
|
4
4
|
*/
|
|
5
5
|
import './augmentations.js';
|
|
6
|
-
export type {
|
|
6
|
+
export type { BaseTokenClaims, TokenConfig, RefreshTokenData, RefreshResponse, ClaimsValidationConfig, CustomClaimsCallback, ImpersonationContext, CustomTokenClaims, ExtractClaims, } from './token.js';
|
|
7
7
|
export type { OAuthProviderConfig, GoogleProviderConfig, MicrosoftProviderConfig, GithubProviderConfig, Auth0ProviderConfig, MockProviderConfig, CustomProviderConfig, OAuthConfig, } from './providers.js';
|
|
8
8
|
export type { CookieConfig, TokenRefreshConfig, EncryptionConfig, StorageConfig, } from './refresh.js';
|
|
9
9
|
export type { AuthCodeData, TokenExchangeRequest, TokenExchangeResponse, AuthCodeConfig, } from './authCode.js';
|
|
@@ -21,11 +21,16 @@ export interface ImpersonationContext {
|
|
|
21
21
|
originalClaims?: Record<string, unknown>;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* JWT
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Base JWT token claims interface
|
|
25
|
+
*
|
|
26
|
+
* Defines the standard JWT claims that are always present in tokens.
|
|
27
|
+
* This serves as the foundation for CustomTokenClaims, which extends these
|
|
28
|
+
* base claims with your application-specific custom claims.
|
|
29
|
+
*
|
|
30
|
+
* Use CustomTokenClaims<T> to add your own claims on top of these base claims.
|
|
31
|
+
* This is what gets stored in the JWT and attached to event.context.user.
|
|
27
32
|
*/
|
|
28
|
-
export interface
|
|
33
|
+
export interface BaseTokenClaims {
|
|
29
34
|
/** Subject identifier (user ID) - required claim */
|
|
30
35
|
sub: string;
|
|
31
36
|
/** User email address */
|
|
@@ -71,6 +76,8 @@ export interface RefreshTokenData {
|
|
|
71
76
|
providerUserInfo: Record<string, unknown>;
|
|
72
77
|
/** Provider name for dynamic custom claims generation during refresh (e.g., 'google', 'github', 'microsoft', 'auth0') */
|
|
73
78
|
provider: string;
|
|
79
|
+
/** Resolved custom claims from initial authentication - reused during token refresh to maintain consistency */
|
|
80
|
+
customClaims?: Record<string, unknown>;
|
|
74
81
|
}
|
|
75
82
|
/**
|
|
76
83
|
* Response from token refresh operation
|
|
@@ -114,30 +121,39 @@ export type JSONValue = string | number | boolean | null | undefined | string[]
|
|
|
114
121
|
/**
|
|
115
122
|
* Helper type for creating custom token payloads with type safety
|
|
116
123
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
124
|
+
* Combines BaseTokenClaims (standard JWT claims like sub, email, name) with your
|
|
125
|
+
* application-specific custom claims. This is the primary type you'll use when
|
|
126
|
+
* working with authenticated users in your application.
|
|
127
|
+
*
|
|
128
|
+
* The relationship:
|
|
129
|
+
* - BaseTokenClaims: Standard JWT claims (sub, email, name, iss, exp, etc.)
|
|
130
|
+
* - CustomTokenClaims<T>: BaseTokenClaims + your custom claims (role, permissions, etc.)
|
|
131
|
+
*
|
|
132
|
+
* Ensures type safety by preventing override of standard JWT claims and ensuring
|
|
133
|
+
* all custom claims are JSON-serializable.
|
|
120
134
|
*
|
|
121
|
-
* @template T - Record of custom claims to add to the token
|
|
135
|
+
* @template T - Record of custom claims to add to the base token claims
|
|
122
136
|
*
|
|
123
137
|
* @example
|
|
124
138
|
* ```typescript
|
|
125
|
-
* // Define your custom claims
|
|
126
|
-
* type
|
|
139
|
+
* // Define your custom claims on top of base claims
|
|
140
|
+
* type AppTokenClaims = CustomTokenClaims<{
|
|
127
141
|
* role: string
|
|
128
142
|
* permissions: string[]
|
|
129
143
|
* organizationId: string
|
|
130
144
|
* }>
|
|
131
145
|
*
|
|
132
|
-
* // Use with useAuth
|
|
133
|
-
*
|
|
134
|
-
*
|
|
146
|
+
* // Use with useAuth - you get both base claims (sub, email, name)
|
|
147
|
+
* // and your custom claims (role, permissions, organizationId)
|
|
148
|
+
* const { user } = useAuth<AppTokenClaims>()
|
|
149
|
+
* console.log(user.value?.email) // Base claim
|
|
150
|
+
* console.log(user.value?.role) // Custom claim - Type-safe access
|
|
135
151
|
* ```
|
|
136
152
|
*
|
|
137
153
|
* @example
|
|
138
154
|
* ```typescript
|
|
139
155
|
* // With nested objects (one level)
|
|
140
|
-
* type
|
|
156
|
+
* type AppTokenClaims = CustomTokenClaims<{
|
|
141
157
|
* role: string
|
|
142
158
|
* metadata: {
|
|
143
159
|
* tenantId: string
|
|
@@ -149,27 +165,27 @@ export type JSONValue = string | number | boolean | null | undefined | string[]
|
|
|
149
165
|
* @warning Never include sensitive data like passwords, API keys, or secrets in JWT tokens
|
|
150
166
|
* @warning Keep token payloads small (< 1KB recommended) for performance
|
|
151
167
|
*/
|
|
152
|
-
export type CustomTokenClaims<T extends Record<string, JSONValue>> =
|
|
168
|
+
export type CustomTokenClaims<T extends Record<string, JSONValue>> = BaseTokenClaims & T;
|
|
153
169
|
/**
|
|
154
170
|
* Utility type to extract only custom claims from a token payload
|
|
155
171
|
*
|
|
156
|
-
* Removes all standard
|
|
172
|
+
* Removes all standard BaseTokenClaims fields, leaving only your custom claims.
|
|
157
173
|
* Useful for type composition and claim validation.
|
|
158
174
|
*
|
|
159
|
-
* @template T - A token payload type extending
|
|
175
|
+
* @template T - A token payload type extending BaseTokenClaims
|
|
160
176
|
*
|
|
161
177
|
* @example
|
|
162
178
|
* ```typescript
|
|
163
|
-
* type
|
|
179
|
+
* type AppTokenClaims = CustomTokenClaims<{
|
|
164
180
|
* role: string
|
|
165
181
|
* permissions: string[]
|
|
166
182
|
* }>
|
|
167
183
|
*
|
|
168
|
-
* type CustomClaims = ExtractClaims<
|
|
184
|
+
* type CustomClaims = ExtractClaims<AppTokenClaims>
|
|
169
185
|
* // Result: { role: string, permissions: string[] }
|
|
170
186
|
* ```
|
|
171
187
|
*/
|
|
172
|
-
export type ExtractClaims<T extends
|
|
188
|
+
export type ExtractClaims<T extends BaseTokenClaims> = Omit<T, keyof BaseTokenClaims>;
|
|
173
189
|
/**
|
|
174
190
|
* Custom claims callback function
|
|
175
191
|
* Receives the full OAuth provider user data and tokens
|