MSAL React / Azure AD B2C AuthProvider sample request
Can you please provide us with a sample that demonstrates how to create an authentication provider using MSAL React & Create React App?
I managed to find the following resources:
https://medium.com/ascentic-technology/authentication-with-msal-js-2fe281098038 (good starting point but uses older MSAL libraries + uses class components. So it's a little bit outdated. https://gist.github.com/ayeshnipun/77bae1b89dc1f69c8c6d4e44d140bd33#file-auth-provider-js (Authentication provider)
I also found this one:
https://github.com/microsoftgraph/msgraph-sample-reactspa
This one kind of demonstrates what I'm after (https://github.com/microsoftgraph/msgraph-sample-reactspa/blob/main/graph-tutorial/src/AppContext.tsx) but it also adds several other concepts. It would be nice to have like a sample SPA in this repo with only login and logout using a well-architected authentication provider pattern/solution.
I think this would be a great idea for an article on Microsoft docs and in this repo.
What I would like to see in that article/sample SPA:
- The SPA should be based on Create React App
- The react app should integrate with Azure AD B2C
- The sample should contain a well-structured AuthProvider react function component for the most common scenarios (log in, log out etc)
- The UI should have protected routes and non-protected routes (I'm thinking a simple react router dashboard type of sample with a spinner/loader/splashscreen, login, logout, registration, reset password etc)
- The AuthProvider react component should handle the authenticated / unauthenticated user
- If authenticated you should be able to see the protected routes
- If not authenticated you should not be able to see the protected routes
- Show how to do login and logout with both redirect and pop-up (like in the other samples in this repo)
- Both a .js and .tsx version of the sample
Again it would be nice to see this type of info somewhere and that you could use as a starting point.
Also, I'm curious about whether you know of any other resources where I can read more about how to build your own authentication provider using MSAL React + Azure AD B2C?
Hi @spacecat, Thanks for reaching out. Let me look at the articles you sent, and I will get back to you with a response as soon as possible.
Hi @spacecat. Apologies for the late response. I went through the samples and the articles you sent. I needed help seeing the benefits of using an AuthProvider class using msal-react. In msal-react, you can easily use the hooks to perform the actions in the AuthProvider class without creating it. Why would you like us to use an AuthProvider class in the samples, and what are its benefits?
Hi, and thank you for the reply.
I purchased https://themeforest.net/item/metronic-responsive-admin-dashboard-template/4021469 a few weeks ago - it's a complete React application. And I've been studying it and in particular the authentication components.
Now what I want to do is to take their auth provider and replace it with MSAL React. And I'm not sure how to do that? I've asked in their support support forum if they had any suggestions and this is the response that I got:
We aren't experts in msal. From my point of you, would be better to copy AuthProvider and rewrite it in the msal way (and then use it), instead of wrapping AuthProvider.
Here's the entire thread just for completeness: https://devs.keenthemes.com/question/how-to-replace-authprovider-with-microsoft-authentication-library-for-react-msal-react
And here is the AuthProvider they're referring to:
import {
FC,
useState,
useEffect,
createContext,
useContext,
useRef,
Dispatch,
SetStateAction,
} from 'react'
import {LayoutSplashScreen} from '../../../../_metronic/layout/core'
import {AuthModel, UserModel} from './_models'
import * as authHelper from './AuthHelpers'
import {getUserByToken} from './_requests'
import {WithChildren} from '../../../../_metronic/helpers'
type AuthContextProps = {
auth: AuthModel | undefined
saveAuth: (auth: AuthModel | undefined) => void
currentUser: UserModel | undefined
setCurrentUser: Dispatch<SetStateAction<UserModel | undefined>>
logout: () => void
}
const initAuthContextPropsState = {
auth: authHelper.getAuth(),
saveAuth: () => {},
currentUser: undefined,
setCurrentUser: () => {},
logout: () => {},
}
const AuthContext = createContext<AuthContextProps>(initAuthContextPropsState)
const useAuth = () => {
return useContext(AuthContext)
}
const AuthProvider: FC<WithChildren> = ({children}) => {
const [auth, setAuth] = useState<AuthModel | undefined>(authHelper.getAuth())
const [currentUser, setCurrentUser] = useState<UserModel | undefined>()
const saveAuth = (auth: AuthModel | undefined) => {
setAuth(auth)
if (auth) {
authHelper.setAuth(auth)
} else {
authHelper.removeAuth()
}
}
const logout = () => {
saveAuth(undefined)
setCurrentUser(undefined)
}
return (
<AuthContext.Provider value={{auth, saveAuth, currentUser, setCurrentUser, logout}}>
{children}
</AuthContext.Provider>
)
}
const AuthInit: FC<WithChildren> = ({children}) => {
const {auth, logout, setCurrentUser} = useAuth()
const didRequest = useRef(false)
const [showSplashScreen, setShowSplashScreen] = useState(true)
// We should request user by authToken (IN OUR EXAMPLE IT'S API_TOKEN) before rendering the application
useEffect(() => {
const requestUser = async (apiToken: string) => {
try {
if (!didRequest.current) {
const {data} = await getUserByToken(apiToken)
if (data) {
setCurrentUser(data)
}
}
} catch (error) {
console.error(error)
if (!didRequest.current) {
logout()
}
} finally {
setShowSplashScreen(false)
}
return () => (didRequest.current = true)
}
if (auth && auth.api_token) {
requestUser(auth.api_token)
} else {
logout()
setShowSplashScreen(false)
}
// eslint-disable-next-line
}, [])
return showSplashScreen ? <LayoutSplashScreen /> : <>{children}</>
}
export {AuthProvider, AuthInit, useAuth}
Here's how they use their auth provider from the Login component:
/* eslint-disable jsx-a11y/anchor-is-valid */
import {useState} from 'react'
import * as Yup from 'yup'
import clsx from 'clsx'
import {Link} from 'react-router-dom'
import {useFormik} from 'formik'
import {getUserByToken, login} from '../core/_requests'
import {toAbsoluteUrl} from '../../../../_metronic/helpers'
import {useAuth} from '../core/Auth'
const loginSchema = Yup.object().shape({
email: Yup.string()
.email('Wrong email format')
.min(3, 'Minimum 3 symbols')
.max(50, 'Maximum 50 symbols')
.required('Email is required'),
password: Yup.string()
.min(3, 'Minimum 3 symbols')
.max(50, 'Maximum 50 symbols')
.required('Password is required'),
})
const initialValues = {
email: '[email protected]',
password: 'demo',
}
/*
Formik+YUP+Typescript:
https://jaredpalmer.com/formik/docs/tutorial#getfieldprops
https://medium.com/@maurice.de.beijer/yup-validation-and-typescript-and-formik-6c342578a20e
*/
export function Login() {
const [loading, setLoading] = useState(false)
const {saveAuth, setCurrentUser} = useAuth()
const formik = useFormik({
initialValues,
validationSchema: loginSchema,
onSubmit: async (values, {setStatus, setSubmitting}) => {
setLoading(true)
try {
const {data: auth} = await login(values.email, values.password)
saveAuth(auth)
const {data: user} = await getUserByToken(auth.api_token)
setCurrentUser(user)
} catch (error) {
console.error(error)
saveAuth(undefined)
setStatus('The login details are incorrect')
setSubmitting(false)
setLoading(false)
}
},
})
return (
<form
...
</form>
)
}
Models:
export interface AuthModel {
api_token: string
refreshToken?: string
}
export interface UserModel {
id: number
username: string
password: string | undefined
email: string
first_name: string
last_name: string
fullname?: string
occupation?: string
companyName?: string
phone?: string
roles?: Array<number>
pic?: string
language?: 'en' | 'de' | 'es' | 'fr' | 'ja' | 'zh' | 'ru'
timeZone?: string
website?: 'https://keenthemes.com'
emailSettings?: UserEmailSettingsModel
auth?: AuthModel
communication?: UserCommunicationModel
address?: UserAddressModel
socialNetworks?: UserSocialNetworksModel
}
index.tsx
import {createRoot} from 'react-dom/client'
// Axios
import axios from 'axios'
import {Chart, registerables} from 'chart.js'
import {QueryClient, QueryClientProvider} from 'react-query'
import {ReactQueryDevtools} from 'react-query/devtools'
// Apps
import {MetronicI18nProvider} from './_metronic/i18n/Metronici18n'
/**
* TIP: Replace this style import with rtl styles to enable rtl mode
*
* import './_metronic/assets/css/style.rtl.css'
**/
import './_metronic/assets/sass/style.scss'
import './_metronic/assets/sass/plugins.scss'
import './_metronic/assets/sass/style.react.scss'
import {AppRoutes} from './app/routing/AppRoutes'
import {AuthProvider, setupAxios} from './app/modules/auth'
/**
* Creates `axios-mock-adapter` instance for provided `axios` instance, add
* basic Metronic mocks and returns it.
*
* @see https://github.com/ctimmerm/axios-mock-adapter
*/
/**
* Inject Metronic interceptors for axios.
*
* @see https://github.com/axios/axios#interceptors
*/
setupAxios(axios)
Chart.register(...registerables)
const queryClient = new QueryClient()
const container = document.getElementById('root')
if (container) {
createRoot(container).render(
<QueryClientProvider client={queryClient}>
<MetronicI18nProvider>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</MetronicI18nProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
AppRoutes.tsx
/**
* High level router.
*
* Note: It's recommended to compose related routes in internal router
* components (e.g: `src/app/modules/Auth/pages/AuthPage`, `src/app/BasePage`).
*/
import {FC} from 'react'
import {Routes, Route, BrowserRouter, Navigate} from 'react-router-dom'
import {PrivateRoutes} from './PrivateRoutes'
import {ErrorsPage} from '../modules/errors/ErrorsPage'
import {Logout, AuthPage, useAuth} from '../modules/auth'
import {App} from '../App'
/**
* Base URL of the website.
*
* @see https://facebook.github.io/create-react-app/docs/using-the-public-folder
*/
const {PUBLIC_URL} = process.env
const AppRoutes: FC = () => {
const {currentUser} = useAuth()
return (
<BrowserRouter basename={PUBLIC_URL}>
<Routes>
<Route element={<App />}>
<Route path='error/*' element={<ErrorsPage />} />
<Route path='logout' element={<Logout />} />
{currentUser ? (
<>
<Route path='/*' element={<PrivateRoutes />} />
<Route index element={<Navigate to='/dashboard' />} />
</>
) : (
<>
<Route path='auth/*' element={<AuthPage />} />
<Route path='*' element={<Navigate to='/auth' />} />
</>
)}
</Route>
</Routes>
</BrowserRouter>
)
}
export {AppRoutes}
App.tsx
import {Suspense} from 'react'
import {Outlet} from 'react-router-dom'
import {I18nProvider} from '../_metronic/i18n/i18nProvider'
import {LayoutProvider, LayoutSplashScreen} from '../_metronic/layout/core'
import {MasterInit} from '../_metronic/layout/MasterInit'
import {AuthInit} from './modules/auth'
const App = () => {
return (
<Suspense fallback={<LayoutSplashScreen />}>
<I18nProvider>
<LayoutProvider>
<AuthInit>
<Outlet />
<MasterInit />
</AuthInit>
</LayoutProvider>
</I18nProvider>
</Suspense>
)
}
export {App}
AuthPage.tsx
import {Route, Routes} from 'react-router-dom'
import {Registration} from './components/Registration'
import {ForgotPassword} from './components/ForgotPassword'
import {Login} from './components/Login'
import {AuthLayout} from './AuthLayout'
const AuthPage = () => (
<Routes>
<Route element={<AuthLayout />}>
<Route path='login' element={<Login />} />
<Route path='registration' element={<Registration />} />
<Route path='forgot-password' element={<ForgotPassword />} />
<Route index element={<Login />} />
</Route>
</Routes>
)
export {AuthPage}
And now I'm building my own auth provider using MSAL React - I'm using https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/blob/main/1-Authentication/2-sign-in-b2c/README.md as my starting point:
import { useMsal } from '@azure/msal-react';
import { createContext, useState } from 'react';
import { loginRequest } from './authConfig';
export const AuthProviderContext = createContext({
logIn: undefined,
logOut: undefined
});
export default function AuthProvider({ children }) {
const auth = useAuthProviderContext();
return (
<AuthProviderContext.Provider value={auth}>
{children}
</AuthProviderContext.Provider>
);
}
function useAuthProviderContext() {
const { instance } = useMsal();
const logIn = async () => {
console.log('LOGGING IN...');
await instance.loginRedirect(loginRequest);
};
const logOut = async () => {
console.log('LOGGING OUT...');
await instance.logoutRedirect();
};
return {
logIn,
logOut
};
}
index.js
import React from "react";
import ReactDOM from "react-dom";
import { PublicClientApplication, EventType } from "@azure/msal-browser";
import App from "./App.jsx";
import { msalConfig } from "./authConfig";
import "bootstrap/dist/css/bootstrap.min.css";
import "./styles/index.css";
export const msalInstance = new PublicClientApplication(msalConfig);
msalInstance.addEventCallback(event => {
if (event.eventType === EventType.LOGIN_SUCCESS) {
console.log(event);
msalInstance.setActiveAccount(event.payload.account);
}
});
ReactDOM.render(
<React.StrictMode>
<App msalInstance={msalInstance} />
</React.StrictMode>,
document.getElementById("root")
);
App.jsx - And here I'm wrapping my AuthProvider with MsalProvider.
import React, { useState, useEffect } from "react";
import { MsalProvider, AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "@azure/msal-react";
import { EventType, InteractionType } from "@azure/msal-browser";
import { msalConfig, b2cPolicies } from "./authConfig";
import { PageLayout, IdTokenClaims } from "./ui.jsx";
import Button from "react-bootstrap/Button";
import "./styles/App.css";
import AuthProvider from "./auth-provider";
const IdTokenContent = () => {
const { accounts } = useMsal();
const [idTokenClaims, setIdTokenClaims] = useState(null);
function GetIdTokenClaims() {
setIdTokenClaims(accounts[0].idTokenClaims)
}
return (
<>
<h5 className="card-title">Welcome {accounts[0].name}</h5>
{idTokenClaims ?
<IdTokenClaims idTokenClaims={idTokenClaims} />
:
<Button variant="secondary" onClick={GetIdTokenClaims}>View ID Token Claims</Button>
}
</>
);
};
const MainContent = () => {
const { instance } = useMsal();
useEffect(() => {
const callbackId = instance.addEventCallback((event) => {
if (event.eventType === EventType.LOGIN_FAILURE) {
if (event.error && event.error.errorMessage.indexOf("AADB2C90118") > -1) {
if (event.interactionType === InteractionType.Redirect) {
instance.loginRedirect(b2cPolicies.authorities.forgotPassword);
} else if (event.interactionType === InteractionType.Popup) {
instance.loginPopup(b2cPolicies.authorities.forgotPassword)
.catch(e => {
return;
});
}
}
}
if (event.eventType === EventType.LOGIN_SUCCESS) {
if (event?.payload) {
if (event.payload.idTokenClaims["acr"] === b2cPolicies.names.forgotPassword) {
window.alert("Password has been reset successfully. \nPlease sign-in with your new password");
return instance.logout();
}
}
}
});
return () => {
if (callbackId) {
instance.removeEventCallback(callbackId);
}
};
}, []);
return (
<div className="App">
<AuthenticatedTemplate>
<IdTokenContent />
</AuthenticatedTemplate>
<UnauthenticatedTemplate>
<h5 className="card-title">Please sign-in to see your profile information.</h5>
</UnauthenticatedTemplate>
</div>
);
};
export default function App({ msalInstance }) {
return (
<MsalProvider instance={msalInstance}>
<AuthProvider>
<PageLayout>
<MainContent />
</PageLayout>
</AuthProvider>
</MsalProvider>
);
}
ui.jsx - And here I'm using my auth provider and calling authProviderContext.logOut and authProviderContext.logIn.
import React from "react";
import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "@azure/msal-react";
import { Navbar, Button, Dropdown, DropdownButton } from "react-bootstrap";
import { loginRequest, b2cPolicies } from "./authConfig";
import { useContext, useState, useEffect } from 'react';
import { AuthProviderContext } from './auth-provider';
const NavigationBar = () => {
const { instance } = useMsal();
const authProviderContext = useContext(AuthProviderContext);
const handleLogin = () => {
instance.loginPopup(loginRequest)
.catch((error) => console.log(error))
}
return (
<>
<AuthenticatedTemplate>
<div className="ml-auto">
<Button variant="info" onClick={() => instance.loginPopup(b2cPolicies.authorities.editProfile)} className="ml-auto">Edit Profile</Button>
<DropdownButton variant="warning" className="ml-auto" drop="left" title="Sign Out">
<Dropdown.Item as="button" onClick={() => instance.logoutPopup({ postLogoutRedirectUri: "/", mainWindowRedirectUri: "/" })}>Sign out using Popup</Dropdown.Item>
<Dropdown.Item as="button" onClick={() => instance.logoutRedirect({ postLogoutRedirectUri: "/" })}>Sign out using Redirect</Dropdown.Item>
<Dropdown.Item as="button" onClick={authProviderContext.logOut}>LOG OUT</Dropdown.Item>
</DropdownButton>
</div>
</AuthenticatedTemplate>
<UnauthenticatedTemplate>
<DropdownButton variant="secondary" className="ml-auto" drop="left" title="Sign In">
<Dropdown.Item as="button" onClick={handleLogin}>Sign in using Popup</Dropdown.Item>
<Dropdown.Item as="button" onClick={() => instance.loginRedirect(loginRequest)}>Sign in using Redirect</Dropdown.Item>
<Dropdown.Item as="button" onClick={authProviderContext.logIn}>LOG IN</Dropdown.Item>
</DropdownButton>
</UnauthenticatedTemplate>
</>
);
};
export const PageLayout = (props) => {
const [username, setUsername] = useState('');
const { instance } = useMsal();
useEffect(() => {
const currentAccount = instance.getActiveAccount();
if (currentAccount) {
setUsername(currentAccount.username);
}
}, [instance]);
return (
<>
<Navbar bg="primary" variant="dark">
<a className="navbar-brand" href="/">Microsoft identity platform</a>
<NavigationBar />
</Navbar>
<br />
<h5><center>Welcome to the Microsoft Authentication Library For React Tutorial</center></h5>
<br />
{props.children}
<br />
<AuthenticatedTemplate>
<footer>
<center>How did we do?
<a href="https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUMlRHSkc5U1NLUkxFNEtVN0dEOTFNQkdTWiQlQCN0PWcu" target="_blank"> Share your experience!</a>
</center>
</footer>
<span>Username: {username}</span>
</AuthenticatedTemplate>
</>
);
};
export const IdTokenClaims = (props) => {
return (
<div id="token-div">
<p><strong>Audience: </strong> {props.idTokenClaims.aud}</p>
<p><strong>Issuer: </strong> {props.idTokenClaims.iss}</p>
<p><strong>OID: </strong> {props.idTokenClaims.oid}</p>
<p><strong>UPN: </strong> {props.idTokenClaims.preferred_username}</p>
</div>
);
}
I would then take my auth provider and replace theirs with mine.
Why would you like us to use an AuthProvider class in the samples, and what are its benefits?
I've 15+ years of experience in .NET/Web development as a full-stack developer but I've only built very little in React. So I can't really say what the actual benefit would be other than you would have all of your auth logic (login, logout, etc) in one place and then you can call them from anywhere in your app.
And also I think this is probably how you'd typically build a React app with auth by having a separate component handling all of that functionality.
And one last reason would be so that devs not so familiar with MSAL React and authentication in a real world app could benefit from seeing how it's done in the real world. I think having those types of samples would be invaluable.
You can drop the AuthProvider class and use msal-react out of the box, as shown in the samples, or you can create an AuthProvider class and replace it with the one they provided in the tutorial. Both approaches will work fine. Here are more resources that can help you with msal-react and b2c:
- Samples in the MSAL.js repo.
- MSAL-React documentation in the MSAL.js repo.
- Check our video tutorial on MSAL react.
- What is Azure Active Directory B2C.
Unfortunately, we don't have any implementation for msal-react with an AuthProvider class. I will speak to my team about your feedback and see what we can do with our samples in the future.
Thank you, I think I've got a good idea now on how to move forward.