Next JS Authentication setup using Masl || Azure

Next JS Authentication setup using Masl || Azure

msal

The msal-react library was released earlier this year for production use, providing a great set of tools for authenticating users with Azure AD. msal-react is based on the well-known msal-browser library and reduces boilerplate code by providing some valuable hooks. In this article, we will see how users already signed into an Azure AD account can be authenticated automatically when entering your React application. Further, we will see an example of how to acquire and provide access tokens with all API calls using axios. Finally, we’ll create an admin role for our application and provide the role with the access token. Typescript is used throughout this article, but the concepts would of course apply to JavaScript as well.

The current Microsoft provided tutorial for the msal-react library describes how to manually sign in users using Azure AD with “Sign In” and “Sign Out” buttons: https://docs.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-react. However, as many businesses use SSO, it would be great to automatically sign in users when entering your application. Assuming you have already followed the Microsoft tutorial, automatic sign-in can be achieved by using the useMsalAuthentication hook.

  "dependencies": {
    "@azure/msal-browser": "^2.23.0",
    "@azure/msal-react": "^1.3.2",
    }

lets get started

Create a MSAL instance firs

import * as msal from '@azure/msal-browser';
import { authority, clientId } from '../constants/app.constants';

const msalConfig = {
  auth: {
    clientId: clientId, // This is the ONLY mandatory field that you need to supply.
    authority: authority, // Defaults to "https://login.microsoftonline.com/common"
    redirectUri: '/', // Points to window.location.origin. You must register this URI on Azure Portal/App Registration.
    postLogoutRedirectUri: '/login', // 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', // This configures where your cache will be stored
    storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
  },
};
export const loginRequest = {
  scopes: ['User.Read'],
};

export const apiRequest = {
  scopes: ['https://api-dev.roshalimaging.org/api/user_impersonation'],
};

const msalInstance = new msal.PublicClientApplication(msalConfig);

export { msalInstance };

we can update __app.ts with MsalProvider

import React from "react";
import "../styles/globals.css";
import "../styles/home.css";
import type { AppProps } from "next/app";
import { MsalProvider } from '@azure/msal-react';
import { msalInstance } from '../services/masl';
import { AuthProvider } from "contexts/auth";
import { NextPage } from "next";
type CustomPage = NextPage & {
  requiresAuth?: boolean;
  redirectUnauthenticatedTo?: string;
};
interface CustomAppProps extends Omit<AppProps, "Component"> {
  Component: CustomPage;
}


function MyApp({ Component, pageProps }: CustomAppProps) {

  return (
    <MsalProvider instance={msalInstance}>
      <AuthProvider>
        <Component {...pageProps} />
      </AuthProvider>
    </MsalProvider>
  );
}


export default MyApp;

In th above example i am using some additional Context Provider, that is juts to track the login session User post Login, there may be other ways which MsalProvider provided to track the current login user but there is delay in detecting the login user so i have created one, this AuthProvider only passing login user to the next js layout

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

import { useRouter } from "next/router";
import { useMsal } from "@azure/msal-react";

export const AuthContext = React.createContext(
  {} as {
    user: any,
    updateUser: (user: any) => void
  }
);

