@civic/auth 0.9.6-beta.1 → 0.9.6-beta.2

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 (219) 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 +109 -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 +241 -352
  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 +2 -4
  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/session.d.ts +51 -0
  81. package/dist/server/session.d.ts.map +1 -1
  82. package/dist/server/session.js +276 -15
  83. package/dist/server/session.js.map +1 -1
  84. package/dist/shared/components/SVGLoading.js +1 -1
  85. package/dist/shared/components/SVGLoading.js.map +1 -1
  86. package/dist/shared/components/UserButtonPresentation.d.ts.map +1 -0
  87. package/dist/shared/components/UserButtonPresentation.js.map +1 -0
  88. package/dist/shared/hooks/index.d.ts +1 -2
  89. package/dist/shared/hooks/index.d.ts.map +1 -1
  90. package/dist/shared/hooks/index.js +1 -2
  91. package/dist/shared/hooks/index.js.map +1 -1
  92. package/dist/shared/hooks/useBfcacheHandler.d.ts +23 -0
  93. package/dist/shared/hooks/useBfcacheHandler.d.ts.map +1 -0
  94. package/dist/shared/hooks/useBfcacheHandler.js +65 -0
  95. package/dist/shared/hooks/useBfcacheHandler.js.map +1 -0
  96. package/dist/shared/index.d.ts +1 -0
  97. package/dist/shared/index.d.ts.map +1 -1
  98. package/dist/shared/index.js +1 -0
  99. package/dist/shared/index.js.map +1 -1
  100. package/dist/shared/lib/util.d.ts +32 -0
  101. package/dist/shared/lib/util.d.ts.map +1 -1
  102. package/dist/shared/lib/util.js +79 -0
  103. package/dist/shared/lib/util.js.map +1 -1
  104. package/dist/shared/providers/AuthStatusContext.d.ts.map +1 -1
  105. package/dist/shared/providers/AuthStatusContext.js +2 -1
  106. package/dist/shared/providers/AuthStatusContext.js.map +1 -1
  107. package/dist/shared/providers/CivicAuthConfigContext.d.ts +2 -1
  108. package/dist/shared/providers/CivicAuthConfigContext.d.ts.map +1 -1
  109. package/dist/shared/providers/CivicAuthConfigContext.js +5 -2
  110. package/dist/shared/providers/CivicAuthConfigContext.js.map +1 -1
  111. package/dist/shared/providers/types.d.ts +1 -0
  112. package/dist/shared/providers/types.d.ts.map +1 -1
  113. package/dist/shared/providers/types.js.map +1 -1
  114. package/dist/shared/utils/locationChange.d.ts +34 -0
  115. package/dist/shared/utils/locationChange.d.ts.map +1 -0
  116. package/dist/shared/utils/locationChange.js +28 -0
  117. package/dist/shared/utils/locationChange.js.map +1 -0
  118. package/dist/shared/version.d.ts +1 -1
  119. package/dist/shared/version.js +1 -1
  120. package/dist/shared/version.js.map +1 -1
  121. package/dist/vanillajs/auth/AuthenticationEvents.d.ts +10 -1
  122. package/dist/vanillajs/auth/AuthenticationEvents.d.ts.map +1 -1
  123. package/dist/vanillajs/auth/AuthenticationEvents.js +29 -0
  124. package/dist/vanillajs/auth/AuthenticationEvents.js.map +1 -1
  125. package/dist/vanillajs/auth/BackendAuthenticationRefresher.d.ts.map +1 -1
  126. package/dist/vanillajs/auth/BackendAuthenticationRefresher.js +2 -2
  127. package/dist/vanillajs/auth/BackendAuthenticationRefresher.js.map +1 -1
  128. package/dist/vanillajs/auth/CivicAuth.d.ts +32 -0
  129. package/dist/vanillajs/auth/CivicAuth.d.ts.map +1 -1
  130. package/dist/vanillajs/auth/CivicAuth.js +255 -55
  131. package/dist/vanillajs/auth/CivicAuth.js.map +1 -1
  132. package/dist/vanillajs/auth/SessionManager.d.ts +3 -2
  133. package/dist/vanillajs/auth/SessionManager.d.ts.map +1 -1
  134. package/dist/vanillajs/auth/SessionManager.js +33 -7
  135. package/dist/vanillajs/auth/SessionManager.js.map +1 -1
  136. package/dist/vanillajs/auth/config/ConfigProcessor.d.ts.map +1 -1
  137. package/dist/vanillajs/auth/config/ConfigProcessor.js +2 -14
  138. package/dist/vanillajs/auth/config/ConfigProcessor.js.map +1 -1
  139. package/dist/vanillajs/auth/handlers/IframeAuthHandler.d.ts.map +1 -1
  140. package/dist/vanillajs/auth/handlers/IframeAuthHandler.js +64 -11
  141. package/dist/vanillajs/auth/handlers/IframeAuthHandler.js.map +1 -1
  142. package/dist/vanillajs/auth/handlers/MessageHandler.d.ts.map +1 -1
  143. package/dist/vanillajs/auth/handlers/MessageHandler.js +4 -1
  144. package/dist/vanillajs/auth/handlers/MessageHandler.js.map +1 -1
  145. package/dist/vanillajs/auth/handlers/PopupHandler.d.ts.map +1 -1
  146. package/dist/vanillajs/auth/handlers/PopupHandler.js +3 -1
  147. package/dist/vanillajs/auth/handlers/PopupHandler.js.map +1 -1
  148. package/dist/vanillajs/auth/types/AuthTypes.d.ts +11 -1
  149. package/dist/vanillajs/auth/types/AuthTypes.d.ts.map +1 -1
  150. package/dist/vanillajs/auth/types/AuthTypes.js.map +1 -1
  151. package/dist/vanillajs/iframe/IframeManager.d.ts +22 -1
  152. package/dist/vanillajs/iframe/IframeManager.d.ts.map +1 -1
  153. package/dist/vanillajs/iframe/IframeManager.js +184 -22
  154. package/dist/vanillajs/iframe/IframeManager.js.map +1 -1
  155. package/dist/vanillajs/types/index.d.ts +1 -1
  156. package/dist/vanillajs/types/index.d.ts.map +1 -1
  157. package/dist/vanillajs/types/index.js +1 -1
  158. package/dist/vanillajs/types/index.js.map +1 -1
  159. package/dist/vanillajs/ui/LoadingComponents.d.ts +4 -0
  160. package/dist/vanillajs/ui/LoadingComponents.d.ts.map +1 -1
  161. package/dist/vanillajs/ui/LoadingComponents.js +51 -1
  162. package/dist/vanillajs/ui/LoadingComponents.js.map +1 -1
  163. package/package.json +5 -5
  164. package/dist/nextjs/hooks/index.d.ts +0 -2
  165. package/dist/nextjs/hooks/index.d.ts.map +0 -1
  166. package/dist/nextjs/hooks/index.js +0 -2
  167. package/dist/nextjs/hooks/index.js.map +0 -1
  168. package/dist/nextjs/hooks/usePrevious.d.ts +0 -2
  169. package/dist/nextjs/hooks/usePrevious.d.ts.map +0 -1
  170. package/dist/nextjs/hooks/usePrevious.js +0 -9
  171. package/dist/nextjs/hooks/usePrevious.js.map +0 -1
  172. package/dist/nextjs/hooks/useUserCookie.d.ts +0 -9
  173. package/dist/nextjs/hooks/useUserCookie.d.ts.map +0 -1
  174. package/dist/nextjs/hooks/useUserCookie.js +0 -109
  175. package/dist/nextjs/hooks/useUserCookie.js.map +0 -1
  176. package/dist/react-router-7/components/UserButtonPresentation.d.ts.map +0 -1
  177. package/dist/react-router-7/components/UserButtonPresentation.js.map +0 -1
  178. package/dist/shared/components/BlockDisplay.d.ts +0 -6
  179. package/dist/shared/components/BlockDisplay.d.ts.map +0 -1
  180. package/dist/shared/components/BlockDisplay.js +0 -30
  181. package/dist/shared/components/BlockDisplay.js.map +0 -1
  182. package/dist/shared/components/CivicAuthIframe.d.ts +0 -10
  183. package/dist/shared/components/CivicAuthIframe.d.ts.map +0 -1
  184. package/dist/shared/components/CivicAuthIframe.js +0 -49
  185. package/dist/shared/components/CivicAuthIframe.js.map +0 -1
  186. package/dist/shared/components/CivicAuthIframeContainer.d.ts +0 -15
  187. package/dist/shared/components/CivicAuthIframeContainer.d.ts.map +0 -1
  188. package/dist/shared/components/CivicAuthIframeContainer.js +0 -177
  189. package/dist/shared/components/CivicAuthIframeContainer.js.map +0 -1
  190. package/dist/shared/components/CivicAuthLogoutIframeContainer.d.ts +0 -6
  191. package/dist/shared/components/CivicAuthLogoutIframeContainer.d.ts.map +0 -1
  192. package/dist/shared/components/CivicAuthLogoutIframeContainer.js +0 -51
  193. package/dist/shared/components/CivicAuthLogoutIframeContainer.js.map +0 -1
  194. package/dist/shared/components/IFrameAndLoading.d.ts +0 -7
  195. package/dist/shared/components/IFrameAndLoading.d.ts.map +0 -1
  196. package/dist/shared/components/IFrameAndLoading.js +0 -66
  197. package/dist/shared/components/IFrameAndLoading.js.map +0 -1
  198. package/dist/shared/hooks/useAuth.d.ts +0 -3
  199. package/dist/shared/hooks/useAuth.d.ts.map +0 -1
  200. package/dist/shared/hooks/useAuth.js +0 -12
  201. package/dist/shared/hooks/useAuth.js.map +0 -1
  202. package/dist/shared/hooks/useIframe.d.ts +0 -3
  203. package/dist/shared/hooks/useIframe.d.ts.map +0 -1
  204. package/dist/shared/hooks/useIframe.js +0 -13
  205. package/dist/shared/hooks/useIframe.js.map +0 -1
  206. package/dist/shared/hooks/useIsInIframe.d.ts +0 -7
  207. package/dist/shared/hooks/useIsInIframe.d.ts.map +0 -1
  208. package/dist/shared/hooks/useIsInIframe.js +0 -23
  209. package/dist/shared/hooks/useIsInIframe.js.map +0 -1
  210. package/dist/shared/hooks/useSignIn.d.ts +0 -20
  211. package/dist/shared/hooks/useSignIn.d.ts.map +0 -1
  212. package/dist/shared/hooks/useSignIn.js +0 -358
  213. package/dist/shared/hooks/useSignIn.js.map +0 -1
  214. package/dist/shared/providers/IframeProvider.d.ts +0 -28
  215. package/dist/shared/providers/IframeProvider.d.ts.map +0 -1
  216. package/dist/shared/providers/IframeProvider.js +0 -64
  217. package/dist/shared/providers/IframeProvider.js.map +0 -1
  218. /package/dist/{react-router-7 → shared}/components/UserButtonPresentation.d.ts +0 -0
  219. /package/dist/{react-router-7 → shared}/components/UserButtonPresentation.js +0 -0
