@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +102 -1
  3. package/dist/lib/oauth.d.ts +12 -1
  4. package/dist/lib/oauth.d.ts.map +1 -1
  5. package/dist/lib/oauth.js +29 -1
  6. package/dist/lib/oauth.js.map +1 -1
  7. package/dist/nextjs/config.d.ts +2 -11
  8. package/dist/nextjs/config.d.ts.map +1 -1
  9. package/dist/nextjs/config.js.map +1 -1
  10. package/dist/nextjs/middleware.d.ts.map +1 -1
  11. package/dist/nextjs/middleware.js +18 -3
  12. package/dist/nextjs/middleware.js.map +1 -1
  13. package/dist/nextjs/routeHandler.d.ts.map +1 -1
  14. package/dist/nextjs/routeHandler.js +15 -71
  15. package/dist/nextjs/routeHandler.js.map +1 -1
  16. package/dist/nextjs/utils.d.ts +9 -3
  17. package/dist/nextjs/utils.d.ts.map +1 -1
  18. package/dist/nextjs/utils.js +10 -52
  19. package/dist/nextjs/utils.js.map +1 -1
  20. package/dist/server/config.d.ts +23 -0
  21. package/dist/server/config.d.ts.map +1 -1
  22. package/dist/server/config.js.map +1 -1
  23. package/dist/server/session.d.ts +57 -0
  24. package/dist/server/session.d.ts.map +1 -1
  25. package/dist/server/session.js +205 -9
  26. package/dist/server/session.js.map +1 -1
  27. package/dist/shared/lib/cookieConfig.d.ts.map +1 -1
  28. package/dist/shared/lib/cookieConfig.js +6 -1
  29. package/dist/shared/lib/cookieConfig.js.map +1 -1
  30. package/dist/shared/lib/types.d.ts +5 -1
  31. package/dist/shared/lib/types.d.ts.map +1 -1
  32. package/dist/shared/lib/types.js +4 -0
  33. package/dist/shared/lib/types.js.map +1 -1
  34. package/dist/shared/lib/util.d.ts +38 -1
  35. package/dist/shared/lib/util.d.ts.map +1 -1
  36. package/dist/shared/lib/util.js +95 -0
  37. package/dist/shared/lib/util.js.map +1 -1
  38. package/dist/shared/version.d.ts +1 -1
  39. package/dist/shared/version.d.ts.map +1 -1
  40. package/dist/shared/version.js +1 -1
  41. package/dist/shared/version.js.map +1 -1
  42. 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"]}
@@ -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":"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"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"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"}
@@ -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: options?.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
- const redirectUrl = loginSuccessUrl || this.authConfig.loginSuccessUrl || "/";
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
- const frontendUrl = options?.frontendUrl ||
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
- const frontendUrl = options?.frontendUrl ||
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
- const frontendUrl = options?.frontendUrl ||
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
- return { redirectTo: this.authConfig.loginSuccessUrl || "/" };
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