@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,50 @@
|
|
|
1
|
+
import { defineEventHandler, getQuery, createError, sendRedirect } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { createLogger } from "../../utils/logger.js";
|
|
4
|
+
import { validateAndIncrementAttempts, retrieveAndDeleteMagicCode } from "../../utils/magicCodeStore.js";
|
|
5
|
+
import { createResetSession } from "../../utils/resetSessionStore.js";
|
|
6
|
+
const logger = createLogger("PasswordResetVerify");
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const config = useRuntimeConfig();
|
|
9
|
+
const passwordConfig = config.nuxtAegis?.providers?.password;
|
|
10
|
+
if (!passwordConfig) {
|
|
11
|
+
throw createError({
|
|
12
|
+
statusCode: 404,
|
|
13
|
+
message: "Password provider is not configured"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const query = getQuery(event);
|
|
17
|
+
const code = query.code;
|
|
18
|
+
if (!code) {
|
|
19
|
+
throw createError({
|
|
20
|
+
statusCode: 400,
|
|
21
|
+
message: "Verification code is required"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const magicCodeData = await validateAndIncrementAttempts(code);
|
|
25
|
+
if (!magicCodeData) {
|
|
26
|
+
throw createError({
|
|
27
|
+
statusCode: 400,
|
|
28
|
+
message: "Invalid or expired code"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (magicCodeData.type !== "reset") {
|
|
32
|
+
throw createError({
|
|
33
|
+
statusCode: 400,
|
|
34
|
+
message: "Invalid code type"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const { email } = magicCodeData;
|
|
38
|
+
try {
|
|
39
|
+
const sessionId = await createResetSession(email);
|
|
40
|
+
await retrieveAndDeleteMagicCode(code);
|
|
41
|
+
const redirectUrl = `/reset-password?session=${sessionId}`;
|
|
42
|
+
return await sendRedirect(event, redirectUrl, 302);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.error("Reset verification failed", error);
|
|
45
|
+
throw createError({
|
|
46
|
+
statusCode: 500,
|
|
47
|
+
message: "Verification failed"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RefreshResponse } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* POST /auth/refresh
|
|
4
|
+
* Refresh endpoint to obtain new access tokens
|
|
5
|
+
* Rejects refresh requests for impersonated sessions
|
|
6
|
+
*/
|
|
7
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<RefreshResponse>>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { defineEventHandler, getCookie, createError } from "h3";
|
|
2
|
+
import { generateToken, verifyToken } from "../utils/jwt.js";
|
|
3
|
+
import {
|
|
4
|
+
generateAndStoreRefreshToken,
|
|
5
|
+
hashRefreshToken,
|
|
6
|
+
getRefreshTokenData,
|
|
7
|
+
revokeRefreshToken
|
|
8
|
+
} from "../utils/refreshToken.js";
|
|
9
|
+
import { setRefreshTokenCookie } from "../utils/cookies.js";
|
|
10
|
+
import { processCustomClaims } from "../utils/customClaims.js";
|
|
11
|
+
import { useRuntimeConfig } from "#imports";
|
|
12
|
+
export default defineEventHandler(async (event) => {
|
|
13
|
+
const config = useRuntimeConfig(event);
|
|
14
|
+
const cookieConfig = config.nuxtAegis?.tokenRefresh?.cookie;
|
|
15
|
+
const tokenConfig = config.nuxtAegis?.token;
|
|
16
|
+
const tokenRefreshConfig = config.nuxtAegis?.tokenRefresh;
|
|
17
|
+
if (!tokenConfig || !tokenConfig.secret) {
|
|
18
|
+
throw createError({
|
|
19
|
+
statusCode: 500,
|
|
20
|
+
statusMessage: "Internal Server Error",
|
|
21
|
+
message: "Token configuration is missing"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const cookieName = cookieConfig?.cookieName || "nuxt-aegis-refresh";
|
|
25
|
+
const refreshToken = getCookie(event, cookieName);
|
|
26
|
+
if (!refreshToken) {
|
|
27
|
+
throw createError({
|
|
28
|
+
statusCode: 401,
|
|
29
|
+
statusMessage: "Unauthorized",
|
|
30
|
+
message: "No refresh token found"
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const authHeader = event.node.req.headers.authorization;
|
|
35
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
36
|
+
const token = authHeader.substring(7);
|
|
37
|
+
const currentToken = await verifyToken(token, tokenConfig.secret, false);
|
|
38
|
+
if (currentToken?.impersonation) {
|
|
39
|
+
throw createError({
|
|
40
|
+
statusCode: 403,
|
|
41
|
+
statusMessage: "Forbidden",
|
|
42
|
+
message: "Cannot refresh impersonated session. Please call unimpersonate to restore your original session."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const hashedRefreshToken = hashRefreshToken(refreshToken);
|
|
47
|
+
const storedRefreshToken = await getRefreshTokenData(hashedRefreshToken, event);
|
|
48
|
+
const isRevoked = storedRefreshToken?.isRevoked || false;
|
|
49
|
+
const isExpired = storedRefreshToken?.expiresAt ? Date.now() > storedRefreshToken.expiresAt : true;
|
|
50
|
+
if (!storedRefreshToken || isRevoked || isExpired) {
|
|
51
|
+
throw createError({
|
|
52
|
+
statusCode: 401,
|
|
53
|
+
statusMessage: "Unauthorized",
|
|
54
|
+
message: "Invalid or expired refresh token"
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const providerUserInfo = storedRefreshToken.providerUserInfo;
|
|
58
|
+
const provider = storedRefreshToken.provider;
|
|
59
|
+
let customClaims = {};
|
|
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
|
+
}
|
|
67
|
+
const payload = {
|
|
68
|
+
sub: String(providerUserInfo.sub || providerUserInfo.email || providerUserInfo.id || ""),
|
|
69
|
+
email: providerUserInfo.email,
|
|
70
|
+
name: providerUserInfo.name,
|
|
71
|
+
picture: providerUserInfo.picture,
|
|
72
|
+
provider
|
|
73
|
+
// Include provider name in JWT payload
|
|
74
|
+
};
|
|
75
|
+
const newToken = await generateToken(payload, tokenConfig, customClaims);
|
|
76
|
+
const newRefreshToken = await generateAndStoreRefreshToken(
|
|
77
|
+
providerUserInfo,
|
|
78
|
+
// RS-2: Store complete OAuth provider user data
|
|
79
|
+
provider,
|
|
80
|
+
// Store provider name
|
|
81
|
+
tokenRefreshConfig,
|
|
82
|
+
hashedRefreshToken,
|
|
83
|
+
// Pass previous token hash for rotation tracking
|
|
84
|
+
event
|
|
85
|
+
);
|
|
86
|
+
if (newRefreshToken) {
|
|
87
|
+
setRefreshTokenCookie(event, newRefreshToken, cookieConfig);
|
|
88
|
+
}
|
|
89
|
+
await revokeRefreshToken(hashedRefreshToken, event);
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
message: "Token refreshed successfully",
|
|
93
|
+
accessToken: newToken
|
|
94
|
+
};
|
|
95
|
+
} catch {
|
|
96
|
+
throw createError({
|
|
97
|
+
statusCode: 401,
|
|
98
|
+
statusMessage: "Unauthorized",
|
|
99
|
+
message: "Token refresh failed"
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TokenExchangeResponse } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* POST /auth/token
|
|
4
|
+
* Token Exchange Endpoint - Authorization CODE to JWT Tokens
|
|
5
|
+
*
|
|
6
|
+
* This endpoint implements the second step of the OAuth 2.0 authorization code flow.
|
|
7
|
+
* It exchanges a short-lived authorization CODE for application-specific JWT tokens.
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Accept authorization CODE from request body
|
|
11
|
+
* 2. Validate CODE exists in server-side key-value store
|
|
12
|
+
* 3. Retrieve and delete CODE atomically (single-use enforcement)
|
|
13
|
+
* 4. Generate JWT access token and refresh token
|
|
14
|
+
* 5. Return access token in JSON response body
|
|
15
|
+
* 6. Set refresh token as HttpOnly, Secure cookie
|
|
16
|
+
*
|
|
17
|
+
* Security:
|
|
18
|
+
* - Single-use CODE enforcement via immediate deletion
|
|
19
|
+
* - Generic 401 error for invalid/expired/reused CODEs
|
|
20
|
+
* - No specific failure reasons revealed to prevent information leakage
|
|
21
|
+
*
|
|
22
|
+
* @returns TokenExchangeResponse with access token and metadata
|
|
23
|
+
* @throws 400 Bad Request if CODE is missing from request body
|
|
24
|
+
* @throws 401 Unauthorized if CODE is invalid, expired, or already used (EH-4: Generic error)
|
|
25
|
+
* @throws 500 Internal Server Error if token generation fails
|
|
26
|
+
*/
|
|
27
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<TokenExchangeResponse>>;
|
|
28
|
+
export default _default;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { defineEventHandler, readBody, createError } from "h3";
|
|
2
|
+
import { retrieveAndDeleteAuthCode } from "../utils/authCodeStore.js";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { generateAuthTokens } from "../utils/auth.js";
|
|
5
|
+
import { setRefreshTokenCookie } from "../utils/cookies.js";
|
|
6
|
+
import { createLogger } from "../utils/logger.js";
|
|
7
|
+
const logger = createLogger("TokenExchange");
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
const body = await readBody(event);
|
|
10
|
+
if (!body?.code) {
|
|
11
|
+
logger.security("Token exchange attempted without authorization code", {
|
|
12
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13
|
+
event: "TOKEN_EXCHANGE_MISSING_CODE",
|
|
14
|
+
severity: "warning"
|
|
15
|
+
});
|
|
16
|
+
throw createError({
|
|
17
|
+
statusCode: 400,
|
|
18
|
+
statusMessage: "Bad Request",
|
|
19
|
+
message: "Missing authorization code"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const authCodeData = await retrieveAndDeleteAuthCode(body.code);
|
|
23
|
+
if (!authCodeData) {
|
|
24
|
+
logger.security("Token exchange failed - invalid authorization code", {
|
|
25
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26
|
+
event: "TOKEN_EXCHANGE_INVALID_CODE",
|
|
27
|
+
codePrefix: `${body.code.substring(0, 8)}...`,
|
|
28
|
+
severity: "warning"
|
|
29
|
+
});
|
|
30
|
+
throw createError({
|
|
31
|
+
statusCode: 401,
|
|
32
|
+
statusMessage: "Unauthorized",
|
|
33
|
+
message: "Invalid or expired authorization code"
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const { accessToken, refreshToken } = await generateAuthTokens(
|
|
38
|
+
event,
|
|
39
|
+
authCodeData.providerUserInfo,
|
|
40
|
+
authCodeData.provider,
|
|
41
|
+
authCodeData.customClaims
|
|
42
|
+
);
|
|
43
|
+
if (refreshToken) {
|
|
44
|
+
const cookieConfig = useRuntimeConfig(event).nuxtAegis?.tokenRefresh?.cookie;
|
|
45
|
+
setRefreshTokenCookie(event, refreshToken, cookieConfig);
|
|
46
|
+
}
|
|
47
|
+
const tokenConfig = useRuntimeConfig(event).nuxtAegis?.token;
|
|
48
|
+
const expiresIn = tokenConfig?.expiresIn;
|
|
49
|
+
let expiresInSeconds;
|
|
50
|
+
if (expiresIn) {
|
|
51
|
+
if (typeof expiresIn === "number") {
|
|
52
|
+
expiresInSeconds = expiresIn;
|
|
53
|
+
} else if (typeof expiresIn === "string") {
|
|
54
|
+
const match = expiresIn.match(/^(\d+)([smhd])$/);
|
|
55
|
+
if (match && match[1] && match[2]) {
|
|
56
|
+
const value = Number.parseInt(match[1], 10);
|
|
57
|
+
const unit = match[2];
|
|
58
|
+
const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
59
|
+
const multiplier = multipliers[unit];
|
|
60
|
+
if (multiplier !== void 0) {
|
|
61
|
+
expiresInSeconds = value * multiplier;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const response = {
|
|
67
|
+
accessToken,
|
|
68
|
+
tokenType: "Bearer",
|
|
69
|
+
expiresIn: expiresInSeconds
|
|
70
|
+
};
|
|
71
|
+
logger.security("JWT tokens generated successfully", {
|
|
72
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
73
|
+
event: "TOKEN_GENERATION_SUCCESS",
|
|
74
|
+
expiresIn: expiresInSeconds
|
|
75
|
+
});
|
|
76
|
+
return response;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.error("Token generation error", {
|
|
79
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
80
|
+
event: "TOKEN_GENERATION_ERROR",
|
|
81
|
+
error: import.meta.dev ? error : "Error details hidden in production",
|
|
82
|
+
severity: "error"
|
|
83
|
+
});
|
|
84
|
+
throw createError({
|
|
85
|
+
statusCode: 500,
|
|
86
|
+
statusMessage: "Internal Server Error",
|
|
87
|
+
message: "Failed to generate authentication tokens"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /auth/unimpersonate
|
|
3
|
+
* Ends impersonation and restores the original user session
|
|
4
|
+
*
|
|
5
|
+
* Response:
|
|
6
|
+
* - accessToken: string - JWT for restored original session
|
|
7
|
+
* - Sets refresh token cookie for normal session
|
|
8
|
+
*
|
|
9
|
+
* Requirements:
|
|
10
|
+
* - Impersonation feature must be enabled
|
|
11
|
+
* - Current session must be impersonated
|
|
12
|
+
*/
|
|
13
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
14
|
+
accessToken: string;
|
|
15
|
+
}>>;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { defineEventHandler, createError, getHeader, setCookie } from "h3";
|
|
2
|
+
import { verifyToken } from "../utils/jwt.js";
|
|
3
|
+
import { endImpersonation } from "../utils/impersonation.js";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
import { createLogger } from "../utils/logger.js";
|
|
6
|
+
const logger = createLogger("Unimpersonate");
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const config = useRuntimeConfig();
|
|
9
|
+
if (!config.nuxtAegis?.impersonation?.enabled) {
|
|
10
|
+
throw createError({
|
|
11
|
+
statusCode: 404,
|
|
12
|
+
message: "Impersonation feature is not enabled"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const authHeader = getHeader(event, "authorization");
|
|
17
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
18
|
+
throw createError({
|
|
19
|
+
statusCode: 401,
|
|
20
|
+
message: "Authentication required"
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const token = authHeader.substring(7);
|
|
24
|
+
const tokenConfig = config.nuxtAegis?.token;
|
|
25
|
+
if (!tokenConfig?.secret) {
|
|
26
|
+
throw createError({
|
|
27
|
+
statusCode: 500,
|
|
28
|
+
message: "Token configuration is missing"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const currentToken = await verifyToken(token, tokenConfig.secret);
|
|
32
|
+
if (!currentToken) {
|
|
33
|
+
throw createError({
|
|
34
|
+
statusCode: 401,
|
|
35
|
+
message: "Invalid or expired token"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const { accessToken, refreshTokenId } = await endImpersonation(currentToken, event);
|
|
39
|
+
const cookieConfig = config.nuxtAegis?.tokenRefresh?.cookie;
|
|
40
|
+
const cookieName = cookieConfig?.cookieName || "nuxt-aegis-refresh";
|
|
41
|
+
setCookie(event, cookieName, refreshTokenId, {
|
|
42
|
+
httpOnly: true,
|
|
43
|
+
secure: cookieConfig?.secure ?? true,
|
|
44
|
+
sameSite: cookieConfig?.sameSite || "lax",
|
|
45
|
+
path: cookieConfig?.path || "/",
|
|
46
|
+
maxAge: cookieConfig?.maxAge,
|
|
47
|
+
domain: cookieConfig?.domain
|
|
48
|
+
});
|
|
49
|
+
logger.security("Impersonation ended", {
|
|
50
|
+
originalUser: currentToken.impersonation?.originalUserId,
|
|
51
|
+
wasImpersonating: currentToken.sub
|
|
52
|
+
});
|
|
53
|
+
return { accessToken };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.error("Unimpersonate failed:", error);
|
|
56
|
+
const err = error;
|
|
57
|
+
if (err.statusCode) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
throw createError({
|
|
61
|
+
statusCode: 500,
|
|
62
|
+
message: err.message || "Failed to end impersonation"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { TokenPayload } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generate authentication tokens from user data with optional custom claims
|
|
5
|
+
* This is the recommended way to generate tokens after successful OAuth authentication
|
|
6
|
+
*
|
|
7
|
+
* @param event - H3Event object
|
|
8
|
+
* @param providerUserInfo - Complete user object from the OAuth provider (will be stored with refresh token)
|
|
9
|
+
* @param provider - Provider name (e.g., 'google', 'github', 'microsoft', 'auth0')
|
|
10
|
+
* @param customClaims - Optional custom claims to add to the JWT (already processed)
|
|
11
|
+
* @returns Object containing accessToken and refreshToken
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const tokens = await generateAuthTokens(event, providerUserInfo, 'google', {
|
|
16
|
+
* role: 'admin',
|
|
17
|
+
* permissions: ['read', 'write'],
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateAuthTokens(event: H3Event, providerUserInfo: Record<string, unknown>, provider: string, customClaims?: Record<string, unknown>): Promise<{
|
|
22
|
+
accessToken: string;
|
|
23
|
+
refreshToken?: string;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Ensures the event has an authenticated user and narrows the type
|
|
27
|
+
* Returns the event with narrowed context type for better type inference
|
|
28
|
+
*
|
|
29
|
+
* @param event - H3Event object
|
|
30
|
+
* @returns Event with guaranteed user context
|
|
31
|
+
* @throws 401 Unauthorized if user is not authenticated
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* export default defineEventHandler((event) => {
|
|
36
|
+
* const authedEvent = requireAuth(event)
|
|
37
|
+
* // TypeScript knows authedEvent.context.user is defined
|
|
38
|
+
* return { userId: authedEvent.context.user.sub }
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* // With custom claims typing
|
|
45
|
+
* interface MyTokenPayload extends TokenPayload {
|
|
46
|
+
* role: string
|
|
47
|
+
* permissions: string[]
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* export default defineEventHandler((event) => {
|
|
51
|
+
* const authedEvent = requireAuth<MyTokenPayload>(event)
|
|
52
|
+
* // TypeScript knows about role and permissions
|
|
53
|
+
* return { role: authedEvent.context.user.role }
|
|
54
|
+
* })
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function requireAuth<T extends TokenPayload = TokenPayload>(event: H3Event): H3Event & {
|
|
58
|
+
context: {
|
|
59
|
+
user: T;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Get the authenticated user from the event context
|
|
64
|
+
* This is a convenience function that combines requireAuth and context extraction
|
|
65
|
+
*
|
|
66
|
+
* @param event - H3Event object
|
|
67
|
+
* @returns The authenticated user payload
|
|
68
|
+
* @throws 401 Unauthorized if user is not authenticated
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* export default defineEventHandler((event) => {
|
|
73
|
+
* const user = getAuthUser(event)
|
|
74
|
+
* // TypeScript knows user is defined
|
|
75
|
+
* return { userId: user.sub, email: user.email }
|
|
76
|
+
* })
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // With custom claims typing
|
|
82
|
+
* interface MyTokenPayload extends TokenPayload {
|
|
83
|
+
* role: string
|
|
84
|
+
* permissions: string[]
|
|
85
|
+
* }
|
|
86
|
+
*
|
|
87
|
+
* export default defineEventHandler((event) => {
|
|
88
|
+
* const user = getAuthUser<MyTokenPayload>(event)
|
|
89
|
+
* // TypeScript knows about role and permissions
|
|
90
|
+
* return { role: user.role, permissions: user.permissions }
|
|
91
|
+
* })
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare function getAuthUser<T extends TokenPayload = TokenPayload>(event: H3Event): T;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
import { createError } from "h3";
|
|
3
|
+
import { generateAndStoreRefreshToken } from "./refreshToken.js";
|
|
4
|
+
import { generateToken } from "./jwt.js";
|
|
5
|
+
export async function generateAuthTokens(event, providerUserInfo, provider, customClaims) {
|
|
6
|
+
const config = useRuntimeConfig(event);
|
|
7
|
+
const tokenConfig = config.nuxtAegis?.token;
|
|
8
|
+
const tokenRefreshConfig = config.nuxtAegis?.tokenRefresh;
|
|
9
|
+
if (!tokenConfig || !tokenConfig.secret) {
|
|
10
|
+
throw new Error("Token configuration is missing. Please configure nuxtAegis.token in your nuxt.config.ts");
|
|
11
|
+
}
|
|
12
|
+
const payload = {
|
|
13
|
+
sub: String(providerUserInfo.sub || providerUserInfo.email || providerUserInfo.id || ""),
|
|
14
|
+
email: providerUserInfo.email,
|
|
15
|
+
name: providerUserInfo.name,
|
|
16
|
+
picture: providerUserInfo.picture,
|
|
17
|
+
provider
|
|
18
|
+
// Include provider name in JWT payload
|
|
19
|
+
};
|
|
20
|
+
const refreshToken = await generateAndStoreRefreshToken(
|
|
21
|
+
providerUserInfo,
|
|
22
|
+
// RS-2: Store complete user object
|
|
23
|
+
provider,
|
|
24
|
+
// Store provider name for custom claims refresh
|
|
25
|
+
tokenRefreshConfig,
|
|
26
|
+
void 0,
|
|
27
|
+
// No previous token hash for initial auth
|
|
28
|
+
event
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
accessToken: await generateToken(payload, tokenConfig, customClaims),
|
|
32
|
+
refreshToken
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function requireAuth(event) {
|
|
36
|
+
if (!event.context.user) {
|
|
37
|
+
throw createError({
|
|
38
|
+
statusCode: 401,
|
|
39
|
+
statusMessage: "Unauthorized",
|
|
40
|
+
message: "Authentication required"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return event;
|
|
44
|
+
}
|
|
45
|
+
export function getAuthUser(event) {
|
|
46
|
+
if (!event.context.user) {
|
|
47
|
+
throw createError({
|
|
48
|
+
statusCode: 401,
|
|
49
|
+
statusMessage: "Unauthorized",
|
|
50
|
+
message: "Authentication required"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return event.context.user;
|
|
54
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { AuthCodeData } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Authorization Code Store Utilities
|
|
5
|
+
*
|
|
6
|
+
* Provides server-side storage and management for short-lived authorization CODEs
|
|
7
|
+
* used in the OAuth 2.0 authorization code flow.
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Generate and store authorization CODE after OAuth authentication
|
|
11
|
+
* 2. Store CODE with user info and provider tokens in memory
|
|
12
|
+
* 3. Set 60-second expiration (configurable via CF-9)
|
|
13
|
+
* 4. Retrieve and delete CODE when exchanging for tokens
|
|
14
|
+
* 5. Ensure single-use by immediate deletion
|
|
15
|
+
* 6. Automatic cleanup of expired CODEs
|
|
16
|
+
*
|
|
17
|
+
* Security Features:
|
|
18
|
+
* - Cryptographically secure random CODE generation (crypto.randomBytes)
|
|
19
|
+
* - Single-use enforcement (delete immediately after retrieval)
|
|
20
|
+
* - Automatic cleanup of expired CODEs
|
|
21
|
+
* - Validation before allowing token exchange
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Generate a cryptographically secure authorization code
|
|
25
|
+
*
|
|
26
|
+
* @returns A base64url-encoded random authorization code
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const code = generateAuthCode()
|
|
31
|
+
* // Returns: "X7k9mP2nQ5vL8wR4tY6uZ3bN1cM0dF5g"
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateAuthCode(): string;
|
|
35
|
+
/**
|
|
36
|
+
* Store authorization code with associated user and provider data
|
|
37
|
+
* Server-side in-memory key-value store for temporary authorization CODE storage
|
|
38
|
+
* Associate CODE with user information and provider tokens
|
|
39
|
+
* Set expiration time of 60 seconds
|
|
40
|
+
* Store CODE in server-side in-memory key-value store
|
|
41
|
+
*
|
|
42
|
+
* @param code - The authorization code to store
|
|
43
|
+
* @param providerUserInfo - Complete OAuth provider user data
|
|
44
|
+
* @param providerTokens - Tokens received from OAuth provider
|
|
45
|
+
* @param providerTokens.access_token - Provider access token
|
|
46
|
+
* @param providerTokens.refresh_token - Optional provider refresh token
|
|
47
|
+
* @param providerTokens.id_token - Optional provider ID token
|
|
48
|
+
* @param providerTokens.expires_in - Optional token expiration time
|
|
49
|
+
* @param provider - Provider name (e.g., 'google', 'github', 'microsoft', 'auth0')
|
|
50
|
+
* @param customClaims - Resolved custom claims (already processed from static or callback config)
|
|
51
|
+
* @param expiresIn - Expiration time in seconds (default: 60)
|
|
52
|
+
* @param _event - H3Event for Nitro storage access (optional, currently unused)
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* await storeAuthCode('X7k9mP...', providerUserInfo, providerTokens, 'google', customClaims)
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function storeAuthCode(code: string, providerUserInfo: Record<string, unknown>, providerTokens: {
|
|
60
|
+
access_token: string;
|
|
61
|
+
refresh_token?: string;
|
|
62
|
+
id_token?: string;
|
|
63
|
+
expires_in?: number;
|
|
64
|
+
}, provider: string, customClaims: Record<string, unknown> | undefined, expiresIn?: number, // CS-4: Default 60 seconds
|
|
65
|
+
_event?: H3Event): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Validate that an authorization code exists and has not expired
|
|
68
|
+
*
|
|
69
|
+
* @param code - The authorization code to validate
|
|
70
|
+
* @returns The auth code data if valid, null if invalid or expired
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* const data = await validateAuthCode('X7k9mP...')
|
|
75
|
+
* if (!data) {
|
|
76
|
+
* throw new Error('Invalid or expired code')
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare function validateAuthCode(code: string): Promise<AuthCodeData | null>;
|
|
81
|
+
/**
|
|
82
|
+
* Retrieve and delete authorization code in a single atomic operation
|
|
83
|
+
* Immediately delete CODE after successful exchange for single-use enforcement
|
|
84
|
+
* Prevent CODE reuse by validating existence before deletion
|
|
85
|
+
* Ensure single-use only
|
|
86
|
+
* Delete CODE from store after validation
|
|
87
|
+
*
|
|
88
|
+
* @param code - The authorization code to retrieve and delete
|
|
89
|
+
* @returns The auth code data if valid, null if invalid, expired, or already used
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const data = await retrieveAndDeleteAuthCode('X7k9mP...')
|
|
94
|
+
* if (!data) {
|
|
95
|
+
* // Code was invalid, expired, or already used
|
|
96
|
+
* throw new Error('Invalid authorization code')
|
|
97
|
+
* }
|
|
98
|
+
* // Code is valid and has been consumed (deleted)
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function retrieveAndDeleteAuthCode(code: string): Promise<AuthCodeData | null>;
|
|
102
|
+
/**
|
|
103
|
+
* Clean up expired authorization codes from storage
|
|
104
|
+
* Automatically remove expired CODEs
|
|
105
|
+
* Support automatic cleanup without blocking requests
|
|
106
|
+
* Automatic cleanup of expired CODEs
|
|
107
|
+
*
|
|
108
|
+
* This function should be called periodically to prevent memory buildup
|
|
109
|
+
* from expired codes that were never exchanged.
|
|
110
|
+
*
|
|
111
|
+
* @returns Number of codes cleaned up
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* // Run cleanup periodically
|
|
116
|
+
* const cleaned = await cleanupExpiredAuthCodes()
|
|
117
|
+
* console.log(`Cleaned up ${cleaned} expired codes`)
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export declare function cleanupExpiredAuthCodes(): Promise<number>;
|
|
121
|
+
/**
|
|
122
|
+
* Get statistics about the authorization code store
|
|
123
|
+
* Useful for monitoring and debugging
|
|
124
|
+
*
|
|
125
|
+
* @returns Statistics object with total, expired, and valid code counts
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const stats = await getAuthCodeStats()
|
|
130
|
+
* console.log(`Total: ${stats.total}, Valid: ${stats.valid}, Expired: ${stats.expired}`)
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare function getAuthCodeStats(): Promise<{
|
|
134
|
+
total: number;
|
|
135
|
+
valid: number;
|
|
136
|
+
expired: number;
|
|
137
|
+
}>;
|