@evenicanpm/storefront-core 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evenicanpm/storefront-core",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Core module for D365/e4 Headless Storefront",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -32,13 +32,14 @@
32
32
  "peerDependencies": {
33
33
  "@msdyn365-commerce/retail-proxy": "9.50",
34
34
  "@tanstack/react-query": "^5.76.1",
35
+ "better-auth": "*",
35
36
  "graphql": "^16.11.0",
36
37
  "graphql-request": "^7.1.2",
37
38
  "jsonwebtoken": "^9.0.2",
38
39
  "lodash": "^4.17.21",
39
40
  "next": "^15.2.1",
40
- "next-auth": "^4.24.11",
41
41
  "object-mapper": "^6.2.0",
42
+ "set-cookie-parser": "^2.7.1",
42
43
  "typedoc": "^0.28.9",
43
44
  "typedoc-plugin-merge-modules": "^7.0.0",
44
45
  "typescript": "^5.8.3",
@@ -49,5 +50,5 @@
49
50
  "esbuild": "0.19.11"
50
51
  }
51
52
  },
52
- "gitHead": "8227589295d3d63586f9b26fff8d643b070989b3"
53
+ "gitHead": "21ad4044713e4d6625e30c745dd6cd5c7c52b0b3"
53
54
  }
@@ -1,8 +1,8 @@
1
1
  "use server";
2
2
  import type { ICallerContext } from "@msdyn365-commerce/retail-proxy";
3
- import { cookies } from "next/headers";
3
+ import { cookies, headers as getHeaders } from "next/headers";
4
4
  import { getLocale } from "next-intl/server";
5
- import { manuallyRetrieveSessionCookie } from "@/auth/next-auth-cookie-manager";
5
+ import { auth } from "@/auth/better-auth";
6
6
 
