@civic/auth 0.9.6-beta.1 → 0.10.0-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 (223) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/nextjs/actions.d.ts +12 -0
  3. package/dist/nextjs/actions.d.ts.map +1 -0
  4. package/dist/nextjs/actions.js +26 -0
  5. package/dist/nextjs/actions.js.map +1 -0
  6. package/dist/nextjs/config.d.ts +2 -0
  7. package/dist/nextjs/config.d.ts.map +1 -1
  8. package/dist/nextjs/config.js +3 -2
  9. package/dist/nextjs/config.js.map +1 -1
  10. package/dist/nextjs/cookies.d.ts.map +1 -1
  11. package/dist/nextjs/cookies.js +45 -3
  12. package/dist/nextjs/cookies.js.map +1 -1
  13. package/dist/nextjs/hooks/useInitialAuthConfig.d.ts +31 -0
  14. package/dist/nextjs/hooks/useInitialAuthConfig.d.ts.map +1 -0
  15. package/dist/nextjs/hooks/useInitialAuthConfig.js +113 -0
  16. package/dist/nextjs/hooks/useInitialAuthConfig.js.map +1 -0
  17. package/dist/nextjs/index.d.ts +1 -0
  18. package/dist/nextjs/index.d.ts.map +1 -1
  19. package/dist/nextjs/index.js +13 -3
  20. package/dist/nextjs/index.js.map +1 -1
  21. package/dist/nextjs/providers/NextAuthProvider.d.ts +6 -7
  22. package/dist/nextjs/providers/NextAuthProvider.d.ts.map +1 -1
  23. package/dist/nextjs/providers/NextAuthProvider.js +19 -138
  24. package/dist/nextjs/providers/NextAuthProvider.js.map +1 -1
  25. package/dist/nextjs/providers/NextAuthProviderClient.d.ts +11 -0
  26. package/dist/nextjs/providers/NextAuthProviderClient.d.ts.map +1 -0
  27. package/dist/nextjs/providers/NextAuthProviderClient.js +62 -0
  28. package/dist/nextjs/providers/NextAuthProviderClient.js.map +1 -0
  29. package/dist/nextjs/providers/ServerUserContext.d.ts +2 -0
  30. package/dist/nextjs/providers/ServerUserContext.d.ts.map +1 -0
  31. package/dist/nextjs/providers/ServerUserContext.js +5 -0
  32. package/dist/nextjs/providers/ServerUserContext.js.map +1 -0
  33. package/dist/nextjs/routeHandler.d.ts.map +1 -1
  34. package/dist/nextjs/routeHandler.js +240 -341
  35. package/dist/nextjs/routeHandler.js.map +1 -1
  36. package/dist/react-router-7/components/UserButton.js +1 -1
  37. package/dist/react-router-7/components/UserButton.js.map +1 -1
  38. package/dist/react-router-7/routeHandler.d.ts.map +1 -1
  39. package/dist/react-router-7/routeHandler.js +1 -0
  40. package/dist/react-router-7/routeHandler.js.map +1 -1
  41. package/dist/react-router-7/useUser.d.ts.map +1 -1
  42. package/dist/react-router-7/useUser.js +13 -2
  43. package/dist/react-router-7/useUser.js.map +1 -1
  44. package/dist/reactjs/components/ButtonContentOrLoader.d.ts.map +1 -1
  45. package/dist/reactjs/components/ButtonContentOrLoader.js +1 -3
  46. package/dist/reactjs/components/ButtonContentOrLoader.js.map +1 -1
  47. package/dist/reactjs/components/CivicAuthIframeContainer.d.ts +2 -0
  48. package/dist/reactjs/components/CivicAuthIframeContainer.d.ts.map +1 -0
  49. package/dist/reactjs/components/CivicAuthIframeContainer.js +26 -0
  50. package/dist/reactjs/components/CivicAuthIframeContainer.js.map +1 -0
  51. package/dist/reactjs/components/SignInButton.d.ts.map +1 -1
  52. package/dist/reactjs/components/SignInButton.js +11 -1
  53. package/dist/reactjs/components/SignInButton.js.map +1 -1
  54. package/dist/reactjs/components/UserButton.d.ts +9 -2
  55. package/dist/reactjs/components/UserButton.d.ts.map +1 -1
  56. package/dist/reactjs/components/UserButton.js +41 -9
  57. package/dist/reactjs/components/UserButton.js.map +1 -1
  58. package/dist/reactjs/components/index.d.ts +1 -0
  59. package/dist/reactjs/components/index.d.ts.map +1 -1
  60. package/dist/reactjs/components/index.js +1 -0
  61. package/dist/reactjs/components/index.js.map +1 -1
  62. package/dist/reactjs/core/GlobalAuthManager.d.ts +26 -0
  63. package/dist/reactjs/core/GlobalAuthManager.d.ts.map +1 -1
  64. package/dist/reactjs/core/GlobalAuthManager.js +76 -5
  65. package/dist/reactjs/core/GlobalAuthManager.js.map +1 -1
  66. package/dist/reactjs/hooks/useUser.d.ts +19 -2
  67. package/dist/reactjs/hooks/useUser.d.ts.map +1 -1
  68. package/dist/reactjs/hooks/useUser.js +95 -7
  69. package/dist/reactjs/hooks/useUser.js.map +1 -1
  70. package/dist/reactjs/index.d.ts +1 -2
  71. package/dist/reactjs/index.d.ts.map +1 -1
  72. package/dist/reactjs/index.js +1 -2
  73. package/dist/reactjs/index.js.map +1 -1
  74. package/dist/server/ServerAuthenticationResolver.d.ts.map +1 -1
  75. package/dist/server/ServerAuthenticationResolver.js +18 -0
  76. package/dist/server/ServerAuthenticationResolver.js.map +1 -1
  77. package/dist/server/index.d.ts +1 -1
  78. package/dist/server/index.d.ts.map +1 -1
  79. package/dist/server/index.js.map +1 -1
  80. package/dist/server/logout.d.ts.map +1 -1
  81. package/dist/server/logout.js +11 -2
  82. package/dist/server/logout.js.map +1 -1
  83. package/dist/server/session.d.ts +51 -0
  84. package/dist/server/session.d.ts.map +1 -1
  85. package/dist/server/session.js +296 -17
  86. package/dist/server/session.js.map +1 -1
  87. package/dist/shared/components/SVGLoading.js +1 -1
  88. package/dist/shared/components/SVGLoading.js.map +1 -1
  89. package/dist/shared/components/UserButtonPresentation.d.ts.map +1 -0
  90. package/dist/shared/components/UserButtonPresentation.js.map +1 -0
  91. package/dist/shared/hooks/index.d.ts +1 -2
  92. package/dist/shared/hooks/index.d.ts.map +1 -1
  93. package/dist/shared/hooks/index.js +1 -2
  94. package/dist/shared/hooks/index.js.map +1 -1
  95. package/dist/shared/hooks/useBfcacheHandler.d.ts +23 -0
  96. package/dist/shared/hooks/useBfcacheHandler.d.ts.map +1 -0
  97. package/dist/shared/hooks/useBfcacheHandler.js +65 -0
  98. package/dist/shared/hooks/useBfcacheHandler.js.map +1 -0
  99. package/dist/shared/index.d.ts +1 -0
  100. package/dist/shared/index.d.ts.map +1 -1
  101. package/dist/shared/index.js +1 -0
  102. package/dist/shared/index.js.map +1 -1
  103. package/dist/shared/lib/util.d.ts +32 -0
  104. package/dist/shared/lib/util.d.ts.map +1 -1
  105. package/dist/shared/lib/util.js +79 -0
  106. package/dist/shared/lib/util.js.map +1 -1
  107. package/dist/shared/providers/AuthStatusContext.d.ts.map +1 -1
  108. package/dist/shared/providers/AuthStatusContext.js +2 -1
  109. package/dist/shared/providers/AuthStatusContext.js.map +1 -1
  110. package/dist/shared/providers/CivicAuthConfigContext.d.ts +2 -1
  111. package/dist/shared/providers/CivicAuthConfigContext.d.ts.map +1 -1
  112. package/dist/shared/providers/CivicAuthConfigContext.js +5 -2
  113. package/dist/shared/providers/CivicAuthConfigContext.js.map +1 -1
  114. package/dist/shared/providers/types.d.ts +1 -0
  115. package/dist/shared/providers/types.d.ts.map +1 -1
  116. package/dist/shared/providers/types.js.map +1 -1
  117. package/dist/shared/utils/locationChange.d.ts +34 -0
  118. package/dist/shared/utils/locationChange.d.ts.map +1 -0
  119. package/dist/shared/utils/locationChange.js +28 -0
  120. package/dist/shared/utils/locationChange.js.map +1 -0
  121. package/dist/shared/version.d.ts +1 -1
  122. package/dist/shared/version.d.ts.map +1 -1
  123. package/dist/shared/version.js +1 -1
  124. package/dist/shared/version.js.map +1 -1
  125. package/dist/vanillajs/auth/AuthenticationEvents.d.ts +10 -1
  126. package/dist/vanillajs/auth/AuthenticationEvents.d.ts.map +1 -1
  127. package/dist/vanillajs/auth/AuthenticationEvents.js +29 -0
  128. package/dist/vanillajs/auth/AuthenticationEvents.js.map +1 -1
  129. package/dist/vanillajs/auth/BackendAuthenticationRefresher.d.ts.map +1 -1
  130. package/dist/vanillajs/auth/BackendAuthenticationRefresher.js +2 -2
  131. package/dist/vanillajs/auth/BackendAuthenticationRefresher.js.map +1 -1
  132. package/dist/vanillajs/auth/CivicAuth.d.ts +32 -0
  133. package/dist/vanillajs/auth/CivicAuth.d.ts.map +1 -1
  134. package/dist/vanillajs/auth/CivicAuth.js +270 -55
  135. package/dist/vanillajs/auth/CivicAuth.js.map +1 -1
  136. package/dist/vanillajs/auth/SessionManager.d.ts +3 -2
  137. package/dist/vanillajs/auth/SessionManager.d.ts.map +1 -1
  138. package/dist/vanillajs/auth/SessionManager.js +33 -7
  139. package/dist/vanillajs/auth/SessionManager.js.map +1 -1
  140. package/dist/vanillajs/auth/config/ConfigProcessor.d.ts.map +1 -1
  141. package/dist/vanillajs/auth/config/ConfigProcessor.js +2 -14
  142. package/dist/vanillajs/auth/config/ConfigProcessor.js.map +1 -1
  143. package/dist/vanillajs/auth/handlers/IframeAuthHandler.d.ts.map +1 -1
  144. package/dist/vanillajs/auth/handlers/IframeAuthHandler.js +64 -11
  145. package/dist/vanillajs/auth/handlers/IframeAuthHandler.js.map +1 -1
  146. package/dist/vanillajs/auth/handlers/MessageHandler.d.ts.map +1 -1
  147. package/dist/vanillajs/auth/handlers/MessageHandler.js +4 -1
  148. package/dist/vanillajs/auth/handlers/MessageHandler.js.map +1 -1
  149. package/dist/vanillajs/auth/handlers/PopupHandler.d.ts.map +1 -1
  150. package/dist/vanillajs/auth/handlers/PopupHandler.js +3 -1
  151. package/dist/vanillajs/auth/handlers/PopupHandler.js.map +1 -1
  152. package/dist/vanillajs/auth/types/AuthTypes.d.ts +11 -1
  153. package/dist/vanillajs/auth/types/AuthTypes.d.ts.map +1 -1
  154. package/dist/vanillajs/auth/types/AuthTypes.js.map +1 -1
  155. package/dist/vanillajs/iframe/IframeManager.d.ts +22 -1
  156. package/dist/vanillajs/iframe/IframeManager.d.ts.map +1 -1
  157. package/dist/vanillajs/iframe/IframeManager.js +184 -22
  158. package/dist/vanillajs/iframe/IframeManager.js.map +1 -1
  159. package/dist/vanillajs/types/index.d.ts +1 -1
  160. package/dist/vanillajs/types/index.d.ts.map +1 -1
  161. package/dist/vanillajs/types/index.js +1 -1
  162. package/dist/vanillajs/types/index.js.map +1 -1
  163. package/dist/vanillajs/ui/LoadingComponents.d.ts +4 -0
  164. package/dist/vanillajs/ui/LoadingComponents.d.ts.map +1 -1
  165. package/dist/vanillajs/ui/LoadingComponents.js +51 -1
  166. package/dist/vanillajs/ui/LoadingComponents.js.map +1 -1
  167. package/package.json +3 -3
  168. package/dist/nextjs/hooks/index.d.ts +0 -2
  169. package/dist/nextjs/hooks/index.d.ts.map +0 -1
  170. package/dist/nextjs/hooks/index.js +0 -2
  171. package/dist/nextjs/hooks/index.js.map +0 -1
  172. package/dist/nextjs/hooks/usePrevious.d.ts +0 -2
  173. package/dist/nextjs/hooks/usePrevious.d.ts.map +0 -1
  174. package/dist/nextjs/hooks/usePrevious.js +0 -9
  175. package/dist/nextjs/hooks/usePrevious.js.map +0 -1
  176. package/dist/nextjs/hooks/useUserCookie.d.ts +0 -9
  177. package/dist/nextjs/hooks/useUserCookie.d.ts.map +0 -1
  178. package/dist/nextjs/hooks/useUserCookie.js +0 -109
  179. package/dist/nextjs/hooks/useUserCookie.js.map +0 -1
  180. package/dist/react-router-7/components/UserButtonPresentation.d.ts.map +0 -1
  181. package/dist/react-router-7/components/UserButtonPresentation.js.map +0 -1
  182. package/dist/shared/components/BlockDisplay.d.ts +0 -6
  183. package/dist/shared/components/BlockDisplay.d.ts.map +0 -1
  184. package/dist/shared/components/BlockDisplay.js +0 -30
  185. package/dist/shared/components/BlockDisplay.js.map +0 -1
  186. package/dist/shared/components/CivicAuthIframe.d.ts +0 -10
  187. package/dist/shared/components/CivicAuthIframe.d.ts.map +0 -1
  188. package/dist/shared/components/CivicAuthIframe.js +0 -49
  189. package/dist/shared/components/CivicAuthIframe.js.map +0 -1
  190. package/dist/shared/components/CivicAuthIframeContainer.d.ts +0 -15
  191. package/dist/shared/components/CivicAuthIframeContainer.d.ts.map +0 -1
  192. package/dist/shared/components/CivicAuthIframeContainer.js +0 -177
  193. package/dist/shared/components/CivicAuthIframeContainer.js.map +0 -1
  194. package/dist/shared/components/CivicAuthLogoutIframeContainer.d.ts +0 -6
  195. package/dist/shared/components/CivicAuthLogoutIframeContainer.d.ts.map +0 -1
  196. package/dist/shared/components/CivicAuthLogoutIframeContainer.js +0 -51
  197. package/dist/shared/components/CivicAuthLogoutIframeContainer.js.map +0 -1
  198. package/dist/shared/components/IFrameAndLoading.d.ts +0 -7
  199. package/dist/shared/components/IFrameAndLoading.d.ts.map +0 -1
  200. package/dist/shared/components/IFrameAndLoading.js +0 -66
  201. package/dist/shared/components/IFrameAndLoading.js.map +0 -1
  202. package/dist/shared/hooks/useAuth.d.ts +0 -3
  203. package/dist/shared/hooks/useAuth.d.ts.map +0 -1
  204. package/dist/shared/hooks/useAuth.js +0 -12
  205. package/dist/shared/hooks/useAuth.js.map +0 -1
  206. package/dist/shared/hooks/useIframe.d.ts +0 -3
  207. package/dist/shared/hooks/useIframe.d.ts.map +0 -1
  208. package/dist/shared/hooks/useIframe.js +0 -13
  209. package/dist/shared/hooks/useIframe.js.map +0 -1
  210. package/dist/shared/hooks/useIsInIframe.d.ts +0 -7
  211. package/dist/shared/hooks/useIsInIframe.d.ts.map +0 -1
  212. package/dist/shared/hooks/useIsInIframe.js +0 -23
  213. package/dist/shared/hooks/useIsInIframe.js.map +0 -1
  214. package/dist/shared/hooks/useSignIn.d.ts +0 -20
  215. package/dist/shared/hooks/useSignIn.d.ts.map +0 -1
  216. package/dist/shared/hooks/useSignIn.js +0 -358
  217. package/dist/shared/hooks/useSignIn.js.map +0 -1
  218. package/dist/shared/providers/IframeProvider.d.ts +0 -28
  219. package/dist/shared/providers/IframeProvider.d.ts.map +0 -1
  220. package/dist/shared/providers/IframeProvider.js +0 -64
  221. package/dist/shared/providers/IframeProvider.js.map +0 -1
  222. /package/dist/{react-router-7 → shared}/components/UserButtonPresentation.d.ts +0 -0
  223. /package/dist/{react-router-7 → shared}/components/UserButtonPresentation.js +0 -0
