Search code examples
reactjsazure-ad-msalazure-rbacmsal-reactreact-aad-msal

How to Redirect automatically to a page/path on login - MSAL React SPA


I am working on a single page application (SPA) app that grants access to specific paths in the application, based on roles setup in Azure AD for the user logging in. As per this https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/5-AccessControl/1-call-api-roles

This is my 'authConfig.js' file - you can see the redirectUri

const clientId = window.REACT_APP_CLIENTID
export const msalConfig = {
    auth: {
        clientId: clientId,
        authority: window.REACT_APP_AUTHORITY,
        redirectUri: 'http://localhost:3000/todolist/', // You must register this URI on Azure Portal/App Registration. Defaults to window.location.origin
        postLogoutRedirectUri: "/", // Indicates the page to navigate after logout.
        navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
    },
    cache: {
        cacheLocation: "sessionStorage", // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
        storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
    },
    system: {
        loggerOptions: {
            loggerCallback: (level, message, containsPii) => {
                if (containsPii) {
                    return;
                }
                switch (level) {
                    case LogLevel.Error:
                        console.error(message);
                        return;
                    case LogLevel.Info:
                        console.info(message);
                        return;
                    case LogLevel.Verbose:
                        console.debug(message);
                        return;
                    case LogLevel.Warning:
                        console.warn(message);
                        return;
                }
            }
        }
    }
};

/**
 * Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
 */
export const protectedResources = {
    apiTodoList: {
        todoListEndpoint: window.REACT_APP_APIENDPOINT+"/api/v2/support/list",
        scopes: [window.REACT_APP_APIENDPOINT+"/access_as_user"],
    },
}

/**
 * Scopes you add here will be prompted for user consent during sign-in.
 * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
 * For more information about OIDC scopes, visit: 
 * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
 */
export const loginRequest = {
    scopes: [...protectedResources.apiTodoList.scopes]
};

export const appRoles = {
    TaskUser: "TaskUser",
    TaskAdmin: "TaskAdmin",
    TrialAdmin: "Trial.Admin",
    GlobalAdmin: "Global.Admin"
}

Here is the App.jsx file (I believe there needs to be some change made here). You can see 'RouteGuard' that renders the Component {TodoList}, when the path 'todolist' is accessed.

import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { MsalProvider } from "@azure/msal-react";

import { RouteGuard } from './components/RouteGuard';
import { PageLayout } from "./components/PageLayout";
import { TodoList } from "./pages/TodoList";


import { appRoles } from "./authConfig";


import "./styles/App.css";


const Pages = () => {
  return (
    <Switch>
      <RouteGuard
        exact
        path='/todolist/'
        roles={[appRoles.TaskUser, appRoles.TaskAdmin, appRoles.TrialAdmin, appRoles.GlobalAdmin]}
        Component={TodoList}
      />
    </Switch>
  )
}

/**
 * msal-react is built on the React context API and all parts of your app that require authentication must be 
 * wrapped in the MsalProvider component. You will first need to initialize an instance of PublicClientApplication 
 * then pass this to MsalProvider as a prop. All components underneath MsalProvider will have access to the 
 * PublicClientApplication instance via context as well as all hooks and components provided by msal-react. For more, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/getting-started.md
 */
const App = ({ instance }) => {

  return (
    <Router>
      <MsalProvider instance={instance}>
        <PageLayout>
          <Pages instance={instance} />
        </PageLayout>
      </MsalProvider>
    </Router>
  );
}

export default App;

So as far as my understanding goes, the path 'todolist' is accessed with the listed role, and component is rendered

When logged in, The navigation bar at the top renders with login request, after authentication (). It has the button rendered, with a click function that 'href's to the path '/todolist'.

import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "@azure/msal-react";

import { Nav, Navbar, Button, Dropdown, DropdownButton} from "react-bootstrap";

import React, { useState, useEffect } from "react";

import { loginRequest } from "../authConfig";

import { InteractionStatus, InteractionType } from "@azure/msal-browser";


import "../styles/App.css";

import logo from "../public/images/logo.jpg";

export const NavigationBar = (props) => {

    const { instance } = useMsal();
    const { inProgress } = useMsal();
    const [isAuthorized, setIsAuthorized] = useState(false);
    //The below function is needed incase you want to login using Popup and not redirect
    const handleLogin = () => {
        instance.loginPopup(loginRequest)
            .catch((error) => console.log(error))
    }

    /**
     * Most applications will need to conditionally render certain components based on whether a user is signed in or not. 
     * msal-react provides 2 easy ways to do this. AuthenticatedTemplate and UnauthenticatedTemplate components will 
     * only render their children if a user is authenticated or unauthenticated, respectively.
     */
    return (
        <>
            <Navbar className="color-custom" variant="dark">
                <a className="navbar-brand" href="/"><img src={logo} className="navbarLogo" alt="TODDOLIST1"/></a>
                <AuthenticatedTemplate>
 <Nav.Link as={Button} id="signupbutton" variant="dark" className="signupNav" href="/todolist"><strong>List</strong></Nav.Link>
                    <Button variant="warning" className="ml-auto" drop="left" title="Sign Out" onClick={() => instance.logoutRedirect({ postLogoutRedirectUri: "/" })}><strong>Sign Out</strong></Button>
                </AuthenticatedTemplate>
                <UnauthenticatedTemplate>
                    <Button variant="dark" className="ml-auto" drop="left" title="Sign In" onClick={() => instance.loginRedirect(loginRequest)}>Sign In</Button>
                </UnauthenticatedTemplate>
            </Navbar>
        </>
    );
};

Here is the RouteGuard.jsx component that renders based on roles/authorization.

import React, { useState, useEffect } from "react";
import { Route } from "react-router-dom";
import { useMsal } from "@azure/msal-react";

export const RouteGuard = ({ Component, ...props }) => {

    const { instance } = useMsal();
    const [isAuthorized, setIsAuthorized] = useState(false);

    const onLoad = async () => {
        const currentAccount = instance.getActiveAccount();

        if (currentAccount && currentAccount.idTokenClaims['roles']) {
            let intersection = props.roles
                .filter(role => currentAccount.idTokenClaims['roles'].includes(role));

            if (intersection.length > 0) {
                setIsAuthorized(true);
            }
        }
    }

    useEffect(() => {
        onLoad();
    }, [instance]);

    return (
        <>
            
            {
                isAuthorized
                    ?
                    <Route {...props} render={routeProps => <Component {...routeProps} />} />
                    :
                    <div className="data-area-div">
                        <h3>You are unauthorized to view this content.</h3>
                    </div>
            }
        </>
    );
};

I want the application to directly go to the '/todolist' and render the components within. My redirect uri, does not seem to work. When i login with the required role, it always renders 'You are unauthorized to view this content' as per the RouteGuard file. The URI is /signuplist/ but still the children props are not rendered. ONLY WHEN I CLICK the button 'Todolist' (as per NavigationBar.jsx), does it go and render the child props properly. Redirection does not work as expected. I want it to directly go to /todolist and render the page, child components Any suggestions ?


Solution

  • In case my last comment worked out for you, let me make it into an official answer so it can be recorded.

    Basically,

    define: const { instance, accounts, inProgress } = useMsal();

    Then try to redirect when inProgress !== 'login'.