Azure 31: Entra ID & MSAL.js — SPA Authentication
easy⏱ 5 mincourseazure
Entra ID in 60 seconds
An Entra ID tenant is your org's identity directory. Inside it you create an App Registration for each app — it gets a client ID, an authority URL (https://login.microsoftonline.com/{tenantId}), and a set of exposed scopes (e.g. api://my-api/read). Users sign in, consent to scopes, and the app receives an access token (JWT) to call APIs.
// App Registration essentials
// Application (client) ID: aaaa1111-....
// Directory (tenant) ID: bbbb2222-....
// Redirect URI (SPA): https://app.contoso.com/auth
// Exposed API scope: api://orders-api/orders.read
OAuth 2.0 Authorization Code + PKCE
SPAs can't keep a client secret, so they use Authorization Code flow with PKCE. The app generates a random code_verifier, hashes it into a code_challenge, sends the user to Entra ID, and exchanges the returned code for tokens — proving possession of the verifier. MSAL.js handles all of this; you just call loginRedirect or loginPopup, then acquireTokenSilent for subsequent API calls.
import { PublicClientApplication } from '@azure/msal-browser';
const msal = new PublicClientApplication({
auth: {
clientId: 'aaaa1111-....',
authority: 'https://login.microsoftonline.com/bbbb2222-....',
redirectUri: window.location.origin,
},
});
await msal.loginRedirect({ scopes: ['api://orders-api/orders.read'] });
React integration with msal-react
@azure/msal-react wraps your app in an <MsalProvider>. Hooks like useMsal(), useIsAuthenticated(), and useMsalAuthentication() give components live auth state. Wrap protected UI in <AuthenticatedTemplate> and <UnauthenticatedTemplate> to switch rendering without writing if chains.
<MsalProvider instance={msal}>
<AuthenticatedTemplate>
<Dashboard />
</AuthenticatedTemplate>
<UnauthenticatedTemplate>
<SignInButton />
</UnauthenticatedTemplate>
</MsalProvider>
Build an MSAL client simulator
Create a MsalClient that tracks an in-memory token cache { scope -> { token, expiresAt } }. Implement login(user), acquireTokenSilent(scope) that returns the cached token if not expired, and acquireTokenInteractive(scope) that is called when silent fails. Log every cache hit, miss, and interactive fallback.
Prefer acquireTokenSilent — fall back to interactive
In production code, always call acquireTokenSilent first. It returns a cached token without a network round trip. Only on InteractionRequiredAuthError should you fall back to acquireTokenRedirect or acquireTokenPopup. This pattern keeps the UX smooth — users stay signed in across tabs and reloads.
Quiz: Delegated vs Application permissions
Your API is called by a user-facing SPA. Which permission type do you expose? Delegated — the token represents a user acting through the app. Application permissions are for daemons/services with no user context (client credentials flow), not SPAs.