@@ -1,18 +1,12 @@
1
- import { LOGOUT_SUCCESS_TEXT, TOKEN_EXCHANGE_SUCCESS_TEXT, TOKEN_EXCHANGE_TRIGGER_TEXT, } from "../constants.js";
1
+ import { CivicAuth } from "@civic/auth/server";
2
+ import { LOGOUT_SUCCESS_TEXT } from "../constants.js";
2
3
  import { loggers } from "../lib/logger.js";
3
- import { displayModeFromState, loginSuccessUrlFromState, serverTokenExchangeFromState, } from "../lib/oauth.js";
4
+ import { displayModeFromState } from "../lib/oauth.js";
4
5
  import { resolveAuthConfig } from "../nextjs/config.js";
5
6
  import { clearAuthCookies, NextjsCookieStorage } from "../nextjs/cookies.js";
6
- import { getUser } from "../nextjs/index.js";
7
- import { resolveCallbackUrl } from "../nextjs/utils.js";
8
- import { resolveOAuthAccessCode } from "../server/login.js";
9
- import { GenericPublicClientPKCEProducer } from "../services/PKCE.js";
10
- import { CodeVerifier, OAuthTokenTypes } from "../shared/lib/types.js";
11
- import { GenericUserSession } from "../shared/lib/UserSession.js";
12
- import { clearTokens, generateOauthLogoutUrl } from "../shared/lib/util.js";
7
+ import { CodeVerifier, UserStorage } from "../shared/lib/types.js";
13
8
  import { revalidatePath } from "next/cache.js";
