@civic/auth 0.13.0 → 0.13.1-beta.0
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/CHANGELOG.md +4 -0
- package/README.md +102 -1
- package/dist/lib/oauth.d.ts +12 -1
- package/dist/lib/oauth.d.ts.map +1 -1
- package/dist/lib/oauth.js +29 -1
- package/dist/lib/oauth.js.map +1 -1
- package/dist/nextjs/config.d.ts +2 -11
- package/dist/nextjs/config.d.ts.map +1 -1
- package/dist/nextjs/config.js.map +1 -1
- package/dist/nextjs/middleware.d.ts.map +1 -1
- package/dist/nextjs/middleware.js +18 -3
- package/dist/nextjs/middleware.js.map +1 -1
- package/dist/nextjs/routeHandler.d.ts.map +1 -1
- package/dist/nextjs/routeHandler.js +15 -71
- package/dist/nextjs/routeHandler.js.map +1 -1
- package/dist/nextjs/utils.d.ts +9 -3
- package/dist/nextjs/utils.d.ts.map +1 -1
- package/dist/nextjs/utils.js +10 -52
- package/dist/nextjs/utils.js.map +1 -1
- package/dist/server/config.d.ts +23 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js.map +1 -1
- package/dist/server/session.d.ts +57 -0
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +205 -9
- package/dist/server/session.js.map +1 -1
- package/dist/shared/lib/cookieConfig.d.ts.map +1 -1
- package/dist/shared/lib/cookieConfig.js +6 -1
- package/dist/shared/lib/cookieConfig.js.map +1 -1
- package/dist/shared/lib/types.d.ts +5 -1
- package/dist/shared/lib/types.d.ts.map +1 -1
- package/dist/shared/lib/types.js +4 -0
- package/dist/shared/lib/types.js.map +1 -1
- package/dist/shared/lib/util.d.ts +38 -1
- package/dist/shared/lib/util.d.ts.map +1 -1
- package/dist/shared/lib/util.js +95 -0
- package/dist/shared/lib/util.js.map +1 -1
- package/dist/shared/version.d.ts +1 -1
- package/dist/shared/version.d.ts.map +1 -1
- package/dist/shared/version.js +1 -1
- package/dist/shared/version.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/server/config.ts"],"names":[],"mappings":"","sourcesContent":["import type { Endpoints } from \"@/types.ts\";\n\n/**\n * Configuration for backend authentication endpoints\n * Allows customization of API endpoints when using backend integration (loginUrl)\n */\nexport interface BackendEndpoints {\n /** Endpoint for token refresh (default: \"/auth/refresh\") */\n refresh?: string;\n /** Endpoint for logout (default: \"/auth/logout\") */\n logout?: string;\n /** Endpoint for user info and session validation (default: \"/auth/user\") */\n user?: string;\n /** Endpoint for clearing session/cookies server-side (default: \"/auth/clearsession\") */\n clearSession?: string;\n}\n\nexport type AuthConfig = {\n clientSecret?: string; // Optional client secret for confidential clients\n pkce?: boolean; // Optional PKCE flag, defaults to true if not specified\n redirectUrl: string;\n oauthServer?: string;\n oauthServerBaseUrl?: string;\n challengeUrl?: string;\n refreshUrl?: string;\n endpointOverrides?: Partial<Endpoints> | undefined;\n postLogoutRedirectUrl?: string;\n /**\n * Custom backend endpoints configuration for backend integration\n * Only used when loginUrl is provided. Allows overriding default endpoints.\n */\n backendEndpoints?: BackendEndpoints;\n /**\n * Optional URL to redirect frontend clients back to after successful authentication.\n * When provided, the backend will automatically redirect SPA clients to this URL\n * instead of traditional server-side redirects. Useful for backend + frontend integration.\n * Example: \"http://localhost:5173\" or \"https://your-spa.com\"\n */\n loginSuccessUrl?: string;\n /**\n * Optional CORS configuration for authentication endpoints\n */\n cors?: {\n origin?: string | string[] | boolean;\n credentials?: boolean;\n optionsSuccessStatus?: number;\n allowedHeaders?: string[];\n exposedHeaders?: string[];\n };\n /**\n * Optional logger configuration for authentication middleware\n */\n logger?: {\n enabled?: boolean; // Defaults to true if not specified\n };\n /**\n * Optional flag to disable iframe detection in handleCallback.\n * When true, callbacks will always attempt to redirect instead of returning HTML content for iframes.\n * Useful for testing environments like Cypress where iframe detection may interfere with expected redirects.\n */\n disableIframeDetection?: boolean;\n /**\n * Optional flag to disable automatic token refresh functionality.\n * When true, the SDK will not attempt to refresh expired tokens automatically.\n * This affects both server-side session validation and client-side auto-refresh.\n * Useful for applications that want to handle token lifecycle manually.\n */\n disableRefresh?: boolean;\n} & (\n | {\n /** OAuth client ID - required for standard OAuth flow */\n clientId: string;\n /** Custom login URL for backend integration - optional */\n loginUrl?: string;\n }\n | {\n /** OAuth client ID - optional when using backend integration */\n clientId?: string;\n /** Custom login URL for backend integration - required when clientId is not provided */\n loginUrl: string;\n }\n);\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/server/config.ts"],"names":[],"mappings":"","sourcesContent":["import type { Endpoints } from \"@/types.ts\";\n\n/**\n * Controls how deep links (original URLs) are handled after authentication.\n *\n * - `\"fullUrl\"`: Redirect to the original URL the user tried to access.\n * `loginSuccessUrl` is used as fallback only when no deep link exists.\n * - `\"queryParamsOnly\"`: Redirect to `loginSuccessUrl`, but merge query params from original URL.\n * - `\"disabled\"`: No deep link preservation. Always use `loginSuccessUrl`.\n *\n * @default \"fullUrl\"\n */\nexport type DeepLinkHandling = \"fullUrl\" | \"queryParamsOnly\" | \"disabled\";\n\n/**\n * Configuration for backend authentication endpoints\n * Allows customization of API endpoints when using backend integration (loginUrl)\n */\nexport interface BackendEndpoints {\n /** Endpoint for token refresh (default: \"/auth/refresh\") */\n refresh?: string;\n /** Endpoint for logout (default: \"/auth/logout\") */\n logout?: string;\n /** Endpoint for user info and session validation (default: \"/auth/user\") */\n user?: string;\n /** Endpoint for clearing session/cookies server-side (default: \"/auth/clearsession\") */\n clearSession?: string;\n}\n\nexport type AuthConfig = {\n clientSecret?: string; // Optional client secret for confidential clients\n pkce?: boolean; // Optional PKCE flag, defaults to true if not specified\n redirectUrl: string;\n oauthServer?: string;\n oauthServerBaseUrl?: string;\n challengeUrl?: string;\n refreshUrl?: string;\n endpointOverrides?: Partial<Endpoints> | undefined;\n postLogoutRedirectUrl?: string;\n /**\n * Custom backend endpoints configuration for backend integration\n * Only used when loginUrl is provided. Allows overriding default endpoints.\n */\n backendEndpoints?: BackendEndpoints;\n /**\n * Optional URL to redirect frontend clients back to after successful authentication.\n * When provided, the backend will automatically redirect SPA clients to this URL\n * instead of traditional server-side redirects. Useful for backend + frontend integration.\n * Example: \"http://localhost:5173\" or \"https://your-spa.com\"\n */\n loginSuccessUrl?: string;\n /**\n * Optional CORS configuration for authentication endpoints\n */\n cors?: {\n origin?: string | string[] | boolean;\n credentials?: boolean;\n optionsSuccessStatus?: number;\n allowedHeaders?: string[];\n exposedHeaders?: string[];\n };\n /**\n * Optional logger configuration for authentication middleware\n */\n logger?: {\n enabled?: boolean; // Defaults to true if not specified\n };\n /**\n * Optional flag to disable iframe detection in handleCallback.\n * When true, callbacks will always attempt to redirect instead of returning HTML content for iframes.\n * Useful for testing environments like Cypress where iframe detection may interfere with expected redirects.\n */\n disableIframeDetection?: boolean;\n /**\n * Optional flag to disable automatic token refresh functionality.\n * When true, the SDK will not attempt to refresh expired tokens automatically.\n * This affects both server-side session validation and client-side auto-refresh.\n * Useful for applications that want to handle token lifecycle manually.\n */\n disableRefresh?: boolean;\n /**\n * Optional base path for URL handling.\n * When set, this will be prepended to relative URLs in handleCallback responses.\n * Commonly used with NextJS basePath configuration.\n */\n basePath?: string;\n /**\n * Controls how deep links (original URLs) are handled after authentication.\n * @see DeepLinkHandling\n * @default \"fullUrl\"\n */\n deepLinkHandling?: DeepLinkHandling;\n} & (\n | {\n /** OAuth client ID - required for standard OAuth flow */\n clientId: string;\n /** Custom login URL for backend integration - optional */\n loginUrl?: string;\n }\n | {\n /** OAuth client ID - optional when using backend integration */\n clientId?: string;\n /** Custom login URL for backend integration - required when clientId is not provided */\n loginUrl: string;\n }\n);\n"]}
|
package/dist/server/session.d.ts
CHANGED
|
@@ -91,6 +91,63 @@ export declare class CivicAuth {
|
|
|
91
91
|
* Clear all authentication tokens from storage
|
|
92
92
|
*/
|
|
93
93
|
clearTokens(): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Handles deep linking by computing the return URL and setting it as a cookie.
|
|
96
|
+
* This method encapsulates the deep-linking logic for use by middleware in any framework.
|
|
97
|
+
*
|
|
98
|
+
* The method automatically detects whether the user is at the login URL or a protected route
|
|
99
|
+
* and applies the appropriate logic:
|
|
100
|
+
*
|
|
101
|
+
* **At login URL:**
|
|
102
|
+
* - Auth redirect (marker present): Preserve existing deep link cookie
|
|
103
|
+
* - Fresh navigation with query params: Set cookie with query params
|
|
104
|
+
* - Fresh navigation without params: Clear stale cookie
|
|
105
|
+
*
|
|
106
|
+
* **At protected route:**
|
|
107
|
+
* - Always set the cookie to capture the deep link destination
|
|
108
|
+
*
|
|
109
|
+
* @param requestUrl - The full URL of the request being made (the page the user tried to access)
|
|
110
|
+
* @param originUrl - The origin URL of the application (e.g., "https://myapp.com")
|
|
111
|
+
* @returns The computed deep link destination, or null if deep linking is disabled or the URL is invalid
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* // In middleware for any framework
|
|
116
|
+
* if (!session.authenticated) {
|
|
117
|
+
* await civicAuth.handleDeepLinking(requestUrl, originUrl);
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
handleDeepLinking(requestUrl: string, originUrl: string): Promise<string | null>;
|
|
122
|
+
/**
|
|
123
|
+
* Internal: Handles deep linking when at a protected route.
|
|
124
|
+
* Sets the cookie to capture the user's intended destination and the auth redirect marker.
|
|
125
|
+
*/
|
|
126
|
+
private handleDeepLinkingAtProtectedRoute;
|
|
127
|
+
/**
|
|
128
|
+
* Internal: Handles deep linking when at the login URL.
|
|
129
|
+
* Checks the auth redirect marker and handles appropriately.
|
|
130
|
+
*/
|
|
131
|
+
private handleDeepLinkingAtLoginUrl;
|
|
132
|
+
/**
|
|
133
|
+
* Sets the auth redirect marker cookie. This marker helps distinguish auth redirects
|
|
134
|
+
* from fresh navigations to the login page, preventing the deep link cookie from being
|
|
135
|
+
* incorrectly overwritten.
|
|
136
|
+
*
|
|
137
|
+
* **Note:** This method is automatically called by `handleDeepLinking` when the user is
|
|
138
|
+
* at a protected route. You typically do NOT need to call this method manually unless
|
|
139
|
+
* you have a specific use case where you're not using `handleDeepLinking`.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* // In most cases, just call handleDeepLinking - it sets the marker automatically:
|
|
144
|
+
* if (!isAuthenticated) {
|
|
145
|
+
* await req.civicAuth.handleDeepLinking(requestUrl, originUrl);
|
|
146
|
+
* return res.redirect('/login');
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
setAuthRedirectMarker(): Promise<void>;
|
|
94
151
|
/**
|
|
95
152
|
* Framework-agnostic URL detection and resolution helpers
|
|
96
153
|
* These methods handle proxy environments and can be used by any framework
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,IAAI,EACT,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAE3B,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAoBrD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"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"}
|
package/dist/server/session.js
CHANGED
|
@@ -9,10 +9,10 @@ import { refreshTokens } from "../server/refresh.js";
|
|
|
9
9
|
import { getVersion } from "../shared/index.js";
|
|
10
10
|
import { ServerAuthenticationResolver } from "../server/ServerAuthenticationResolver.js";
|
|
11
11
|
import { DEFAULT_AUTH_SERVER, JWT_PAYLOAD_KNOWN_CLAIM_KEYS, } from "../constants.js";
|
|
12
|
-
import { displayModeFromState, loginSuccessUrlFromState } from "../lib/oauth.js";
|
|
12
|
+
import { displayModeFromState, injectLoginSuccessUrlIntoState, loginSuccessUrlFromState, } from "../lib/oauth.js";
|
|
13
13
|
import { decodeJwt } from "jose";
|
|
14
|
-
import { generateOauthLogoutUrl, getBackendEndpoints, resolveEndpointUrl, sanitizeReturnUrl, } from "../shared/lib/util.js";
|
|
15
|
-
import { CodeVerifier } from "../shared/lib/types.js";
|
|
14
|
+
import { computeDeepLinkDestination, generateOauthLogoutUrl, getBackendEndpoints, prependBasePath, resolveEndpointUrl, sanitizeReturnUrl, } from "../shared/lib/util.js";
|
|
15
|
+
import { AUTH_REDIRECT_MARKER_MAX_AGE, AuthFlowCookie, CodeVerifier, } from "../shared/lib/types.js";
|
|
16
16
|
import { loggers } from "../lib/logger.js";
|
|
17
17
|
// Function to omit keys from an object
|
|
18
18
|
const omitKeys = (keys, obj) => {
|
|
@@ -141,10 +141,29 @@ export class CivicAuth {
|
|
|
141
141
|
* @returns The login URL
|
|
142
142
|
*/
|
|
143
143
|
async buildLoginUrl(options) {
|
|
144
|
+
const logger = loggers.server;
|
|
145
|
+
let finalState = options?.state;
|
|
146
|
+
// If deep linking is enabled, read the return URL cookie and inject into state
|
|
147
|
+
if (this.authConfig.deepLinkHandling !== "disabled") {
|
|
148
|
+
try {
|
|
149
|
+
const returnUrl = await this.storage.get(AuthFlowCookie.RETURN_URL);
|
|
150
|
+
if (returnUrl) {
|
|
151
|
+
logger.debug("[buildLoginUrl] Found RETURN_URL cookie, injecting into state", { returnUrl });
|
|
152
|
+
// Inject the return URL into state, preserving any existing state
|
|
153
|
+
finalState = injectLoginSuccessUrlIntoState(finalState ?? null, returnUrl);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
logger.warn("[buildLoginUrl] Failed to read RETURN_URL cookie", {
|
|
158
|
+
error,
|
|
159
|
+
});
|
|
160
|
+
// Continue without the cookie - don't block login
|
|
161
|
+
}
|
|
162
|
+
}
|
|
144
163
|
return buildLoginUrl({
|
|
145
164
|
...this.authConfig,
|
|
146
165
|
scopes: options?.scopes,
|
|
147
|
-
state:
|
|
166
|
+
state: finalState,
|
|
148
167
|
nonce: options?.nonce,
|
|
149
168
|
framework: "server",
|
|
150
169
|
sdkVersion: getVersion(),
|
|
@@ -220,6 +239,162 @@ export class CivicAuth {
|
|
|
220
239
|
async clearTokens() {
|
|
221
240
|
return clearTokensUtil(this.storage);
|
|
222
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Handles deep linking by computing the return URL and setting it as a cookie.
|
|
244
|
+
* This method encapsulates the deep-linking logic for use by middleware in any framework.
|
|
245
|
+
*
|
|
246
|
+
* The method automatically detects whether the user is at the login URL or a protected route
|
|
247
|
+
* and applies the appropriate logic:
|
|
248
|
+
*
|
|
249
|
+
* **At login URL:**
|
|
250
|
+
* - Auth redirect (marker present): Preserve existing deep link cookie
|
|
251
|
+
* - Fresh navigation with query params: Set cookie with query params
|
|
252
|
+
* - Fresh navigation without params: Clear stale cookie
|
|
253
|
+
*
|
|
254
|
+
* **At protected route:**
|
|
255
|
+
* - Always set the cookie to capture the deep link destination
|
|
256
|
+
*
|
|
257
|
+
* @param requestUrl - The full URL of the request being made (the page the user tried to access)
|
|
258
|
+
* @param originUrl - The origin URL of the application (e.g., "https://myapp.com")
|
|
259
|
+
* @returns The computed deep link destination, or null if deep linking is disabled or the URL is invalid
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* // In middleware for any framework
|
|
264
|
+
* if (!session.authenticated) {
|
|
265
|
+
* await civicAuth.handleDeepLinking(requestUrl, originUrl);
|
|
266
|
+
* }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
async handleDeepLinking(requestUrl, originUrl) {
|
|
270
|
+
const logger = loggers.server;
|
|
271
|
+
const deepLinkHandling = this.authConfig.deepLinkHandling ?? "queryParamsOnly";
|
|
272
|
+
if (deepLinkHandling === "disabled") {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
// Parse the request URL to extract path, search, and hash
|
|
276
|
+
let parsedUrl;
|
|
277
|
+
try {
|
|
278
|
+
parsedUrl = new URL(requestUrl, originUrl);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
logger.warn("[handleDeepLinking] Failed to parse request URL:", {
|
|
282
|
+
requestUrl,
|
|
283
|
+
originUrl,
|
|
284
|
+
error,
|
|
285
|
+
});
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
// Determine if we're at the login URL
|
|
289
|
+
const loginUrl = this.authConfig.loginUrl || "/";
|
|
290
|
+
const loginPathWithoutBasePath = loginUrl.startsWith("/")
|
|
291
|
+
? loginUrl
|
|
292
|
+
: new URL(loginUrl, originUrl).pathname;
|
|
293
|
+
const isAtLoginUrl = parsedUrl.pathname === loginPathWithoutBasePath;
|
|
294
|
+
logger.debug("[handleDeepLinking]:", {
|
|
295
|
+
pathname: parsedUrl.pathname,
|
|
296
|
+
isAtLoginUrl,
|
|
297
|
+
loginUrl: loginPathWithoutBasePath,
|
|
298
|
+
});
|
|
299
|
+
if (isAtLoginUrl) {
|
|
300
|
+
// At login URL - use the updateDeepLinkCookie logic
|
|
301
|
+
return this.handleDeepLinkingAtLoginUrl(parsedUrl, originUrl);
|
|
302
|
+
}
|
|
303
|
+
// At protected route - set the deep link cookie
|
|
304
|
+
return this.handleDeepLinkingAtProtectedRoute(parsedUrl, originUrl);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Internal: Handles deep linking when at a protected route.
|
|
308
|
+
* Sets the cookie to capture the user's intended destination and the auth redirect marker.
|
|
309
|
+
*/
|
|
310
|
+
async handleDeepLinkingAtProtectedRoute(parsedUrl, originUrl) {
|
|
311
|
+
const logger = loggers.server;
|
|
312
|
+
const deepLinkHandling = this.authConfig.deepLinkHandling ?? "queryParamsOnly";
|
|
313
|
+
const returnTo = computeDeepLinkDestination(parsedUrl.pathname, parsedUrl.search, parsedUrl.hash, originUrl, deepLinkHandling, this.authConfig.loginSuccessUrl);
|
|
314
|
+
if (!returnTo) {
|
|
315
|
+
logger.debug("[handleDeepLinking] No deep link destination computed (disabled or invalid URL)");
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
// Set the cookie with the computed return URL
|
|
319
|
+
await this.storage.set(AuthFlowCookie.RETURN_URL, returnTo, {});
|
|
320
|
+
logger.debug("[handleDeepLinking] Set RETURN_URL cookie", { returnTo });
|
|
321
|
+
// Also set the auth redirect marker since we're at a protected route
|
|
322
|
+
// and the user will be redirected to login
|
|
323
|
+
await this.storage.set(AuthFlowCookie.AUTH_REDIRECT_MARKER, "1", {
|
|
324
|
+
maxAge: AUTH_REDIRECT_MARKER_MAX_AGE,
|
|
325
|
+
});
|
|
326
|
+
logger.debug("[handleDeepLinking] Set auth redirect marker");
|
|
327
|
+
return returnTo;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Internal: Handles deep linking when at the login URL.
|
|
331
|
+
* Checks the auth redirect marker and handles appropriately.
|
|
332
|
+
*/
|
|
333
|
+
async handleDeepLinkingAtLoginUrl(parsedUrl, originUrl) {
|
|
334
|
+
const logger = loggers.server;
|
|
335
|
+
const deepLinkHandling = this.authConfig.deepLinkHandling ?? "queryParamsOnly";
|
|
336
|
+
const hasQueryParams = parsedUrl.searchParams.size > 0;
|
|
337
|
+
// Check and clean up the auth redirect marker cookie
|
|
338
|
+
const isAuthRedirect = await this.storage.get(AuthFlowCookie.AUTH_REDIRECT_MARKER);
|
|
339
|
+
await this.storage.delete(AuthFlowCookie.AUTH_REDIRECT_MARKER);
|
|
340
|
+
// Get existing cookie to determine if we need to clear stale data
|
|
341
|
+
const existingCookie = await this.storage.get(AuthFlowCookie.RETURN_URL);
|
|
342
|
+
logger.debug("[handleDeepLinking] At login URL:", {
|
|
343
|
+
hasQueryParams,
|
|
344
|
+
isAuthRedirect: !!isAuthRedirect,
|
|
345
|
+
existingCookie,
|
|
346
|
+
});
|
|
347
|
+
if (isAuthRedirect) {
|
|
348
|
+
// This is a redirect from auth middleware - don't overwrite the cookie
|
|
349
|
+
logger.debug("[handleDeepLinking] Auth redirect detected - preserving existing cookie");
|
|
350
|
+
return existingCookie;
|
|
351
|
+
}
|
|
352
|
+
// Fresh navigation
|
|
353
|
+
if (hasQueryParams) {
|
|
354
|
+
// User visited with query params - capture them
|
|
355
|
+
const returnTo = computeDeepLinkDestination(parsedUrl.pathname, parsedUrl.search, parsedUrl.hash, originUrl, deepLinkHandling, this.authConfig.loginSuccessUrl);
|
|
356
|
+
if (returnTo) {
|
|
357
|
+
logger.debug(`[handleDeepLinking] Fresh visit with params - setting cookie to "${returnTo}"`);
|
|
358
|
+
await this.storage.set(AuthFlowCookie.RETURN_URL, returnTo, {});
|
|
359
|
+
return returnTo;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Fresh navigation without params - clear any stale cookie
|
|
363
|
+
if (existingCookie) {
|
|
364
|
+
logger.debug("[handleDeepLinking] Fresh visit without params - clearing stale cookie");
|
|
365
|
+
await this.storage.delete(AuthFlowCookie.RETURN_URL);
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Sets the auth redirect marker cookie. This marker helps distinguish auth redirects
|
|
371
|
+
* from fresh navigations to the login page, preventing the deep link cookie from being
|
|
372
|
+
* incorrectly overwritten.
|
|
373
|
+
*
|
|
374
|
+
* **Note:** This method is automatically called by `handleDeepLinking` when the user is
|
|
375
|
+
* at a protected route. You typically do NOT need to call this method manually unless
|
|
376
|
+
* you have a specific use case where you're not using `handleDeepLinking`.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```typescript
|
|
380
|
+
* // In most cases, just call handleDeepLinking - it sets the marker automatically:
|
|
381
|
+
* if (!isAuthenticated) {
|
|
382
|
+
* await req.civicAuth.handleDeepLinking(requestUrl, originUrl);
|
|
383
|
+
* return res.redirect('/login');
|
|
384
|
+
* }
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
async setAuthRedirectMarker() {
|
|
388
|
+
const logger = loggers.server;
|
|
389
|
+
const deepLinkHandling = this.authConfig.deepLinkHandling ?? "queryParamsOnly";
|
|
390
|
+
if (deepLinkHandling === "disabled") {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
await this.storage.set(AuthFlowCookie.AUTH_REDIRECT_MARKER, "1", {
|
|
394
|
+
maxAge: AUTH_REDIRECT_MARKER_MAX_AGE,
|
|
395
|
+
});
|
|
396
|
+
logger.debug("[setAuthRedirectMarker] Set auth redirect marker cookie");
|
|
397
|
+
}
|
|
223
398
|
/**
|
|
224
399
|
* Framework-agnostic URL detection and resolution helpers
|
|
225
400
|
* These methods handle proxy environments and can be used by any framework
|
|
@@ -373,7 +548,11 @@ export class CivicAuth {
|
|
|
373
548
|
newSearchParams.delete("loginSuccessUrl");
|
|
374
549
|
// Use preserved deep link if available and valid, otherwise fall back to loginSuccessUrl or "/"
|
|
375
550
|
// Note: Do NOT fall back to currentUrl.pathname as that's the callback URL, which would cause a loop
|
|
376
|
-
|
|
551
|
+
let redirectUrl = loginSuccessUrl || this.authConfig.loginSuccessUrl || "/";
|
|
552
|
+
// Apply basePath if configured
|
|
553
|
+
if (this.authConfig.basePath) {
|
|
554
|
+
redirectUrl = prependBasePath(redirectUrl, this.authConfig.basePath);
|
|
555
|
+
}
|
|
377
556
|
return {
|
|
378
557
|
content: {
|
|
379
558
|
success: true,
|
|
@@ -405,9 +584,13 @@ export class CivicAuth {
|
|
|
405
584
|
// "User already authenticated, skipping iframe workaround",
|
|
406
585
|
const user = await this.getUser();
|
|
407
586
|
const loginSuccessUrlFromStateValue = loginSuccessUrlFromState(state);
|
|
408
|
-
|
|
587
|
+
let frontendUrl = options?.frontendUrl ||
|
|
409
588
|
loginSuccessUrlFromStateValue ||
|
|
410
589
|
this.authConfig.loginSuccessUrl;
|
|
590
|
+
// Apply basePath to frontendUrl if configured
|
|
591
|
+
if (frontendUrl && this.authConfig.basePath) {
|
|
592
|
+
frontendUrl = prependBasePath(frontendUrl, this.authConfig.basePath);
|
|
593
|
+
}
|
|
411
594
|
// Check if this is an iframe context - if so, generate iframe completion HTML
|
|
412
595
|
const stateDisplayMode = displayModeFromState(state, undefined);
|
|
413
596
|
const isConfiguredForIframe = stateDisplayMode === "iframe";
|
|
@@ -440,9 +623,13 @@ export class CivicAuth {
|
|
|
440
623
|
if (isConfiguredForIframe && !this.authConfig.disableIframeDetection) {
|
|
441
624
|
// Generate HTML that will trigger same-domain callback
|
|
442
625
|
const loginSuccessUrlFromStateValue = loginSuccessUrlFromState(state);
|
|
443
|
-
|
|
626
|
+
let frontendUrl = options?.frontendUrl ||
|
|
444
627
|
loginSuccessUrlFromStateValue ||
|
|
445
628
|
this.authConfig.loginSuccessUrl;
|
|
629
|
+
// Apply basePath to frontendUrl if configured
|
|
630
|
+
if (frontendUrl && this.authConfig.basePath) {
|
|
631
|
+
frontendUrl = prependBasePath(frontendUrl, this.authConfig.basePath);
|
|
632
|
+
}
|
|
446
633
|
const callbackUrl = req.url || "";
|
|
447
634
|
const sameDomainHtml = this.generateSameDomainCallbackHtml(callbackUrl, frontendUrl);
|
|
448
635
|
return { content: sameDomainHtml };
|
|
@@ -458,9 +645,13 @@ export class CivicAuth {
|
|
|
458
645
|
// Extract loginSuccessUrl from state if present
|
|
459
646
|
const loginSuccessUrlFromStateValue = loginSuccessUrlFromState(state);
|
|
460
647
|
// Priority: options.frontendUrl > loginSuccessUrl from state > config loginSuccessUrl
|
|
461
|
-
|
|
648
|
+
let frontendUrl = options?.frontendUrl ||
|
|
462
649
|
loginSuccessUrlFromStateValue ||
|
|
463
650
|
this.authConfig.loginSuccessUrl;
|
|
651
|
+
// Apply basePath to frontendUrl if configured
|
|
652
|
+
if (frontendUrl && this.authConfig.basePath) {
|
|
653
|
+
frontendUrl = prependBasePath(frontendUrl, this.authConfig.basePath);
|
|
654
|
+
}
|
|
464
655
|
// Priority 1: Check state for display mode configuration
|
|
465
656
|
const stateDisplayMode = displayModeFromState(state, undefined);
|
|
466
657
|
const isConfiguredForIframe = stateDisplayMode === "iframe";
|
|
@@ -525,7 +716,12 @@ export class CivicAuth {
|
|
|
525
716
|
}
|
|
526
717
|
// Absolute fallback: redirect to loginSuccessUrl or "/"
|
|
527
718
|
// Never return JSON for browser navigation - that would display raw JSON to the user
|
|
528
|
-
|
|
719
|
+
const fallbackUrl = this.authConfig.loginSuccessUrl || "/";
|
|
720
|
+
return {
|
|
721
|
+
redirectTo: this.authConfig.basePath
|
|
722
|
+
? prependBasePath(fallbackUrl, this.authConfig.basePath)
|
|
723
|
+
: fallbackUrl,
|
|
724
|
+
};
|
|
529
725
|
}
|
|
530
726
|
/**
|
|
531
727
|
* Generate HTML content for iframe completion that sends postMessage to parent
|