@@ -1,18 +1,11 @@
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";
13
- import { revalidatePath } from "next/cache.js";
7
+ import { CodeVerifier, UserStorage } from "../shared/lib/types.js";
14
8
  import { NextResponse } from "next/server.js";
15
- import { NextServerAuthenticationRefresherImpl } from "./NextServerAuthenticationRefresherImpl.js";
16
9
  const logger = loggers.nextjs.handlers.auth;
17
10
  class AuthError extends Error {
18
11
  status;
@@ -22,397 +15,270 @@ class AuthError extends Error {
22
15
  this.name = "AuthError";
23
16
  }
24
17
  }
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
18
  /**
65
- * create a code verifier and challenge for PKCE
66
- * saving the verifier in a cookie for later use
67
- * @returns {Promise<NextResponse>}
19
+ * Helper to convert NextRequest to UrlDetectionRequest for framework-agnostic URL handling
68
20
  */
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,
21
+ const toUrlDetectionRequest = (request) => ({
22
+ url: request.url,
23
+ headers: Object.fromEntries(request.headers.entries()),
24
+ searchParams: {
25
+ get: (name) => request.nextUrl.searchParams.get(name),
26
+ },
27
+ cookies: {
28
+ get: (name) => request.cookies.get(name),
29
+ },
30
+ });
31
+ /**
32
+ * Helper to create CivicAuth instance for a request
33
+ * Now handles appUrl detection for proxy environments
34
+ */
35
+ const createCivicAuth = (request, config) => {
36
+ const resolvedConfig = resolveAuthConfig(config);
37
+ const cookieStorage = new NextjsCookieStorage({
38
+ ...resolvedConfig.cookies?.tokens,
39
+ [UserStorage.USER]: resolvedConfig.cookies?.user,
40
+ });
41
+ // Convert to framework-agnostic request format
42
+ const urlDetectionRequest = toUrlDetectionRequest(request);
43
+ // Get appUrl from client (for proxy environments)
44
+ const clientAppUrl = CivicAuth.getAppUrl(urlDetectionRequest);
45
+ // Use baseUrl from config, then client appUrl, then request origin
46
+ // This matches the main branch priority: config > client > request
47
+ const appUrl = resolvedConfig.baseUrl ||
48
+ clientAppUrl ||
49
+ new URL(urlDetectionRequest.url).origin;
50
+ // Build absolute URLs using detected appUrl or request origin
51
+ const absoluteCallbackUrl = resolvedConfig.callbackUrl.startsWith("http")
52
+ ? resolvedConfig.callbackUrl
53
+ : CivicAuth.toAbsoluteUrl(urlDetectionRequest, resolvedConfig.callbackUrl, appUrl);
54
+ const absoluteLogoutCallbackUrl = resolvedConfig.logoutCallbackUrl.startsWith("http")
55
+ ? resolvedConfig.logoutCallbackUrl
56
+ : CivicAuth.toAbsoluteUrl(urlDetectionRequest, resolvedConfig.logoutCallbackUrl, appUrl);
57
+ const civicAuth = new CivicAuth(cookieStorage, {
58
+ clientId: resolvedConfig.clientId,
59
+ redirectUrl: absoluteCallbackUrl,
60
+ oauthServer: resolvedConfig.oauthServer,
61
+ postLogoutRedirectUrl: absoluteLogoutCallbackUrl,
62
+ loginSuccessUrl: request.url,
84
63
  });
64
+ return {
65
+ civicAuth,
66
+ cookieStorage,
67
+ appUrl, // Return appUrl for use in other functions
68
+ urlDetectionRequest, // Return for use in handlers
69
+ };
85
70
  };
