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

Get OIDC access tokens once the authentication redirect is successful #3250

Merged
2 changes: 2 additions & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@patternfly/patternfly": "^4.183.1",
"@patternfly/react-core": "^4.198.19",
"@patternfly/react-table": "^4.75.2",
"@react-keycloak/web": "^3.4.0",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
Expand All @@ -21,6 +22,7 @@
"gulp": "^4.0.2",
"jest": "^27.5.1",
"js-cookie": "^3.0.1",
"keycloak-js": "^21.0.1",
"less-watch-compiler": "^1.16.3",
"patternfly": "^3.9.0",
"react": "^17.0.2",
Expand Down
37 changes: 29 additions & 8 deletions dashboard/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,29 @@ import { AuthForm } from "modules/components/AuthComponent/common-components";
import AuthLayout from "modules/containers/AuthLayout";
import ComingSoonPage from "modules/components/EmptyPageComponent/ComingSoon";
import Cookies from "js-cookie";
import LoginForm from "modules/components/AuthComponent/LoginForm";
import MainLayout from "modules/containers/MainLayout";
import NoMatchingPage from "modules/components/EmptyPageComponent/NoMatchingPage";
import OverviewComponent from "modules/components/OverviewComponent";
import ProfileComponent from "modules/components/ProfileComponent";
import SignupForm from "modules/components/AuthComponent/SignupForm";
import TableOfContent from "modules/components/TableOfContent";
import TableWithFavorite from "modules/components/TableComponent";
import favicon from "./assets/logo/favicon.ico";
import { fetchEndpoints } from "./actions/endpointAction";
import { getUserDetails } from "actions/authActions";
import { showToast } from "actions/toastActions";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { ReactKeycloakProvider } from '@react-keycloak/web';