7
7
  /**
8
8
  * Retrieves the D365 context from the "D365Context" cookie, enriches it with the current session token,
@@ -21,14 +21,16 @@ import { manuallyRetrieveSessionCookie } from "@/auth/next-auth-cookie-manager";
21
21
  export async function getContextFromCookie(): Promise<ICallerContext> {
22
22
  const cookieStore = await cookies();
23
23
  const d365Context = cookieStore.get("D365Context");
24
- const token = await manuallyRetrieveSessionCookie();
24
+ const session = await auth.api.getSession({ headers: await getHeaders() });
25
+ const accessToken =
26
+ (session?.session as { accessToken?: string })?.accessToken || "";
25
27
  const locale = await getLocale();
26
28
  if (d365Context) {
27
29
  const context = JSON.parse(d365Context.value) as ICallerContext;
28
30
  context.requestContext.operationId = crypto.randomUUID();
29
31
  context.requestContext.user = {
30
- isAuthenticated: !!token?.accessToken,
31
- token: token?.accessToken ?? "",
32
+ isAuthenticated: !!accessToken,
33
+ token: accessToken,
32
34
  };
33
35
  context.requestContext.locale = locale;
34
36
  return context;
@@ -0,0 +1,35 @@
1
+ import createGraphQLClient from "@evenicanpm/storefront-core/src/lib/create-graphql-client";
2
+ import type { NextRequest, NextResponse } from "next/server";
3
+ import { parse, splitCookiesString } from "set-cookie-parser";
4
+
5
+ const setE4SessionCookies = async (
6
+ response: NextResponse,
7
+ request: NextRequest,
8
+ ) => {
9
+ const cookiesAlreadySet = request.cookies.get("storefront");
10
+ if (cookiesAlreadySet) return response;
11
+
12
+ const e4Api = await createGraphQLClient();
13
+
14
+ const session = await e4Api.e4ReadSessionCustomer();
15
+
16
+ const cookieString = session?.headers?.get("set-cookie");
17
+ if (!cookieString) return response;
18
+ const splitCookies = parse(
19
+ splitCookiesString(decodeURIComponent(cookieString)),
20
+ );
21
+
22
+ splitCookies.forEach((cookie) => {
23
+ response.cookies.set({
24
+ name: cookie.name,
25
+ value: cookie.value,
26
+ httpOnly: true,
27
+ expires: cookie.expires,
28
+ secure: true,
29
+ });
30
+ });
31
+
32
+ return response;
33
+ };
34
+
35
+ export default setE4SessionCookies;
@@ -2,9 +2,8 @@
2
2
 
3
3
  import { getSdk } from "@evenicanpm/storefront-core/src/api-manager/datasources/e4/graphqlRequestSdk";
4
4
  import { GraphQLClient } from "graphql-request";
5
- import { cookies } from "next/headers";
6
- import { getServerSession } from "next-auth/next";
7
- import authOptions from "@/auth/auth-options";
5
+ import { cookies, headers as getHeaders } from "next/headers";
6
+ import { auth } from "@/auth/better-auth";
8
7
 
9
8
  /**
10
9
  * Initializes and returns a cached GraphQL client
@@ -28,9 +27,11 @@ const getGraphQLClient = async (withAuth = false) => {
28
27
  }
29
28
 
30
29
  if (withAuth) {
31
- const session = await getServerSession(authOptions());
32
- if (session?.accessToken) {
33
- headers.Authorization = `Bearer ${session.accessToken}`;
30
+ const session = await auth.api.getSession({ headers: await getHeaders() });
31
+ const accessToken = (session?.session as { accessToken?: string })
32
+ ?.accessToken;
33
+ if (accessToken) {
34
+ headers.Authorization = `Bearer ${accessToken}`;
34
35
  }
35
36
  }
36
37
 
@@ -7,14 +7,9 @@ import {
7
7
  useQuery,
8
8
  useSuspenseQuery,
9
9
  } from "@tanstack/react-query";
10
- import { Mutex } from "async-mutex";
11
- import jwt from "jsonwebtoken";
12
- import { getSession } from "next-auth/react";
13
- import { manuallyRetrieveSessionCookie } from "@/auth/next-auth-cookie-manager";
14
10
  import signOut from "@/auth/signout";
15
11
 
16
12
  const QUERY_ROUTE = "/api";
17
- const mutex = new Mutex();
18
13
 
19
14
  export type ApiPath<T extends keyof DatasourceApis = keyof DatasourceApis> = [
20
15
  T,
@@ -27,22 +22,6 @@ export type Query<TInput, TOutput> = ReturnType<
27
22
 
28
23
  // --- Shared helpers (no closure dependencies) ---
29
24
 
30
- const checkTokenExpiry = async (): Promise<boolean> => {
31
- let token: string | undefined;
32
- if (isServer) {
33
- const session = (await manuallyRetrieveSessionCookie()) ?? undefined;
34
- token = session?.accessToken;
35
- } else {
36
- const session = await getSession();
37
- token = session?.accessToken;
38
- }
39
- const decoded = token && (jwt.decode(token) as { exp?: number } | null);
40
- const currentTime = Math.floor(Date.now() / 1000);
41
- return (
42
- !!decoded && typeof decoded.exp === "number" && decoded.exp < currentTime
43
- );
44
- };
45
-
46
25
  const doFetch = async <TInput>(
47
26
  input: TInput | undefined,
48
27
  apiManagerPath: ApiPath,
@@ -60,27 +39,6 @@ const doFetch = async <TInput>(
60
39
  return result.json();
61
40
  };
62
41
 
63
- // Acquires a mutex before fetching so concurrent requests don't each trigger
64
- // a token refresh simultaneously (double-check pattern after acquiring lock).
65
- const withTokenRefresh = async <TInput>(
66
- input: TInput | undefined,
67
- apiManagerPath: ApiPath,
68
- ) => {
69
- const release = await mutex.acquire();
70
- try {
71
- const headers = new Headers();
72
- if (await checkTokenExpiry()) {
73
- headers.append("should-refresh-token", "true");
74
- }
75
- return await doFetch(input, apiManagerPath, headers);
76
- } catch (error) {
77
- console.error("Error fetching data:", error);
78
- throw new Error("Failed to fetch data");
79
- } finally {
80
- release();
81
- }
82
- };
83
-
84
42
  // --- Factory ---
85
43
 
86
44
  export const createQuery = <TInput, TOutput>(
@@ -103,10 +61,7 @@ export const createQuery = <TInput, TOutput>(
103
61
  return getApiFromPath(apiManager, apiManagerPath)(input);
104
62
  }
105
63
  : async () => {
106
- const isExpired = await checkTokenExpiry();
107
- return isExpired
108
- ? withTokenRefresh(input, apiManagerPath)
109
- : doFetch(input, apiManagerPath);
64
+ return await doFetch(input, apiManagerPath);
110
65
  },
111
66
  enabled: config?.disableProperties
112
67
  ? config.disableProperties.every((prop) => !!input?.[prop])
@@ -0,0 +1,89 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { genericOAuth, customSession } from "better-auth/plugins";
3
+ import { nextCookies } from "better-auth/next-js";
4
+
5
+ const tenantName = process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_TENANT_NAME || "";
6
+ const loginAuthority = `https://${tenantName}.ciamlogin.com/${tenantName}.onmicrosoft.com`;
7
+
8
+ export const auth = betterAuth({
9
+ secret: process.env.BETTER_AUTH_SECRET,
10
+ baseURL: process.env.BETTER_AUTH_URL || process.env.NEXTAUTH_URL,
11
+ session: {
12
+ cookieCache: {
13
+ enabled: true,
14
+ maxAge: 60 * 60, // 1 hour
15
+ strategy: "jwe",
16
+ refreshCache: true,
17
+ },
18
+ },
19
+ account: {
20
+ storeStateStrategy: "cookie",
21
+ storeAccountCookie: true,
22
+ },
23
+ plugins: [
24
+ genericOAuth({
25
+ config: [
26
+ {
27
+ providerId: "entra-external-id",
28
+ clientId: process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_ID || "",
29
+ clientSecret:
30
+ process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_SECRET || "",
31
+ discoveryUrl: `${loginAuthority}/v2.0/.well-known/openid-configuration`,
32
+ scopes: (
33
+ process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CUSTOM_SCOPES ||
34
+ "offline_access openid email profile"
35
+ ).split(" "),
36
+ pkce: true,
37
+ prompt: "login",
38
+ getUserInfo: async (tokens) => {
39
+ // Decode the id_token to get user claims
40
+ const idToken = tokens.idToken;
41
+ if (idToken) {
42
+ const parts = idToken.split(".");
43
+ if (parts.length === 3) {
44
+ const payload = JSON.parse(
45
+ Buffer.from(parts[1], "base64url").toString(),
46
+ );
47
+ return {
48
+ id: payload.oid || payload.sub,
49
+ name: payload.name,
50
+ email:
51
+ payload.email ||
52
+ payload.preferred_username ||
53
+ payload.emails?.[0],
54
+ emailVerified: !!payload.email_verified,
55
+ };
56
+ }
57
+ }
58
+ return null;
59
+ },
60
+ },
61
+ ],
62
+ }),
63
+ customSession(async ({ user, session }, ctx) => {
64
+ let accessToken = "";
65
+ try {
66
+ const accounts = await ctx.context.internalAdapter.findAccounts(
67
+ user.id,
68
+ );
69
+ const account = accounts?.find(
70
+ (a) => a.providerId === "entra-external-id",
71
+ );
72
+ // For Entra External ID, use id_token as the access token
73
+ accessToken = account?.idToken || account?.accessToken || "";
74
+ } catch {
75
+ // Account data may not be accessible in all modes
76
+ }
77
+ return {
78
+ user,
79
+ session: {
80
+ ...session,
81
+ accessToken,
82
+ },
83
+ };
84
+ }),
85
+ nextCookies(), // must be last plugin
86
+ ],
87
+ });
88
+
89
+ export type Session = typeof auth.$Infer.Session;
@@ -1,42 +1,13 @@
1
- const fetchCsrfToken = async (): Promise<string> => {
2
- try {
3
- const response = await fetch("/api/auth/csrf");
4
- if (!response.ok) {
5
- throw new Error(`Failed to fetch CSRF token: ${response.statusText}`);
6
- }
7
- const data: { csrfToken: string } = await response.json();
8
- return data.csrfToken;
9
- } catch (error) {
10
- console.error("Error fetching CSRF token:", error);
11
- throw error;
12
- }
13
- };
1
+ import { createAuthClient } from "better-auth/react";
2
+
3
+ const authClient = createAuthClient();
14
4
 
15
5
  /**
16
- * Alternative to signOut from "next-auth/react" - https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/react.tsx
17
- * Solution to known next-auth issue - https://github.com/nextauthjs/next-auth/issues/4612
6
+ * Signs the user out and redirects to the home page.
18
7
  */