86
- async function performTokenExchangeAndSetCookies(config, code, state, appUrl) {
71
+ /**
72
+ * Login handler - backend OAuth login initiation endpoint
73
+ * Uses CivicAuth.buildLoginUrl()
74
+ */
75
+ async function handleLogin(request, config) {
87
76
  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
77
  try {
94
- await resolveOAuthAccessCode(code, state, cookieStorage, {
95
- ...resolvedConfigs,
96
- redirectUrl: callbackUrl,
78
+ const frontendState = request.nextUrl.searchParams.get("state");
79
+ // Store appUrl in cookie if provided as query parameter
80
+ const appUrlFromQuery = request.nextUrl.searchParams.get("appUrl");
81
+ if (appUrlFromQuery) {
82
+ const cookieStorage = new NextjsCookieStorage(resolvedConfigs.cookies?.tokens ?? {});
83
+ await cookieStorage.set(CodeVerifier.APP_URL, appUrlFromQuery);
84
+ }
85
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
86
+ const url = await civicAuth.buildLoginUrl({
87
+ state: frontendState || undefined,
97
88
  });
89
+ logger.info("[LOGIN_HANDLER] Redirecting to OAuth login URL", {
90
+ loginUrl: url.toString(),
91
+ });
92
+ return NextResponse.redirect(url.toString());
98
93
  }
99
94
  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);
95
+ logger.error("[LOGIN_HANDLER] Backend login error:", error);
96
+ const urlDetectionRequest = toUrlDetectionRequest(request);
97
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
98
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, "/?error=login_failed", appUrl));
106
99
  }