const ProtectedRoute = ({ redirectPath = APP_ROUTES.AUTH_LOGIN, children }) => {
const eventLogger = (event, error) => {
// We might want to consider to refresh the tokens here
// if the event === 'onTokenExpired'
dbutenhof marked this conversation as resolved.
Show resolved Hide resolved
};

const tokenLogger = (tokens) => {
// Placeholder for to perform action when new token is generated
// console.log('onKeycloakTokens', tokens["refreshToken"]);
};

const ProtectedRoute = ({ redirectPath = APP_ROUTES.AUTH, children }) => {
const loggedIn = Cookies.get("isLoggedIn");
const dispatch = useDispatch();

Expand All @@ -47,25 +55,36 @@ const HomeRoute = ({ redirectPath = APP_ROUTES.HOME }) => {

const App = () => {
const dispatch = useDispatch();
const { keycloak } = useSelector(
state => state.apiEndpoint
);
webbnh marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
const faviconLogo = document.getElementById("favicon");
faviconLogo?.setAttribute("href", favicon);

dispatch(fetchEndpoints);
dispatch(getUserDetails());
}, [dispatch]);

return (
<div className="App">
{ keycloak && (
dbutenhof marked this conversation as resolved.
Show resolved Hide resolved
<ReactKeycloakProvider
authClient={keycloak}
onEvent={eventLogger}
onTokens={tokenLogger}
initOptions={{
onLoad:'check-sso',
checkLoginIframe: true,
enableLogging: true
}}
>
<BrowserRouter>
<Routes>
<Route path="/" element={<HomeRoute />}></Route>
<Route path={"/" + APP_ROUTES.HOME}>
<Route element={<AuthLayout />}>
<Route path={APP_ROUTES.AUTH_LOGIN} element={<LoginForm />} />
<Route path={APP_ROUTES.AUTH} element={<AuthForm />} />
<Route path={APP_ROUTES.AUTH_SIGNUP} element={<SignupForm />} />
</Route>
<Route element={<MainLayout />}>
<Route index element={<TableWithFavorite />} />
Expand Down Expand Up @@ -97,6 +116,8 @@ const App = () => {
</Route>
</Routes>
</BrowserRouter>
</ReactKeycloakProvider>
)}
</div>
);
};
Expand Down
216 changes: 46 additions & 170 deletions dashboard/src/actions/authActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,114 +2,58 @@ import * as APP_ROUTES from "utils/routeConstants";
import * as CONSTANTS from "../assets/constants/authConstants";
import * as TYPES from "./types";

import API from "../utils/axiosInstance";
import Cookies from "js-cookie";
import { SUCCESS } from "assets/constants/overviewConstants";
import { showToast } from "actions/toastActions";
import { uid } from "../utils/helper";


// Create an Authentication Request
export const authenticationRequest = () => async (dispatch, getState) => {
try {
const endpoints = getState().apiEndpoint.endpoints;
const oidcServer = endpoints.openid.server;
const oidcRealm = endpoints.openid.realm;
const oidcClient = endpoints.openid.client;
// URI parameters ref: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
// Refer Step 3 of pbench/docs/user_authentication/third_party_token_management.md
const uri = `${oidcServer}/realms/${oidcRealm}/protocol/openid-connect/auth`;
const queryParams = [
'client_id=' + oidcClient,
'response_type=code',
'redirect_uri=' + window.location.href.split('?')[0],
'scope=profile',
'prompt=login',
'max_age=120'
];
window.location.href = uri + '?' + queryParams.join('&');
} catch (error) {
const alerts = getState().userAuth.alerts;
dispatch(error?.response
? toggleLoginBtn(true)
: { type: TYPES.OPENID_ERROR }
);
const alert = {
title: error?.response ? error.response.data?.message : error?.message,
key: uid(),
};
dispatch({
type: TYPES.USER_NOTION_ALERTS,
payload: [...alerts, alert],
});
dispatch({ type: TYPES.COMPLETED });
/**
* Loop to check if the endpoints are loaded.
* @param {getState} getState object.
* @return {promise} promise object
*/
export function waitForKeycloak (getState) {
const waitStart = Date.now();
const maxWait = 10000; // Milliseconds
webbnh marked this conversation as resolved.
Show resolved Hide resolved
/**
* Loop to check if the endpoints are loaded.
* @param {resolve} resolve object.
* @param {reject} reject object
*/
function check(resolve, reject) {
webbnh marked this conversation as resolved.
Show resolved Hide resolved
if (Object.keys(getState().apiEndpoint.endpoints).length !== 0) {
webbnh marked this conversation as resolved.
Show resolved Hide resolved
resolve("Loaded");
} else if (Date.now() - waitStart > maxWait) {
reject(new Error('Something went wrong'));
webbnh marked this conversation as resolved.
Show resolved Hide resolved
} else {
setTimeout(check, 250, resolve, reject);
}
};

export const makeLoginRequest =
(details, navigate) => async (dispatch, getState) => {
try {
dispatch({ type: TYPES.LOADING });
// empty the alerts
dispatch({
type: TYPES.USER_NOTION_ALERTS,
payload: [],
});
const endpoints = getState().apiEndpoint.endpoints;
const response = await API.post(endpoints?.api?.login, {
...details,
});
if (response.status === 200 && Object.keys(response.data).length > 0) {
const keepUser = getState().userAuth.keepLoggedIn;
const expiryTime = keepUser
? CONSTANTS.EXPIRY_KEEPUSER_DAYS
: CONSTANTS.EXPIRY_DEFAULT_DAYS;
Cookies.set("isLoggedIn", true, { expires: expiryTime });
Cookies.set("token", response.data?.auth_token, {
expires: expiryTime,
});
Cookies.set("username", response.data?.username, {
expires: expiryTime,
});
const loginDetails = {
isLoggedIn: true,
token: response.data?.auth_token,
username: response.data?.username,
};
await dispatch({
type: TYPES.SET_LOGIN_DETAILS,
payload: loginDetails,
});

navigate(APP_ROUTES.OVERVIEW);
};
return new Promise((resolve, reject) => {
check(resolve, reject);
});
webbnh marked this conversation as resolved.
Show resolved Hide resolved
}

dispatch(showToast(SUCCESS, "Logged in successfully!"));
}
dispatch({ type: TYPES.COMPLETED });
} catch (error) {
const alerts = getState().userAuth.alerts;
let alert = {};
if (error?.response) {
alert = {
title: error?.response?.data?.message,
key: uid(),
};
dispatch(toggleLoginBtn(true));
} else {
alert = {
title: error?.message,
key: uid(),
};
dispatch({ type: TYPES.NETWORK_ERROR });
}
alerts.push(alert);
// Perform some house keeping when the user logs in
export const authCookies = () => async (dispatch, getState) => {
webbnh marked this conversation as resolved.
Show resolved Hide resolved
await waitForKeycloak(getState);
const keycloak = getState().apiEndpoint.keycloak;
if (keycloak.authenticated){
Cookies.set("isLoggedIn", true, { expires: keycloak.refreshTokenParsed?.exp });
Cookies.set("username", keycloak.tokenParsed?.preferred_username, { expires: keycloak.refreshTokenParsed?.exp });
const userPayload = {
"username": keycloak.tokenParsed?.preferred_username,
"email": keycloak.tokenParsed?.email,
"first_name": keycloak.tokenParsed?.given_name,
"last_name": keycloak.tokenParsed?.family_name,
};
webbnh marked this conversation as resolved.
Show resolved Hide resolved
dispatch({
type: TYPES.USER_NOTION_ALERTS,
payload: alerts,
type: TYPES.GET_USER_DETAILS,
payload: userPayload,
});
webbnh marked this conversation as resolved.
Show resolved Hide resolved
dispatch({ type: TYPES.COMPLETED });
dispatch(showToast(SUCCESS, "Logged in successfully!"));
}
};
}

export const movePage = (toPage, navigate) => async (dispatch) => {
// empty the alerts
Expand All @@ -120,83 +64,15 @@ export const movePage = (toPage, navigate) => async (dispatch) => {
navigate(toPage);
};

export const setUserLoggedInState = (value) => async (dispatch) => {
dispatch({
type: TYPES.KEEP_USER_LOGGED_IN,
payload: value,
});
};

export const registerUser =
(details, navigate) => async (dispatch, getState) => {
try {
dispatch({ type: TYPES.LOADING });
// empty the alerts
dispatch({
type: TYPES.USER_NOTION_ALERTS,
payload: [],
});
const endpoints = getState().apiEndpoint.endpoints;
const response = await API.post(endpoints?.api?.register, {
...details,
});
if (response.status === 201) {
dispatch(showToast(SUCCESS, "Account created!", "Login to continue"));
navigate(APP_ROUTES.AUTH_LOGIN);
}
dispatch({ type: TYPES.COMPLETED });
} catch (error) {
const alerts = getState().userAuth.alerts;
let amsg = {};
document.querySelector(".signup-card").scrollTo(0, 0);
if (error?.response) {
amsg = error?.response?.data?.message;
dispatch(toggleSignupBtn(true));
} else {
amsg = error?.message;
dispatch({ type: TYPES.NETWORK_ERROR });
}
const alert = { title: amsg, key: uid() };
alerts.push(alert);
dispatch({
type: TYPES.USER_NOTION_ALERTS,
payload: alerts,
});
dispatch({ type: TYPES.COMPLETED });
}
};

export const toggleSignupBtn = (isDisabled) => async (dispatch) => {
dispatch({
type: TYPES.SET_SIGNUP_BUTTON,
payload: isDisabled,
});
};

export const toggleLoginBtn = (isDisabled) => async (dispatch) => {
dispatch({
type: TYPES.SET_LOGIN_BUTTON,
payload: isDisabled,
});
};

export const getUserDetails = () => async (dispatch) => {
const loginDetails = {
isLoggedIn: Cookies.get("isLoggedIn"),
token: Cookies.get("token"),
username: Cookies.get("username"),
};
dispatch({
type: TYPES.SET_LOGIN_DETAILS,
payload: loginDetails,
});
webbnh marked this conversation as resolved.
Show resolved Hide resolved
};
export const logout = () => async (dispatch) => {
export const logout = () => async (dispatch, getState) => {
dispatch({ type: TYPES.LOADING });
// const keycloak = useKeycloak();
const keycloak = getState().apiEndpoint.keycloak;
webbnh marked this conversation as resolved.
Show resolved Hide resolved
const keys = ["username", "token", "isLoggedIn"];
for (const key of keys) {
Cookies.remove(key);
}
webbnh marked this conversation as resolved.
Show resolved Hide resolved
keycloak.logout();
dispatch({ type: TYPES.COMPLETED });
setTimeout(() => {
window.location.href = APP_ROUTES.AUTH;
Expand Down
10 changes: 10 additions & 0 deletions dashboard/src/actions/endpointAction.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as TYPES from "./types";
import Keycloak from 'keycloak-js';

export const fetchEndpoints = async (dispatch) => {
try {
Expand All @@ -12,6 +13,15 @@ export const fetchEndpoints = async (dispatch) => {
type: TYPES.SET_ENDPOINTS,
payload: data,
});
const keycloak = new Keycloak({
url: data?.openid?.server,
realm: data?.openid?.realm,
clientId: data?.openid?.client,
dbutenhof marked this conversation as resolved.
Show resolved Hide resolved
});
dispatch({
type: TYPES.SET_KEYCLOAK,
payload: keycloak,
});
} catch (error) {
dispatch({
type: TYPES.SHOW_TOAST,
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/actions/overviewActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import API from "../utils/axiosInstance";
import { DANGER } from "assets/constants/toastConstants";
import { findNoOfDays } from "utils/dateFunctions";
import { showToast } from "./toastActions";
import Cookies from "js-cookie";

export const getDatasets = () => async (dispatch, getState) => {
const alreadyRendered = getState().overview.loadingDone;
try {
const username = getState().userAuth.loginDetails.username;
const username = Cookies.get("username");;

if (alreadyRendered) {
dispatch({ type: TYPES.LOADING });
Expand Down
Loading