@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,72 @@
|
|
|
1
|
+
import type { OAuthConfig, OAuthProviderConfig } from '../../types/index.js';
|
|
2
|
+
type ProviderKey = 'google' | 'microsoft' | 'github' | 'auth0' | 'mock';
|
|
3
|
+
/**
|
|
4
|
+
* Type mapping from provider keys to their configuration types
|
|
5
|
+
*/
|
|
6
|
+
type ProviderConfigMap = {
|
|
7
|
+
google: import('../../types').GoogleProviderConfig;
|
|
8
|
+
microsoft: import('../../types').MicrosoftProviderConfig;
|
|
9
|
+
github: import('../../types').GithubProviderConfig;
|
|
10
|
+
auth0: import('../../types').Auth0ProviderConfig;
|
|
11
|
+
mock: import('../../types').MockProviderConfig;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Validates and filters custom authorization parameters
|
|
15
|
+
* Removes protected OAuth parameters and logs warnings when they are attempted
|
|
16
|
+
*
|
|
17
|
+
* @param authorizationParams - Custom parameters from configuration
|
|
18
|
+
* @param providerKey - Provider identifier for logging
|
|
19
|
+
* @returns Filtered parameters safe to merge with OAuth query
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateAuthorizationParams(authorizationParams: Record<string, string> | undefined, providerKey: string): Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* OAuth provider implementation interface
|
|
24
|
+
* Defines the structure required for OAuth provider implementations
|
|
25
|
+
*/
|
|
26
|
+
export interface OAuthProviderImplementation<TKey extends ProviderKey = ProviderKey> {
|
|
27
|
+
/** Default configuration for the provider */
|
|
28
|
+
defaultConfig: Partial<OAuthProviderConfig>;
|
|
29
|
+
/** Authorization URL for the provider */
|
|
30
|
+
authorizeUrl: string;
|
|
31
|
+
/** Token exchange URL for the provider */
|
|
32
|
+
tokenUrl: string;
|
|
33
|
+
/** User info URL for the provider */
|
|
34
|
+
userInfoUrl: string;
|
|
35
|
+
/** Runtime config key for this provider (e.g., 'google', 'microsoft') */
|
|
36
|
+
runtimeConfigKey: TKey;
|
|
37
|
+
/** Extract user info from provider response */
|
|
38
|
+
extractUser: (userResponse: unknown) => {
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
};
|
|
41
|
+
/** Build authorization URL query parameters */
|
|
42
|
+
buildAuthQuery: (config: OAuthProviderConfig, redirectUri: string, state?: string) => Record<string, string>;
|
|
43
|
+
/** Build token exchange body parameters */
|
|
44
|
+
buildTokenBody: (config: OAuthProviderConfig, code: string, redirectUri: string) => Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Helper function to create a properly typed OAuth provider implementation
|
|
48
|
+
* Infers the provider key from the runtimeConfigKey property
|
|
49
|
+
*/
|
|
50
|
+
export declare function defineOAuthProvider<TKey extends ProviderKey>(implementation: OAuthProviderImplementation<TKey>): OAuthProviderImplementation<TKey>;
|
|
51
|
+
/**
|
|
52
|
+
* Base OAuth event handler that provides common OAuth flow functionality
|
|
53
|
+
*
|
|
54
|
+
* Handles the complete OAuth 2.0 authorization code flow with CODE-based token delivery:
|
|
55
|
+
*
|
|
56
|
+
* Initial Request (no code parameter):
|
|
57
|
+
* - Redirect user to provider's authorization page
|
|
58
|
+
* - Initiate OAuth flow
|
|
59
|
+
*
|
|
60
|
+
* Callback Request (with code parameter from provider):
|
|
61
|
+
* 1. Exchange authorization code for provider tokens
|
|
62
|
+
* 2. Validate tokens and extract user information
|
|
63
|
+
* 3. Generate cryptographically secure authorization CODE
|
|
64
|
+
* 4. Store CODE with user data (60s expiration, configurable)
|
|
65
|
+
* 5. Redirect to /auth/callback with CODE as query parameter
|
|
66
|
+
*
|
|
67
|
+
* Error Handling:
|
|
68
|
+
* - Generic error redirect to prevent information leakage
|
|
69
|
+
* - Security event logging for all failures
|
|
70
|
+
*/
|
|
71
|
+
export declare function defineOAuthEventHandler<TKey extends ProviderKey, TConfig extends ProviderConfigMap[TKey] = ProviderConfigMap[TKey]>(implementation: OAuthProviderImplementation<TKey>, { config, onError, customClaims: _customClaims, onUserInfo: _onUserInfo, onSuccess: _onSuccess, }: OAuthConfig<TConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { eventHandler, getRequestURL, getQuery, sendRedirect, createError } from "h3";
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
import { useNitroApp, useRuntimeConfig } from "#imports";
|
|
4
|
+
import { withQuery } from "ufo";
|
|
5
|
+
import { generateAuthCode, storeAuthCode } from "../utils/authCodeStore.js";
|
|
6
|
+
import { createLogger } from "../utils/logger.js";
|
|
7
|
+
import { useAegisHandler } from "../utils/handler.js";
|
|
8
|
+
const logger = createLogger("OAuth");
|
|
9
|
+
const PROTECTED_PARAMS = ["client_id", "redirect_uri", "code", "grant_type"];
|
|
10
|
+
export function validateAuthorizationParams(authorizationParams, providerKey) {
|
|
11
|
+
if (!authorizationParams) {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
const filtered = {};
|
|
15
|
+
const protectedFound = [];
|
|
16
|
+
for (const [key, value] of Object.entries(authorizationParams)) {
|
|
17
|
+
if (PROTECTED_PARAMS.includes(key)) {
|
|
18
|
+
protectedFound.push(key);
|
|
19
|
+
} else {
|
|
20
|
+
filtered[key] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (protectedFound.length > 0) {
|
|
24
|
+
logger.warn(`Protected OAuth parameters cannot be overridden in authorizationParams for ${providerKey}:`, {
|
|
25
|
+
attempted: protectedFound,
|
|
26
|
+
protected: PROTECTED_PARAMS,
|
|
27
|
+
message: "These parameters are ignored for security reasons"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return filtered;
|
|
31
|
+
}
|
|
32
|
+
export function defineOAuthProvider(implementation) {
|
|
33
|
+
return implementation;
|
|
34
|
+
}
|
|
35
|
+
function getOAuthRedirectUri(event) {
|
|
36
|
+
const requestURL = getRequestURL(event);
|
|
37
|
+
return `${requestURL.protocol}//${requestURL.host}${requestURL.pathname}`;
|
|
38
|
+
}
|
|
39
|
+
export function defineOAuthEventHandler(implementation, {
|
|
40
|
+
config,
|
|
41
|
+
onError,
|
|
42
|
+
customClaims: _customClaims,
|
|
43
|
+
onUserInfo: _onUserInfo,
|
|
44
|
+
onSuccess: _onSuccess
|
|
45
|
+
}) {
|
|
46
|
+
return eventHandler(async (event) => {
|
|
47
|
+
try {
|
|
48
|
+
const runtimeConfig = useRuntimeConfig(event).nuxtAegis;
|
|
49
|
+
const providerRuntimeConfig = runtimeConfig.providers?.[implementation.runtimeConfigKey] || {};
|
|
50
|
+
const mergedConfig = defu(config, providerRuntimeConfig, implementation.defaultConfig);
|
|
51
|
+
if (!mergedConfig.clientId || !mergedConfig.clientSecret) {
|
|
52
|
+
throw createError({
|
|
53
|
+
statusCode: 500,
|
|
54
|
+
statusMessage: "Internal Server Error",
|
|
55
|
+
message: `OAuth provider ${implementation.runtimeConfigKey} is not properly configured`
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const query = getQuery(event);
|
|
59
|
+
if (query.error) {
|
|
60
|
+
throw createError({
|
|
61
|
+
statusCode: 400,
|
|
62
|
+
statusMessage: "OAuth Error",
|
|
63
|
+
message: `OAuth provider returned error: ${query.error}`
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const redirectUri = mergedConfig.redirectUri || getOAuthRedirectUri(event);
|
|
67
|
+
if (!query.code) {
|
|
68
|
+
const authQuery = implementation.buildAuthQuery(mergedConfig, redirectUri, query.state);
|
|
69
|
+
return sendRedirect(event, withQuery(implementation.authorizeUrl, authQuery));
|
|
70
|
+
}
|
|
71
|
+
const tokenBody = implementation.buildTokenBody(mergedConfig, query.code, redirectUri);
|
|
72
|
+
const tokenResponse = await $fetch(implementation.tokenUrl, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
76
|
+
"Accept": "application/json"
|
|
77
|
+
},
|
|
78
|
+
body: new URLSearchParams(tokenBody)
|
|
79
|
+
});
|
|
80
|
+
const { access_token, refresh_token, id_token, expires_in } = tokenResponse;
|
|
81
|
+
if (!access_token) {
|
|
82
|
+
throw createError({
|
|
83
|
+
statusCode: 400,
|
|
84
|
+
statusMessage: "OAuth Token Error",
|
|
85
|
+
message: "Failed to obtain access token from provider"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const userResponse = await $fetch(implementation.userInfoUrl, {
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${access_token}`
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
let providerUserInfo = implementation.extractUser(userResponse);
|
|
94
|
+
const tokens = { access_token, refresh_token, id_token, expires_in };
|
|
95
|
+
if (_onUserInfo) {
|
|
96
|
+
providerUserInfo = await _onUserInfo(providerUserInfo, tokens, event);
|
|
97
|
+
} else {
|
|
98
|
+
const handler = useAegisHandler();
|
|
99
|
+
if (handler?.onUserInfo) {
|
|
100
|
+
const hookPayload = {
|
|
101
|
+
providerUserInfo,
|
|
102
|
+
tokens,
|
|
103
|
+
provider: implementation.runtimeConfigKey,
|
|
104
|
+
event
|
|
105
|
+
};
|
|
106
|
+
const transformedUser = await handler.onUserInfo(hookPayload);
|
|
107
|
+
if (transformedUser) {
|
|
108
|
+
providerUserInfo = transformedUser;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let resolvedCustomClaims;
|
|
113
|
+
if (_customClaims) {
|
|
114
|
+
if (typeof _customClaims === "function") {
|
|
115
|
+
resolvedCustomClaims = await _customClaims(providerUserInfo, tokens);
|
|
116
|
+
} else {
|
|
117
|
+
resolvedCustomClaims = _customClaims;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (_onSuccess) {
|
|
121
|
+
await _onSuccess({
|
|
122
|
+
providerUserInfo,
|
|
123
|
+
tokens,
|
|
124
|
+
provider: implementation.runtimeConfigKey,
|
|
125
|
+
event
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const nitroApp = useNitroApp();
|
|
129
|
+
const successPayload = {
|
|
130
|
+
providerUserInfo,
|
|
131
|
+
tokens,
|
|
132
|
+
provider: implementation.runtimeConfigKey,
|
|
133
|
+
event
|
|
134
|
+
};
|
|
135
|
+
await nitroApp.hooks.callHook("nuxt-aegis:success", successPayload);
|
|
136
|
+
try {
|
|
137
|
+
const authCode = generateAuthCode();
|
|
138
|
+
const authCodeExpiresIn = runtimeConfig.authCode?.expiresIn || 60;
|
|
139
|
+
await storeAuthCode(
|
|
140
|
+
authCode,
|
|
141
|
+
providerUserInfo,
|
|
142
|
+
tokens,
|
|
143
|
+
implementation.runtimeConfigKey,
|
|
144
|
+
resolvedCustomClaims,
|
|
145
|
+
authCodeExpiresIn,
|
|
146
|
+
event
|
|
147
|
+
);
|
|
148
|
+
logger.security("OAuth authentication successful, redirecting with CODE", {
|
|
149
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
150
|
+
event: "OAUTH_SUCCESS_REDIRECT",
|
|
151
|
+
codePrefix: `${authCode.substring(0, 8)}...`
|
|
152
|
+
});
|
|
153
|
+
const callbackUrl = new URL(runtimeConfig.endpoints?.callbackPath || "/auth/callback", getOAuthRedirectUri(event));
|
|
154
|
+
callbackUrl.searchParams.set("code", authCode);
|
|
155
|
+
return sendRedirect(event, callbackUrl.href);
|
|
156
|
+
} catch (codeError) {
|
|
157
|
+
logger.error("Authorization code generation/storage failed", {
|
|
158
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
159
|
+
event: "CODE_GENERATION_ERROR",
|
|
160
|
+
error: import.meta.dev ? codeError : "Error details hidden in production",
|
|
161
|
+
severity: "error"
|
|
162
|
+
});
|
|
163
|
+
const errorUrl = new URL(runtimeConfig.endpoints?.callbackPath || "/auth/callback", getOAuthRedirectUri(event));
|
|
164
|
+
errorUrl.searchParams.set("error", "authentication_failed");
|
|
165
|
+
return sendRedirect(event, errorUrl.href);
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error("OAuth authentication error", {
|
|
169
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
170
|
+
event: "OAUTH_AUTH_ERROR",
|
|
171
|
+
error: import.meta.dev ? error : "Error details hidden in production",
|
|
172
|
+
severity: "error"
|
|
173
|
+
});
|
|
174
|
+
if (onError) {
|
|
175
|
+
return await onError(event, error);
|
|
176
|
+
}
|
|
177
|
+
const runtimeConfig = useRuntimeConfig(event).nuxtAegis;
|
|
178
|
+
const errorUrl = new URL(runtimeConfig.endpoints?.callbackPath || "/auth/callback", getOAuthRedirectUri(event));
|
|
179
|
+
errorUrl.searchParams.set("error", "authentication_failed");
|
|
180
|
+
return sendRedirect(event, errorUrl.href);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /auth/impersonate
|
|
3
|
+
* Allows privileged users (admins) to impersonate another user
|
|
4
|
+
*
|
|
5
|
+
* Request body:
|
|
6
|
+
* - targetUserId: string (required) - The ID of the user to impersonate
|
|
7
|
+
* - reason: string (optional) - Reason for impersonation
|
|
8
|
+
*
|
|
9
|
+
* Response:
|
|
10
|
+
* - accessToken: string - JWT for impersonated session (no refresh token)
|
|
11
|
+
*
|
|
12
|
+
* Requirements:
|
|
13
|
+
* - Impersonation feature must be enabled in config
|
|
14
|
+
* - Requester must pass permission check (default: admin role)
|
|
15
|
+
* - Target user must exist
|
|
16
|
+
* - Cannot impersonate while already impersonating
|
|
17
|
+
*/
|
|
18
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
19
|
+
accessToken: string;
|
|
20
|
+
}>>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineEventHandler, readBody, createError, getHeader } from "h3";
|
|
2
|
+
import { verifyToken } from "../utils/jwt.js";
|
|
3
|
+
import { startImpersonation } from "../utils/impersonation.js";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
import { createLogger } from "../utils/logger.js";
|
|
6
|
+
const logger = createLogger("Impersonate");
|
|
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 currentUser = await verifyToken(token, tokenConfig.secret);
|
|
32
|
+
if (!currentUser) {
|
|
33
|
+
throw createError({
|
|
34
|
+
statusCode: 401,
|
|
35
|
+
message: "Invalid or expired token"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const body = await readBody(event);
|
|
39
|
+
if (!body || !body.targetUserId) {
|
|
40
|
+
throw createError({
|
|
41
|
+
statusCode: 400,
|
|
42
|
+
message: "targetUserId is required"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const accessToken = await startImpersonation(
|
|
46
|
+
currentUser,
|
|
47
|
+
body.targetUserId,
|
|
48
|
+
body.reason,
|
|
49
|
+
event
|
|
50
|
+
);
|
|
51
|
+
logger.security("Impersonation started", {
|
|
52
|
+
admin: currentUser.sub,
|
|
53
|
+
target: body.targetUserId,
|
|
54
|
+
reason: body.reason
|
|
55
|
+
});
|
|
56
|
+
return { accessToken };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.error("Impersonation failed:", error);
|
|
59
|
+
const err = error;
|
|
60
|
+
if (err.statusCode) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
throw createError({
|
|
64
|
+
statusCode: 500,
|
|
65
|
+
message: err.message || "Failed to start impersonation"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /auth/logout
|
|
3
|
+
* Ends the user session by clearing authentication cookies and revoking refresh token
|
|
4
|
+
*/
|
|
5
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
6
|
+
success: boolean;
|
|
7
|
+
message: string;
|
|
8
|
+
}>>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineEventHandler, getCookie } from "h3";
|
|
2
|
+
import { hashRefreshToken, revokeRefreshToken } from "../utils/refreshToken.js";
|
|
3
|
+
import { clearToken } from "../utils/cookies.js";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
import { createLogger } from "../utils/logger.js";
|
|
6
|
+
const logger = createLogger("Logout");
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const config = useRuntimeConfig(event);
|
|
9
|
+
const cookieConfig = config.nuxtAegis?.tokenRefresh?.cookie;
|
|
10
|
+
try {
|
|
11
|
+
const cookieName = cookieConfig?.cookieName || "nuxt-aegis-refresh";
|
|
12
|
+
const refreshToken = getCookie(event, cookieName);
|
|
13
|
+
if (refreshToken) {
|
|
14
|
+
const hashedRefreshToken = hashRefreshToken(refreshToken);
|
|
15
|
+
await revokeRefreshToken(hashedRefreshToken, event);
|
|
16
|
+
}
|
|
17
|
+
clearToken(event, cookieConfig);
|
|
18
|
+
return { success: true, message: "Logout successful" };
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logger.error("Logout error:", error);
|
|
21
|
+
clearToken(event, cookieConfig);
|
|
22
|
+
return { success: true, message: "Logout completed" };
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineEventHandler, createError } from "h3";
|
|
2
|
+
export default defineEventHandler(async (event) => {
|
|
3
|
+
if (event.context.user) {
|
|
4
|
+
return event.context.user;
|
|
5
|
+
}
|
|
6
|
+
throw createError({
|
|
7
|
+
statusCode: 401,
|
|
8
|
+
statusMessage: "Unauthorized",
|
|
9
|
+
message: "Authentication required"
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock OAuth Authorization Endpoint
|
|
3
|
+
* Simulates: OAuth provider's authorization endpoint
|
|
4
|
+
*
|
|
5
|
+
* DEVELOPMENT/TEST ONLY
|
|
6
|
+
*
|
|
7
|
+
* This simulates an OAuth provider's authorization flow.
|
|
8
|
+
* In real OAuth, users would see a login/consent page.
|
|
9
|
+
* For testing, we auto-approve and redirect back immediately.
|
|
10
|
+
*
|
|
11
|
+
* Query Parameters:
|
|
12
|
+
* - response_type: Should be 'code'
|
|
13
|
+
* - client_id: OAuth client ID
|
|
14
|
+
* - redirect_uri: Where to redirect back
|
|
15
|
+
* - scope: Requested scopes
|
|
16
|
+
* - state: Optional state parameter for CSRF protection
|
|
17
|
+
* - user: (Optional) User persona identifier from mockUsers config
|
|
18
|
+
* - mock_error: (Optional) Simulate OAuth error response
|
|
19
|
+
*
|
|
20
|
+
* Error Simulation:
|
|
21
|
+
* - ?mock_error=access_denied - User denied access
|
|
22
|
+
* - ?mock_error=invalid_request - Invalid request parameters
|
|
23
|
+
* - ?mock_error=unauthorized_client - Client not authorized
|
|
24
|
+
* - ?mock_error=invalid_scope - Invalid scope requested
|
|
25
|
+
* - ?mock_error=server_error - Server error
|
|
26
|
+
* - ?mock_error=temporarily_unavailable - Service temporarily unavailable
|
|
27
|
+
*/
|
|
28
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
29
|
+
export default _default;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { defineEventHandler, getQuery, sendRedirect, createError } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { generateMockCode, storeMockCode } from "../../utils/mockCodeStore.js";
|
|
5
|
+
import { createLogger } from "../../utils/logger.js";
|
|
6
|
+
const logger = createLogger("MockAuthorize");
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const query = getQuery(event);
|
|
9
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
10
|
+
const mockConfig = runtimeConfig.nuxtAegis?.providers?.mock;
|
|
11
|
+
if (!mockConfig) {
|
|
12
|
+
throw createError({
|
|
13
|
+
statusCode: 500,
|
|
14
|
+
statusMessage: "Internal Server Error",
|
|
15
|
+
message: "[nuxt-aegis] Mock provider not configured"
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (!query.client_id) {
|
|
19
|
+
throw createError({
|
|
20
|
+
statusCode: 400,
|
|
21
|
+
statusMessage: "Bad Request",
|
|
22
|
+
message: "Missing required parameter: client_id"
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (!query.redirect_uri || typeof query.redirect_uri !== "string") {
|
|
26
|
+
throw createError({
|
|
27
|
+
statusCode: 400,
|
|
28
|
+
statusMessage: "Bad Request",
|
|
29
|
+
message: "Missing required parameter: redirect_uri"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (query.response_type !== "code") {
|
|
33
|
+
throw createError({
|
|
34
|
+
statusCode: 400,
|
|
35
|
+
statusMessage: "Bad Request",
|
|
36
|
+
message: 'Invalid response_type. Must be "code"'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (query.mock_error) {
|
|
40
|
+
const validErrors = [
|
|
41
|
+
"access_denied",
|
|
42
|
+
"invalid_request",
|
|
43
|
+
"unauthorized_client",
|
|
44
|
+
"invalid_scope",
|
|
45
|
+
"server_error",
|
|
46
|
+
"temporarily_unavailable"
|
|
47
|
+
];
|
|
48
|
+
const errorCode = query.mock_error;
|
|
49
|
+
const errorDescription = getErrorDescription(errorCode);
|
|
50
|
+
if (!validErrors.includes(errorCode)) {
|
|
51
|
+
console.warn(`[nuxt-aegis] Invalid mock_error: ${errorCode}. Valid errors:`, validErrors);
|
|
52
|
+
}
|
|
53
|
+
return sendRedirect(event, withQuery(query.redirect_uri, {
|
|
54
|
+
error: errorCode,
|
|
55
|
+
error_description: errorDescription,
|
|
56
|
+
state: query.state || ""
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
const userParam = query.user;
|
|
60
|
+
const userId = userParam || mockConfig.defaultUser || Object.keys(mockConfig.mockUsers)[0];
|
|
61
|
+
if (!userId) {
|
|
62
|
+
throw createError({
|
|
63
|
+
statusCode: 500,
|
|
64
|
+
statusMessage: "Internal Server Error",
|
|
65
|
+
message: "[nuxt-aegis] Mock provider has no users configured"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (!mockConfig.mockUsers[userId]) {
|
|
69
|
+
throw createError({
|
|
70
|
+
statusCode: 400,
|
|
71
|
+
statusMessage: "Bad Request",
|
|
72
|
+
message: `[nuxt-aegis] Invalid user persona: ${userId}. Available: ${Object.keys(mockConfig.mockUsers).join(", ")}`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const code = generateMockCode();
|
|
76
|
+
storeMockCode({
|
|
77
|
+
code,
|
|
78
|
+
userId,
|
|
79
|
+
// Store the user persona identifier
|
|
80
|
+
clientId: query.client_id,
|
|
81
|
+
redirectUri: query.redirect_uri
|
|
82
|
+
});
|
|
83
|
+
logger.debug("Generated authorization code:", {
|
|
84
|
+
code: code.substring(0, 20) + "...",
|
|
85
|
+
user: userId,
|
|
86
|
+
clientId: query.client_id
|
|
87
|
+
});
|
|
88
|
+
return sendRedirect(event, withQuery(query.redirect_uri, {
|
|
89
|
+
code,
|
|
90
|
+
state: query.state || ""
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
function getErrorDescription(errorCode) {
|
|
94
|
+
const descriptions = {
|
|
95
|
+
access_denied: "The user denied the authorization request",
|
|
96
|
+
invalid_request: "The request is missing a required parameter or is otherwise malformed",
|
|
97
|
+
unauthorized_client: "The client is not authorized to request an authorization code",
|
|
98
|
+
invalid_scope: "The requested scope is invalid, unknown, or malformed",
|
|
99
|
+
server_error: "The authorization server encountered an unexpected error",
|
|
100
|
+
temporarily_unavailable: "The authorization server is currently unable to handle the request"
|
|
101
|
+
};
|
|
102
|
+
return descriptions[errorCode] || "Mock OAuth error for testing";
|
|
103
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock OAuth Token Endpoint
|
|
3
|
+
* Simulates: OAuth provider's token exchange endpoint
|
|
4
|
+
*
|
|
5
|
+
* DEVELOPMENT/TEST ONLY
|
|
6
|
+
*
|
|
7
|
+
* Exchanges authorization code for OAuth tokens.
|
|
8
|
+
*
|
|
9
|
+
* Request Body:
|
|
10
|
+
* - code: Authorization code from authorize endpoint
|
|
11
|
+
* - client_id: OAuth client ID
|
|
12
|
+
* - client_secret: OAuth client secret
|
|
13
|
+
* - redirect_uri: Same redirect_uri used in authorize
|
|
14
|
+
* - grant_type: Should be 'authorization_code'
|
|
15
|
+
*
|
|
16
|
+
* Response:
|
|
17
|
+
* - access_token: OAuth access token
|
|
18
|
+
* - refresh_token: OAuth refresh token
|
|
19
|
+
* - id_token: JWT ID token (optional)
|
|
20
|
+
* - expires_in: Token lifetime in seconds
|
|
21
|
+
* - token_type: Always 'Bearer'
|
|
22
|
+
*/
|
|
23
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
24
|
+
access_token: string;
|
|
25
|
+
refresh_token: string;
|
|
26
|
+
id_token: string;
|
|
27
|
+
expires_in: number;
|
|
28
|
+
token_type: string;
|
|
29
|
+
scope: string;
|
|
30
|
+
}>>;
|
|
31
|
+
export default _default;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { defineEventHandler, readBody, createError } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { retrieveAndDeleteMockCode, storeMockToken } from "../../utils/mockCodeStore.js";
|
|
4
|
+
import { createLogger } from "../../utils/logger.js";
|
|
5
|
+
const logger = createLogger("MockToken");
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
const body = await readBody(event);
|
|
8
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
9
|
+
const mockConfig = runtimeConfig.nuxtAegis?.providers?.mock;
|
|
10
|
+
if (!mockConfig) {
|
|
11
|
+
throw createError({
|
|
12
|
+
statusCode: 500,
|
|
13
|
+
statusMessage: "Internal Server Error",
|
|
14
|
+
message: "[nuxt-aegis] Mock provider not configured"
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (!body.code) {
|
|
18
|
+
throw createError({
|
|
19
|
+
statusCode: 400,
|
|
20
|
+
statusMessage: "invalid_request",
|
|
21
|
+
message: "Missing required parameter: code"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (!body.client_id) {
|
|
25
|
+
throw createError({
|
|
26
|
+
statusCode: 400,
|
|
27
|
+
statusMessage: "invalid_request",
|
|
28
|
+
message: "Missing required parameter: client_id"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!body.client_secret) {
|
|
32
|
+
throw createError({
|
|
33
|
+
statusCode: 400,
|
|
34
|
+
statusMessage: "invalid_request",
|
|
35
|
+
message: "Missing required parameter: client_secret"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (body.grant_type !== "authorization_code") {
|
|
39
|
+
throw createError({
|
|
40
|
+
statusCode: 400,
|
|
41
|
+
statusMessage: "unsupported_grant_type",
|
|
42
|
+
message: 'Invalid grant_type. Must be "authorization_code"'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const codeData = retrieveAndDeleteMockCode(body.code);
|
|
46
|
+
if (!codeData) {
|
|
47
|
+
throw createError({
|
|
48
|
+
statusCode: 400,
|
|
49
|
+
statusMessage: "invalid_grant",
|
|
50
|
+
message: "Invalid authorization code"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (codeData.clientId !== body.client_id) {
|
|
54
|
+
throw createError({
|
|
55
|
+
statusCode: 400,
|
|
56
|
+
statusMessage: "invalid_grant",
|
|
57
|
+
message: "Client ID mismatch"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (codeData.redirectUri !== body.redirect_uri) {
|
|
61
|
+
throw createError({
|
|
62
|
+
statusCode: 400,
|
|
63
|
+
statusMessage: "invalid_grant",
|
|
64
|
+
message: "Redirect URI mismatch"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (body.client_secret !== mockConfig.clientSecret) {
|
|
68
|
+
throw createError({
|
|
69
|
+
statusCode: 401,
|
|
70
|
+
statusMessage: "invalid_client",
|
|
71
|
+
message: "Invalid client credentials"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
logger.debug("Token exchange successful for user:", codeData.userId);
|
|
75
|
+
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
76
|
+
const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
77
|
+
const accessToken = `mock_aegis_access_${randomHex}`;
|
|
78
|
+
storeMockToken(accessToken, codeData.userId);
|
|
79
|
+
return {
|
|
80
|
+
access_token: accessToken,
|
|
81
|
+
refresh_token: `mock_aegis_refresh_${randomHex.slice(0, 32)}`,
|
|
82
|
+
id_token: `mock.aegis.idtoken.${randomHex.slice(0, 24)}`,
|
|
83
|
+
expires_in: 3600,
|
|
84
|
+
// 1 hour
|
|
85
|
+
token_type: "Bearer",
|
|
86
|
+
scope: mockConfig.scopes?.join(" ") || "openid profile email"
|
|
87
|
+
};
|
|
88
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock OAuth UserInfo Endpoint
|
|
3
|
+
* Simulates: OAuth provider's user profile endpoint
|
|
4
|
+
*
|
|
5
|
+
* DEVELOPMENT/TEST ONLY
|
|
6
|
+
*
|
|
7
|
+
* Returns user information based on the access token.
|
|
8
|
+
* The user data is retrieved from the mockUsers configuration
|
|
9
|
+
* using the token->user mapping stored during token exchange.
|
|
10
|
+
*
|
|
11
|
+
* Headers:
|
|
12
|
+
* - Authorization: Bearer <access_token>
|
|
13
|
+
*
|
|
14
|
+
* Response:
|
|
15
|
+
* User profile data from mockUsers configuration, including:
|
|
16
|
+
* - sub: User subject identifier (required)
|
|
17
|
+
* - email: User's email address (required)
|
|
18
|
+
* - name: User's full name (required)
|
|
19
|
+
* - Additional custom claims as defined in mockUsers
|
|
20
|
+
*/
|
|
21
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
sub: string;
|
|
24
|
+
email: string;
|
|
25
|
+
name: string;
|
|
26
|
+
}>>;
|
|
27
|
+
export default _default;
|