19
8
  const signOut = async (): Promise<void> => {
20
9
  try {
21
- const csrfToken = await fetchCsrfToken();
22
-
23
- const formData = new URLSearchParams({
24
- csrfToken,
25
- json: "true",
26
- });
27
-
28
- const response = await fetch("/api/auth/signout", {
29
- method: "POST",
30
- headers: {
31
- "Content-Type": "application/x-www-form-urlencoded",
32
- },
33
- body: formData,
34
- });
35
-
36
- if (!response.ok) {
37
- throw new Error(`Failed to sign out: ${response.statusText}`);
38
- }
39
-
10
+ await authClient.signOut();
40
11
  globalThis.location.href = "/";
41
12
  } catch (error) {
42
13
  console.error(error);
@@ -7,8 +7,15 @@ vi.mock("@evenicanpm/storefront-core/src/hooks/use-nextauth-session", () => ({
7
7
  useAppSession: vi.fn(() => ({ session: null })),
8
8
  }));
9
9
 
10
- vi.mock("next-auth/react", () => ({
11
- useSession: () => ({ data: null, status: "unauthenticated" }),
10
+ vi.mock("better-auth/react", () => ({
11
+ createAuthClient: () => ({
12
+ useSession: () => ({ data: null, isPending: false }),
13
+ signIn: { oauth2: vi.fn() },
14
+ }),
15
+ }));
16
+
17
+ vi.mock("better-auth/client/plugins", () => ({
18
+ genericOAuthClient: () => ({}),
12
19
  }));
13
20
 
14
21
  vi.mock(
@@ -4,6 +4,14 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
4
4
  import { NextIntlClientProvider } from "next-intl";
5
5
  import { vi } from "vitest";
6
6
 
7
+ const { mockMutate, mockHandleRedirectPromise, mockUseSession } = vi.hoisted(
8
+ () => ({
9
+ mockMutate: vi.fn().mockResolvedValue({}),
10
+ mockHandleRedirectPromise: vi.fn().mockResolvedValue(null),
11
+ mockUseSession: vi.fn(),
12
+ }),
13
+ );
14
+
7
15
  vi.mock(
8
16
  "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-init",
9
17
  () => ({
@@ -11,10 +19,6 @@ vi.mock(
11
19
  }),
12
20
  );
13
21
 
14
- const mockMutate = vi.fn().mockResolvedValue({});
15
- const mockHandleRedirectPromise = vi.fn().mockResolvedValue(null);
16
- const mockUseSession = vi.fn();
17
-
18
22
  vi.mock(
19
23
  "@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user",
20
24
  () => ({
@@ -36,9 +40,17 @@ vi.mock("next/navigation", () => ({
36
40
  usePathname: () => "/mock-path",
37
41
  }));
38
42
 
39
- vi.doMock("next-auth/react", () => ({
40
- useSession: mockUseSession,
41
- signIn: vi.fn(),
43
+ vi.mock("better-auth/react", () => {
44
+ return {
45
+ createAuthClient: () => ({
46
+ useSession: mockUseSession,
47
+ signIn: { oauth2: vi.fn() },
48
+ }),
49
+ };
50
+ });
51
+
52
+ vi.mock("better-auth/client/plugins", () => ({
53
+ genericOAuthClient: () => ({}),
42
54
  }));
43
55
 
44
56
  vi.mock("@evenicanpm/storefront-core/src/auth/msal", () => ({
@@ -58,7 +70,7 @@ describe.skip("User Component", async () => {
58
70
  it("renders login button when user is not authenticated", () => {
59
71
  mockUseSession.mockReturnValue({
60
72
  data: null,
61
- status: "unauthenticated",
73
+ isPending: false,
62
74
  });
63
75
  render(
64
76
  <NextIntlClientProvider locale="en" messages={messages}>
@@ -75,9 +87,9 @@ describe.skip("User Component", async () => {
75
87
  mockUseSession.mockReturnValue({
76
88
  data: {
77
89
  user: { email: "test@example.com" },
78
- accessToken: "mockAccessToken",
90
+ session: { accessToken: "mockAccessToken" },
79
91
  },
80
- status: "authenticated",
92
+ isPending: false,
81
93
  });
82
94
 
83
95
  render(
@@ -94,10 +106,10 @@ describe.skip("User Component", async () => {
94
106
  expect(userMenuButton).toBeInTheDocument();
95
107
  });
96
108
 
97
- it("calls signOut when session.error is present", () => {
109
+ it("calls signOut when session is null after pending", () => {
98
110
  mockUseSession.mockReturnValue({
99
- data: { error: "Some auth error" },
100
- status: "authenticated",
111
+ data: null,
112
+ isPending: false,
101
113
  });
102
114
 
103
115
  render(
@@ -113,8 +125,11 @@ describe.skip("User Component", async () => {
113
125
  const mockSignOut = vi.mocked(signOut);
114
126
 
115
127
  mockUseSession.mockReturnValue({
116
- data: { user: { email: "test@example.com" }, accessToken: "token" },
117
- status: "authenticated",
128
+ data: {
129
+ user: { email: "test@example.com" },
130
+ session: { accessToken: "token" },
131
+ },
132
+ isPending: false,
118
133
  });
119
134
 
120
135
  render(
@@ -139,9 +154,9 @@ describe.skip("User Component", async () => {
139
154
  mockUseSession.mockReturnValue({
140
155
  data: {
141
156
  user: { email: "test@example.com" },
142
- accessToken: "mockAccessToken",
157
+ session: { accessToken: "mockAccessToken" },
143
158
  },
144
- status: "authenticated",
159
+ isPending: false,
145
160
  });
146
161
 
147
162
  vi.mocked(mockHandleRedirectPromise).mockResolvedValue({
@@ -193,9 +208,9 @@ describe.skip("User Component", async () => {
193
208
  mockUseSession.mockReturnValue({
194
209
  data: {
195
210
  user: { email: "test@example.com" },
196
- accessToken: "mockAccessToken",
211
+ session: { accessToken: "mockAccessToken" },
197
212
  },
198
- status: "authenticated",
213
+ isPending: false,
199
214
  });
200
215
 
201
216
  vi.mocked(mockHandleRedirectPromise).mockRejectedValue(
@@ -23,7 +23,13 @@ import {
23
23
  } from "@mui/material";
24
24
  import { useRouter } from "next/navigation";
25
25
  // Auth components
26
- import { signIn, useSession } from "next-auth/react";
26
+ import { createAuthClient } from "better-auth/react";
27
+ import { genericOAuthClient } from "better-auth/client/plugins";
28
+
29
+ const authClient = createAuthClient({
30
+ plugins: [genericOAuthClient()],
31
+ });
32
+ const { useSession: useBetterAuthSession } = authClient;
27
33
  import type React from "react";
28
34
  import {
29
35
  createContext,
@@ -52,7 +58,7 @@ interface UserContextValue {
52
58
  status: string;
53
59
  menuItems: typeof MENU_ITEMS;
54
60
  handleLogout: () => Promise<void>;
55
- signIn: typeof signIn;
61
+ handleSignIn: () => void;
56
62
  signOut: () => Promise<void>;
57
63
  anchorEl: HTMLButtonElement | null;
58
64
  setAnchorEl: (el: HTMLButtonElement | null) => void;
@@ -66,12 +72,15 @@ interface Props {
66
72
  }
67
73
 
68
74
  const User = ({ children }: Props) => {
69
- const { data: session, status } = useSession();
75
+ const { data: session, isPending } = useBetterAuthSession();
76
+ const status = isPending
77
+ ? "loading"
78
+ : session
79
+ ? "authenticated"
80
+ : "unauthenticated";
70
81
 
71
- if (session?.error) {
72
- // Custom error set in server - /api/auth/[...nextauth]
73
- console.warn("Auth Error", session?.error);
74
- signOutCustom();
82
+ if (session === null && !isPending) {
83
+ // No error handling needed - session is simply null when not authenticated
75
84
  }
76
85
 
77
86
  const isAuthenticated = !!session;
@@ -81,12 +90,12 @@ const User = ({ children }: Props) => {
81
90
 
82
91
  // session init
83
92
  useEffect(() => {
84
- if (isAuthenticated) {
85
- const { accessToken, user } = session;
86
- const email = user?.email;
87
- if (accessToken && email) {
88
- sessionInit({ accessToken, email });
89
- }
93
+ if (isAuthenticated && session?.user?.email) {
94
+ const accessToken =
95
+ (session.session as { accessToken?: string })?.accessToken || "";
96
+ sessionInit({ accessToken, email: session.user.email }).catch(
97
+ console.error,
98
+ );
90
99
  }
91
100
  }, [isAuthenticated, session, sessionInit]);
92
101
 
@@ -124,6 +133,13 @@ const User = ({ children }: Props) => {
124
133
  }
125
134
  };
126
135
 
136
+ const handleSignIn = () => {
137
+ authClient.signIn.oauth2({
138
+ providerId: "entra-external-id",
139
+ callbackURL: "/",
140
+ });
141
+ };
142
+
127
143
  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
128
144
  const open = Boolean(anchorEl);
129
145
 
@@ -132,7 +148,7 @@ const User = ({ children }: Props) => {
132
148
  status,
133
149
  menuItems: MENU_ITEMS,
134
150
  handleLogout,
135
- signIn,
151
+ handleSignIn,
136
152
  signOut: signOutCustom,
137
153
  anchorEl,
138
154
  setAnchorEl,
@@ -149,13 +165,13 @@ User.MobileTrigger = function MobileTrigger() {
149
165
  const ctx = useContext(UserContext);
150
166
  const router = useRouter();
151
167
  if (!ctx) return null;
152
- const { isAuthenticated, signIn } = ctx;
168
+ const { isAuthenticated, handleSignIn } = ctx;
153
169
  return (
154
170
  <IconButton
155
171
  aria-label="User Menu"
156
172
  onClick={() => {
157
173
  if (isAuthenticated) router.push("/account/profile");
158
- else signIn();
174
+ else handleSignIn();
159
175
  }}
160
176
  >
161
177
  <MobileUserIcon style={{ fontSize: "1.25rem" }} />
@@ -239,13 +255,13 @@ User.MenuItem = function MenuItem({
239
255
  User.LoginButton = function LoginButton() {
240
256
  const ctx = useContext(UserContext);
241
257
  if (!ctx) return null;
242
- const { signIn } = ctx;
258
+ const { handleSignIn } = ctx;
243
259
  return (
244
260
  <Button
245
261
  size="small"
246
262
  variant="contained"
247
263
  color="primary"
248
- onClick={() => signIn()}
264
+ onClick={() => handleSignIn()}
249
265
  >
250
266
  <T path="Account.Menu.btnLogin" />
251
267
  </Button>
@@ -8,9 +8,11 @@ import { AddToWishListDialogContext } from "@evenicanpm/storefront-core/src/comp
8
8
  // LOCAL COMPONENTS
9
9
  import DialogHeader from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header";
10
10
  import { Dialog, DialogContent } from "@mui/material";
11
- import { useSession } from "next-auth/react";
11
+ import { createAuthClient } from "better-auth/react";
12
12
  import type React from "react";
13
13
 
14
+ const { useSession } = createAuthClient();
15
+
14
16
  export interface AddToWishlistDialogProps {
15
17
  open: boolean;
16
18
  handleClose: () => void;
@@ -9,8 +9,10 @@ import { H2 } from "@evenicanpm/storefront-core/src/components/Typography";
9
9
  import CreateWishlistButton from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/create-wishlist/create-wishlist-button";
10
10
  import { IconButton, styled, type Theme } from "@mui/material";
11
11
  import useMediaQuery from "@mui/material/useMediaQuery";
12
- import { useSession } from "next-auth/react";
12
+ import { createAuthClient } from "better-auth/react";
13
13
  import { useTranslations } from "next-intl";
14
+
15
+ const { useSession } = createAuthClient();
14
16
  // ICONS
15
17
  import { MdFavorite } from "react-icons/md";
16
18
 
@@ -1,36 +1,31 @@
1
1
  // hooks/useAppSession.ts
2
2
 
3
3
  import useSessionInit from "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-init";
4
- import useSessionLogout from "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-logout";
5
4
  // API hooks
6
5
  import useUpdateUser from "@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user";
7
6
  import {
8
7
  getMsalInstance,
9
8
  initializeMsal,
10
9
  } from "@evenicanpm/storefront-core/src/auth/msal";
11
- import { useSession } from "next-auth/react";
10
+ import { createAuthClient } from "better-auth/react";
12
11
  import { useEffect } from "react";
13
12
 
13
+ const authClient = createAuthClient();
14
+ const { useSession: useBetterAuthSession } = authClient;
15
+
14
16
  export function useAppSession() {
15
- const { data: session } = useSession();
17
+ const { data: session, isPending } = useBetterAuthSession();
16
18
  const { mutateAsync: sessionInit } = useSessionInit();
17
19
  const { mutateAsync: updateUser } = useUpdateUser();
18
- const { mutateAsync: sessionLogout } = useSessionLogout();
19
-
20
- // Handle custom auth errors
21
- useEffect(() => {
22
- if (session?.error) {
23
- console.warn("Auth Error", session.error);
24
- sessionLogout({}); // Or your custom signOut
25
- }
26
- }, [session, sessionLogout]);
27
20
 
28
21
  // Initialize session in API
29
22
  useEffect(() => {
30
23
  if (!session) return;
31
24
 
32
- const { accessToken, user } = session;
33
- if (accessToken && user?.email) {
25
+ const { user } = session;
26
+ const accessToken =
27
+ (session.session as { accessToken?: string })?.accessToken || "";
28
+ if (user?.email) {
34
29
  sessionInit({ accessToken, email: user.email }).catch(console.error);
35
30
  }
36
31
  }, [session, sessionInit]);
@@ -64,5 +59,6 @@ export function useAppSession() {
64
59
  return {
65
60
  session,
66
61
  isAuthenticated: !!session,
62
+ isPending,
67
63
  };
68
64
  }
@@ -10,8 +10,10 @@ import { NextIntlClientProvider } from "next-intl";
10
10
  import type React from "react";
11
11
  import { vi } from "vitest";
12
12
 
13
- vi.mock("next-auth/react", () => ({
14
- // Empty mock for next-auth to stop throwing errors
13
+ vi.mock("better-auth/react", () => ({
14
+ createAuthClient: () => ({
15
+ useSession: () => ({ data: null, isPending: false }),
16
+ }),
15
17
  }));
16
18
 
17
19
  // global.fetch = vi.fn(() => Promise.resolve({ json: () => ({}) }));
@@ -1,10 +0,0 @@
1
- "use server";
2
- import { decode } from "next-auth/jwt";
3
-
4
- export async function decodeToken(token: string) {
5
- const secret = process.env.NEXTAUTH_SECRET; //this shouldn't be from key vault.
6
- if (!secret) {
7
- throw new Error("Environment variable NEXTAUTH_SECRET is not set.");
8
- }
9
- return decode({ token: token, secret });
10
- }
@@ -1,70 +0,0 @@
1
- import type { Account, Session } from "next-auth";
2
- import type { JWT } from "next-auth/jwt";
3
- import { describe, expect, it } from "vitest";
4
- import authOptions, { type ExtendedJwtToken } from "@/auth/auth-options";
5
-
6
- describe("next-auth option callbacks", () => {
7
- describe("jwt callback", () => {
8
- it("should store accessToken and refreshToken when account and profile are available", async () => {
9
- const token: ExtendedJwtToken = {
10
- accessToken: "",
11
- accessTokenExpiresAt: 0,
12
- provider: "azure-ad-b2c",
13
- refreshTokenExpiresAt: 0,
14
- exp: 0,
15
- };
16
- const account: Account = {
17
- access_token: "testAccessToken",
18
- refresh_token: "testRefreshToken",
19
- expires_on: Math.floor(Date.now() / 1000) + 3600,
20
- refresh_token_expires_in: 86400,
21
- providerAccountId: "test-provider-account-id",
22
- provider: "azure-ad-b2c",
23
- type: "oauth",
24
- };
25
-
26
- let newToken: ExtendedJwtToken | undefined;
27
- if (authOptions()?.callbacks?.jwt) {
28
- newToken = await authOptions().callbacks.jwt({
29
- token,
30
- user: { id: "123" },
31
- account,
32
- profile: {},
33
- });
34
- }
35
-
36
- expect(newToken?.accessToken).toBe("testAccessToken");
37
- expect(newToken?.refreshToken).toBe("testRefreshToken");
38
- expect(newToken?.accessTokenExpiresAt).toBe(token.accessTokenExpiresAt); // We're not setting this or using it anymore
39
- expect(newToken?.refreshTokenExpiresAt).toBe(token.refreshTokenExpiresAt); // We're not setting this or using it anymore);
40
- });
41
- });
42
-
43
- describe("session callback", () => {
44
- it("should pass accessToken and error to session", async () => {
45
- const session = { expires: "", user: { name: "123" } };
46
- const token: JWT = {
47
- accessToken: "sessionToken",
48
- accessTokenExpiresAt: 0,
49
- refreshToken: "",
50
- refreshTokenExpiresAt: 0,
51
- error: "SomeError",
52
- };
53
- let newSession: Session | null;
54
- if (authOptions()?.callbacks?.session) {
55
- newSession = await authOptions().callbacks.session({
56
- session,
57
- token,
58
- user: { id: "123", email: "", emailVerified: new Date(Date.now()) },
59
- newSession: {},
60
- trigger: "update",
61
- });
62
- } else {
63
- newSession = null;
64
- }
65
-
66
- expect(newSession?.accessToken).toBe("sessionToken");
67
- expect(newSession?.error).toBe("SomeError");
68
- });
69
- });
70
- });
@@ -1,101 +0,0 @@
1
- import { deleteCartCookie } from "@evenicanpm/storefront-core/src/lib/cart-cookie-handler";
2
- import type {
3
- GetServerSidePropsContext,
4
- NextApiRequest,
5
- NextApiResponse,
6
- } from "next";
7
- import type { NextAuthOptions, Session } from "next-auth";
8
- import type { JWT } from "next-auth/jwt";
9
- import { getServerSession } from "next-auth/next";
10
- import { getProviders } from "@/auth/providers";
11
-
12
- // Split providers into separate array so we can iterate them and match the provider ID
13
- export type ExtendedJwtToken = JWT & {
14
- provider: string;
15
- exp: number;
16
- };
17
-
18
- export type ExtendedSession = Session & { token?: JWT };
19
-
20
- const createAuthOptions = (): NextAuthOptions => {
21
- const providers = getProviders();
22
- const secret = process.env.NEXTAUTH_SECRET; // internally used by next-auth, shouldn't be from key vault.
23
- return {
24
- providers,
25
- secret,
26
- cookies: {
27
- csrfToken: {
28
- name: "next-auth.csrf-token",
29
- options: {
30
- httpOnly: true,
31
- sameSite: "none",
32
- path: "/",
33
- secure: true,
34
- },
35
- },
36
- pkceCodeVerifier: {
37
- name: "next-auth.pkce.code_verifier",
38
- options: {
39
- httpOnly: true,
40
- sameSite: "none",
41
- path: "/",
42
- secure: true,
43
- },
44
- },
45
- nonce: {
46
- name: "next-auth.nonce",
47
- options: {
48
- httpOnly: true,
49
- sameSite: "none",
50
- path: "/",
51
- secure: true,
52
- },
53
- },
54
- },
55
- // Order in which callbacks are called = signin > redirect > jwt > session. Thats why session callback has token info.
56
- callbacks: {
57
- // By default, NextAuth uses "session" with strategy: "jwt"
58
- async jwt({ token, account, profile }) {
59
- const _token = token as ExtendedJwtToken;
60
- // account and profile is available only in first callback after signin
61
- if (account && profile) {
62
- _token.provider = account.provider;
63
- // use id_token for entra external id and access_token for aadb2c
64
- _token.accessToken =
65
- _token.provider === "entra-external-id"
66
- ? account.id_token
67
- : account.access_token;
68
- _token.refreshToken = account.refresh_token;
69
- }
70
- return _token;
71
- // return await checkAccessTokenExpiry(_token)
72
- },
73
- async session({ session, token }) {
74
- const _session = { ...session } as ExtendedSession;
75
- // Way to pass data to the browser
76
- // Use this error to force logout in UI. No proper way to signout user from server.
77
- _session.error = token?.error;
78
- _session.accessToken = token.accessToken;
79
-
80
- return _session;
81
- },
82
- },
83
- events: {
84
- signOut: async () => {
85
- await deleteCartCookie();
86
- },
87
- },
88
- };
89
- };
90
-
91
- export function auth(
92
- // <-- use this function to access the jwt from React components
93
- ...args:
94
- | [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
95
- | [NextApiRequest, NextApiResponse]
96
- | []
97
- ) {
98
- return getServerSession(...args, createAuthOptions());
99
- }
100
-
101
- export default createAuthOptions;
@@ -1,87 +0,0 @@
1
- "use server";
2
- import type { RequestCookie } from "next/dist/compiled/@edge-runtime/cookies";
3
- import { cookies } from "next/headers";
4
- import { decode, encode, type JWT } from "next-auth/jwt";
5
- import type { ExtendedJwtToken } from "@/auth/auth-options";
6
-
7
- const NEXT_AUTH_COOKIE_NAME = "next-auth.session-token";
8
- const SECURE_COOKIE_PREFIX = "__Secure-";
9
-
10
- /**
11
- * Retrieves and decodes the NextAuth session cookie from the request.
12
- *
13
- * This function handles both single and chunked session cookies:
14
- * - If the session cookie is under 4KB, it is stored as a single cookie.
15
- * - If the session cookie exceeds 4KB, it is split into multiple chunked cookies with indexed names.
16
- *
17
- * The function attempts to retrieve the single cookie first. If not found,
18
- * it iteratively collects all chunked cookies, concatenates their values,
19
- * and decodes the resulting token.
20
- *
21
- * @returns A Promise resolving to the decoded session object, or `undefined` if no session cookie is found.
22
- */
23
- const manuallyRetrieveSessionCookie = async () => {
24
- const cookieStore = await cookies();
25
- let cookie: RequestCookie | undefined;
26
- // Single token when the cookie size is < 4k
27
- cookie = cookieStore.get(`${SECURE_COOKIE_PREFIX}${NEXT_AUTH_COOKIE_NAME}`);
28
- //If the cookie is found, return it. the cookie is under 4kb
29
- if (cookie) {
30
- return decode({
31
- token: cookie.value,
32
- secret: process.env.NEXTAUTH_SECRET || "",
33
- });
34
- }
35
- //If the cookie is not found, we need to see if there is a chunked cookie
36
- //The chunked cookie is stored in multiple cookies with the same name and an index
37
- //The cookie is chunked when the cookie size is > 4k
38
- //The value is just split between the cookies, so we can just concat them.
39
- let i = 0;
40
- const cookieValues: string[] = [];
41
- do {
42
- cookie = cookieStore.get(
43
- `${SECURE_COOKIE_PREFIX}${NEXT_AUTH_COOKIE_NAME}.${i}`,
44
- );
45
- if (cookie) {
46
- cookieValues.push(cookie.value);
47
- }
48
- i++;
49
- } while (cookie !== undefined);
50
- if (cookieValues.length) {
51
- return decode({
52
- token: cookieValues.join(""),
53
- secret: process.env.NEXTAUTH_SECRET || "",
54
- });
55
- }
56
-
57
- //If no cookie is found, return undefined
58
- return undefined;
59
- };
60
-
61
- /**
62
- * Manually sets the NextAuth session cookie with the provided JWT token.
63
- *
64
- * This function encodes the given JWT or ExtendedJwtToken and sets it as a secure,
65
- * HTTP-only cookie named `__Secure-next-auth.session-token`. The cookie is set using
66
- * the current cookie store, and the encoding uses the secret from the `NEXTAUTH_SECRET`
67
- * environment variable.
68
- *
69
- * @param token - The JWT or ExtendedJwtToken to be encoded and stored in the session cookie.
70
- * @returns A promise that resolves when the cookie has been set.
71
- */
72
- const manuallySetSessionCookie = async (token: JWT | ExtendedJwtToken) => {
73
- const cookieStore = await cookies();
74
- // Set the cookie with the name __Secure-next-auth.session-token
75
- const value = await encode({
76
- token,
77
- secret: process.env.NEXTAUTH_SECRET || "",
78
- });
79
- cookieStore.set({
80
- name: `${SECURE_COOKIE_PREFIX}${NEXT_AUTH_COOKIE_NAME}`,
81
- value,
82
- httpOnly: true,
83
- secure: true,
84
- });
85
- };
86
-
87
- export { manuallyRetrieveSessionCookie, manuallySetSessionCookie };
@@ -1,32 +0,0 @@
1
- import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
2
-
3
- // By default, NextAuth tries to use either PKCE or Client Secret-based authentication, depending on how the provider is configured.
4
- // If clientSecret is provided, it assumes client secret authentication (which doesn't use code_challenge).
5
- // If clientSecret is missing, it assumes PKCE.
6
- // But Azure AD B2C sometimes requires PKCE explicitly, so NextAuth needs to be told to always use PKCE.
7
- const azureADB2CProvider = () => {
8
- const CLIENT_ID = process.env.NEXT_PUBLIC_AZURE_AD_B2C_CLIENTID || "";
9
- const SCOPE = `${process.env.NEXT_PUBLIC_AZURE_AD_B2C_API_PERMISSION} offline_access openid`;
10
- const LOGIN_AUTHORITY = process.env.NEXT_PUBLIC_AZURE_AD_B2C_LOGIN_AUTHORITY;
11
-
12
- return AzureADB2CProvider({
13
- clientId: CLIENT_ID,
14
- clientSecret: "",
15
- issuer: `${LOGIN_AUTHORITY}/v2.0`, // either (tenantId and primaryUserFlow) or issuer must be provided
16
- authorization: {
17
- // We can also send params from UI - signIn("azure-ad-b2c", {}, { prompt: "login" })
18
- params: {
19
- scope: SCOPE,
20
- // When a user logs in once, the session remains active, and next time Azure AD B2C automatically logs the user in without showing the sign-in page again
21
- // This option forces login screen every time.
22
- prompt: "login",
23
- },
24
- },
25
- checks: ["pkce"],
26
- client: {
27
- token_endpoint_auth_method: "none", // Disables Client Secret, ensuring that only PKCE is used
28
- },
29
- });
30
- };
31
-
32
- export default azureADB2CProvider;
@@ -1,24 +0,0 @@
1
- import AuthentikProvider from "next-auth/providers/authentik";
2
-
3
- const clientId = "DRWJuuA4HEWyhSAsDjTgUHrsDo7CmeUa1p5UVN2e";
4
- const clientSecret =
5
- "4PyTnKqbO6fzDngGrLkD9YiFTxXASsJH7WPoMBeFCpezfrooZE3j2FCnWYXfO5ioFm8Uwx95kHspXsNRjl9S3uq7CBZcJUpbFfXywDMEGqrxXKT4biEuiihOTHL65qOY";
6
- const issuer =
7
- "https://authentik-server.orangeocean-cbcb289b.eastus.azurecontainerapps.io/application/o/sandboxauthapp";
8
- const wellKnown =
9
- "https://authentik-server.orangeocean-cbcb289b.eastus.azurecontainerapps.io/application/o/sandboxauthapp/.well-known/openid-configuration";
10
- const authentikProvider = AuthentikProvider({
11
- clientId,
12
- clientSecret,
13
- issuer,
14
- id: "Authentik",
15
- name: "Authentik",
16
- wellKnown: wellKnown,
17
- authorization: {
18
- params: {
19
- scope: "openid email profile offline_access",
20
- },
21
- },
22
- });
23
-
24
- export default authentikProvider;
@@ -1,50 +0,0 @@
1
- import type { Provider } from "next-auth/providers";
2
-
3
- const createEntraExternalIdProvider = (): Provider => {
4
- const clientId = process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_ID || "";
5
- const customScopes =
6
- process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CUSTOM_SCOPES || "";
7
- const tenantName =
8
- process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_TENANT_NAME || "";
9
- const clientSecret =
10
- process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_SECRET || "";
11
-
12
- // Build auth url and wellKnown from the loginAuthority
13
- const loginAuthority = `https://${tenantName}.ciamlogin.com/${tenantName}.onmicrosoft.com`;
14
- const authUrl = `${loginAuthority}/oauth2/v2.0/authorize`;
15
- const wellKnownUrl = `${loginAuthority}/v2.0/.well-known/openid-configuration`;
16
-
17
- return {
18
- id: "entra-external-id",
19
- clientId: clientId,
20
- client: {
21
- client_id: clientId,
22
- response_types: ["code id_token"],
23
- client_secret: clientSecret,
24
- // token_endpoint_auth_method: 'none',
25
- },
26
- type: "oauth",
27
- idToken: true,
28
- name: "Microsoft Entra External ID",
29
- authorization: {
30
- url: authUrl,
31
- params: {
32
- scope: customScopes,
33
- prompt: "login",
34
- response_type: "code id_token",
35
- response_mode: "form_post",
36
- },
37
- },
38
- wellKnown: wellKnownUrl,
39
- profile(profile) {
40
- return {
41
- id: profile.oid,
42
- name: profile.name,
43
- email: profile.email,
44
- };
45
- },
46
- checks: ["pkce", "nonce"],
47
- };
48
- };
49
-
50
- export default createEntraExternalIdProvider;
@@ -1,16 +0,0 @@
1
- import azureADB2CProvider from "@/auth/providers/aadb2c-provider";
2
- import authentikProvider from "@/auth/providers/authentik-provider";
3
- import createEntraExternalIdProvider from "@/auth/providers/entra-external-id-provider";
4
- import keycloakProvider from "@/auth/providers/keycloak-provider";
5
-
6
- export const getProviders = () => {
7
- const azureProvider = azureADB2CProvider();
8
- const entraExternalIdProvider = createEntraExternalIdProvider();
9
- // once we stop hard coding others use loader function here
10
- return [
11
- authentikProvider,
12
- azureProvider,
13
- keycloakProvider,
14
- entraExternalIdProvider,
15
- ];
16
- };
@@ -1,22 +0,0 @@
1
- import KeycloakProvider from "next-auth/providers/keycloak";
2
-
3
- const clientid = "b6229a60-b0de-4f86-9891-e0b58d756e39";
4
- const issuer =
5
- "https://keycloak-app-g9dmfkf6atdqdrcn.eastus2-01.azurewebsites.net/realms/e4headless";
6
- const wellKnown =
7
- "https://keycloak-app-g9dmfkf6atdqdrcn.eastus2-01.azurewebsites.net/realms/e4headless/.well-known/openid-configuration";
8
- const keycloakProvider = KeycloakProvider({
9
- clientId: clientid,
10
- clientSecret: "",
11
- issuer: issuer,
12
- id: "Keycloak",
13
- name: "Keycloak",
14
- wellKnown: wellKnown,
15
- authorization: {
16
- params: {
17
- scope: "openid email profile offline_access",
18
- },
19
- },
20
- });
21
-
22
- export default keycloakProvider;
@@ -1,132 +0,0 @@
1
- "use server";
2
- import jwt from "jsonwebtoken";
3
- import type { JWT } from "next-auth/jwt";
4
- import type { AuthorizationEndpointHandler } from "next-auth/providers";
5
- import type { ExtendedJwtToken } from "@/auth/auth-options";
6
- import { getProviders } from "@/auth/providers";
7
-
8
- type TokenResponse = {
9
- token: ExtendedJwtToken;
10
- isRefreshed: boolean;
11
- };
12
-
13
- const checkAccessTokenExpiry = async (
14
- token: ExtendedJwtToken,
15
- ): Promise<TokenResponse> => {
16
- const providers = getProviders();
17
- const decryptedToken = jwt.decode(token?.accessToken ?? "") as jwt.JwtPayload;
18
- if (!decryptedToken) {
19
- token.error = "Token Invalid";
20
- return { token, isRefreshed: false };
21
- }
22
-
23
- if (!decryptedToken.exp) {
24
- token.error = "noExpiryOnToken";
25
- return {
26
- token,
27
- isRefreshed: false,
28
- };
29
- }
30
- const expiry = decryptedToken.exp * 1000;
31
- const nowMinusBuffer = 5 * 60 * 1000; // 5 minutes
32
- // Check if token is expired or will expire in 5 minutes
33
- // return token if access Token is not expired yet
34
- if (Date.now() < expiry - nowMinusBuffer) {
35
- return {
36
- token,
37
- isRefreshed: false,
38
- };
39
- }
40
-
41
- const refreshTokenExpiry = token.exp * 1000;
42
-
43
- // Avoid making request for access token if refresh token is expired.
44
- if (Date.now() >= refreshTokenExpiry) {
45
- token.error = "RefreshTokenExpired";
46
- return {
47
- token,
48
- isRefreshed: false,
49
- };
50
- }
51
-
52
- //Find the provider that matches the provider cookie
53
- const matchedProvider = providers.find(
54
- (p) => p.id.toLowerCase() === token.provider.toLowerCase(),
55
- );
56
- if (!matchedProvider || !("wellKnown" in matchedProvider)) {
57
- token.error = "NoProviderMatch";
58
- return {
59
- token,
60
- isRefreshed: false,
61
- };
62
- }
63
-
64
- const wellKnown = matchedProvider.wellKnown;
65
- const clientId = matchedProvider.options?.clientId;
66
- const scope = (
67
- matchedProvider.options?.authorization as AuthorizationEndpointHandler
68
- )?.params?.scope;
69
-
70
- if (!wellKnown || !clientId || !scope) {
71
- return {
72
- token: { ...token, error: "MissingProviderInfo" },
73
- isRefreshed: false,
74
- };
75
- }
76
-
77
- const newToken = await refreshAccessToken(token, wellKnown, clientId, scope);
78
-
79
- return {
80
- token: newToken as ExtendedJwtToken,
81
- isRefreshed: !newToken.error,
82
- };
83
- };
84
-
85
- const refreshAccessToken = async (
86
- token: JWT,
87
- wellKnown: string,
88
- clientId: string,
89
- scope: string,
90
- ) => {
91
- if (!token.refreshToken) {
92
- return { ...token, error: "NoRefreshToken" };
93
- }
94
-
95
- const refreshUrl = await getTokenEndpoint(wellKnown);
96
- try {
97
- const response = await fetch(refreshUrl, {
98
- method: "POST",
99
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
100
- body: new URLSearchParams({
101
- client_id: clientId,
102
- grant_type: "refresh_token",
103
- refresh_token: token.refreshToken,
104
- scope: scope,
105
- }),
106
- });
107
- const refreshedTokens = await response.json();
108
- if (!response.ok) {
109
- throw new Error(refreshedTokens.error_description);
110
- }
111
- return {
112
- ...token,
113
- accessToken: refreshedTokens.access_token,
114
- refreshToken: refreshedTokens.refresh_token,
115
- };
116
- } catch (error) {
117
- console.error("Error refreshing web token:", error);
118
- return { ...token, error: "RefreshAccessTokenError" };
119
- }
120
- };
121
-
122
- const getTokenEndpoint = async (wellKnown: string) => {
123
- const json = await getWellKnownData(wellKnown);
124
- return json.token_endpoint;
125
- };
126
-
127
- const getWellKnownData = async (wellKnown: string) => {
128
- const data = await fetch(wellKnown);
129
- return data.json();
130
- };
131
-
132
- export default checkAccessTokenExpiry;
@@ -1,38 +0,0 @@
1
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2
- import type { NextAuthOptions } from "next-auth";
3
-
4
- declare module "next-auth" {
5
- /**
6
- * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
7
- */
8
- interface Session {
9
- error?: string;
10
- accessToken?: string;
11
- }
12
- }
13
-
14
- declare module "next-auth/jwt" {
15
- interface JWT {
16
- accessToken?: string;
17
- accessTokenExpiresAt: number;
18
- refreshToken?: string;
19
- refreshTokenExpiresAt: number;
20
- error?: string;
21
- }
22
- }
23
-
24
- /**
25
- * Usually contains information about the provider being used
26
- * and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
27
- */
28
- declare module "next-auth/providers/azure-ad-b2c" {
29
- interface Account {
30
- access_token: string;
31
- expires_on: number;
32
- refresh_token: string;
33
- refresh_token_expires_in: number;
34
- }
35
-
36
- /** The OAuth profile returned from your provider */
37
- // interface Profile {}
38
- }