export const AuthProvider = ({ children }: any) => {
  const [user, setUser] = useState<any>(null);
  const updateUser = (user: any) => {
    setUser(user)
  }

  return (
    <AuthContext.Provider
      value={{
        user,
        updateUser
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

My _app.js which is using all these providers together

import React from "react";
import "../styles/globals.css";
import "../styles/home.css";
import type { AppProps } from "next/app";
import { MsalProvider } from '@azure/msal-react';
import { msalInstance } from '../services/masl';
import { AuthProvider } from "contexts/auth";
import { NextPage } from "next";
type CustomPage = NextPage & {
  requiresAuth?: boolean;
  redirectUnauthenticatedTo?: string;
};
interface CustomAppProps extends Omit<AppProps, "Component"> {
  Component: CustomPage;
}


function MyApp({ Component, pageProps }: CustomAppProps) {

  return (
    <MsalProvider instance={msalInstance}>
      <AuthProvider>
        <Component {...pageProps} />
      </AuthProvider>
    </MsalProvider>
  );
}


export default MyApp;

I have created Dashboard layout so all other components can use this to render their children


   
import React, { useContext, useEffect } from 'react';
import { PageHeader } from "components/common/page-header";
import { PageFooter } from "components/common/page-footer";
import { useRouter } from 'next/router';
import { PageSideNav } from "components/common/page-side-nav";
import { AuthContext } from 'contexts/auth';

export default function Dashboard({ children }: any) {
  const {user, updateUser}  = useContext(AuthContext);

  return (
       <div className="flex-grow bg-white">
      <div className='min-h-screen flex bg-pc-base-gray-5 flex-col'>
        <PageHeader user={user}/>
        <div className="flex overflow-hidden" style={{ paddingTop: '4rem' }}>
          <PageSideNav />
          <div id="main-content" className=" h-full w-full relative overflow-y-auto" style={{ marginLeft: '256px' }}>
            {children}
          </div>
        </div>
        <PageFooter />
      </div>
    </div>
    
  )
}

AuthContext will supply user Data to all the components

const {user, updateUser}  = useContext(AuthContext);

Now My dashboard page which i want to show to the User after successful Login, here on landing screen i also want to fetch data from APIs Here i have faced lots of challenged as if you try to fetch token before Authentication gets completed then you will get lots of different errors, So simple solution is get access token silently and then using useEffect make api calls

// just an example to showcase
export default function Dashboard() {
  const router = useRouter()
  const isAuthenticated = useIsAuthenticated();
  const [accessToken, setAccessToken] = useState(null);

  const RequestAccessToken = async () => {
    return new Promise((resolve, reject) => {
      const request = {
        ...apiRequest,
        account: accounts[0],
      };
      // Silently acquires an access token which is then attached to a request for Microsoft Graph data
      return instance.handleRedirectPromise().then(() => {
        return instance
          .acquireTokenSilent(request)
          .then((response: any) => {
            setAccessToken(response.accessToken);
            resolve(response.accessToken);
          })
          .catch((e: any) => {
            instance.acquireTokenPopup(request).then((response: any) => {
              resolve(response.accessToken);
            });
          });

      });
    });
  };
  const getBootstrapData = async () => {
    try {
      // lets run all api calls together 
      const token = (await RequestAccessToken()) as string;
      // make this api call 
      const orderPromise = fetchAllOrders(token);
      await orderPromise;
    } catch (err) {
      console.log(err);
    }
  };

  useEffect(() => {
    getBootstrapData();
    // return () => null;
  }, []);
  return (
    <>
      <DashboardLayout>
      <h1>My Home Page Post Login</h1>
      </DashboardLayout>
    </>
  )
}  

lets build some pages and see how they looks like [login] While building application we always want these re-direction logic

  • if user is already login then while hitting login -> redirect users to /dashboard
  • if user is not logged In then while hitting /dashboard -> take user to login screen

This is like creating a route guard that will prevent user from accessing protected pages, I tried building this but failed so i am just using some hack to do this Example My login screen here i am just showing and hiding things based on login status

import LoginLayout from '../layouts/auth.layout';
import { useIsAuthenticated } from "@azure/msal-react";
import { ButtonPrimary } from "../components/btn/button";
import React from "react";
import {
  AuthenticatedTemplate,
  UnauthenticatedTemplate,
  useMsal
} from '@azure/msal-react';
import router from 'next/router';
import { loginRequest } from 'services/masl';
function Login() {
  const { instance } = useMsal();
  const isAuthenticated = useIsAuthenticated();

  const handleLogin = async (e: any) => {
    e.preventDefault()
    instance.loginRedirect(loginRequest)
    await instance.handleRedirectPromise();
  }

  const goToDashboard = () => {
    router.push('/dashboard');
  }
  if(isAuthenticated){
    router.push('/dashboard');
  }
  return (
    <div>
      <div className="App">
        <div className="login">
          <div className="login-content__left"></div>
          <div className="login-content__right">
            <UnauthenticatedTemplate>
              <div className="login-logo">
              
              </div>
              <h3 style={{ marginBottom: "8px" }}>Client Login</h3>
              <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
                eiusmod tempor incididunt ut labore et dolore magna.
              </p>
              <div
                style={{
                  display: "flex",
                  flexDirection: "column",
                  gap: "8px",
                  marginBottom: "40px",
                }}
              >
                <a href="/">Forgot Password?</a>
                <a href="/">Sign in with a different role</a>
              </div>
              <ButtonPrimary handleClick={handleLogin} className="rounded-none w-48 large px-4" size="normal" variant="primary" >Sign In</ButtonPrimary>
            </UnauthenticatedTemplate>
            <AuthenticatedTemplate>
              <ButtonPrimary handleClick={goToDashboard} className="rounded-none w-48 large px-4" size="normal" variant="primary" >Go to Dashboard</ButtonPrimary>
            </AuthenticatedTemplate>

          </div>
        </div>
      </div>
    </div>
  );
}
export default Login;

we can use const isAuthenticated = useIsAuthenticated(); to know the user login status and based on that we can do user re-direction

we can render different template data based on login status, This is just to show and hide JSX data based on login status, In this case we don't need to worry about redirect after login, based on Login status we can show and hide data

function WelcomeUser() {
  const { accounts } = useMsal();
  const username = accounts[0].username;

  return <p>Welcome, {username}</p>;
}

export default function Home() {
  return (
    <div>
      <Head>
        <title>Azure AD Authentication using MSAL and Next.js</title>
      </Head>

      <AuthenticatedTemplate>
        <p>This will only render if a user is signed-in.</p>
        <WelcomeUser />
      </AuthenticatedTemplate>
      <UnauthenticatedTemplate>
        <p>This will only render if a user is not signed-in.</p>
        <SignInButton />
      </UnauthenticatedTemplate>
    </div>
  );
}

Users will now be authenticated automatically when entering the application. The user is redirected to a sign-in page if not already logged into an account.

Add access token to all API calls using axios

Typically, you want to send the access token acquired in the front-end application to your back-end services. As the token will eventually expire, it would not be a good idea to temporarily store the token locally before sending it with each API call. This can be solved by intercepting the API calls, getting a valid token on the fly, and assigning it to the Authorization header. In order to minimize duplication, a component intercepting axios API calls can be wrapped around the application’s root component.

Create a component named RequestInterceptor.tsx and add the following code:

import React from 'react';
import { useMsal, useAccount } from '@azure/msal-react';
import axios from 'axios';
import { loginRequest } from '../../authentication/authConfig';

interface RequestInterceptorProps {
  children: JSX.Element;
}

const RequestInterceptor: React.FC<RequestInterceptorProps> = ({ children }: RequestInterceptorProps) => {
  const { instance, accounts } = useMsal();
  const account = useAccount(accounts[0]);

  /* eslint-disable no-param-reassign */
  axios.interceptors.request.use(async (config) => {
    if (!account) {
      throw Error('No active account! Verify a user has been signed in.');
    }

    const response = await instance.acquireTokenSilent({
      ...loginRequest,
      account,
    });

    const bearer = `Bearer ${response.accessToken}`;
    config.headers.Authorization = bearer;

    return config;
  });
  /* eslint-enable no-param-reassign */

  return <>{children}</>;
};

export default RequestInterceptor;
function App(): JSX.Element {
  useMsalAuthentication(InteractionType.Redirect);

  return (
    <PageLayout>
      <AuthenticatedTemplate>
        <RequestInterceptor>
          <RootComponent />
        </RequestInterceptor>
      </AuthenticatedTemplate>
      <UnauthenticatedTemplate>
        <p>You are not signed in! Please sign in.</p>
      </UnauthenticatedTemplate>
    </PageLayout>
  );
}

Or we an use instance.acquireTokenSilent(request) before making api calls, get the token first and then access apis

  const getBootstrapData = async () => {
    try {
      // lets run all api calls together
      const token = (await RequestAccessToken()) as string;
      const orderPromise = fetchAllOrders(token);
    } catch (err) {
      console.log(err);
    }
  };
  const fetchAllOrders = async (token: string) => {
    try {
      const { orders } = await new ApiService().fetchServiceOrders(token);
      console.log(orders);
      setOrders(orders || []);
    } catch (err) { }
  }
  useEffect(() => {
    getBootstrapData();
    // return () => null;
  }, []);

    const RequestAccessToken = async () => {
    return new Promise((resolve, reject) => {
      const request = {
        ...apiRequest,
        account: accounts[0],
      };
      // Silently acquires an access token which is then attached to a request for Microsoft Graph data
      return instance.handleRedirectPromise().then(() => {
        return instance
          .acquireTokenSilent(request)
          .then((response: any) => {
            setAccessToken(response.accessToken);
            resolve(response.accessToken);
          })
          .catch((e: any) => {
            instance.acquireTokenPopup(request).then((response: any) => {
              resolve(response.accessToken);
            });
          });

      });
    });
  };

Comments