107
- const userSession = new GenericUserSession(cookieStorage);
108
- await userSession.set(user);
109
100
  }
110
101
  async function handleRefresh(request, config) {
111
102
  const resolvedConfigs = resolveAuthConfig(config);
112
- const cookieStorage = getCookieStorageWithUserOverrides(config);
113
- const userSession = new GenericUserSession(cookieStorage);
114
- const targetUrl = request.nextUrl.searchParams.get("targetUrl");
115
103
  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");
104
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
105
+ await civicAuth.refreshTokens();
106
+ logger.info("[REFRESH_HANDLER] Tokens refreshed successfully");
107
+ const targetUrl = request.nextUrl.searchParams.get("targetUrl");
133
108
  if (targetUrl) {
134
109
  // Success: clear the refresh attempt tracking cookie and redirect to target
135
110
  const response = NextResponse.redirect(targetUrl);
136
111
  response.cookies.delete("_civic_last_refresh");
137
112
  return response;
138
113
  }
139
- // For backend flows, tokens might be null since they're managed in HTTP-only cookies
140
- const response = NextResponse.json({
114
+ return NextResponse.json({
141
115
  status: "success",
142
- tokens: tokens || null,
116
+ message: "Tokens refreshed",
143
117
  });
144
- return response;
145
118
  }
146
119
  catch (error) {
147
- logger.error("handleRefresh: Token refresh failed, clearing tokens:", error);
148
- await clearTokens(cookieStorage);
149
- await userSession.clear();
120
+ logger.error("[REFRESH_HANDLER] Token refresh error:", error);
121
+ const targetUrl = request.nextUrl.searchParams.get("targetUrl");
150
122
  if (targetUrl) {
151
- logger.warn("handleRefresh: Refresh failed, redirecting to targetUrl");
152
123
  return NextResponse.redirect(targetUrl);
153
124
  }
154
- return NextResponse.json({ status: "failed" });
125
+ return NextResponse.json({ error: "Token refresh failed" }, { status: 500 });
155
126
  }
156
127
  }
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
128
  async function handleCallback(request, config) {
235
129
  const resolvedConfigs = resolveAuthConfig(config);
236
130
  const code = request.nextUrl.searchParams.get("code");
237
131
  const state = request.nextUrl.searchParams.get("state");
132
+ const error = request.nextUrl.searchParams.get("error");
133
+ if (error) {
134
+ logger.error("OAuth error in callback:", error);
135
+ const urlDetectionRequest = toUrlDetectionRequest(request);
136
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
137
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, "/?error=oauth_error", appUrl));
138
+ }
238
139
  if (!code || !state)
