Authenticate SPFx Solutions with MSAL2: Part 2 (Implementation)
Best Practices SharePoint Online SPFx Development Microsoft 365 Security Software EngineeringPart I of this series covers the limitations of SharePoint Framework’s built-in authentication methods and presents a more secure alternative that uses the Microsoft Authentication Library (MSAL) directly in SPFx solutions. Now, let’s dive into the implementation details of this approach.
Implementing MSAL2 Authentication in SPFx
Let’s now finally focus on the main application that contains the coding and the MSAL implementation. I’ve decided to build the whole implementation in TypeScript (within a React app for SPFx) by using @azure/msal-browser
.
Note
Please note that I’ve chosen the implementation with@azure/msal-browser
instead of the modularised version of @azure/msal-react
for educational purposes 😃.
Let’s assume we will build this dependency of modules or classes (as you wish to say 😃) with these dependencies between:
Note
This Mermaid diagram was created by usingClaude Sonnet 3.7 from my real implementation – fantastic, isn’t it?
The core of our implementation is the AuthenticationContext
. By implementing the AuthenticationContextProps
interface, this component manages the authentication state (isAuthenticated
, userScopes
and accessToken
) as well as a function for reauthentication and makes these guys available throughout the application:
import { Configuration, PublicClientApplication } from "@azure/msal-browser";
import * as React from "react";
interface AuthenticationContextProps {
isAuthenticated: boolean;
userScopes?: string;
accessToken?: string;
reauthenticate: () => void;
}
export const AuthenticationContext = React.createContext<AuthenticationContextProps>({
isAuthenticated: false,
reauthenticate: () => { },
});
As already mentioned, AuthenticationContext
provides functionality for the authentication status for components that rely on authentication information whereas the AuthenticationContextProvider
component initializes MSAL and handles even the token acquisition:
export const AuthenticationContextProvider = (props: AuthenticationContextProviderProps): JSX.Element => {
// State definitions
const [isAuthenticated, setIsAuthenticated] = React.useState<boolean>(false);
const [msalObj, setMsalInstance] = React.useState<PublicClientApplication | undefined>(undefined);
const [userScopes, setUserScopes] = React.useState<string>('');
const [accessToken, setAccessToken] = React.useState<string>('');
// MSAL configuration
const config: Configuration = {
auth: {
clientId: props.clientId,
authority: `https://login.microsoftonline.com/${props.tenantId}`,
redirectUri: props.redirectUri ?? window.location.origin,
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: false,
},
};
// ... implementation of authentication methods ...
}
To make this work, make sure you pass these properties (and just omit the optionals – at least scopes
!) to the AuthenticationContextProvider
:
interface AuthenticationContextProviderProps extends React.PropsWithChildren<{}> {
clientId: string; // The client id of your app registration in Microsoft Entra ID
tenantId: string; // Your tenant id (that holds the app registration)
scopes?: string; // Optional – never set this; The scopes that can be consented to the calling user (will overwrite the permissions that are set and consented by an admin in case the calling user has admin rights!)
redirectUri?: string; // Optional; the redirect uri
}
Handle the (Silent) Token Acquisition
The implementation attempts to acquire a token silently (by calling the login()
function the first time), which ensures a smooth user experience:
async function login(): Promise<void> {
try {
if (msalObj) {
const result = await msalObj.acquireTokenSilent({
account: msalObj.getAllAccounts()[0],
scopes: props.scopes ? [...props.scopes.split(',')] : [],
});
console.log('Silent token result:', result);
if (msalObj && result.accessToken) {
const accounts = msalObj.getAllAccounts();
setIsAuthenticated(accounts.length > 0);
setUserScopes(result.scopes.join(', '));
setAccessToken(result.accessToken);
}
}
} catch (error) {
console.error("Error acquiring token silently:", error);
}
}
Integrating with SPFx WebPart
The webpart initializes with these parameters that are set in the webpart property pane. They provide abiltiy to connect to our dedicated Entra ID application by providing…
- the according
applicationID
(which is the application / client ID of the app registration), - the
redirectUri
(which is the URL where the user is redirected after the authentication) - as well as the
tenantIdentifier
(which is the tenant ID of the app registration).
as defined in the ISpFxMsalAuthDemoWebPartProps
interface:
export interface ISpFxMsalAuthDemoWebPartProps {
applicationID: string;
redirectUri: string;
tenantIdentifier: string;
scopes: string;
apiCall: string;
}
The scopes
(which are the permissions that are requested by the app registration), and the apiCall
(which is the URL of the API that is called with the acquired token) are also set in the property pane, but they are not necessary for the authentication process (see note above in the AuthenticationContextProviderProps
interface).
The mentioned properties are then passed to our React component SpFxMsalAuthDemo
:
public render(): void {
const element: React.ReactElement<ISpFxMsalAuthDemoProps> = React.createElement(
SpFxMsalAuthDemo,
{
applicationID: this.properties.applicationID,
redirectUri: this.properties.redirectUri,
tenantIdentifier: this.properties.tenantIdentifier,
scopes: this.properties.scopes,
apiCall: this.properties.apiCall,
httpClient: this.context.httpClient,
userMail: this.context.pageContext.user.email,
}
);
ReactDom.render(element, this.domElement);
}
All necessary modules are imported in the SpFxMsalAuthDemo
component, which acts as the main component of our application. It wraps the AuthenticationContextProvider
which initializes MSAL and provides the authentication context to its children. In this case, the children are the GraphDataDisplay
component, which is responsible for displaying the data retrieved from the Microsoft Graph API:
export const SpFxMsalAuthDemo: React.FC<ISpFxMsalAuthDemoProps> = (props) => {
const { applicationID, tenantIdentifier, scopes, redirectUri } = props;
return (
<AuthenticationContextProvider clientId={applicationID} tenantId={tenantIdentifier} scopes={scopes} redirectUri={redirectUri}>
<GraphDataDisplay httpClient={props.httpClient}
applicationID={applicationID} tenantIdentifier={tenantIdentifier} redirectUri={redirectUri} apiCall={props.apiCall} />
</AuthenticationContextProvider>
)
}
Making Authenticated API Calls (Graph API)
With our AuthenticationContext
in place, descendant components can now consume the authentication state and access token to make authenticated API calls:
export const GraphDataDisplay: React.FC<GraphDataDisplayProps> = (props) => {
const { isAuthenticated, userScopes, accessToken, reauthenticate } = React.useContext(AuthenticationContext);
const { applicationID, tenantIdentifier, redirectUri, httpClient, apiCall } = props;
async function loadUserInfoFromGraph(): Promise<void> {
console.log(`calling graph with access token: ${accessToken?.substring(0, 100)}...`);
const response = await httpClient.get(apiCall, HttpClient.configurations.v1, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
// handle process response (intentionally omitted) ...
}
// handle component rendering (intentionally omitted)...
}
Deep Dive: Understanding the MSAL Authentication Flow
Let’s examine the key parts of the authentication flow in our AuthenticationContextProvider
component:
-
Initialization: The
PublicClientApplication
from MSAL is initialized with our app’s configuration:const msalObj = new PublicClientApplication(config); await msalObj.initialize();
-
Silent Token Acquisition: We first try to get a token silently, which works fine if the user has previously authenticated and an authenticated session is established (this is the case for most use cases in SPFx – otherwise call
acquireTokenPopup()
to trigger an interactive login, even as a fallback for silent token acquisition):const result = await msalObj.acquireTokenSilent({ account: msalObj.getAllAccounts()[0], scopes: props.scopes ? [...props.scopes.split(',')] : [], });
-
Token Storage: MSAL handles token caching in the browser’s
localStorage
:cache: { cacheLocation: "localStorage", storeAuthStateInCookie: false, },
-
Token Usage: The acquired token can now be used in API calls; just (get the token from the component’s state and) add it to the request headers:
const response = await httpClient.get(apiCall, HttpClient.configurations.v1, { headers: { 'Authorization': `Bearer ${accessToken}` } });
Conclusion
Implementing custom authentication in SPFx using MSAL provides several advantages:
- Granular Control: Define exactly which permissions your application needs
- Least Privilege: Adhere to best security practices by requesting only the necessary permissions
- Flexibility: Easily adapt the authentication flow to specific requirements
- Separation of Concerns: Decouple authentication from the SharePoint context
While this approach requires more initial setup than using the built-in SPFx authentication mechanisms, it provides greater control and security for enterprise applications. This pattern is particularly valuable when you’re building solutions that need to access sensitive data or when you want to ensure your application follows the principle of least privilege.
By managing authentication through a custom Entra ID app registration and MSAL, you gain the ability to precisely control which resources your SPFx solution can access, making your applications more secure and compliant with modern security practices.