Keycloak & React Router routing for Public/Login & Authenticated & Role based routes

Viewed 6981

I did see Tyler McGinnis https://tylermcginnis.com/react-router-protected-routes-authentication/ Protected routes and authentication with React Router v4

I use react-router-dom v 5.1.2

I initialize keycloak in index.js & pass it as a prop to App.js from it I can get keycloak.idTokenParsed.preferred_username they are: SuperAdmin, Admin, Manager, User. I already have Public routes & Authenticated routes, the part that I am trying to figure out is how to add Role based routes to it? So that only SuperAdmin has access to /create/ & Admin to /edit/. In Routes.jsx I have const for publicRoutes & privateRoutes do I just make 4 for SuperAdmin, Admin, Manager, User or is there a better way to do it?

Routes.jsx*

import Fcontainer from "../containers/Factories/Fcontainer"
import LContainer from "../containers/Lines/LContainer"
import AddLContainer from "../containers/Lines/AddLContainer"
import AddFContainer from "../containers/Factories/AddFContainer"
import LandingPageContainer from "../containers/common/LandingPageContainer"

export const publicRoutes = [
  { path: "/login", type: "public", name: "landing page", component: LandingPageContainer },
]

export const privateRoutes = [
  { path: "/dir/factories", name: "list of factories", component: Fcontainer },
  { path: "/dir/lines", name: "List of lines", component: LContainer },
  { path: "/dir/lines/create", name: "create line", component: AddLContainer },
  { path: "/dir/factories/create", name: "Create factory", component: AddFContainer },
  { path: "/dir/factories/edit/:factoryId", name: "Edit factory", component: AddFContainer },
  { path: "/dir/lines/edit/:lineId", name: "Edit line", component: AddLContainer }
]

App.js

import React, { useEffect } from "react";
import { Switch, withRouter } from "react-router-dom";
import { publicRoutes, privateRoutes } from "../src/routes/Routes";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import { fetchApplicationStyle } from './actions/GetAppStyleAction'

const AppContainer = ({ keycloak }) => {
  if(keycloak && keycloak.token) {;
    console.log('App.js preferred_username ', keycloak.idTokenParsed.preferred_username);
  }

  useEffect(() => {
    async function fetchStyleApi() {
      await fetchApplicationStyle();
      if (keycloak && keycloak.authenticated) {
        await fetchRoleList()
      }
    }
    fetchStyleApi()
  }, [fetchApplicationStyle, fetchRoleList])

  return (
    <div>
      <div>
        <ThemeProvider theme={theme}>
          {(keycloak && keycloak.token) ?
            <React.Fragment>
              <Switch>
                {privateRoutes.map((prop, key) => {
                console.log('App.js Prop & Key ', prop, key)
                return (
                    <Route
                    path={prop.path}
                    key={key}
                    exact={true}
                    component={prop.component}
                    />
                );
                })}
                <Redirect from={'/'} to={'/fcm/factories'} key={'list of factories'} />
            </Switch>
            </React.Fragment>
            :
            <React.Fragment>
              <Switch>
                {publicRoutes.map((prop, key) => {
                  return (
                    <Route
                      path={prop.path}
                      key={key}
                      exact={true}
                      component={(props) =>
                        <prop.component
                          keycloak={keycloak}
                          key={key} {...props} />
                      }
                    />
                  );
                })}
                <Redirect from={'/'} to={'/login'} key={'login'} />
              </Switch>
            </React.Fragment>
          }
        </ThemeProvider>
      </div>
    </div>
  );
}

const mapStateToProps = state => {
  const { roleAndPrivileges, applicationStyleState } = state;
  const theme = ThemeCreator(applicationStyleState.appThemeData)
  return {
    roleAndPrivileges,
    theme
  }
}
const mapDispatchToProps = { fetchRoleList, fetchApplicationStyle }

const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(withRouter(AppContainer))
export default App
1 Answers

This might help you, but it's implemented with another library, but you can restrict React Routes / Component / Functions

Install @react-keycloak/web library

Pre requires: keycloak-js 9.0.2 or later

npm install --save keycloak-js
npm install --save @react-keycloak/web

Create a keycloak.js file in the src folder of your project with the following content to set up a Keycloak instance as needed.

Set up a Keycloak instance

import Keycloak from 'keycloak-js'
const keycloakConfig = {
  url: 'http://localhost:8080/auth', 
  realm: 'Demo', 
  clientId: 'react-app'
}
const keycloak = new Keycloak(keycloakConfig);
export default keycloak

Then Wrap your App inside KeycloakProvider and pass the keycloak instance as prop

import React from 'react';
import { KeycloakProvider } from '@react-keycloak/web'
import keycloak from './keycloak'
import AppRouter from './routes'

const App = () => {
  return (
    <KeycloakProvider keycloak={keycloak}>
      <AppRouter/>
    </KeycloakProvider>
  )
}

As you see in AppRouter component useKeycloak is being used without 1st param because we don't need that keycloak instance object to be used. But 2nd initialized parameter is being used because PrivateRoute is using keycloak instance, and keycloak object need to be available on PrivateRoute initialization, that means until keycloak initialized Routes cannot be returned

import { useKeycloak } from '@react-keycloak/web';

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

import Menu from '../components/Menu';
import HomePage from '../pages/HomePage';
import { PrivateRoute } from '../utilities/PrivateRoute';
import ProtectedPage from '../pages/ProtectedPage';


export const AppRouter = () => {
    const [, initialized] = useKeycloak();
    if (!initialized) {
        return <h3>Loading ... !!!</h3>;
    }
    return (<>
        <Menu />
        <BrowserRouter>
            <Switch>
                <Route exact path="/" component={HomePage} />
                <PrivateRoute roles={['RealmAdmin']} path="/protected" component={ProtectedPage} />
            </Switch>
        </BrowserRouter>
    </>
    );
};

Create a PrivateRoute.js file in src/utilities folder of your project with the following content.

import { useKeycloak } from '@react-keycloak/web';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';


export function PrivateRoute({ component: Component, roles, ...rest }) {
  const [keycloak] = useKeycloak();

  const isAutherized = (roles) => {
    if (keycloak && roles) {
        return roles.some(r => {
            const realm =  keycloak.hasRealmRole(r);
            const resource = keycloak.hasResourceRole(r);
            return realm || resource;
        });
    }
    return false;
   }

   return (
      <Route
        {...rest}
        render={props => {
            return isAutherized(roles)
                ? <Component {...props} />
                : <Redirect to={{ pathname: '/', }} />
        }}
      />
    )
}

Also, there are two ways of restricting Components/Functions with a Keycloak as below example

https://medium.com/@cagline/authenticate-and-authorize-react-routes-component-with-keycloak-666e85662636

Related