14
9
  import { NextResponse } from "next/server.js";
15
- import { NextServerAuthenticationRefresherImpl } from "./NextServerAuthenticationRefresherImpl.js";
16
10
  const logger = loggers.nextjs.handlers.auth;
17
11
  class AuthError extends Error {
18
12
  status;
@@ -22,306 +16,176 @@ class AuthError extends Error {
22
16
  this.name = "AuthError";
23
17
  }
24
18
  }
25
- const tryUriDecode = (value) => {
26
- try {
27
- return decodeURIComponent(value);
28
- }
29
- catch (e) {
30
- logger.error("Error decoding URI component:", e);
31
- return value;
32
- }
33
- };
34
- const getDecodedQueryParam = (request, paramName) => {
35
- const queryParam = request.nextUrl.searchParams.get(paramName);
36
- if (queryParam) {
37
- return tryUriDecode(queryParam);
38
- }
39
- return null;
40
- };
41
- const getCookieOrQueryParam = (request, cookieName, queryName) => {
42
- // First check the cookie as it might have the full path with base directory
43
- const cookieValue = request.cookies.get(cookieName)?.value;
44
- if (cookieValue) {
45
- return tryUriDecode(cookieValue);
46
- }
47
- // Fallback to query parameter
48
- return getDecodedQueryParam(request, queryName);
49
- };
50
- const getAppUrl = (request) => getCookieOrQueryParam(request, CodeVerifier.APP_URL, "appUrl");
51
- // The loginSuccessUrl can either be decoded from the state parameter, or passed as a cookie or query parameter
52
- const getLoginSuccessUrl = (request, baseUrl) => {
53
- const loginSuccessUrl = loginSuccessUrlFromState(request.nextUrl.searchParams.get("state")) ||
54
- getDecodedQueryParam(request, "loginSuccessUrl");
55
- if (!loginSuccessUrl) {
56
- return null;
57
- }
58
- return baseUrl ? new URL(loginSuccessUrl, baseUrl).href : loginSuccessUrl;
59
- };
60
- const getIdToken = async (config) => {
61
- const cookieStorage = new NextjsCookieStorage(config.cookies?.tokens ?? {});
62
- return cookieStorage.get(OAuthTokenTypes.ID_TOKEN);
63
- };
64
19
  /**
65
- * create a code verifier and challenge for PKCE
66
- * saving the verifier in a cookie for later use
67
- * @returns {Promise<NextResponse>}
20
+ * Helper to convert NextRequest to UrlDetectionRequest for framework-agnostic URL handling
68
21
  */
