- MSAL Authentication in React: Complete Guide – Part 1: Introduction and Setup
- MSAL Authentication in React: Complete Guide – Part 2: MSAL Provider and Authentication Context
- MSAL Authentication in React: Complete Guide – Part 3: Protected Routes and Route Guards
- MSAL Authentication in React: Complete Guide – Part 4: Token Management and API Calls
- MSAL Authentication in React: Complete Guide – Part 5: Advanced Topics and Production Considerations
Welcome to Part 4 of our MSAL authentication series! Now that we have protected routes in place, let’s dive into token management and making authenticated API calls to secure endpoints.
Understanding MSAL Tokens
MSAL handles three types of tokens in your application:
- ID Token: Contains user identity information
- Access Token: Used for calling protected APIs
- Refresh Token: Used to acquire new access tokens
Acquiring Access Tokens
Let’s create a custom hook for token acquisition:
// src/hooks/useAccessToken.js
import { useState, useCallback } from 'react';
import { useMsal } from '@azure/msal-react';
import { InteractionRequiredAuthError } from '@azure/msal-browser';
export const useAccessToken = () => {
const { instance, accounts } = useMsal();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const getAccessToken = useCallback(async (scopes = ['User.Read']) => {
if (accounts.length === 0) {
throw new Error('No accounts found');
}
setIsLoading(true);
setError(null);
const request = {
scopes,
account: accounts[0],
};
try {
// Try to acquire token silently
const response = await instance.acquireTokenSilent(request);
setIsLoading(false);
return response.accessToken;
} catch (err) {
if (err instanceof InteractionRequiredAuthError) {
// If silent acquisition fails, fall back to interactive
try {
const response = await instance.acquireTokenRedirect(request);
setIsLoading(false);
return response?.accessToken;
} catch (interactiveError) {
setError(interactiveError);
setIsLoading(false);
throw interactiveError;
}
} else {
setError(err);
setIsLoading(false);
throw err;
}
}
}, [instance, accounts]);
return { getAccessToken, isLoading, error };
};
Making Authenticated API Calls
Create a reusable hook for making authenticated API calls:
// src/hooks/useApi.js
import { useState, useCallback } from 'react';
import { useMsal } from '@azure/msal-react';
import { InteractionRequiredAuthError } from '@azure/msal-browser';
export const useApi = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const { instance, accounts } = useMsal();
const callApi = useCallback(async (url, scopes = ['User.Read'], options = {}) => {
if (accounts.length === 0) {
throw new Error('No authenticated accounts');
}
setLoading(true);
setError(null);
try {
// Get access token
const tokenRequest = {
scopes,
account: accounts[0],
};
const response = await instance.acquireTokenSilent(tokenRequest);
const accessToken = response.accessToken;
// Make API call
const apiResponse = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!apiResponse.ok) {
throw new Error(`API call failed: ${apiResponse.status}`);
}
const result = await apiResponse.json();
setLoading(false);
return result;
} catch (err) {
if (err instanceof InteractionRequiredAuthError) {
// Redirect to acquire token interactively
instance.acquireTokenRedirect({
scopes,
account: accounts[0],
});
} else {
setError(err);
setLoading(false);
throw err;
}
}
}, [instance, accounts]);
return { callApi, loading, error };
};
Microsoft Graph Integration
Let’s create a component that fetches user data from Microsoft Graph:
// src/components/UserProfile.js
import React, { useState, useEffect } from 'react';
import { useApi } from '../hooks/useApi';
const UserProfile = () => {
const [userData, setUserData] = useState(null);
const { callApi, loading, error } = useApi();
useEffect(() => {
const fetchUserData = async () => {
try {
const data = await callApi('https://graph.microsoft.com/v1.0/me', ['User.Read']);
setUserData(data);
} catch (err) {
console.error('Failed to fetch user data:', err);
}
};
fetchUserData();
}, [callApi]);
if (loading) return <div>Loading user profile...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!userData) return <div>No user data available</div>;
return (
<div className="user-profile">
<h2>User Profile</h2>
<div className="profile-details">
<p><strong>Name:</strong> {userData.displayName}</p>
<p><strong>Email:</strong> {userData.mail || userData.userPrincipalName}</p>
<p><strong>Job Title:</strong> {userData.jobTitle || 'Not specified'}</p>
<p><strong>Department:</strong> {userData.department || 'Not specified'}</p>
</div>
</div>
);
};
export default UserProfile;
Token Caching Best Practices
- Use minimal scopes: Request only the permissions you need
- Handle token expiration: Implement proper retry logic
- Cache tokens securely: Use sessionStorage over localStorage
- Monitor token refresh: Log token acquisition events
What’s Next?
In Part 5, our final installment, we’ll cover advanced topics and production considerations including security best practices, performance optimization, and troubleshooting common issues.