239
140
  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", {
141
+ try {
142
+ const { civicAuth, appUrl, urlDetectionRequest } = createCivicAuth(request, resolvedConfigs);
143
+ // Convert NextRequest to the format expected by handleCallback
144
+ const handleCallbackRequest = {
145
+ headers: Object.fromEntries(request.headers.entries()),
146
+ url: request.url.toString(),
147
+ };
148
+ // Get loginSuccessUrl with proper baseUrl handling
149
+ const loginSuccessUrl = CivicAuth.getLoginSuccessUrl(urlDetectionRequest, appUrl);
150
+ const frontendUrl = loginSuccessUrl || resolvedConfigs.loginSuccessUrl || "/";
151
+ // Use CivicAuth's smart callback handler
152
+ const result = await civicAuth.handleCallback({
153
+ code,
267
154
  state,
268
- serverTokenExchange: serverTokenExchangeFromState(`${state}`),
155
+ req: handleCallbackRequest,
156
+ }, {
157
+ // Pass the properly resolved frontendUrl
158
+ frontendUrl: frontendUrl,
269
159
  });
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);
160
+ if (result.redirectTo) {
161
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, result.redirectTo, appUrl));
283
162
  }
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;
307
- }
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;
314
- }
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
- };
325
- const revalidateUrlPath = async (url) => {
326
- try {
327
- const path = new URL(url).pathname;
328
- revalidatePath(path);
163
+ if (result.content) {
164
+ // Handle both string content and object content
165
+ if (typeof result.content === "string") {
166
+ return new NextResponse(result.content, {
167
+ status: 200,
168
+ headers: {
169
+ "Content-Type": "text/html",
170
+ },
171
+ });
172
+ }
173
+ else {
174
+ // Object content (JSON response)
175
+ return NextResponse.json(result.content);
176
+ }
177
+ }
178
+ // Fallback redirect
179
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, "/", appUrl));
329
180
  }
