@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,77 @@
|
|
|
1
|
+
import { decodeJwt, jwtVerify, SignJWT } from "jose";
|
|
2
|
+
import { filterReservedClaims, validateClaimTypes } from "./customClaims.js";
|
|
3
|
+
import { createLogger } from "./logger.js";
|
|
4
|
+
const logger = createLogger("JWT");
|
|
5
|
+
function validateTokenSize(payload) {
|
|
6
|
+
if (process.env.NODE_ENV === "production") {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const payloadString = JSON.stringify(payload);
|
|
10
|
+
const sizeInBytes = payloadString.length;
|
|
11
|
+
const thresholdBytes = 1024;
|
|
12
|
+
if (sizeInBytes > thresholdBytes) {
|
|
13
|
+
logger.warn(
|
|
14
|
+
`Token payload size (${sizeInBytes} bytes) exceeds recommended threshold (${thresholdBytes} bytes). Consider reducing the payload size by removing unnecessary claims or using references instead of large values. Large tokens can impact performance and may be rejected by some systems.`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function generateToken(payload, config, customClaims) {
|
|
19
|
+
if (!config.secret) {
|
|
20
|
+
throw new Error("Token secret is required");
|
|
21
|
+
}
|
|
22
|
+
const secret = new TextEncoder().encode(config.secret);
|
|
23
|
+
const expiresIn = config.expiresIn || "1h";
|
|
24
|
+
let safeClaims = {};
|
|
25
|
+
if (customClaims) {
|
|
26
|
+
const filtered = filterReservedClaims(customClaims);
|
|
27
|
+
safeClaims = validateClaimTypes(filtered);
|
|
28
|
+
}
|
|
29
|
+
const finalPayload = { ...payload, ...safeClaims };
|
|
30
|
+
validateTokenSize(finalPayload);
|
|
31
|
+
let jwt = new SignJWT(finalPayload).setProtectedHeader({ alg: config.algorithm || "HS256" }).setIssuedAt().setSubject(payload.sub);
|
|
32
|
+
if (config.issuer) {
|
|
33
|
+
jwt = jwt.setIssuer(config.issuer);
|
|
34
|
+
}
|
|
35
|
+
if (config.audience) {
|
|
36
|
+
jwt = jwt.setAudience(config.audience);
|
|
37
|
+
}
|
|
38
|
+
if (typeof expiresIn === "number") {
|
|
39
|
+
jwt = jwt.setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn);
|
|
40
|
+
} else {
|
|
41
|
+
jwt = jwt.setExpirationTime(expiresIn);
|
|
42
|
+
}
|
|
43
|
+
return await jwt.sign(secret);
|
|
44
|
+
}
|
|
45
|
+
export async function updateTokenWithClaims(token, claims, config) {
|
|
46
|
+
if (!config.secret) {
|
|
47
|
+
throw new Error("Token secret is required");
|
|
48
|
+
}
|
|
49
|
+
const secret = new TextEncoder().encode(config.secret);
|
|
50
|
+
const { payload } = await jwtVerify(token, secret);
|
|
51
|
+
const updatedPayload = {
|
|
52
|
+
...payload,
|
|
53
|
+
...claims,
|
|
54
|
+
sub: payload.sub
|
|
55
|
+
};
|
|
56
|
+
return await generateToken(updatedPayload, config);
|
|
57
|
+
}
|
|
58
|
+
export async function verifyToken(token, secret, checkExpiration = true) {
|
|
59
|
+
if (!token || !secret) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
64
|
+
if (checkExpiration) {
|
|
65
|
+
const { payload } = await jwtVerify(token, secretKey);
|
|
66
|
+
return payload;
|
|
67
|
+
} else {
|
|
68
|
+
const payload = decodeJwt(token);
|
|
69
|
+
const beforeExpiry = new Date((payload.exp || Date.now()) - 1e3);
|
|
70
|
+
await jwtVerify(token, secretKey, { currentDate: beforeExpiry });
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
logger.error("Token verification failed");
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger instance for consistent logging across server-side code
|
|
3
|
+
*/
|
|
4
|
+
export interface Logger {
|
|
5
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
6
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
7
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
8
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
9
|
+
security: (message: string, ...args: unknown[]) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a logger instance with a specific context
|
|
13
|
+
* Respects the logging configuration from runtime config
|
|
14
|
+
*
|
|
15
|
+
* @param context - Context identifier for log messages (e.g., 'Auth', 'JWT', 'OAuth')
|
|
16
|
+
* @returns Logger instance with debug, info, warn, error, and security methods
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLogger(context: string): Logger;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { consola } from "consola";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
export function createLogger(context) {
|
|
4
|
+
const prefix = `[Nuxt Aegis][${context}]`;
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const loggingConfig = config.nuxtAegis?.logging;
|
|
7
|
+
const level = loggingConfig?.level || "info";
|
|
8
|
+
if (level === "debug") {
|
|
9
|
+
consola.level = 4;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
debug: (message, ...args) => {
|
|
13
|
+
consola.debug(prefix, message, ...args);
|
|
14
|
+
},
|
|
15
|
+
info: (message, ...args) => {
|
|
16
|
+
const config2 = useRuntimeConfig();
|
|
17
|
+
const loggingConfig2 = config2.nuxtAegis?.logging;
|
|
18
|
+
const level2 = loggingConfig2?.level || "info";
|
|
19
|
+
if (level2 !== "silent" && level2 !== "error" && level2 !== "warn") {
|
|
20
|
+
consola.info(prefix, message, ...args);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
warn: (message, ...args) => {
|
|
24
|
+
const config2 = useRuntimeConfig();
|
|
25
|
+
const loggingConfig2 = config2.nuxtAegis?.logging;
|
|
26
|
+
const level2 = loggingConfig2?.level || "info";
|
|
27
|
+
if (level2 !== "silent" && level2 !== "error") {
|
|
28
|
+
consola.warn(prefix, message, ...args);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
error: (message, ...args) => {
|
|
32
|
+
const config2 = useRuntimeConfig();
|
|
33
|
+
const loggingConfig2 = config2.nuxtAegis?.logging;
|
|
34
|
+
const level2 = loggingConfig2?.level || "info";
|
|
35
|
+
if (level2 !== "silent") {
|
|
36
|
+
consola.error(prefix, message, ...args);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
security: (message, ...args) => {
|
|
40
|
+
const config2 = useRuntimeConfig();
|
|
41
|
+
const loggingConfig2 = config2.nuxtAegis?.logging;
|
|
42
|
+
const level2 = loggingConfig2?.level || "info";
|
|
43
|
+
const securityEnabled = loggingConfig2?.security === true || level2 === "debug";
|
|
44
|
+
if (securityEnabled && level2 !== "silent") {
|
|
45
|
+
consola.warn(`[Nuxt Aegis Security][${context}]`, message, ...args);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface MagicCodeData {
|
|
2
|
+
email: string;
|
|
3
|
+
type: 'register' | 'login' | 'reset';
|
|
4
|
+
hashedPassword?: string;
|
|
5
|
+
attempts: number;
|
|
6
|
+
maxAttempts: number;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Generate a 6-digit magic code
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateMagicCode(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Store a magic code
|
|
17
|
+
*/
|
|
18
|
+
export declare function storeMagicCode(email: string, type: 'register' | 'login' | 'reset', data: Omit<MagicCodeData, 'attempts' | 'createdAt' | 'expiresAt' | 'email' | 'type'>, ttl?: number, // 10 minutes
|
|
19
|
+
maxAttempts?: number): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Validate and increment attempts for a magic code
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateAndIncrementAttempts(code: string): Promise<MagicCodeData | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Retrieve and delete a magic code
|
|
26
|
+
*/
|
|
27
|
+
export declare function retrieveAndDeleteMagicCode(code: string): Promise<MagicCodeData | null>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
2
|
+
import { useStorage } from "#imports";
|
|
3
|
+
import { createLogger } from "./logger.js";
|
|
4
|
+
const logger = createLogger("MagicCode");
|
|
5
|
+
export function generateMagicCode() {
|
|
6
|
+
return randomInt(1e5, 999999).toString();
|
|
7
|
+
}
|
|
8
|
+
export async function storeMagicCode(email, type, data, ttl = 600, maxAttempts = 5) {
|
|
9
|
+
const storage = useStorage();
|
|
10
|
+
const normalizedEmail = email.toLowerCase();
|
|
11
|
+
const code = generateMagicCode();
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const lookupKey = `magic-lookup:${normalizedEmail}:${type}`;
|
|
14
|
+
const existingCode = await storage.getItem(lookupKey);
|
|
15
|
+
if (existingCode) {
|
|
16
|
+
await storage.removeItem(`magic:${existingCode}`);
|
|
17
|
+
}
|
|
18
|
+
const magicCodeData = {
|
|
19
|
+
...data,
|
|
20
|
+
email: normalizedEmail,
|
|
21
|
+
type,
|
|
22
|
+
attempts: 0,
|
|
23
|
+
maxAttempts,
|
|
24
|
+
createdAt: now,
|
|
25
|
+
expiresAt: now + ttl * 1e3
|
|
26
|
+
};
|
|
27
|
+
await storage.setItem(`magic:${code}`, magicCodeData);
|
|
28
|
+
await storage.setItem(lookupKey, code);
|
|
29
|
+
logger.debug(`Magic code stored for ${normalizedEmail} (${type})`);
|
|
30
|
+
return code;
|
|
31
|
+
}
|
|
32
|
+
export async function validateAndIncrementAttempts(code) {
|
|
33
|
+
const storage = useStorage();
|
|
34
|
+
const key = `magic:${code}`;
|
|
35
|
+
const data = await storage.getItem(key);
|
|
36
|
+
if (!data) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (Date.now() > data.expiresAt) {
|
|
40
|
+
await storage.removeItem(key);
|
|
41
|
+
const lookupKey = `magic-lookup:${data.email}:${data.type}`;
|
|
42
|
+
await storage.removeItem(lookupKey);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (data.attempts >= data.maxAttempts) {
|
|
46
|
+
await storage.removeItem(key);
|
|
47
|
+
const lookupKey = `magic-lookup:${data.email}:${data.type}`;
|
|
48
|
+
await storage.removeItem(lookupKey);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
data.attempts++;
|
|
52
|
+
await storage.setItem(key, data);
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
export async function retrieveAndDeleteMagicCode(code) {
|
|
56
|
+
const storage = useStorage();
|
|
57
|
+
const key = `magic:${code}`;
|
|
58
|
+
const data = await storage.getItem(key);
|
|
59
|
+
if (!data) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
await storage.removeItem(key);
|
|
63
|
+
const lookupKey = `magic-lookup:${data.email}:${data.type}`;
|
|
64
|
+
await storage.removeItem(lookupKey);
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Authorization Code Storage
|
|
3
|
+
*
|
|
4
|
+
* Simulates OAuth provider's authorization code storage and validation.
|
|
5
|
+
* This is NOT the Aegis authorization CODE - this simulates the provider's code.
|
|
6
|
+
*
|
|
7
|
+
* Stores temporary authorization codes that are:
|
|
8
|
+
* - Single-use (deleted after exchange)
|
|
9
|
+
* - Short-lived (expire after 60 seconds)
|
|
10
|
+
* - Associated with selected user persona
|
|
11
|
+
*
|
|
12
|
+
* DEVELOPMENT/TEST ONLY - Not available in production
|
|
13
|
+
*/
|
|
14
|
+
interface MockAuthCode {
|
|
15
|
+
code: string;
|
|
16
|
+
/** Selected user persona identifier from mockUsers config */
|
|
17
|
+
userId: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
redirectUri: string;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
expiresAt: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a cryptographically random mock authorization code
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateMockCode(): string;
|
|
27
|
+
/**
|
|
28
|
+
* Store a mock authorization code with expiration
|
|
29
|
+
*
|
|
30
|
+
* @param data - Code data object
|
|
31
|
+
* @param data.code - Authorization code string
|
|
32
|
+
* @param data.userId - User persona identifier
|
|
33
|
+
* @param data.clientId - OAuth client ID
|
|
34
|
+
* @param data.redirectUri - OAuth redirect URI
|
|
35
|
+
*/
|
|
36
|
+
export declare function storeMockCode(data: {
|
|
37
|
+
code: string;
|
|
38
|
+
userId: string;
|
|
39
|
+
clientId: string;
|
|
40
|
+
redirectUri: string;
|
|
41
|
+
}): void;
|
|
42
|
+
/**
|
|
43
|
+
* Retrieve and delete a mock authorization code (single-use enforcement)
|
|
44
|
+
*
|
|
45
|
+
* @param code - Authorization code to retrieve
|
|
46
|
+
* @returns Code data if valid and not expired, null otherwise
|
|
47
|
+
*/
|
|
48
|
+
export declare function retrieveAndDeleteMockCode(code: string): MockAuthCode | null;
|
|
49
|
+
/**
|
|
50
|
+
* Cleanup expired mock codes (maintenance)
|
|
51
|
+
*
|
|
52
|
+
* @returns Number of codes cleaned up
|
|
53
|
+
*/
|
|
54
|
+
export declare function cleanupExpiredMockCodes(): number;
|
|
55
|
+
/**
|
|
56
|
+
* Get count of active mock codes (for debugging)
|
|
57
|
+
*/
|
|
58
|
+
export declare function getMockCodeCount(): number;
|
|
59
|
+
/**
|
|
60
|
+
* Clear all mock codes (for testing)
|
|
61
|
+
*/
|
|
62
|
+
export declare function clearAllMockCodes(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Store a mock access token with user mapping
|
|
65
|
+
* Allows userinfo endpoint to retrieve the correct user
|
|
66
|
+
*
|
|
67
|
+
* @param token - Access token string
|
|
68
|
+
* @param userId - User persona identifier
|
|
69
|
+
*/
|
|
70
|
+
export declare function storeMockToken(token: string, userId: string): void;
|
|
71
|
+
/**
|
|
72
|
+
* Get user ID for a mock access token
|
|
73
|
+
*
|
|
74
|
+
* @param token - Access token string
|
|
75
|
+
* @returns User persona identifier or null if token not found
|
|
76
|
+
*/
|
|
77
|
+
export declare function getUserForMockToken(token: string): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Clear expired mock tokens (maintenance)
|
|
80
|
+
* Tokens older than 2 hours are removed
|
|
81
|
+
*
|
|
82
|
+
* @returns Number of tokens cleaned up
|
|
83
|
+
*/
|
|
84
|
+
export declare function cleanupExpiredMockTokens(): number;
|
|
85
|
+
/**
|
|
86
|
+
* Clear all mock tokens (for testing)
|
|
87
|
+
*/
|
|
88
|
+
export declare function clearAllMockTokens(): void;
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const mockCodes = /* @__PURE__ */ new Map();
|
|
2
|
+
const mockTokens = /* @__PURE__ */ new Map();
|
|
3
|
+
export function generateMockCode() {
|
|
4
|
+
const randomBytes = crypto.getRandomValues(new Uint8Array(16));
|
|
5
|
+
const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
return `mock_code_${randomHex}_${Date.now()}`;
|
|
7
|
+
}
|
|
8
|
+
export function storeMockCode(data) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
mockCodes.set(data.code, {
|
|
11
|
+
...data,
|
|
12
|
+
createdAt: now,
|
|
13
|
+
expiresAt: now + 6e4
|
|
14
|
+
// 60 seconds - standard OAuth code lifetime
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function retrieveAndDeleteMockCode(code) {
|
|
18
|
+
const codeData = mockCodes.get(code);
|
|
19
|
+
if (!codeData) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (Date.now() > codeData.expiresAt) {
|
|
23
|
+
mockCodes.delete(code);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
mockCodes.delete(code);
|
|
27
|
+
return codeData;
|
|
28
|
+
}
|
|
29
|
+
export function cleanupExpiredMockCodes() {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
let cleaned = 0;
|
|
32
|
+
for (const [code, data] of mockCodes.entries()) {
|
|
33
|
+
if (now > data.expiresAt) {
|
|
34
|
+
mockCodes.delete(code);
|
|
35
|
+
cleaned++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return cleaned;
|
|
39
|
+
}
|
|
40
|
+
export function getMockCodeCount() {
|
|
41
|
+
return mockCodes.size;
|
|
42
|
+
}
|
|
43
|
+
export function clearAllMockCodes() {
|
|
44
|
+
mockCodes.clear();
|
|
45
|
+
}
|
|
46
|
+
export function storeMockToken(token, userId) {
|
|
47
|
+
mockTokens.set(token, {
|
|
48
|
+
token,
|
|
49
|
+
userId,
|
|
50
|
+
createdAt: Date.now()
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function getUserForMockToken(token) {
|
|
54
|
+
const tokenData = mockTokens.get(token);
|
|
55
|
+
return tokenData?.userId || null;
|
|
56
|
+
}
|
|
57
|
+
export function cleanupExpiredMockTokens() {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const maxAge = 2 * 60 * 60 * 1e3;
|
|
60
|
+
let cleaned = 0;
|
|
61
|
+
for (const [token, data] of mockTokens.entries()) {
|
|
62
|
+
if (now - data.createdAt > maxAge) {
|
|
63
|
+
mockTokens.delete(token);
|
|
64
|
+
cleaned++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return cleaned;
|
|
68
|
+
}
|
|
69
|
+
export function clearAllMockTokens() {
|
|
70
|
+
mockTokens.clear();
|
|
71
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize email address
|
|
3
|
+
*/
|
|
4
|
+
export declare function normalizeEmail(email: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Validate email format
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateEmailFormat(email: string): {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Hash a password using scrypt
|
|
14
|
+
*/
|
|
15
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Verify a password against a hash
|
|
18
|
+
*/
|
|
19
|
+
export declare function verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Validate password strength
|
|
22
|
+
*/
|
|
23
|
+
export declare function validatePasswordStrength(password: string, policy?: {
|
|
24
|
+
minLength?: number;
|
|
25
|
+
requireUppercase?: boolean;
|
|
26
|
+
requireLowercase?: boolean;
|
|
27
|
+
requireNumber?: boolean;
|
|
28
|
+
requireSpecial?: boolean;
|
|
29
|
+
}): boolean | string[];
|
|
30
|
+
/**
|
|
31
|
+
* Obfuscate magic code for logging
|
|
32
|
+
*/
|
|
33
|
+
export declare function obfuscateMagicCode(code: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { scrypt, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const scryptAsync = promisify(scrypt);
|
|
4
|
+
export function normalizeEmail(email) {
|
|
5
|
+
return email.trim().toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
export function validateEmailFormat(email) {
|
|
8
|
+
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
|
9
|
+
if (!emailRegex.test(email)) {
|
|
10
|
+
return { valid: false, error: "Invalid email format" };
|
|
11
|
+
}
|
|
12
|
+
return { valid: true };
|
|
13
|
+
}
|
|
14
|
+
export async function hashPassword(password) {
|
|
15
|
+
const salt = randomBytes(16).toString("hex");
|
|
16
|
+
const derivedKey = await scryptAsync(password, salt, 64);
|
|
17
|
+
return `${salt}:${derivedKey.toString("hex")}`;
|
|
18
|
+
}
|
|
19
|
+
export async function verifyPassword(password, hash) {
|
|
20
|
+
const [salt, key] = hash.split(":");
|
|
21
|
+
if (!salt || !key) return false;
|
|
22
|
+
const keyBuffer = Buffer.from(key, "hex");
|
|
23
|
+
const derivedKey = await scryptAsync(password, salt, 64);
|
|
24
|
+
return timingSafeEqual(keyBuffer, derivedKey);
|
|
25
|
+
}
|
|
26
|
+
export function validatePasswordStrength(password, policy = {}) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const minLength = policy.minLength ?? 8;
|
|
29
|
+
if (password.length < minLength) {
|
|
30
|
+
errors.push(`Password must be at least ${minLength} characters long`);
|
|
31
|
+
}
|
|
32
|
+
if (policy.requireUppercase && !/[A-Z]/.test(password)) {
|
|
33
|
+
errors.push("Password must contain at least one uppercase letter");
|
|
34
|
+
}
|
|
35
|
+
if (policy.requireLowercase && !/[a-z]/.test(password)) {
|
|
36
|
+
errors.push("Password must contain at least one lowercase letter");
|
|
37
|
+
}
|
|
38
|
+
if (policy.requireNumber && !/\d/.test(password)) {
|
|
39
|
+
errors.push("Password must contain at least one number");
|
|
40
|
+
}
|
|
41
|
+
if (policy.requireSpecial && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
|
42
|
+
errors.push("Password must contain at least one special character");
|
|
43
|
+
}
|
|
44
|
+
return errors.length === 0 ? true : errors;
|
|
45
|
+
}
|
|
46
|
+
export function obfuscateMagicCode(code) {
|
|
47
|
+
return code.slice(-2).padStart(code.length, "*");
|
|
48
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { RefreshTokenData, TokenRefreshConfig, EncryptionConfig } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Hash a refresh token using SHA-256
|
|
5
|
+
* @param token - The refresh token to hash
|
|
6
|
+
* @returns The hashed token as a hex string
|
|
7
|
+
*/
|
|
8
|
+
export declare function hashRefreshToken(token: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Encrypt data using AES-256-GCM
|
|
11
|
+
* @param data - Data to encrypt
|
|
12
|
+
* @param key - Encryption key (must be 32 bytes for AES-256)
|
|
13
|
+
* @returns Encrypted data as base64 string with IV prepended
|
|
14
|
+
*/
|
|
15
|
+
export declare function encryptData(data: unknown, key: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Decrypt data using AES-256-GCM
|
|
18
|
+
* @param encrypted - Encrypted data as base64 string
|
|
19
|
+
* @param key - Encryption key (must be 32 bytes for AES-256)
|
|
20
|
+
* @returns Decrypted data
|
|
21
|
+
*/
|
|
22
|
+
export declare function decryptData(encrypted: string, key: string): unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Get encryption config from runtime config
|
|
25
|
+
* @param event - H3 event (optional, for server context)
|
|
26
|
+
* @returns Encryption configuration
|
|
27
|
+
*/
|
|
28
|
+
export declare function getEncryptionConfig(event?: H3Event): EncryptionConfig;
|
|
29
|
+
/**
|
|
30
|
+
* Store refresh token data in persistent storage
|
|
31
|
+
* Handles encryption transparently if enabled
|
|
32
|
+
* @param tokenHash - Hashed refresh token (used as storage key)
|
|
33
|
+
* @param data - Refresh token data to store
|
|
34
|
+
* @param event - H3 event for runtime config access
|
|
35
|
+
*/
|
|
36
|
+
export declare function storeRefreshTokenData(tokenHash: string, data: RefreshTokenData, event?: H3Event): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Retrieve refresh token data from persistent storage
|
|
39
|
+
* Handles decryption transparently if enabled
|
|
40
|
+
* @param tokenHash - Hashed refresh token (storage key)
|
|
41
|
+
* @param event - H3 event for runtime config access
|
|
42
|
+
* @returns Refresh token data or null if not found
|
|
43
|
+
*/
|
|
44
|
+
export declare function getRefreshTokenData(tokenHash: string, event?: H3Event): Promise<RefreshTokenData | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Delete refresh token data from storage
|
|
47
|
+
* @param tokenHash - Hashed refresh token (storage key)
|
|
48
|
+
*/
|
|
49
|
+
export declare function deleteRefreshTokenData(tokenHash: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Revoke a refresh token (mark as revoked without deleting)
|
|
52
|
+
* @param tokenHash - Hashed refresh token (storage key)
|
|
53
|
+
* @param event - H3 event for runtime config access
|
|
54
|
+
*/
|
|
55
|
+
export declare function revokeRefreshToken(tokenHash: string, event?: H3Event): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Generates a refresh token and stores it with user data
|
|
58
|
+
*
|
|
59
|
+
* @param providerUserInfo - Complete OAuth provider user data
|
|
60
|
+
* @param provider - Provider name (e.g., 'google', 'github', 'microsoft', 'auth0')
|
|
61
|
+
* @param config - Token refresh configuration
|
|
62
|
+
* @param previousTokenHash - Hash of previous refresh token for rotation tracking
|
|
63
|
+
* @param event - H3Event for Nitro storage access
|
|
64
|
+
* @returns The generated refresh token string
|
|
65
|
+
*/
|
|
66
|
+
export declare function generateAndStoreRefreshToken(providerUserInfo: Record<string, unknown>, provider: string, config: TokenRefreshConfig, previousTokenHash?: string, event?: H3Event): Promise<string | undefined>;
|
|
67
|
+
/**
|
|
68
|
+
* Delete all refresh tokens for a specific user
|
|
69
|
+
* Used during password change or account deletion
|
|
70
|
+
* @param email - User email to match
|
|
71
|
+
* @param exceptTokenHash - Optional token hash to preserve (e.g. current session)
|
|
72
|
+
* @param event - H3 event for runtime config access
|
|
73
|
+
*/
|
|
74
|
+
export declare function deleteUserRefreshTokens(email: string, exceptTokenHash?: string, event?: H3Event): Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash, createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
|
+
import { useStorage, useRuntimeConfig } from "#imports";
|
|
3
|
+
export function hashRefreshToken(token) {
|
|
4
|
+
return createHash("sha256").update(token).digest("hex");
|
|
5
|
+
}
|
|
6
|
+
export function encryptData(data, key) {
|
|
7
|
+
const keyBuffer = Buffer.from(key.padEnd(32, "0").slice(0, 32));
|
|
8
|
+
const iv = randomBytes(12);
|
|
9
|
+
const cipher = createCipheriv("aes-256-gcm", keyBuffer, iv);
|
|
10
|
+
const jsonData = JSON.stringify(data);
|
|
11
|
+
let encrypted = cipher.update(jsonData, "utf8", "base64");
|
|
12
|
+
encrypted += cipher.final("base64");
|
|
13
|
+
const authTag = cipher.getAuthTag();
|
|
14
|
+
const combined = Buffer.concat([
|
|
15
|
+
iv,
|
|
16
|
+
authTag,
|
|
17
|
+
Buffer.from(encrypted, "base64")
|
|
18
|
+
]);
|
|
19
|
+
return combined.toString("base64");
|
|
20
|
+
}
|
|
21
|
+
export function decryptData(encrypted, key) {
|
|
22
|
+
try {
|
|
23
|
+
const keyBuffer = Buffer.from(key.padEnd(32, "0").slice(0, 32));
|
|
24
|
+
const combined = Buffer.from(encrypted, "base64");
|
|
25
|
+
const iv = combined.subarray(0, 12);
|
|
26
|
+
const authTag = combined.subarray(12, 28);
|
|
27
|
+
const encryptedData = combined.subarray(28);
|
|
28
|
+
const decipher = createDecipheriv("aes-256-gcm", keyBuffer, iv);
|
|
29
|
+
decipher.setAuthTag(authTag);
|
|
30
|
+
let decrypted = decipher.update(encryptedData.toString("base64"), "base64", "utf8");
|
|
31
|
+
decrypted += decipher.final("utf8");
|
|
32
|
+
return JSON.parse(decrypted);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error("Failed to decrypt data");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function getEncryptionConfig(event) {
|
|
38
|
+
const config = event ? useRuntimeConfig(event) : useRuntimeConfig();
|
|
39
|
+
return config.nuxtAegis?.tokenRefresh?.encryption || { enabled: false };
|
|
40
|
+
}
|
|
41
|
+
export async function storeRefreshTokenData(tokenHash, data, event) {
|
|
42
|
+
const encryptionConfig = getEncryptionConfig(event);
|
|
43
|
+
let dataToStore;
|
|
44
|
+
if (encryptionConfig.enabled) {
|
|
45
|
+
if (!encryptionConfig.key) {
|
|
46
|
+
throw new Error("[Nuxt Aegis] Encryption is enabled but no encryption key is configured");
|
|
47
|
+
}
|
|
48
|
+
const encrypted = encryptData(data, encryptionConfig.key);
|
|
49
|
+
dataToStore = { encrypted };
|
|
50
|
+
} else {
|
|
51
|
+
dataToStore = data;
|
|
52
|
+
}
|
|
53
|
+
await useStorage("refreshTokenStore").setItem(`${tokenHash}`, dataToStore);
|
|
54
|
+
}
|
|
55
|
+
export async function getRefreshTokenData(tokenHash, event) {
|
|
56
|
+
const encryptionConfig = getEncryptionConfig(event);
|
|
57
|
+
const storedData = await useStorage("refreshTokenStore").getItem(`${tokenHash}`);
|
|
58
|
+
if (!storedData) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if ("encrypted" in storedData && typeof storedData.encrypted === "string") {
|
|
62
|
+
if (!encryptionConfig.key) {
|
|
63
|
+
throw new Error("[Nuxt Aegis] Data is encrypted but no encryption key is configured");
|
|
64
|
+
}
|
|
65
|
+
return decryptData(storedData.encrypted, encryptionConfig.key);
|
|
66
|
+
}
|
|
67
|
+
return storedData;
|
|
68
|
+
}
|
|
69
|
+
export async function deleteRefreshTokenData(tokenHash) {
|
|
70
|
+
await useStorage("refreshTokenStore").removeItem(`${tokenHash}`);
|
|
71
|
+
}
|
|
72
|
+
export async function revokeRefreshToken(tokenHash, event) {
|
|
73
|
+
const data = await getRefreshTokenData(tokenHash, event);
|
|
74
|
+
if (!data) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
data.isRevoked = true;
|
|
78
|
+
await storeRefreshTokenData(tokenHash, data, event);
|
|
79
|
+
}
|
|
80
|
+
export async function generateAndStoreRefreshToken(providerUserInfo, provider, config, previousTokenHash, event) {
|
|
81
|
+
const refreshToken = randomBytes(32).toString("base64url");
|
|
82
|
+
const expiresIn = config.cookie?.maxAge || 604800;
|
|
83
|
+
await storeRefreshTokenData(hashRefreshToken(refreshToken), {
|
|
84
|
+
sub: String(providerUserInfo.sub || providerUserInfo.email || providerUserInfo.id || ""),
|
|
85
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
86
|
+
isRevoked: false,
|
|
87
|
+
previousTokenHash,
|
|
88
|
+
providerUserInfo,
|
|
89
|
+
// Store complete OAuth provider user data
|
|
90
|
+
provider
|
|
91
|
+
// Store provider name for custom claims refresh
|
|
92
|
+
}, event);
|
|
93
|
+
return refreshToken;
|
|
94
|
+
}
|
|
95
|
+
export async function deleteUserRefreshTokens(email, exceptTokenHash, event) {
|
|
96
|
+
const storage = useStorage("refreshTokenStore");
|
|
97
|
+
const keys = await storage.getKeys();
|
|
98
|
+
const normalizedEmail = email.toLowerCase();
|
|
99
|
+
for (const key of keys) {
|
|
100
|
+
if (key === exceptTokenHash) continue;
|
|
101
|
+
const data = await getRefreshTokenData(key, event);
|
|
102
|
+
if (!data) continue;
|
|
103
|
+
const userEmail = data.providerUserInfo?.email?.toLowerCase();
|
|
104
|
+
if (userEmail === normalizedEmail) {
|
|
105
|
+
await deleteRefreshTokenData(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ResetSessionData {
|
|
2
|
+
email: string;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Create a password reset session
|
|
7
|
+
*/
|
|
8
|
+
export declare function createResetSession(email: string, ttl?: number): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Validate and delete a password reset session
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateAndDeleteResetSession(sessionId: string): Promise<string | null>;
|