Compare commits

..
16 changed files with 1186 additions and 67 deletions

No files matched your search

+1
View File
@@ -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",
+2 -1
View File
@@ -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"
+1 -19
View File
@@ -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
View File
@@ -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
+6
View File
@@ -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:[]
};
+8 -5
View File
@@ -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),
};
@@ -23,6 +23,7 @@ export const idpIcon = (
zitadel: <ZitadelIcon size={size} />,
authentik: <AuthentikIcon size={size} />,
keycloak: <KeycloakIcon size={size} />,
adfs: <MicrosoftIcon size={size} />,
oidc: <KeyRound size={size} className="text-nb-gray-400" />,
};
+97 -10
View File
@@ -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>
+14 -6
View File
@@ -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>
</>
);
};
+47
View File
@@ -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 {}
};
+3 -1
View File
@@ -44,7 +44,8 @@ export type SSOIdentityProviderType =
| "pocketid"
| "microsoft"
| "authentik"
| "keycloak";
| "keycloak"
| "adfs";
export const SSOIdentityProviderOptions: {
value: SSOIdentityProviderType;
@@ -59,6 +60,7 @@ export const SSOIdentityProviderOptions: {
{ value: "pocketid", label: "PocketID" },
{ value: "authentik", label: "Authentik" },
{ value: "keycloak", label: "Keycloak" },
{ value: "adfs", label: "Microsoft AD FS" },
];
export const getSSOIdentityProviderLabelByType = (
@@ -49,6 +49,7 @@ const issuerHints: Partial<Record<SSOIdentityProviderType, string>> = {
okta: "https://{ORG}.okta.com",
entra: "https://login.microsoftonline.com/{TENANT_ID}/v2.0",
pocketid: "https://pocketid.example.com",
adfs: "https://adfs.example.com/adfs",
};
const defaultNames: Record<SSOIdentityProviderType, string> = {
@@ -61,6 +62,7 @@ const defaultNames: Record<SSOIdentityProviderType, string> = {
pocketid: "PocketID",
authentik: "Authentik",
keycloak: "Keycloak",
adfs: "Microsoft AD FS",
};
type Props = {
@@ -49,6 +49,7 @@ export const idpTypeLabels: Record<SSOIdentityProviderType, string> = {
microsoft: "Microsoft",
authentik: "Authentik",
keycloak: "Keycloak",
adfs: "Microsoft AD FS",
};
type ActionCellProps = {
+55 -22
View File
@@ -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",
},
});
};
+2
View File
@@ -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,