330
181
  catch (error) {
331
- logger.warn("Failed to revalidate path after logout:", error);
182
+ logger.error("[CALLBACK_HANDLER] OAuth callback error:", error);
183
+ const urlDetectionRequest = toUrlDetectionRequest(request);
184
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
185
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, "/?error=callback_failed", appUrl));
332
186
  }
333
- };
187
+ }
334
188
  export async function handleLogout(request, config) {
335
189
  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
343
- 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,
190
+ try {
191
+ logger.info("[LOGOUT_HANDLER] Backend logout endpoint called");
192
+ // Check for client-provided logoutRedirectUrl query parameter
193
+ const requestUrl = new URL(request.url);
194
+ const clientLogoutRedirectUrl = requestUrl.searchParams.get("logoutRedirectUrl");
195
+ const frontendState = new URL(request.url).searchParams.get("state");
196
+ // If client provided a logoutRedirectUrl, override the config
197
+ let configToUse = resolvedConfigs;
198
+ if (clientLogoutRedirectUrl) {
199
+ configToUse = {
200
+ ...resolvedConfigs,
201
+ logoutCallbackUrl: clientLogoutRedirectUrl,
202
+ };
203
+ logger.info("[LOGOUT_HANDLER] Overriding logout callback URL", {
204
+ original: resolvedConfigs.logoutCallbackUrl,
205
+ override: clientLogoutRedirectUrl,
206
+ });
207
+ }
208
+ const { civicAuth } = createCivicAuth(request, configToUse);
209
+ const logoutUrl = await civicAuth.buildLogoutRedirectUrl({
210
+ state: frontendState || undefined,
348
211
  });
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 : ""}`);
354
- }
355
- const displayMode = displayModeFromState(state, "iframe");
356
- if (displayMode === "iframe") {
357
- // clear auth cookies immediately before calling the logout endpoint to give faster UX
358
- await clearAuthCookies();
359
- await revalidateUrlPath(request.url);
360
- }
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
- }
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,
212
+ await civicAuth.clearTokens();
213
+ // Convert to URL object to modify parameters
214
+ const url = new URL(logoutUrl.toString());
215
+ // Remove the state parameter to avoid it showing up in the frontend URL
216
+ url.searchParams.delete("state");
217
+ logger.info("[LOGOUT_HANDLER] Returning logout URL for client-side iframe handling", {
218
+ logoutUrl: url.toString(),
382
219
  });
220
+ return NextResponse.redirect(url.toString());
383
221
  }
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;
222
+ catch (error) {
223
+ logger.error("[LOGOUT_HANDLER] Logout error:", error);
224
+ // If logout URL generation fails, clear tokens and redirect to home
225
+ try {
226
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
227
+ await civicAuth.clearTokens();
228
+ // Use client-provided logoutRedirectUrl for fallback too
229
+ const requestUrl = new URL(request.url);
230
+ const clientLogoutRedirectUrl = requestUrl.searchParams.get("logoutRedirectUrl");
231
+ const urlDetectionRequest = toUrlDetectionRequest(request);
232
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
233
+ const fallbackUrl = clientLogoutRedirectUrl || resolvedConfigs.logoutCallbackUrl;
234
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, fallbackUrl, appUrl));
235
+ }
236
+ catch (clearError) {
237
+ logger.error("[LOGOUT_HANDLER] Failed to clear tokens:", clearError);
238
+ // Final fallback
239
+ const requestUrl = new URL(request.url);
240
+ const clientLogoutRedirectUrl = requestUrl.searchParams.get("logoutRedirectUrl");
241
+ const urlDetectionRequest = toUrlDetectionRequest(request);
242
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
243
+ const fallbackUrl = clientLogoutRedirectUrl || resolvedConfigs.logoutCallbackUrl;
244
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, fallbackUrl, appUrl));
245
+ }
393
246
  }
394
- logger.debug("handleLogoutComplete: redirecting to postLogoutRedirectUrl", {
395
- postLogoutRedirectUrl,
396
- });
397
- revalidateUrlPath(postLogoutRedirectUrl);
398
- return NextResponse.redirect(postLogoutRedirectUrl);
399
- };
247
+ }
400
248
  export async function handleLogoutCallback(request, config) {
401
249
  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");
250
+ try {
251
+ logger.info("[LOGOUT_CALLBACK_HANDLER] Backend logout callback endpoint called");
252
+ // Clear authentication cookies
406
253
  await clearAuthCookies();
407
- return handleLogoutComplete({ request, resolvedConfigs });
254
+ // Get framework-agnostic request and create CivicAuth instance
255
+ const urlDetectionRequest = toUrlDetectionRequest(request);
256
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
257
+ // Get the state parameter for iframe detection
258
+ const state = request.nextUrl.searchParams.get("state");
259
+ // If this is an iframe request, return HTML with logout success signal
260
+ if (state && displayModeFromState(state, "iframe") === "iframe") {
261
+ // For iframe mode, include the post-logout redirect URL in the HTML
262
+ const postLogoutRedirectUrl = civicAuth.getPostLogoutRedirectUrl(urlDetectionRequest);
263
+ 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>`);
264
+ response.headers.set("Content-Type", "text/html; charset=utf-8");
265
+ logger.info("[LOGOUT_CALLBACK_HANDLER] Returning iframe logout success HTML", { postLogoutRedirectUrl });
266
+ return response;
267
+ }
268
+ // For non-iframe requests, redirect to the logout callback URL or post-logout URL
269
+ const redirectUrl = civicAuth.getPostLogoutRedirectUrl(urlDetectionRequest);
270
+ logger.info("[LOGOUT_CALLBACK_HANDLER] Redirecting to logout callback URL", {
271
+ logoutCallbackUrl: resolvedConfigs.logoutCallbackUrl,
272
+ redirectUrl,
273
+ });
274
+ return NextResponse.redirect(redirectUrl);
275
+ }
276
+ catch (error) {
277
+ logger.error("[LOGOUT_CALLBACK_HANDLER] Logout callback error:", error);
278
+ const urlDetectionRequest = toUrlDetectionRequest(request);
279
+ const appUrl = CivicAuth.getAppUrl(urlDetectionRequest);
280
+ return NextResponse.redirect(CivicAuth.toAbsoluteUrl(urlDetectionRequest, resolvedConfigs.logoutCallbackUrl, appUrl));
408
281
  }
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
282
  }
