@evenicanpm/storefront-core 2.0.1 → 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 +4 -3
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +1 -1
- package/src/api-manager/datasources/d365/d365-organization.datasource.ts +1 -1
- package/src/api-manager/datasources/d365/d365-product.datasource.ts +2 -2
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +1 -1
- package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +7 -5
- package/src/api-manager/datasources/e4/middleware.ts +35 -0
- package/src/api-manager/lib/get-graphql-client.ts +7 -6
- package/src/api-manager/services/create-query.ts +1 -46
- package/src/auth/better-auth.ts +89 -0
- package/src/auth/signout.ts +5 -34
- package/src/components/header/__tests__/header.test.tsx +9 -2
- package/src/components/header/__tests__/user.test.tsx +34 -19
- package/src/components/header/components/user.tsx +34 -18
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-dialog.tsx +3 -1
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +3 -1
- package/src/hooks/use-nextauth-session.ts +10 -14
- package/src/pages/cart/__tests__/checkout-form.test.tsx +4 -2
- package/src/api-manager/datasources/d365/utils/decode-jwt.ts +0 -10
- package/src/auth/auth-options.test.ts +0 -70
- package/src/auth/auth-options.ts +0 -101
- package/src/auth/next-auth-cookie-manager.ts +0 -87
- package/src/auth/providers/aadb2c-provider.ts +0 -32
- package/src/auth/providers/authentik-provider.ts +0 -24
- package/src/auth/providers/entra-external-id-provider.ts +0 -50
- package/src/auth/providers/index.ts +0 -16
- package/src/auth/providers/keycloak-provider.ts +0 -22
- package/src/auth/refresh-token.ts +0 -132
- package/src/auth/types/next-auth.d.ts +0 -38
|
@@ -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 {
|
|
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 } =
|
|
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 {
|
|
33
|
-
|
|
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("
|
|
14
|
-
|
|
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
|
-
});
|
package/src/auth/auth-options.ts
DELETED
|
@@ -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
|
-
}
|