@snokam/auth 2.2.22-alpha.0 → 2.3.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/CHANGELOG.md +374 -1041
- package/package.json +36 -48
- package/src/app-router/components/AuthComplete.styled.tsx +17 -0
- package/src/app-router/components/AuthComplete.tsx +52 -0
- package/src/app-router/components/Login.tsx +120 -0
- package/src/app-router/components/Provider/index.tsx +55 -0
- package/src/app-router/index.tsx +56 -0
- package/src/app-router/server.ts +93 -0
- package/src/build-proxy.tsx +160 -0
- package/src/components/Authenticate.tsx +38 -0
- package/src/components/Authenticated.tsx +11 -0
- package/src/components/Consent/index.tsx +35 -0
- package/src/components/EmployeeGate/index.tsx +59 -0
- package/src/components/Error/index.tsx +90 -0
- package/src/components/Error/styled.tsx +63 -0
- package/src/components/InactiveEmployee/index.tsx +18 -0
- package/src/components/LoggedOut/index.tsx +23 -0
- package/src/components/NotProvisioned/index.tsx +19 -0
- package/src/components/Unauthenticated.tsx +11 -0
- package/src/config.ts +9 -0
- package/src/handoff/handlers.ts +146 -0
- package/src/handoff/return-to.ts +40 -0
- package/src/handoff/ticket.ts +97 -0
- package/src/hooks.ts +146 -0
- package/src/impersonation/context.tsx +185 -0
- package/src/impersonation/cookies.ts +124 -0
- package/src/impersonation/handlers.ts +234 -0
- package/src/impersonation/routes.ts +30 -0
- package/src/impersonation/ticket.ts +79 -0
- package/src/index.tsx +31 -0
- package/src/options.tsx +389 -0
- package/src/page-router/components/Login.tsx +50 -0
- package/src/page-router/components/Provider/index.tsx +45 -0
- package/src/page-router/index.tsx +22 -0
- package/src/page-router/server.ts +6 -0
- package/src/password/api.ts +183 -0
- package/src/roles/context.tsx +54 -0
- package/src/route-handler/token.ts +124 -0
- package/src/route-handler.ts +52 -0
- package/src/scopes/context.tsx +127 -0
- package/src/scopes/matcher.ts +47 -0
- package/src/server/auth.tsx +30 -0
- package/src/server/handler.tsx +68 -0
- package/src/server/token.tsx +10 -0
- package/{lib/server.d.ts → src/server.tsx} +7 -4
- package/src/sso/exchange.ts +64 -0
- package/src/sso/tenant-cookie.ts +18 -0
- package/src/theme/employee-theme-sync.tsx +55 -0
- package/src/token/claims.ts +12 -0
- package/src/utils/callback-url.ts +17 -0
- package/src/utils/consent.ts +22 -0
- package/src/utils/cookies.ts +75 -0
- package/src/utils/cors.ts +68 -0
- package/src/utils/params.ts +15 -0
- package/src/utils/types.ts +52 -0
- package/src/utils/url.ts +19 -0
- package/lib/components/Authenticate.d.ts +0 -2
- package/lib/components/Authenticated.d.ts +0 -6
- package/lib/components/Error/index.d.ts +0 -5
- package/lib/components/Login.d.ts +0 -2
- package/lib/components/Provider/index.d.ts +0 -7
- package/lib/components/Unauthenticated.d.ts +0 -6
- package/lib/hooks.d.ts +0 -10
- package/lib/index.d.ts +0 -14
- package/lib/index.js +0 -42
- package/lib/index.js.map +0 -1
- package/lib/middleware.d.ts +0 -7
- package/lib/middleware.js +0 -2
- package/lib/middleware.js.map +0 -1
- package/lib/options.d.ts +0 -5
- package/lib/options.js +0 -2
- package/lib/options.js.map +0 -1
- package/lib/server/auth.d.ts +0 -11
- package/lib/server/handler.d.ts +0 -14
- package/lib/server/token.d.ts +0 -8
- package/lib/server.js +0 -2
- package/lib/server.js.map +0 -1
- package/lib/utils/cookies.d.ts +0 -5
- package/lib/utils/params.d.ts +0 -1
- package/lib/utils/types.d.ts +0 -39
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snokam/auth",
|
|
3
3
|
"description": "Snøkam auth components",
|
|
4
|
-
"version": "2.
|
|
5
|
-
"main": "
|
|
6
|
-
"
|
|
4
|
+
"version": "2.3.0",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"types": "src/index.tsx",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
8
|
+
"src",
|
|
9
9
|
"package.json",
|
|
10
10
|
"CHANGELOG.md"
|
|
11
11
|
],
|
|
@@ -14,58 +14,46 @@
|
|
|
14
14
|
"url": "ssh://git@github.com:snokam/components.git",
|
|
15
15
|
"directory": "@snokam/auth"
|
|
16
16
|
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"start": "webpack --watch --config=../../../webpack/webpack.dev.js",
|
|
19
|
-
"build": "webpack --config=../../../webpack/webpack.prod.js",
|
|
20
|
-
"build:doc": "webpack --config=../../../webpack/webpack.dev.js",
|
|
21
|
-
"clean": "rm -rf lib"
|
|
22
|
-
},
|
|
23
17
|
"publishConfig": {
|
|
24
18
|
"access": "public"
|
|
25
19
|
},
|
|
26
20
|
"dependencies": {
|
|
27
|
-
"@azure/msal-node": "^
|
|
21
|
+
"@azure/msal-node": "^5.4.2",
|
|
28
22
|
"@emotion/react": "^11.14.0",
|
|
29
23
|
"@emotion/styled": "^11.14.1",
|
|
30
|
-
"@mui/material": "^
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
24
|
+
"@mui/material": "^9.2.0",
|
|
25
|
+
"next-auth": "4.24.15",
|
|
26
|
+
"path-to-regexp": "^8.4.2",
|
|
27
|
+
"@snokam/employees-client": "1.0.0",
|
|
28
|
+
"@snokam/identity": "1.1.0",
|
|
29
|
+
"@snokam/config": "1.1.0",
|
|
30
|
+
"@snokam/platform-client": "1.0.0",
|
|
31
|
+
"@snokam/react-query": "1.1.0",
|
|
32
|
+
"@snokam/ui": "1.1.0"
|
|
34
33
|
},
|
|
35
34
|
"peerDependencies": {
|
|
36
|
-
"next": "
|
|
37
|
-
"react": "
|
|
38
|
-
"react-dom": "
|
|
35
|
+
"next": "16.2.11",
|
|
36
|
+
"react": "19.2.8",
|
|
37
|
+
"react-dom": "19.2.8"
|
|
39
38
|
},
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
"require": "./lib/index.js",
|
|
44
|
-
"types": "./lib/index.d.ts"
|
|
45
|
-
},
|
|
46
|
-
"./server": {
|
|
47
|
-
"import": "./lib/server.js",
|
|
48
|
-
"require": "./lib/server.js",
|
|
49
|
-
"types": "./lib/server.d.ts"
|
|
50
|
-
},
|
|
51
|
-
"./middleware": {
|
|
52
|
-
"import": "./lib/middleware.js",
|
|
53
|
-
"require": "./lib/middleware.js",
|
|
54
|
-
"types": "./lib/middleware.d.ts"
|
|
55
|
-
}
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19.2.17",
|
|
41
|
+
"@types/react-dom": "^19.2.3"
|
|
56
42
|
},
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
"exports": {
|
|
44
|
+
".": "./src/index.tsx",
|
|
45
|
+
"./config": "./src/config.ts",
|
|
46
|
+
"./server": "./src/server.tsx",
|
|
47
|
+
"./middleware": "./src/build-proxy.tsx",
|
|
48
|
+
"./route-handler": "./src/route-handler.ts",
|
|
49
|
+
"./page-router": "./src/page-router/index.tsx",
|
|
50
|
+
"./page-router/server": "./src/page-router/server.ts",
|
|
51
|
+
"./app-router": "./src/app-router/index.tsx",
|
|
52
|
+
"./app-router/server": "./src/app-router/server.ts",
|
|
53
|
+
"./password": "./src/password/api.ts",
|
|
54
|
+
"./sso-tenant": "./src/sso/tenant-cookie.ts"
|
|
69
55
|
},
|
|
70
|
-
"
|
|
71
|
-
|
|
56
|
+
"scripts": {
|
|
57
|
+
"lint": "eslint ."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import styled from "@emotion/styled";
|
|
2
|
+
|
|
3
|
+
export const RetryLink = styled.a`
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
padding: 0.6rem 1.4rem;
|
|
8
|
+
border-radius: 999px;
|
|
9
|
+
background: var(--o-cyan);
|
|
10
|
+
color: var(--o-cyan-ink);
|
|
11
|
+
font-weight: 600;
|
|
12
|
+
text-decoration: none;
|
|
13
|
+
|
|
14
|
+
&:hover {
|
|
15
|
+
filter: brightness(0.95);
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useSearchParams } from "next/navigation";
|
|
5
|
+
import { signIn } from "next-auth/react";
|
|
6
|
+
import { PageLoader } from "@snokam/ui";
|
|
7
|
+
import Error from "../../components/Error";
|
|
8
|
+
import { RetryLink } from "./AuthComplete.styled";
|
|
9
|
+
|
|
10
|
+
export const AuthComplete = () => {
|
|
11
|
+
const params = useSearchParams();
|
|
12
|
+
const done = useRef(false);
|
|
13
|
+
const [failed, setFailed] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (done.current) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const ticket = params.get("ticket");
|
|
20
|
+
if (!ticket) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
done.current = true;
|
|
24
|
+
signIn("handoff", {
|
|
25
|
+
ticket,
|
|
26
|
+
callbackUrl: params.get("callbackUrl") || "/",
|
|
27
|
+
redirect: false,
|
|
28
|
+
}).then((result) => {
|
|
29
|
+
if (result?.ok && result.url) {
|
|
30
|
+
window.location.href = result.url;
|
|
31
|
+
} else {
|
|
32
|
+
setFailed(true);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}, [params]);
|
|
36
|
+
|
|
37
|
+
if (failed) {
|
|
38
|
+
return (
|
|
39
|
+
<Error
|
|
40
|
+
heading="Innlogging mislyktes"
|
|
41
|
+
message="Vi klarte ikke å fullføre innloggingen. Prøv igjen, eller ta kontakt med support hvis problemet vedvarer."
|
|
42
|
+
action={
|
|
43
|
+
<RetryLink href="/auth/login?callbackUrl=%2F">Prøv igjen</RetryLink>
|
|
44
|
+
}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return <PageLoader />;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default AuthComplete;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from "react";
|
|
4
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
|
+
import { useAuth } from "../../hooks";
|
|
6
|
+
import { signIn } from "next-auth/react";
|
|
7
|
+
import { PageLoader } from "@snokam/ui";
|
|
8
|
+
import { useOptionalConfig } from "@snokam/config";
|
|
9
|
+
import { safeCallbackTarget } from "../../utils/callback-url";
|
|
10
|
+
import Error from "../../components/Error";
|
|
11
|
+
import Consent from "../../components/Consent";
|
|
12
|
+
import LoggedOut from "../../components/LoggedOut";
|
|
13
|
+
|
|
14
|
+
export const Login = () => {
|
|
15
|
+
const { inProgress, isAuthenticated } = useAuth();
|
|
16
|
+
const [callbackUrl, setCallbackUrl] = useState<string | null>(null);
|
|
17
|
+
const [authParams, setAuthParams] = useState<Record<string, string>>({});
|
|
18
|
+
const [errorKind, setErrorKind] = useState<string | null>(null);
|
|
19
|
+
const [loggedOut, setLoggedOut] = useState(false);
|
|
20
|
+
const hasSignedIn = useRef(false);
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const searchParams = useSearchParams();
|
|
23
|
+
const config = useOptionalConfig();
|
|
24
|
+
const central = config?.NEXT_PUBLIC_CENTRAL_AUTH_URL;
|
|
25
|
+
const isError = !!errorKind;
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const rawCallbackUrl = searchParams.get("callbackUrl");
|
|
29
|
+
setCallbackUrl(rawCallbackUrl ? safeCallbackTarget(rawCallbackUrl) : null);
|
|
30
|
+
setErrorKind(searchParams.get("error"));
|
|
31
|
+
setLoggedOut(searchParams.get("loggedOut") === "1");
|
|
32
|
+
|
|
33
|
+
const authParamsObj: Record<string, string> = {};
|
|
34
|
+
searchParams.forEach((value, key) => {
|
|
35
|
+
if (key !== "error" && key !== "callbackUrl" && key !== "loggedOut") {
|
|
36
|
+
authParamsObj[key] = value;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
setAuthParams(authParamsObj);
|
|
40
|
+
}, [searchParams]);
|
|
41
|
+
|
|
42
|
+
const startSignIn = (extraParams: Record<string, string> = {}) => {
|
|
43
|
+
const target = callbackUrl || "/";
|
|
44
|
+
const onLocalhost = window.location.hostname === "localhost";
|
|
45
|
+
const onCentral =
|
|
46
|
+
onLocalhost || (central && central.startsWith(window.location.origin));
|
|
47
|
+
if (central && !onCentral) {
|
|
48
|
+
const returnTo = `${window.location.origin}${target}`;
|
|
49
|
+
const prompt = extraParams.prompt
|
|
50
|
+
? `&prompt=${encodeURIComponent(extraParams.prompt)}`
|
|
51
|
+
: "";
|
|
52
|
+
window.location.href = `${central}/handoff/start?returnTo=${encodeURIComponent(
|
|
53
|
+
returnTo
|
|
54
|
+
)}${prompt}`;
|
|
55
|
+
} else {
|
|
56
|
+
signIn(
|
|
57
|
+
"azure-ad",
|
|
58
|
+
{ callbackUrl: target },
|
|
59
|
+
{ ...authParams, ...extraParams }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!callbackUrl) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (isError) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (hasSignedIn.current) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (inProgress) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isAuthenticated) {
|
|
79
|
+
router.replace(callbackUrl || "/");
|
|
80
|
+
} else if (!loggedOut) {
|
|
81
|
+
hasSignedIn.current = true;
|
|
82
|
+
startSignIn();
|
|
83
|
+
}
|
|
84
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
|
+
}, [
|
|
86
|
+
inProgress,
|
|
87
|
+
isAuthenticated,
|
|
88
|
+
callbackUrl,
|
|
89
|
+
router,
|
|
90
|
+
isError,
|
|
91
|
+
central,
|
|
92
|
+
loggedOut,
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
if (loggedOut && !isAuthenticated && !isError) {
|
|
96
|
+
return (
|
|
97
|
+
<LoggedOut onLogin={() => startSignIn({ prompt: "select_account" })} />
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (errorKind === "consent") {
|
|
102
|
+
if (
|
|
103
|
+
typeof window !== "undefined" &&
|
|
104
|
+
!sessionStorage.getItem("auth-consent-retried")
|
|
105
|
+
) {
|
|
106
|
+
sessionStorage.setItem("auth-consent-retried", "1");
|
|
107
|
+
signIn("azure-ad", { callbackUrl: callbackUrl || "/" }, authParams);
|
|
108
|
+
return <PageLoader />;
|
|
109
|
+
}
|
|
110
|
+
return <Consent />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isError) {
|
|
114
|
+
return <Error />;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return <PageLoader />;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default Login;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, ReactNode, Suspense, useEffect } from "react";
|
|
4
|
+
import { useSearchParams } from "next/navigation";
|
|
5
|
+
import { Auth } from "../../../utils/types";
|
|
6
|
+
import { Authenticate } from "../../../components/Authenticate";
|
|
7
|
+
import { SessionProvider } from "next-auth/react";
|
|
8
|
+
import { useAuth } from "../../../hooks";
|
|
9
|
+
import { EmployeeThemeSync } from "../../../theme/employee-theme-sync";
|
|
10
|
+
|
|
11
|
+
export const Provider = memo((props: Auth.Provider.Props) => {
|
|
12
|
+
const { session, children } = props;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<SessionProvider session={session ?? undefined}>
|
|
16
|
+
<EmployeeThemeSync />
|
|
17
|
+
<Suspense fallback={<>{children}</>}>
|
|
18
|
+
<ProviderInner {...props}>{children}</ProviderInner>
|
|
19
|
+
</Suspense>
|
|
20
|
+
</SessionProvider>
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const ProviderInner = (props: Auth.Provider.Props) => {
|
|
25
|
+
const { children, enforceLogin } = props;
|
|
26
|
+
const searchParams = useSearchParams();
|
|
27
|
+
const enforceLoginParam = searchParams?.get("enforceLogin");
|
|
28
|
+
|
|
29
|
+
if (enforceLoginParam || enforceLogin) {
|
|
30
|
+
return (
|
|
31
|
+
<Authenticate provider="azure-ad" {...props}>
|
|
32
|
+
{children}
|
|
33
|
+
</Authenticate>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return <Wrapper>{children}</Wrapper>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const Wrapper = ({ children }: { children: ReactNode }) => {
|
|
41
|
+
const { signOut } = useAuth();
|
|
42
|
+
const searchParams = useSearchParams();
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (searchParams?.get("logout") === "true") {
|
|
46
|
+
signOut({
|
|
47
|
+
callbackUrl: "/auth/login?callbackUrl=/",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}, [searchParams, signOut]);
|
|
51
|
+
|
|
52
|
+
return <>{children}</>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default Provider;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Provider } from "./components/Provider";
|
|
4
|
+
import { Authenticate } from "../components/Authenticate";
|
|
5
|
+
import { Login } from "./components/Login";
|
|
6
|
+
import { AuthComplete } from "./components/AuthComplete";
|
|
7
|
+
import { Error } from "../components/Error";
|
|
8
|
+
import { NotProvisioned } from "../components/NotProvisioned";
|
|
9
|
+
import { EmployeeGate } from "../components/EmployeeGate";
|
|
10
|
+
|
|
11
|
+
export { Unauthenticated } from "../components/Unauthenticated";
|
|
12
|
+
export { Authenticated } from "../components/Authenticated";
|
|
13
|
+
export { useAuth } from "../hooks";
|
|
14
|
+
export { EmployeeThemeSync } from "../theme/employee-theme-sync";
|
|
15
|
+
export { ScopesProvider, useScopes, useHasScope } from "../scopes/context";
|
|
16
|
+
export {
|
|
17
|
+
usePlatformRoles,
|
|
18
|
+
useHasPlatformRole,
|
|
19
|
+
PLATFORM_ADMIN,
|
|
20
|
+
PLATFORM_OPERATOR,
|
|
21
|
+
} from "../roles/context";
|
|
22
|
+
export type { RolesState } from "../roles/context";
|
|
23
|
+
export type { ScopesState } from "../scopes/context";
|
|
24
|
+
export {
|
|
25
|
+
scopeGranted,
|
|
26
|
+
resolveEmployeeScopes,
|
|
27
|
+
type ScopeAccess,
|
|
28
|
+
} from "../scopes/matcher";
|
|
29
|
+
export {
|
|
30
|
+
ImpersonationProvider,
|
|
31
|
+
useImpersonation,
|
|
32
|
+
} from "../impersonation/context";
|
|
33
|
+
export type { ImpersonationActive } from "../impersonation/cookies";
|
|
34
|
+
export { signIn, SessionProvider } from "next-auth/react";
|
|
35
|
+
export {
|
|
36
|
+
Login,
|
|
37
|
+
AuthComplete,
|
|
38
|
+
Error,
|
|
39
|
+
Provider,
|
|
40
|
+
Authenticate,
|
|
41
|
+
NotProvisioned,
|
|
42
|
+
EmployeeGate,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default {
|
|
46
|
+
Login,
|
|
47
|
+
AuthComplete,
|
|
48
|
+
Error,
|
|
49
|
+
Provider,
|
|
50
|
+
Authenticate,
|
|
51
|
+
NotProvisioned,
|
|
52
|
+
EmployeeGate,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type { Session } from "next-auth";
|
|
56
|
+
export type { Auth } from "../utils/types";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getServerSession as nextAuthGetServerSession,
|
|
3
|
+
type Session,
|
|
4
|
+
} from "next-auth";
|
|
5
|
+
import type { NextAuthOptions } from "next-auth";
|
|
6
|
+
import { headers } from "next/headers";
|
|
7
|
+
import { getTenantDomains } from "@snokam/config";
|
|
8
|
+
import { getManagedBearer } from "@snokam/identity";
|
|
9
|
+
|
|
10
|
+
export { getServerToken } from "../server/token";
|
|
11
|
+
export { getNextAuthOptions } from "../options";
|
|
12
|
+
export type { Auth } from "../utils/types";
|
|
13
|
+
|
|
14
|
+
export async function serverTenantToken(): Promise<string | undefined> {
|
|
15
|
+
const scope = serverApiScope();
|
|
16
|
+
if (!scope) return undefined;
|
|
17
|
+
return getManagedBearer(scope);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function resolveServerApiBase(service: string): Promise<string> {
|
|
21
|
+
if (!service) return "";
|
|
22
|
+
const envKey = `NEXT_PUBLIC_${service.toUpperCase().replace(/-/g, "_")}_API_BASE_URL`;
|
|
23
|
+
const override = process.env[envKey];
|
|
24
|
+
if (
|
|
25
|
+
override &&
|
|
26
|
+
(override.includes("localhost") || override.includes("127.0.0.1"))
|
|
27
|
+
) {
|
|
28
|
+
return override;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const requestHeaders = await headers();
|
|
32
|
+
const host =
|
|
33
|
+
requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
|
|
34
|
+
const domains = await getTenantDomains({
|
|
35
|
+
host,
|
|
36
|
+
platformApiBase: process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? "",
|
|
37
|
+
authToken: await serverTenantToken(),
|
|
38
|
+
});
|
|
39
|
+
const custom = domains.find(
|
|
40
|
+
(domain) => domain.type === "backend" && domain.env === envKey
|
|
41
|
+
);
|
|
42
|
+
if (custom?.url) return custom.url;
|
|
43
|
+
|
|
44
|
+
const bare = host?.split(":")[0];
|
|
45
|
+
const tenantHost = (
|
|
46
|
+
bare === "localhost" || bare === "127.0.0.1"
|
|
47
|
+
? process.env.NEXT_PUBLIC_TENANT_HOST
|
|
48
|
+
: host
|
|
49
|
+
)?.split(":")[0];
|
|
50
|
+
return tenantHost ? `https://${tenantHost}/api/${service}` : (override ?? "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function serverApiScope(): string {
|
|
54
|
+
const clientId = process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID ?? "";
|
|
55
|
+
return clientId ? `api://${clientId}/user_impersonation` : "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function resolveServerAppUrl(app: string): Promise<string> {
|
|
59
|
+
const requestHeaders = await headers();
|
|
60
|
+
const host =
|
|
61
|
+
requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
|
|
62
|
+
const domains = await getTenantDomains({
|
|
63
|
+
host,
|
|
64
|
+
platformApiBase: process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? "",
|
|
65
|
+
authToken: await serverTenantToken(),
|
|
66
|
+
});
|
|
67
|
+
const match = domains.find(
|
|
68
|
+
(domain) => domain.type === "frontend" && domain.app === app
|
|
69
|
+
);
|
|
70
|
+
if (match?.url) return match.url;
|
|
71
|
+
|
|
72
|
+
const bare = host?.split(":")[0];
|
|
73
|
+
const effective =
|
|
74
|
+
!bare || bare === "localhost" || bare === "127.0.0.1"
|
|
75
|
+
? process.env.NEXT_PUBLIC_TENANT_HOST?.split(":")[0]
|
|
76
|
+
: host;
|
|
77
|
+
return effective ? `https://${effective}` : "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getServerSession(
|
|
81
|
+
authOptions: NextAuthOptions
|
|
82
|
+
): Promise<Session | null> {
|
|
83
|
+
return nextAuthGetServerSession(authOptions);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getAuth(authOptions: NextAuthOptions) {
|
|
87
|
+
const session = await getServerSession(authOptions);
|
|
88
|
+
return {
|
|
89
|
+
isAuthenticated: !!session,
|
|
90
|
+
session,
|
|
91
|
+
user: session?.user,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { match } from "path-to-regexp";
|
|
2
|
+
import { getToken } from "next-auth/jwt";
|
|
3
|
+
import { type NextRequestWithAuth } from "next-auth/middleware";
|
|
4
|
+
import { NextResponse, NextFetchEvent } from "next/server";
|
|
5
|
+
import { Auth } from "./utils/types";
|
|
6
|
+
import { getSessionCookieSettings } from "./utils/cookies";
|
|
7
|
+
import { addCorsHeaders } from "./utils/cors";
|
|
8
|
+
import { getAuthUrl } from "./utils/url";
|
|
9
|
+
|
|
10
|
+
export function buildAuthMiddleware({
|
|
11
|
+
matcher = [],
|
|
12
|
+
callbacks = {},
|
|
13
|
+
protectAllRoutes,
|
|
14
|
+
cors = {},
|
|
15
|
+
...rest
|
|
16
|
+
}: Auth.Middleware.Props = {}): Auth.Middleware.Builder {
|
|
17
|
+
const requireLogin = process.env.IS_PR === "true";
|
|
18
|
+
const { name } = getSessionCookieSettings(getAuthUrl());
|
|
19
|
+
|
|
20
|
+
const shouldProtectPath = (pathname: string): boolean => {
|
|
21
|
+
if (pathname.startsWith("/api/healthy")) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (pathname.startsWith("/api/ready")) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (pathname.startsWith("/auth/")) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (requireLogin) {
|
|
34
|
+
return !pathname.startsWith("/_next") && !/\.[^/]+$/.test(pathname);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (protectAllRoutes && matcher?.length === 0) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return matcher.some((pattern) => {
|
|
42
|
+
if (!pattern) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (pattern.startsWith("/") && pattern.endsWith(")")) {
|
|
47
|
+
const cleanPattern = pattern.substring(1);
|
|
48
|
+
let fixedPattern = cleanPattern;
|
|
49
|
+
if (cleanPattern.startsWith("((?!") && !cleanPattern.startsWith("^")) {
|
|
50
|
+
fixedPattern = "^" + cleanPattern;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const regex = new RegExp(fixedPattern);
|
|
54
|
+
const cleanPathname = pathname.startsWith("/")
|
|
55
|
+
? pathname.substring(1)
|
|
56
|
+
: pathname;
|
|
57
|
+
return regex.test(cleanPathname);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (pattern.endsWith(":path*")) {
|
|
61
|
+
const base = pattern.replace(/\/:path\*$/, "");
|
|
62
|
+
return pathname.startsWith(base);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return match(pattern, { end: true })(pathname) !== false;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn(`Invalid path pattern in auth middleware: ${error}`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const signInPage = "/auth/login";
|
|
75
|
+
const errorPage = "/api/auth/error";
|
|
76
|
+
const publicPaths = ["/api/auth", "/_next", "/favicon.ico"];
|
|
77
|
+
|
|
78
|
+
const authorized =
|
|
79
|
+
callbacks.authorized ??
|
|
80
|
+
(({ req, token }: { req: NextRequestWithAuth; token: unknown }) => {
|
|
81
|
+
return shouldProtectPath(req.nextUrl.pathname) ? !!token : true;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const authMiddleware = async (req: NextRequestWithAuth) => {
|
|
85
|
+
const { pathname, search, origin, basePath } = req.nextUrl;
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
[signInPage, errorPage].includes(pathname) ||
|
|
89
|
+
publicPaths.some((p) => pathname.startsWith(p))
|
|
90
|
+
) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const secret =
|
|
95
|
+
rest.secret ?? process.env.NEXTAUTH_SECRET ?? process.env.AUTH_SECRET;
|
|
96
|
+
if (!secret) {
|
|
97
|
+
console.error("[next-auth][error][NO_SECRET]");
|
|
98
|
+
const errorUrl = new URL(`${basePath}${errorPage}`, origin);
|
|
99
|
+
errorUrl.searchParams.append("error", "Configuration");
|
|
100
|
+
return NextResponse.redirect(errorUrl);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const token = await getToken({
|
|
104
|
+
req,
|
|
105
|
+
decode: rest.jwt?.decode,
|
|
106
|
+
cookieName: rest.cookies?.sessionToken?.name ?? name,
|
|
107
|
+
secret,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const isAuthorized = (await authorized({ req, token })) ?? !!token;
|
|
111
|
+
if (isAuthorized) {
|
|
112
|
+
req.nextauth = { token };
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const signInUrl = new URL(`${basePath}${signInPage}`, origin);
|
|
117
|
+
signInUrl.searchParams.append(
|
|
118
|
+
"callbackUrl",
|
|
119
|
+
`${basePath}${pathname}${search}`
|
|
120
|
+
);
|
|
121
|
+
return NextResponse.redirect(signInUrl);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const corsEnabled = cors.enabled !== false;
|
|
125
|
+
const corsAllRoutes = cors.allRoutes === true;
|
|
126
|
+
|
|
127
|
+
const middleware = async (
|
|
128
|
+
request: NextRequestWithAuth,
|
|
129
|
+
_event: NextFetchEvent
|
|
130
|
+
) => {
|
|
131
|
+
const origin = request.headers.get("origin");
|
|
132
|
+
const isApiRoute = request.nextUrl.pathname.startsWith("/api/");
|
|
133
|
+
const applyCors = corsEnabled && (isApiRoute || corsAllRoutes);
|
|
134
|
+
|
|
135
|
+
if (applyCors && request.method === "OPTIONS") {
|
|
136
|
+
const response = new NextResponse(null, { status: 204 });
|
|
137
|
+
return addCorsHeaders(response, origin, cors);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const authResponse = await authMiddleware(request);
|
|
141
|
+
|
|
142
|
+
if (applyCors) {
|
|
143
|
+
const response =
|
|
144
|
+
authResponse instanceof NextResponse
|
|
145
|
+
? authResponse
|
|
146
|
+
: NextResponse.next();
|
|
147
|
+
return addCorsHeaders(response, origin, cors);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return authResponse;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const config = matcher?.length > 0 ? { matcher } : undefined;
|
|
154
|
+
|
|
155
|
+
return { proxy: middleware, config };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { proxy, config } = buildAuthMiddleware();
|
|
159
|
+
export { config };
|
|
160
|
+
export default proxy;
|