417
283
  /**
418
284
  * Creates an authentication handler for Next.js API routes
@@ -434,7 +300,8 @@ export const handler = (authConfig = {}) => async (request) => {
434
300
  const lastSegment = pathSegments[pathSegments.length - 1];
435
301
  switch (lastSegment) {
436
302
  case "challenge":
437
- return await handleChallenge(request, config);
303
+ case "login":
304
+ return await handleLogin(request, config);
438
305
  case "callback":
439
306
  return await handleCallback(request, config);
440
307
  case "refresh":
@@ -443,6 +310,8 @@ export const handler = (authConfig = {}) => async (request) => {
443
310
  return await handleLogout(request, config);
444
311
  case "logoutcallback":
445
312
  return await handleLogoutCallback(request, config);
313
+ case "user":
314
+ return await handleUser(request, config);
446
315
  default:
447
316
  throw new AuthError(`Invalid auth route: ${pathname}`, 404);
448
317
  }
@@ -456,4 +325,24 @@ export const handler = (authConfig = {}) => async (request) => {
456
325
  return response;
457
326
  }
458
327
  };
328
+ /**
329
+ * User endpoint - returns current user data as JSON
330
+ * Uses CivicAuth.isLoggedIn() and getUser()
331
+ */
332
+ async function handleUser(request, config) {
333
+ const resolvedConfigs = resolveAuthConfig(config);
334
+ try {
335
+ const { civicAuth } = createCivicAuth(request, resolvedConfigs);
336
+ const isLoggedIn = await civicAuth.isLoggedIn();
337
+ if (!isLoggedIn) {
338
+ return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
339
+ }
340
+ const user = await civicAuth.getUser();
341
+ return NextResponse.json({ user });
342
+ }
343
+ catch (error) {
344
+ logger.error("[USER_HANDLER] User endpoint error:", error);
345
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
346
+ }
347
+ }
459
348
  //# sourceMappingURL=routeHandler.js.map