69
- async function handleChallenge(request, config) {
70
- const cookieStorage = new NextjsCookieStorage(config.cookies?.tokens ?? {});
71
- const pkceProducer = new GenericPublicClientPKCEProducer(cookieStorage);
72
- const challenge = await pkceProducer.getCodeChallenge();
73
- const appUrl = request.nextUrl.searchParams.get("appUrl");
74
- if (appUrl) {
75
- await cookieStorage.set(CodeVerifier.APP_URL, appUrl);
76
- }
77
- return NextResponse.json({ status: "success", challenge });
78
- }
79
- const getCookieStorageWithUserOverrides = (config) => {
80
- const resolvedConfigs = resolveAuthConfig(config);
81
- return new NextjsCookieStorage({
82
- ...resolvedConfigs.cookies.tokens,
83
- user: resolvedConfigs.cookies.user,
22
+ const toUrlDetectionRequest = (request) => ({
23
+ url: request.url,
24
+ headers: Object.fromEntries(request.headers.entries()),
25
+ searchParams: {
26
+ get: (name) => request.nextUrl.searchParams.get(name),
27
+ },
28
+ cookies: {
29
+ get: (name) => request.cookies.get(name),
30
+ },
31
+ });
32
+ /**
33
+ * Helper to create CivicAuth instance for a request
34
+ * Now handles appUrl detection for proxy environments
35
+ */
36
+ const createCivicAuth = (request, config) => {
37
+ const resolvedConfig = resolveAuthConfig(config);
38
+ const cookieStorage = new NextjsCookieStorage({
39
+ ...resolvedConfig.cookies?.tokens,
40
+ [UserStorage.USER]: resolvedConfig.cookies?.user,
41
+ });
42
+ // Convert to framework-agnostic request format
43
+ const urlDetectionRequest = toUrlDetectionRequest(request);
44
+ // Get appUrl from client (for proxy environments)
45
+ const clientAppUrl = CivicAuth.getAppUrl(urlDetectionRequest);
46
+ // Use baseUrl from config, then client appUrl, then request origin
47
+ // This matches the main branch priority: config > client > request
48
+ const appUrl = resolvedConfig.baseUrl ||
49
+ clientAppUrl ||
50
+ new URL(urlDetectionRequest.url).origin;
51
+ // Build absolute URLs using detected appUrl or request origin
52
+ const absoluteCallbackUrl = resolvedConfig.callbackUrl.startsWith("http")
53
+ ? resolvedConfig.callbackUrl
54
+ : CivicAuth.toAbsoluteUrl(urlDetectionRequest, resolvedConfig.callbackUrl, appUrl);
55
+ const absoluteLogoutCallbackUrl = resolvedConfig.logoutCallbackUrl.startsWith("http")
56
+ ? resolvedConfig.logoutCallbackUrl
57
+ : CivicAuth.toAbsoluteUrl(urlDetectionRequest, resolvedConfig.logoutCallbackUrl, appUrl);
58
+ const civicAuth = new CivicAuth(cookieStorage, {
59
+ clientId: resolvedConfig.clientId,
60
+ redirectUrl: absoluteCallbackUrl,
61
+ oauthServer: resolvedConfig.oauthServer,
62
+ postLogoutRedirectUrl: absoluteLogoutCallbackUrl,
63
+ loginSuccessUrl: request.url,
84
64
  });
65
+ return {
66
+ civicAuth,
67
+ cookieStorage,
68
+ appUrl, // Return appUrl for use in other functions
69
+ urlDetectionRequest, // Return for use in handlers
70
+ };
85
71
  };
86
- async function performTokenExchangeAndSetCookies(config, code, state, appUrl) {
72
+ /**
73
+ * Login handler - backend OAuth login initiation endpoint
74
+ * Uses CivicAuth.buildLoginUrl()
75
+ */
76
+ async function handleLogin(request, config) {
87
77
  const resolvedConfigs = resolveAuthConfig(config);
88
- // TODO This is messy, better would be to fix the config.cookies type to always be <name: settings>
89
- // rather than nesting the tokens-related ones *and* code-verifier inside "tokens"
90
- // (despite code-verifier not relating directly to tokens)
91
- const cookieStorage = getCookieStorageWithUserOverrides(config);
92
- const callbackUrl = resolveCallbackUrl(resolvedConfigs, appUrl);
93
78
  try {
94
- await resolveOAuthAccessCode(code, state, cookieStorage, {
95
- ...resolvedConfigs,
96
- redirectUrl: callbackUrl,
79
+ const frontendState = request.nextUrl.searchParams.get("state");
80
+ // Store appUrl in cookie if provided as query parameter
81
+ const appUrlFromQuery = request.nextUrl.searchParams.get("appUrl");
82
+ if (appUrlFromQuery) {
83
+ const cookieStorage = new NextjsCookieStorage(resolvedConfigs.cookies?.tokens ?? {});
84
+ await cookieStorage.set(CodeVerifier.APP_URL, appUrlFromQuery);
85
+ }
86
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
87
+ const url = await civicAuth.buildLoginUrl({
88
+ state: frontendState || undefined,
97
89
  });
90
+ logger.info("[LOGIN_HANDLER] Redirecting to OAuth login URL", {
91
+ loginUrl: url.toString(),
92
+ });
93
+ return NextResponse.redirect(url.toString());
98
94
  }
99
95
  catch (error) {
100
- logger.error("Token exchange failed:", error);
101
- throw new AuthError("Failed to authenticate user", 401);
102
- }
103
- const user = await getUser();
104
- if (!user) {
105
- throw new AuthError("Failed to get user info", 401);
96
+ logger.error("[LOGIN_HANDLER] Backend login error:", error);
97
+ const urlDetectionRequest = toUrlDetectionRequest(request);
98
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
99
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, "/?error=login_failed", appUrl));
106
100
  }
107
- const userSession = new GenericUserSession(cookieStorage);
108
- await userSession.set(user);
109
101
  }
110
102
  async function handleRefresh(request, config) {
111
103
  const resolvedConfigs = resolveAuthConfig(config);
112
- const cookieStorage = getCookieStorageWithUserOverrides(config);
113
- const userSession = new GenericUserSession(cookieStorage);
114
- const targetUrl = request.nextUrl.searchParams.get("targetUrl");
115
104
  try {
116
- const onError = (error) => {
117
- logger.error("handleRefresh: Token refresh failed:", error);
118
- throw new AuthError("Failed to refresh tokens", 500);
119
- };
120
- const refresher = await NextServerAuthenticationRefresherImpl.build({
121
- clientId: resolvedConfigs.clientId,
122
- oauthServer: resolvedConfigs.oauthServer,
123
- redirectUrl: resolvedConfigs.callbackUrl,
124
- refreshUrl: resolvedConfigs.refreshUrl,
125
- }, cookieStorage, onError);
126
- const tokens = await refresher.refreshAccessToken();
127
- const user = await getUser();
128
- if (!user) {
129
- throw new AuthError("Failed to get user info", 401);
130
- }
131
- await userSession.set(user);
132
- logger.debug("handleRefresh: Token refresh successful");
105
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
106
+ await civicAuth.refreshTokens();
107
+ logger.info("[REFRESH_HANDLER] Tokens refreshed successfully");
108
+ const targetUrl = request.nextUrl.searchParams.get("targetUrl");
133
109
  if (targetUrl) {
134
110
  // Success: clear the refresh attempt tracking cookie and redirect to target
135
111
  const response = NextResponse.redirect(targetUrl);
136
112
  response.cookies.delete("_civic_last_refresh");
137
113
  return response;
138
114
  }
139
- // For backend flows, tokens might be null since they're managed in HTTP-only cookies
140
- const response = NextResponse.json({
115
+ return NextResponse.json({
141
116
  status: "success",
142
- tokens: tokens || null,
117
+ message: "Tokens refreshed",
143
118
  });
144
- return response;
145
119
  }
146
120
  catch (error) {
147
- logger.error("handleRefresh: Token refresh failed, clearing tokens:", error);
148
- await clearTokens(cookieStorage);
149
- await userSession.clear();
121
+ logger.error("[REFRESH_HANDLER] Token refresh error:", error);
122
+ const targetUrl = request.nextUrl.searchParams.get("targetUrl");
150
123
  if (targetUrl) {
151
- logger.warn("handleRefresh: Refresh failed, redirecting to targetUrl");
152
124
  return NextResponse.redirect(targetUrl);
153
125
  }
154
- return NextResponse.json({ status: "failed" });
126
+ return NextResponse.json({ error: "Token refresh failed" }, { status: 500 });
155
127
  }
156
128
  }
157
- const generateHtmlResponseWithCallback = (request, callbackUrl, loginSuccessUrl) => {
158
- // we need to replace the URL with resolved config in case the server is hosted
159
- // behind a reverse proxy or load balancer
160
- const requestUrl = new URL(request.url);
161
- const fetchUrl = `${callbackUrl}?${requestUrl.searchParams.toString()}&sameDomainCallback=true`;
162
- const loginSuccessSegment = loginSuccessUrl
163
- ? `&loginSuccessUrl=${encodeURIComponent(loginSuccessUrl)}`
164
- : "";
165
- const html = `<html lang="en">
166
- <body>
167
- <span style="display:none">
168
- <script>
169
- window.onload = function () {
170
- // Get the complete URL including origin and path
171
- // This ensures we capture any base path like /directory
172
- const appUrl = window.location.href.substring(
173
- 0,
174
- window.location.href.indexOf("/api/auth")
175
- );
176
- fetch('${fetchUrl}&appUrl=' + encodeURIComponent(appUrl) + '${loginSuccessSegment}').then((response) => {
177
- response.json().then((jsonResponse) => {
178
- // For login: Redirect back to the callback route, so Case 2 in handleTokenExchangeComplete will be triggered
179
- // For logout: Redirect to the postLogoutRedirectUrl
180
- if(jsonResponse.redirectUrl) {
181
- window.location.href = jsonResponse.redirectUrl;
182
- }
183
- });
184
- });
185
- };
186
- </script>
187
- </span>
188
- </body>
189
- </html>
190
- `;
191
- const response = new NextResponse(html);
192
- response.headers.set("Content-Type", "text/html; charset=utf-8");
193
- return response;
194
- };
195
- const handleTokenExchangeComplete = async (params) => {
196
- const { request, config, appUrl, loginSuccessUrl, state } = params;
197
- // Case 1: We are being called via fetch to facilitate access to the cookies. Return success json. The iframe has javascript that will reload this route so Case 2 below will be triggered.
198
- if (isCalledFromBrowserFetch(request)) {
199
- logger.debug("CASE 1: sameDomainCallback=true, returning JSON response with redirect URL", { appUrl, loginSuccessUrl, callbackUrl: config.callbackUrl });
200
- const currentUrl = new URL(request.url);
201
- // When the client-side JS redirects back here, we don't want to hit this branch again because we can't return JSON from a redirect.
202
- // So we strip off the sameDomainCallback parameter from the URL.
203
- const newSearchParams = new URLSearchParams(currentUrl.search);
204
- newSearchParams.delete("sameDomainCallback");
205
- // We strip off the origin so reverse proxies don't break the redirect.
206
- const redirectUrl = `${currentUrl.pathname}?${newSearchParams.toString()}${currentUrl.hash}`;
207
- return NextResponse.json({
208
- status: "success",
209
- // This makes the iframe redirect back to this route, so Case 2 below will be triggered.
210
- redirectUrl,
211
- });
212
- }
213
- // Case 2: We are already authenticated and in iframe mode.
214
- // Case 2a: We have a custom loginSuccessUrl, so we have to trigger a top-level redirect to it. We do this by rendering a page with the TOKEN_EXCHANGE_SUCCESS_TEXT, which is then picked up by the iframe container.
215
- // Case 2b: We don't have a custom loginSuccessUrl, so we just redirect to the appUrl. If we don't do this, Cypress tests will fail in the 'no custom loginSuccessUrl' case, because in Cypress an iframe redirect is converted to a top-level redirect,
216
- // which means the iframe container no longer exists and so can't action the redirect.
217
- const user = await getUser();
218
- if (!!user && displayModeFromState(state, "iframe") === "iframe") {
219
- if (loginSuccessUrl) {
220
- logger.debug("CASE 2a: iframe mode with loginSuccessUrl configured. Returning TOKEN_EXCHANGE_SUCCESS_TEXT to trigger redirect to loginSuccessUrl if any", { loginSuccessUrl });
221
- const response = new NextResponse(`<html lang="en"><span style="display:none">${TOKEN_EXCHANGE_SUCCESS_TEXT}</span></html>`);
222
- response.headers.set("Content-Type", "text/html; charset=utf-8");
223
- return response;
224
- }
225
- else {
226
- logger.debug("CASE 2b: iframe mode with no loginSuccessUrl configured. Doing a normal redirect without relying on the iframe container to redirect.");
227
- return NextResponse.redirect(`${appUrl}`);
228
- }
229
- }
230
- // CASE 3: We're not in iframe mode. We can just do a stright http redirect to the final destination, which is either the loginSuccessUrl if specified, or the appUrl.
231
- logger.debug("CASE 3: non-iframe mode, redirecting to loginSuccessUrl");
232
- return NextResponse.redirect(`${loginSuccessUrl || appUrl}`);
233
- };
234
129
  async function handleCallback(request, config) {
235
130
  const resolvedConfigs = resolveAuthConfig(config);
236
131
  const code = request.nextUrl.searchParams.get("code");
237
132
  const state = request.nextUrl.searchParams.get("state");
133
+ const error = request.nextUrl.searchParams.get("error");
134
+ if (error) {
135
+ logger.error("OAuth error in callback:", error);
136
+ const urlDetectionRequest = toUrlDetectionRequest(request);
137
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
138
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, "/?error=oauth_error", appUrl));
139
+ }
238
140
  if (!code || !state)
239
141
  throw new AuthError("Bad parameters", 400);
240
- // appUrl is passed from the client to the server in the query string
241
- // this is necessary because the server does not have access to the client's window.location.origin
242
- // and can not accurately determine the appUrl (specially if the app is behind a reverse proxy)
243
- const appUrl = getAppUrl(request);
244
- // If the integrator has specified a loginSuccessUrl, we'll send the user there after the login completes (including token exchange)
245
- // We pass in the basePath from config to use as the baseUrl, because we might not have access to the app_url cookie at this point if this was a third-party redirect.
246
- const loginSuccessUrl = getLoginSuccessUrl(request, appUrl);
247
- const tokenExchangeCompleteParams = {
248
- request,
249
- config,
250
- appUrl,
251
- state,
252
- loginSuccessUrl,
253
- };
254
- const user = await getUser();
255
- if (user) {
256
- // User already authenticated.
257
- return handleTokenExchangeComplete(tokenExchangeCompleteParams);
258
- }
259
- // User not authenticated yet.
260
- // If we have a code_verifier cookie and the appUrl, we can do a token exchange.
261
- // Otherwise, just render an empty page.
262
- // The initial redirect back from the auth server does not send cookies, because the redirect is from a 3rd-party domain.
263
- // The client will make an additional call to this route with cookies included, at which point we do the token exchange.
264
- const codeVerifier = request.cookies.get(CodeVerifier.COOKIE_NAME);
265
- if (!codeVerifier || !appUrl) {
266
- logger.debug("handleCallback no code_verifier found", {
142
+ try {
143
+ const { civicAuth, appUrl, urlDetectionRequest } = createCivicAuth(request, resolvedConfigs);
144
+ // Convert NextRequest to the format expected by handleCallback
145
+ const handleCallbackRequest = {
146
+ headers: Object.fromEntries(request.headers.entries()),
147
+ url: request.url.toString(),
148
+ };
149
+ // Get loginSuccessUrl with proper baseUrl handling
150
+ const loginSuccessUrl = CivicAuth.getLoginSuccessUrl(urlDetectionRequest, appUrl);
151
+ const frontendUrl = loginSuccessUrl || resolvedConfigs.loginSuccessUrl || "/";
152
+ // Use CivicAuth's smart callback handler
153
+ const result = await civicAuth.handleCallback({
154
+ code,
267
155
  state,
268
- serverTokenExchange: serverTokenExchangeFromState(`${state}`),
156
+ req: handleCallbackRequest,
157
+ }, {
158
+ // Pass the properly resolved frontendUrl
159
+ frontendUrl: frontendUrl,
269
160
  });
270
- let response = new NextResponse(`<html lang="en"><body><span style="display:none">${TOKEN_EXCHANGE_TRIGGER_TEXT}</span></body></html>`);
271
- // in server-side token exchange mode we need to launch a page that will trigger the token exchange
272
- // from the same domain, allowing it access to the code_verifier cookie
273
- // we only need to do this in redirect mode, as the iframe already triggers a client-side token exchange
274
- // if no code-verifier cookie is found
275
- if (state && serverTokenExchangeFromState(state)) {
276
- logger.debug("handleCallback serverTokenExchangeFromState, launching redirect page...", {
277
- requestUrl: request.url,
278
- configCallbackUrl: resolvedConfigs.callbackUrl,
279
- });
280
- // generate a page that will callback to the same domain, allowing access
281
- // to the code_verifier cookie and passing the appUrl.
282
- response = generateHtmlResponseWithCallback(request, resolvedConfigs.callbackUrl, loginSuccessUrl || undefined);
161
+ if (result.redirectTo) {
162
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, result.redirectTo, appUrl));
283
163
  }
284
- logger.debug(`handleCallback no code_verifier found, returning ${TOKEN_EXCHANGE_TRIGGER_TEXT}`);
285
- return response;
286
- }
287
- await performTokenExchangeAndSetCookies(resolvedConfigs, code, state, appUrl);
288
- return handleTokenExchangeComplete(tokenExchangeCompleteParams);
289
- }
290
- /**
291
- * If redirectPath is an absolute path, return it as-is.
292
- * Otherwise for relative paths, append it to the current domain.
293
- * @param redirectPath
294
- * @param currentBasePath
295
- * @returns
296
- */
297
- const getAbsoluteRedirectPath = (redirectPath, currentBasePath) => new URL(redirectPath, currentBasePath).href;
298
- const getPostLogoutRedirectUrl = (request, config) => {
299
- const { loginUrl } = resolveAuthConfig(config);
300
- // if we have a target URL in the request, it's come from civic middleware
301
- // and we should use it as the redirect target.
302
- const targetUrl = request.nextUrl.searchParams.get("targetUrl");
303
- if (targetUrl) {
304
- // If a targetUrl is provided, use it as the redirect target.
305
- // This is useful for redirecting to a specific page after logout.
306
- return targetUrl;
164
+ if (result.content) {
165
+ // Handle both string content and object content
166
+ if (typeof result.content === "string") {
167
+ return new NextResponse(result.content, {
168
+ status: 200,
169
+ headers: {
170
+ "Content-Type": "text/html",
171
+ },
172
+ });
173
+ }
174
+ else {
175
+ // Object content (JSON response)
176
+ return NextResponse.json(result.content);
177
+ }
178
+ }
179
+ // Fallback redirect
180
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, "/", appUrl));
307
181
  }
308
- const redirectTarget = loginUrl ?? "/";
309
- // if the optional loginUrl is provided and it is an absolute URL,
310
- // use it as the redirect target
311
- const isAbsoluteRedirect = /^(https?:\/\/|www\.).+/i.test(redirectTarget);
312
- if (isAbsoluteRedirect) {
313
- return redirectTarget;
182
+ catch (error) {
183
+ logger.error("[CALLBACK_HANDLER] OAuth callback error:", error);
184
+ const urlDetectionRequest = toUrlDetectionRequest(request);
185
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
186
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, "/?error=callback_failed", appUrl));
314
187
  }
315
- // if loginUrl is not defined, the appUrl is passed from the client to the server
316
- // in the query string or cookies. This is necessary because the server does not
317
- // have access to the client's window.location and can not accurately determine
318
- // the appUrl (specially if the app is behind a reverse proxy).
319
- const appUrl = getAppUrl(request);
320
- if (appUrl)
321
- return getAbsoluteRedirectPath(redirectTarget, appUrl);
322
- // If we can't determine the post-logout redirect URL, fallback to the app root as it's the most likely location of the login page.
323
- return request.nextUrl.origin;
324
- };
188
+ }
325
189
  const revalidateUrlPath = async (url) => {
326
190
  try {
327
191
  const path = new URL(url).pathname;
@@ -333,86 +197,98 @@ const revalidateUrlPath = async (url) => {
333
197
  };
334
198
  export async function handleLogout(request, config) {
335
199
  const resolvedConfigs = resolveAuthConfig(config);
336
- // Ensure we have the proper app URL including any base path
337
- const appBaseUrl = getAppUrl(request) || request.url;
338
- // Construct the post-logout URL with the base path included
339
- const postLogoutUrl = new URL(resolvedConfigs.logoutCallbackUrl, appBaseUrl);
340
- // read the id_token from the cookies
341
- const idToken = await getIdToken(resolvedConfigs);
342
- // read the state from the query parameters
200
+ // Get framework-agnostic request for URL utilities
201
+ const urlDetectionRequest = toUrlDetectionRequest(request);
202
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
203
+ // Read the state from the query parameters
343
204
  const state = request.nextUrl.searchParams.get("state");
344
- if (!state || !idToken) {
345
- logger.error("handleLogout: missing state or idToken", {
346
- hasState: !!state,
347
- hasIdToken: !!idToken,
205
+ const clientLogoutRedirectUrl = request.nextUrl.searchParams.get("logoutRedirectUrl");
206
+ try {
207
+ logger.info("[LOGOUT_HANDLER] Backend logout endpoint called");
208
+ // If client provided a logoutRedirectUrl, override the config
209
+ let configToUse = resolvedConfigs;
210
+ if (clientLogoutRedirectUrl) {
211
+ configToUse = {
212
+ ...resolvedConfigs,
213
+ logoutCallbackUrl: clientLogoutRedirectUrl,
214
+ };
215
+ logger.info("[LOGOUT_HANDLER] Overriding logout callback URL", {
216
+ original: resolvedConfigs.logoutCallbackUrl,
217
+ override: clientLogoutRedirectUrl,
218
+ });
219
+ }
220
+ const { civicAuth } = createCivicAuth(request, configToUse);
221
+ // Always redirect to OAuth logout (like main branch)
222
+ // Don't validate session - even invalid local sessions should hit OAuth logout
223
+ logger.info("[LOGOUT_HANDLER] Processing logout request", {
224
+ state: !!state,
348
225
  });
349
- await clearAuthCookies();
350
- // if token or state is missing, the logout call to the server will fail,
351
- // (token has potentially expired already) so go straight to the postLogoutUrl
352
- // so the user can be signed out.
353
- return NextResponse.redirect(`${postLogoutUrl}${state ? "?state=" + state : ""}`);
226
+ // Always redirect to OAuth logout endpoint (like main branch)
227
+ // Client-side iframe logic will handle completion and redirect appropriately
228
+ const logoutUrl = await civicAuth.buildLogoutRedirectUrl({
229
+ state: state || undefined,
230
+ });
231
+ try {
232
+ await civicAuth.clearTokens();
233
+ // Revalidate current path to update authentication state in server components
234
+ await revalidateUrlPath(request.url);
235
+ }
236
+ catch (error) {
237
+ logger.error("[LOGOUT_HANDLER] Error clearing tokens:", error);
238
+ }
239
+ // Remove state parameter from logout URL to prevent it from appearing in frontend URL
240
+ const cleanLogoutUrl = new URL(logoutUrl);
241
+ cleanLogoutUrl.searchParams.delete("state");
242
+ logger.info("[LOGOUT_HANDLER] Redirecting to OAuth logout endpoint", {
243
+ logoutUrl: cleanLogoutUrl.toString(),
244
+ });
245
+ return NextResponse.redirect(cleanLogoutUrl.toString());
354
246
  }
355
- const displayMode = displayModeFromState(state, "iframe");
356
- if (displayMode === "iframe") {
357
- // clear auth cookies immediately before calling the logout endpoint to give faster UX
247
+ catch (error) {
248
+ logger.error("[LOGOUT_HANDLER] Logout error:", error);
249
+ // If logout URL generation fails, clear tokens and redirect to home
358
250
  await clearAuthCookies();
359
- await revalidateUrlPath(request.url);
251
+ const fallbackUrl = clientLogoutRedirectUrl || resolvedConfigs.logoutCallbackUrl;
252
+ const finalFallbackUrl = CivicAuth.toAbsoluteUrl(urlDetectionRequest, fallbackUrl, appUrl);
253
+ return NextResponse.redirect(finalFallbackUrl);
360
254
  }
361
- const logoutUrl = await generateOauthLogoutUrl({
362
- clientId: resolvedConfigs.clientId,
363
- idToken,
364
- state,
365
- redirectUrl: postLogoutUrl.href,
366
- oauthServer: resolvedConfigs.oauthServer,
367
- });
368
- return NextResponse.redirect(`${logoutUrl.href}`);
369
255
  }
370
- const isCalledFromBrowserFetch = (request) => request.url.includes("sameDomainCallback=true");
371
- const handleLogoutComplete = async (params) => {
372
- const { request, resolvedConfigs } = params;
373
- const state = request.nextUrl.searchParams.get("state");
374
- const postLogoutRedirectUrl = getPostLogoutRedirectUrl(request, resolvedConfigs);
375
- // If this is a FETCH call, we can only return json. Trying to redirect or return HTML will fail.
376
- if (isCalledFromBrowserFetch(request)) {
377
- logger.debug("handleLogoutComplete: sameDomainCallback=true, returning JSON response with redirect URL", { postLogoutRedirectUrl });
378
- // The client-side JS will do a window.location.href redirect to postLogoutRedirectUrl when this request returns success.
379
- return NextResponse.json({
380
- status: "success",
381
- redirectUrl: postLogoutRedirectUrl,
382
- });
383
- }
384
- // If this is a redirect inside an iframe and the user is indeed logged out, render some text that makes the parent redirect to the postLogoutRedirectUrl.
385
- const user = await getUser();
386
- if (!user && !!state && displayModeFromState(state, "iframe") === "iframe") {
387
- // User is logged out while in an iframe redirect (not a FETCH call).
388
- // Render some text to make the CivicLogoutIframeContainer redirect to the postLogoutRedirectUrl.
389
- const response = new NextResponse(`<html lang="en"><span style="display:none">${LOGOUT_SUCCESS_TEXT}<a href="${[postLogoutRedirectUrl]}" rel="civic-auth-post-logout-redirect-url"></a></span></html>`);
390
- response.headers.set("Content-Type", "text/html; charset=utf-8");
391
- logger.debug("handleLogoutComplete: iframe mode, rendering HTML with logout success text", { postLogoutRedirectUrl });
392
- return response;
393
- }
394
- logger.debug("handleLogoutComplete: redirecting to postLogoutRedirectUrl", {
395
- postLogoutRedirectUrl,
396
- });
397
- revalidateUrlPath(postLogoutRedirectUrl);
398
- return NextResponse.redirect(postLogoutRedirectUrl);
399
- };
400
256
  export async function handleLogoutCallback(request, config) {
401
257
  const resolvedConfigs = resolveAuthConfig(config);
402
- const canAccessCookies = !!(await getIdToken(resolvedConfigs));
403
- // If we have access to cookies, clear them.
404
- if (canAccessCookies) {
405
- logger.debug("handleLogoutCallback can access cookies: clearing auth cookies");
258
+ try {
259
+ logger.info("[LOGOUT_CALLBACK_HANDLER] Backend logout callback endpoint called");
260
+ // Clear authentication cookies
406
261
  await clearAuthCookies();
407
- return handleLogoutComplete({ request, resolvedConfigs });
262
+ // Get framework-agnostic request and create CivicAuth instance
263
+ const urlDetectionRequest = toUrlDetectionRequest(request);
264
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
265
+ // Get the state parameter for iframe detection
266
+ const state = request.nextUrl.searchParams.get("state");
267
+ // If this is an iframe request, return HTML with logout success signal
268
+ if (state && displayModeFromState(state, "iframe") === "iframe") {
269
+ // For iframe mode, include the post-logout redirect URL in the HTML
270
+ const postLogoutRedirectUrl = civicAuth.getPostLogoutRedirectUrl(urlDetectionRequest);
271
+ const response = new NextResponse(`<html lang="en"><span style="display:none">${LOGOUT_SUCCESS_TEXT}<a href="${postLogoutRedirectUrl}" rel="civic-auth-post-logout-redirect-url"></a></span></html>`);
272
+ response.headers.set("Content-Type", "text/html; charset=utf-8");
273
+ logger.info("[LOGOUT_CALLBACK_HANDLER] Returning iframe logout success HTML", { postLogoutRedirectUrl });
274
+ return response;
275
+ }
276
+ // For non-iframe requests, redirect to the logout callback URL or post-logout URL
277
+ const redirectUrl = civicAuth.getPostLogoutRedirectUrl(urlDetectionRequest);
278
+ logger.info("[LOGOUT_CALLBACK_HANDLER] Redirecting to logout callback URL", {
279
+ logoutCallbackUrl: resolvedConfigs.logoutCallbackUrl,
280
+ redirectUrl,
281
+ });
282
+ // Revalidate the redirect path to update authentication state in server components
283
+ await revalidateUrlPath(redirectUrl);
284
+ return NextResponse.redirect(redirectUrl);
285
+ }
286
+ catch (error) {
287
+ logger.error("[LOGOUT_CALLBACK_HANDLER] Logout callback error:", error);
288
+ const urlDetectionRequest = toUrlDetectionRequest(request);
289
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
290
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, resolvedConfigs.logoutCallbackUrl, appUrl));
408
291
  }
409
- logger.debug("handleLogoutCallback cannot access cookies: generating HTML response with callback");
410
- // If we don't have access to cookies, render some javascript to the client that will:
411
- // 1. make a same-domain fetch call back to this endpoint and receive a '{status: "success"}' back.
412
- // 2. On status: success, set the window.location.href to the post-logout redirect URL (usually the appUrl).
413
- return generateHtmlResponseWithCallback(request,
414
- // The client-side JS will make a fetch call back to this URL.
415
- resolvedConfigs.logoutCallbackUrl);
416
292
  }
417
293
  /**
418
294
  * Creates an authentication handler for Next.js API routes
@@ -434,7 +310,8 @@ export const handler = (authConfig = {}) => async (request) => {
434
310
  const lastSegment = pathSegments[pathSegments.length - 1];
435
311
  switch (lastSegment) {
436
312
  case "challenge":
437
- return await handleChallenge(request, config);
313
+ case "login":
314
+ return await handleLogin(request, config);
438
315
  case "callback":
439
316
  return await handleCallback(request, config);
440
317
  case "refresh":
@@ -443,6 +320,8 @@ export const handler = (authConfig = {}) => async (request) => {
443
320
  return await handleLogout(request, config);
444
321
  case "logoutcallback":
445
322
  return await handleLogoutCallback(request, config);
323
+ case "user":
324
+ return await handleUser(request, config);
446
325
  default:
447
326
  throw new AuthError(`Invalid auth route: ${pathname}`, 404);
448
327
  }
@@ -456,4 +335,24 @@ export const handler = (authConfig = {}) => async (request) => {
456
335
  return response;
457
336
  }
458
337
  };
338
+ /**
339
+ * User endpoint - returns current user data as JSON
340
+ * Uses CivicAuth.isLoggedIn() and getUser()
341
+ */
342
+ async function handleUser(request, config) {
343
+ const resolvedConfigs = resolveAuthConfig(config);
344
+ try {
345
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
346
+ const isLoggedIn = await civicAuth.isLoggedIn();
347
+ if (!isLoggedIn) {
348
+ return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
349
+ }
350
+ const user = await civicAuth.getUser();
351
+ return NextResponse.json({ user });
352
+ }
353
+ catch (error) {
354
+ logger.error("[USER_HANDLER] User endpoint error:", error);
355
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
356
+ }
357
+ }
459
358
  //# sourceMappingURL=routeHandler.js.map