mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff5ca42cbb | ||
|
|
44b27d29f2 | ||
|
|
61834b65c2 | ||
|
|
c178d700a3 | ||
|
|
c4ef73a338 | ||
|
|
c6721a6467 |
No files matched your search
@@ -10,6 +10,7 @@
|
||||
"redirectURI": "$AUTH_REDIRECT_URI",
|
||||
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
|
||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
|
||||
"disableServiceWorker": "$NETBIRD_DISABLE_SERVICE_WORKER",
|
||||
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
|
||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
||||
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
|
||||
|
||||
@@ -60,13 +60,14 @@ export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
|
||||
export NETBIRD_GOOGLE_ANALYTICS_ID=${NETBIRD_GOOGLE_ANALYTICS_ID}
|
||||
export NETBIRD_GOOGLE_TAG_MANAGER_ID=${NETBIRD_GOOGLE_TAG_MANAGER_ID}
|
||||
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
export NETBIRD_DISABLE_SERVICE_WORKER=${NETBIRD_DISABLE_SERVICE_WORKER:-false}
|
||||
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
|
||||
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH}
|
||||
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DISABLE_SERVICE_WORKER \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
|
||||
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
|
||||
|
||||
Generated
+1
-19
@@ -168,7 +168,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -3031,7 +3030,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
||||
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -3041,7 +3039,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -3100,7 +3097,6 @@
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@@ -3581,8 +3577,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.0",
|
||||
@@ -3621,7 +3616,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4044,7 +4038,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4667,7 +4660,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -5196,7 +5188,6 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5394,7 +5385,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -6688,7 +6678,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -7446,7 +7435,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -7654,7 +7642,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7695,7 +7682,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -8590,7 +8576,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -8757,7 +8742,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -8923,7 +8907,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -9251,7 +9234,6 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+2
-3
@@ -6,11 +6,10 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"copy": "copyfiles -f ./node_modules/@axa-fr/react-oidc/dist/OidcServiceWorker.js ./public",
|
||||
"copytrusted": "copyfiles -f ./public/local/OidcTrustedDomains.js ./public",
|
||||
"oidc-gen": "node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public",
|
||||
"dev": "next dev -p 3000",
|
||||
"turbo": "next dev -p 3000 --turbo",
|
||||
"build": "next build",
|
||||
"build": "npm run oidc-gen && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"cypress:open": "cypress open"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
||||
|
||||
// Add here trusted domains, access tokens will be send to
|
||||
const trustedDomains = {
|
||||
default:["http://localhost:3001","http://127.0.0.1:3001", "http://0.0.0.0:33071"],
|
||||
auth0:[]
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
|
||||
// Add here trusted domains, access tokens will be send to
|
||||
|
||||
// Domains used by OIDC server must be also declared here
|
||||
|
||||
const trustedDomains = {
|
||||
default:["$NETBIRD_MGMT_API_ENDPOINT"],
|
||||
auth0:[]
|
||||
};
|
||||
default: [
|
||||
"$NETBIRD_MGMT_API_ENDPOINT",
|
||||
"$NETBIRD_MGMT_GRPC_API_ENDPOINT",
|
||||
].filter((d) => d && d.length > 0),
|
||||
};
|
||||
+97
-10
@@ -2,21 +2,47 @@
|
||||
|
||||
import {
|
||||
AuthorityConfiguration,
|
||||
OidcClient,
|
||||
OidcConfiguration,
|
||||
OidcProvider,
|
||||
} from "@axa-fr/react-oidc";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import loadConfig, { buildExtras } from "@utils/config";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { OIDCError } from "@/auth/OIDCError";
|
||||
import { SecureProvider } from "@/auth/SecureProvider";
|
||||
import { storeSessionLostRedirectPath } from "@/auth/sessionRedirect";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const config = loadConfig();
|
||||
const AUTHENTICATED_CALLBACK_REDIRECT_PATH = "/peers";
|
||||
|
||||
const getOidcRoutePath = (href: string, origin: string) => {
|
||||
try {
|
||||
const url = new URL(href, origin);
|
||||
let path = url.pathname;
|
||||
if (path.endsWith("/")) path = path.slice(0, -1);
|
||||
|
||||
let hash = url.hash;
|
||||
if (hash === "#_=_") hash = "";
|
||||
if (hash) path += hash.split("?")[0];
|
||||
|
||||
return path;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const isOidcCallbackRoute = (href: string, redirectUri: string) =>
|
||||
getOidcRoutePath(href, window.location.origin) ===
|
||||
getOidcRoutePath(redirectUri, window.location.origin);
|
||||
|
||||
const hasUsableTokens = (tokens: { expiresAt?: number } | null | undefined) =>
|
||||
typeof tokens?.expiresAt === "number" && tokens.expiresAt > Date.now() / 1000;
|
||||
|
||||
/**
|
||||
* Unfortunately Auth0 https://<DOMAIN>/.well-known/openid-configuration doesn't contain end_session_endpoint that
|
||||
@@ -40,7 +66,6 @@ const onEvent = (configurationName: any, eventName: any, data: any) => {
|
||||
export default function OIDCProvider({ children }: Props) {
|
||||
const [providerConfig, setProviderConfig] = useState<OidcConfiguration>();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const router = useRouter();
|
||||
const path = usePathname();
|
||||
|
||||
const withCustomHistory = () => {
|
||||
@@ -52,16 +77,30 @@ export default function OIDCProvider({ children }: Props) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setProviderConfig({
|
||||
let isMounted = true;
|
||||
|
||||
// The service worker is disabled in two cases:
|
||||
// 1. tokenSource === "idtoken" — the SW would overwrite the manually-set
|
||||
// idToken header with the access_token, breaking the idToken path.
|
||||
// 2. NETBIRD_DISABLE_SERVICE_WORKER=true — operator escape hatch for
|
||||
// debugging or deployments that hit edge cases with the SW.
|
||||
const useServiceWorker =
|
||||
!config.disableServiceWorker &&
|
||||
config.tokenSource?.toLowerCase() !== "idtoken";
|
||||
|
||||
const nextProviderConfig: OidcConfiguration = {
|
||||
authority: config.authority,
|
||||
client_id: config.clientId,
|
||||
redirect_uri: window.location.origin + config.redirectURI,
|
||||
refresh_time_before_tokens_expiration_in_second: 30,
|
||||
silent_redirect_uri: window.location.origin + config.silentRedirectURI,
|
||||
scope: config.scopesSupported,
|
||||
// disabling service worker
|
||||
//service_worker_relative_url: "/OidcServiceWorker.js",
|
||||
service_worker_only: false,
|
||||
...(useServiceWorker
|
||||
? {
|
||||
service_worker_relative_url: "/OidcServiceWorker.js",
|
||||
service_worker_only: false,
|
||||
}
|
||||
: {}),
|
||||
authority_configuration: config.auth0Auth
|
||||
? auth0AuthorityConfig
|
||||
: undefined,
|
||||
@@ -69,10 +108,53 @@ export default function OIDCProvider({ children }: Props) {
|
||||
...(config.clientSecret
|
||||
? { token_request_extras: { client_secret: config.clientSecret } }
|
||||
: null),
|
||||
});
|
||||
setMounted(true);
|
||||
};
|
||||
|
||||
const finishMounting = () => {
|
||||
if (isMounted) setMounted(true);
|
||||
};
|
||||
|
||||
const redirectAuthenticatedCallback = async () => {
|
||||
// OidcRoutes handles the callback route before OidcSession restores
|
||||
// existing tokens. If a callback URL is reached while the app already
|
||||
// has a valid OIDC session, leave the callback route instead of trying
|
||||
// to process a stale/duplicate callback.
|
||||
if (
|
||||
!isOidcCallbackRoute(
|
||||
window.location.href,
|
||||
nextProviderConfig.redirect_uri,
|
||||
)
|
||||
) {
|
||||
finishMounting();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oidc = OidcClient.getOrCreate(() => fetch)(nextProviderConfig);
|
||||
await oidc.tryKeepExistingSessionAsync();
|
||||
if (!isMounted) return;
|
||||
if (hasUsableTokens(oidc.tokens)) {
|
||||
window.location.replace(AUTHENTICATED_CALLBACK_REDIRECT_PATH);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
finishMounting();
|
||||
};
|
||||
|
||||
setProviderConfig(nextProviderConfig);
|
||||
void redirectAuthenticatedCallback();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSessionLost = () => {
|
||||
storeSessionLostRedirectPath();
|
||||
window.location.replace("/");
|
||||
};
|
||||
|
||||
// We bypass authentication for pages that do not require auth.
|
||||
// E.g., when we just want to show installation steps for public.
|
||||
// Or the instance setup wizard for first-time setup.
|
||||
@@ -89,8 +171,13 @@ export default function OIDCProvider({ children }: Props) {
|
||||
loadingComponent={FullScreenLoading}
|
||||
callbackSuccessComponent={FullScreenLoading}
|
||||
onEvent={onEvent}
|
||||
onSessionLost={() => void 0}
|
||||
//sessionLostComponent={SessionLost}
|
||||
// If session is lost, try to re-initiate authentication flow while
|
||||
// preserving the current deep link for the post-login redirect.
|
||||
onSessionLost={handleSessionLost}
|
||||
// Another tab logged out — fires a separate event in @axa-fr/react-oidc,
|
||||
// not covered by onSessionLost. Without this handler the library would
|
||||
// render its default "Session timed out" UI instead of redirecting.
|
||||
onLogoutFromAnotherTab={handleSessionLost}
|
||||
>
|
||||
<SecureProvider>{children}</SecureProvider>
|
||||
</OidcProvider>
|
||||
|
||||
@@ -2,6 +2,10 @@ import { OidcSecure, useOidc } from "@axa-fr/react-oidc";
|
||||
import { usePathname } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
clearSessionLostRedirectPath,
|
||||
getSessionLostRedirectPath,
|
||||
} from "@/auth/sessionRedirect";
|
||||
|
||||
const QUERY_PARAMS_KEY = "netbird-query-params";
|
||||
const PRESERVE_QUERY_PARAMS_PATHS = ["/peer/ssh", "/peer/rdp"];
|
||||
@@ -27,11 +31,15 @@ type Props = {
|
||||
export const SecureProvider = ({ children }: Props) => {
|
||||
const { isAuthenticated, login } = useOidc();
|
||||
const currentPath = usePathname();
|
||||
const callbackPath = getSessionLostRedirectPath() ?? currentPath;
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !PRESERVE_QUERY_PARAMS_PATHS.includes(currentPath)) {
|
||||
localStorage.removeItem(QUERY_PARAMS_KEY);
|
||||
} else if (!isAuthenticated) {
|
||||
if (isAuthenticated) {
|
||||
clearSessionLostRedirectPath();
|
||||
if (!PRESERVE_QUERY_PARAMS_PATHS.includes(currentPath)) {
|
||||
localStorage.removeItem(QUERY_PARAMS_KEY);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const params = window.location.search.substring(1);
|
||||
if (params) {
|
||||
@@ -49,18 +57,18 @@ export const SecureProvider = ({ children }: Props) => {
|
||||
if (!isAuthenticated) {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!isAuthenticated) {
|
||||
await login(currentPath);
|
||||
await login(callbackPath);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [currentPath, isAuthenticated, login]);
|
||||
}, [callbackPath, isAuthenticated, login]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OidcSecure callbackPath={currentPath}>{children}</OidcSecure>
|
||||
<OidcSecure callbackPath={callbackPath}>{children}</OidcSecure>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
const SESSION_LOST_REDIRECT_PATH_KEY = "netbird-session-lost-redirect-path";
|
||||
|
||||
const isSafeRelativePath = (path: string) =>
|
||||
path.startsWith("/") && !path.startsWith("//");
|
||||
|
||||
export const storeSessionLostRedirectPath = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const redirectPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
|
||||
try {
|
||||
if (
|
||||
!redirectPath ||
|
||||
redirectPath === "/" ||
|
||||
!isSafeRelativePath(redirectPath)
|
||||
) {
|
||||
window.sessionStorage.removeItem(SESSION_LOST_REDIRECT_PATH_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem(
|
||||
SESSION_LOST_REDIRECT_PATH_KEY,
|
||||
redirectPath,
|
||||
);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getSessionLostRedirectPath = () => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
try {
|
||||
const redirectPath = window.sessionStorage.getItem(
|
||||
SESSION_LOST_REDIRECT_PATH_KEY,
|
||||
);
|
||||
if (redirectPath && isSafeRelativePath(redirectPath)) return redirectPath;
|
||||
} catch {}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const clearSessionLostRedirectPath = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
window.sessionStorage.removeItem(SESSION_LOST_REDIRECT_PATH_KEY);
|
||||
} catch {}
|
||||
};
|
||||
+55
-22
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
OidcClient,
|
||||
useOidc,
|
||||
useOidcAccessToken,
|
||||
useOidcFetch,
|
||||
useOidcIdToken,
|
||||
} from "@axa-fr/react-oidc";
|
||||
import loadConfig from "@utils/config";
|
||||
@@ -75,38 +76,70 @@ export function useNetBirdFetch(ignoreError: boolean = false): {
|
||||
fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
|
||||
} {
|
||||
const tokenSource = config.tokenSource || "accessToken";
|
||||
const { idToken } = useOidcIdToken();
|
||||
const { accessToken } = useOidcAccessToken();
|
||||
const token = tokenSource.toLowerCase() == "idtoken" ? idToken : accessToken;
|
||||
const handleErrors = useApiErrorHandling(ignoreError);
|
||||
const useIdToken = tokenSource.toLowerCase() === "idtoken";
|
||||
|
||||
const isTokenExpired = async () => {
|
||||
// Always call hooks unconditionally (React rules), but only use the values
|
||||
// relevant to the configured token source.
|
||||
const { idToken } = useOidcIdToken();
|
||||
// useOidcFetch lets the service worker inject the Authorization header
|
||||
// transparently, so the access token never reaches page-level JS.
|
||||
const { fetch: oidcFetch } = useOidcFetch();
|
||||
// Keep the parameter for API compatibility. Error handling happens in callers
|
||||
// via the rejected ErrorResponse rather than in this low-level fetch wrapper.
|
||||
void ignoreError;
|
||||
|
||||
// Read the current idToken imperatively so the polling loop and the final
|
||||
// header value see refreshed tokens without waiting for a re-render.
|
||||
const getCurrentIdToken = (): string | undefined => {
|
||||
try {
|
||||
return OidcClient.get().tokens?.idToken;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const isIdTokenExpired = async () => {
|
||||
let attempts = 4;
|
||||
while (isExpired(token) && attempts > 0) {
|
||||
while (attempts > 0) {
|
||||
const current = getCurrentIdToken();
|
||||
if (current && !isExpired(current)) return false;
|
||||
await sleep(500);
|
||||
attempts = attempts - 1;
|
||||
}
|
||||
return isExpired(token);
|
||||
const current = getCurrentIdToken();
|
||||
return !current || isExpired(current);
|
||||
};
|
||||
|
||||
const nativeFetch = async (input: RequestInfo, init?: RequestInit) => {
|
||||
const tokenExpired = await isTokenExpired();
|
||||
if (tokenExpired) {
|
||||
return handleErrors({
|
||||
code: 401,
|
||||
message: "token expired",
|
||||
} as ErrorResponse);
|
||||
// Non-default idToken path: token must be extracted to JS to build the
|
||||
// header manually, since the service worker only injects the access token.
|
||||
if (useIdToken) {
|
||||
const tokenExpired = await isIdTokenExpired();
|
||||
if (tokenExpired) {
|
||||
return Promise.reject({
|
||||
code: 401,
|
||||
message: "token expired",
|
||||
} as ErrorResponse);
|
||||
}
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers: {
|
||||
...(init?.headers || {}),
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${getCurrentIdToken() ?? idToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
return fetch(input, {
|
||||
// Default access token path: the service worker intercepts the request and
|
||||
// injects the Authorization header without exposing the token to page JS.
|
||||
return oidcFetch(input, {
|
||||
...init,
|
||||
headers,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Config {
|
||||
redirectURI: string;
|
||||
silentRedirectURI: string;
|
||||
tokenSource: string;
|
||||
disableServiceWorker: boolean;
|
||||
dragQueryParams: boolean;
|
||||
hotjarTrackID?: number;
|
||||
googleAnalyticsID?: string;
|
||||
@@ -63,6 +64,7 @@ const loadConfig = (): Config => {
|
||||
redirectURI: redirectURI,
|
||||
silentRedirectURI: silentRedirectURI,
|
||||
tokenSource: tokenSource,
|
||||
disableServiceWorker: configJson.disableServiceWorker == "true", // Due to substitution we can't use boolean in the config
|
||||
dragQueryParams: configJson.dragQueryParams == "true", // Drags all the query params to the auth layer specified in the URL when accessing dashboard.
|
||||
hotjarTrackID: configJson?.hotjarTrackID || undefined,
|
||||
googleAnalyticsID: configJson?.googleAnalyticsID || undefined,
|
||||
|
||||
Reference in new issue
Block a user