Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebuild how we store keycloak auth tokens #2082

Merged
merged 1 commit into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions frontend/src/js/api/useApi.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { useKeycloak } from "@react-keycloak/web";
import axios, { AxiosRequestConfig } from "axios";
import { useEffect, useRef } from "react";
import { useContext, useEffect, useRef } from "react";
import { useHistory } from "react-router-dom";

import { getStoredAuthToken } from "../authorization/helper";
import { AuthTokenContext } from "../authorization/AuthTokenProvider";
import { isIDPEnabled, isLoginDisabled } from "../environment";

export const useAuthToken = () => {
const { keycloak } = useKeycloak();

return isIDPEnabled ? keycloak.token || "" : getStoredAuthToken() || "";
};

export const useApiUnauthorized = <T>(
requestConfig: Partial<AxiosRequestConfig> = {},
) => {
Expand All @@ -24,12 +17,17 @@ export const useApiUnauthorized = <T>(

export const useApi = <T>(requestConfig: Partial<AxiosRequestConfig> = {}) => {
const history = useHistory();
const authToken = useAuthToken();
const authTokenRef = useRef<string>(authToken);
const { authToken } = useContext(AuthTokenContext);

useEffect(() => {
authTokenRef.current = authToken;
}, [authToken]);
// In order to always have the up to date token,
// especially when polling for a long time within nested loops
const authTokenRef = useRef<string>(authToken);
useEffect(
function updateRef() {
authTokenRef.current = authToken;
},
[authToken],
);

return async (
finalRequestConfig: Partial<AxiosRequestConfig> = {},
Expand Down
59 changes: 25 additions & 34 deletions frontend/src/js/app/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ReactKeycloakProvider } from "@react-keycloak/web";
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";

import keycloak from "../../keycloak";
import {
AuthTokenContextProvider,
useAuthTokenContextValue,
} from "../authorization/AuthTokenProvider";
import KeycloakProvider from "../authorization/KeycloakProvider";
import LoginPage from "../authorization/LoginPage";
import WithAuthToken from "../authorization/WithAuthToken";
import { basename, isIDPEnabled } from "../environment";
import { basename } from "../environment";
import type { TabT } from "../pane/types";

import App from "./App";
Expand All @@ -15,38 +18,26 @@ interface PropsT {
}

const AppRouter = (props: PropsT) => {
const authTokenContextValue = useAuthTokenContextValue();

return (
<ReactKeycloakProvider
authClient={keycloak}
onEvent={(event: unknown, error: unknown) => {
// USEFUL FOR DEBUGGING
// console.log("onKeycloakEvent", event, error);
}}
onTokens={(tokens) => {
// USEFUL FOR DEBUGGING
// console.log("TOKENS ", tokens);
}}
initOptions={{
pkceMethod: "S256",
onLoad: isIDPEnabled ? "login-required" : "check-sso",
// silentCheckSsoRedirectUri:
// window.location.origin + "/silent-check-sso.html",
}}
>
<Router basename={basename}>
<Switch>
<Route path="/login" component={LoginPage} />
<Route
path="/*"
render={(routeProps) => (
<WithAuthToken {...routeProps}>
<App {...props} />
</WithAuthToken>
)}
/>
</Switch>
</Router>
</ReactKeycloakProvider>
<AuthTokenContextProvider value={authTokenContextValue}>
<KeycloakProvider>
<Router basename={basename}>
<Switch>
<Route path="/login" component={LoginPage} />
<Route
path="/*"
render={(routeProps) => (
<WithAuthToken {...routeProps}>
<App {...props} />
</WithAuthToken>
)}
/>
</Switch>
</Router>
</KeycloakProvider>
</AuthTokenContextProvider>
);
};

Expand Down
25 changes: 25 additions & 0 deletions frontend/src/js/authorization/AuthTokenProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createContext, useState } from "react";

import { isIDPEnabled } from "../environment";

import { getStoredAuthToken } from "./helper";

export interface AuthTokenContextValue {
authToken: string;
setAuthToken: (token: string) => void;
}

export const AuthTokenContext = createContext<AuthTokenContextValue>({
authToken: "",
setAuthToken: () => null,
});

export const useAuthTokenContextValue = (): AuthTokenContextValue => {
const [authToken, setAuthToken] = useState<string>(
isIDPEnabled ? "" : getStoredAuthToken() || "",
);

return { authToken, setAuthToken };
};

export const AuthTokenContextProvider = AuthTokenContext.Provider;
35 changes: 35 additions & 0 deletions frontend/src/js/authorization/KeycloakProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ReactKeycloakProvider } from "@react-keycloak/web";
import React, { FC, useContext } from "react";

import keycloak from "../../keycloak";
import { isIDPEnabled } from "../environment";

import { AuthTokenContext } from "./AuthTokenProvider";

const KeycloakProvider: FC = ({ children }) => {
const { setAuthToken } = useContext(AuthTokenContext);

return (
<ReactKeycloakProvider
authClient={keycloak}
onEvent={(event: unknown, error: unknown) => {
// USEFUL FOR DEBUGGING
// console.log("onKeycloakEvent", event, error);
}}
onTokens={(tokens) => {
if (tokens.token) {
setAuthToken(tokens.token);
}
}}
initOptions={{
pkceMethod: "S256",
onLoad: isIDPEnabled ? "login-required" : "check-sso",
// silentCheckSsoRedirectUri:
// window.location.origin + "/silent-check-sso.html",
}}
>
{children}
</ReactKeycloakProvider>
);
};
export default KeycloakProvider;
8 changes: 5 additions & 3 deletions frontend/src/js/authorization/WithAuthToken.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useKeycloak } from "@react-keycloak/web";
import React, { FC } from "react";
import React, { FC, useContext } from "react";
import { useHistory } from "react-router-dom";

import { isLoginDisabled, isIDPEnabled } from "../environment";

import { AuthTokenContext } from "./AuthTokenProvider";
import { storeAuthToken, getStoredAuthToken } from "./helper";

interface PropsT {
Expand All @@ -14,7 +15,8 @@ interface PropsT {

const WithAuthToken: FC<PropsT> = ({ location, children }) => {
const history = useHistory();
const { keycloak, initialized } = useKeycloak();
const { initialized } = useKeycloak();
const { authToken } = useContext(AuthTokenContext);
const goToLogin = () => history.push("/login");

const { search } = location;
Expand All @@ -23,7 +25,7 @@ const WithAuthToken: FC<PropsT> = ({ location, children }) => {

if (accessToken) storeAuthToken(accessToken);

if (isIDPEnabled && (!initialized || !keycloak.token)) {
if (isIDPEnabled && (!initialized || !authToken)) {
return null;
}

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/js/button/DownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styled from "@emotion/styled";
import React, { ReactNode, FC } from "react";
import React, { ReactNode, FC, useContext } from "react";

import { useAuthToken } from "../api/useApi";
import { AuthTokenContext } from "../authorization/AuthTokenProvider";

import IconButton, { IconButtonPropsT } from "./IconButton";

Expand All @@ -21,7 +21,7 @@ const DownloadButton: FC<PropsT> = ({
children,
...restProps
}) => {
const authToken = useAuthToken();
const { authToken } = useContext(AuthTokenContext);

const href = `${url}?access_token=${encodeURIComponent(
authToken,
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/js/button/PreviewButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import styled from "@emotion/styled";
import { StateT } from "app-types";
import React, { FC } from "react";
import React, { FC, useContext } from "react";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";

import type { ColumnDescription } from "../api/types";
import { useAuthToken } from "../api/useApi";
import { AuthTokenContext } from "../authorization/AuthTokenProvider";
import { openPreview } from "../preview/actions";
import WithTooltip from "../tooltip/WithTooltip";

Expand All @@ -27,7 +27,7 @@ const PreviewButton: FC<PropsT> = ({
className,
...restProps
}) => {
const authToken = useAuthToken();
const { authToken } = useContext(AuthTokenContext);
const isLoading = useSelector<StateT, boolean>(
(state) => state.preview.isLoading,
);
Expand Down