I recently had the opportunity to build two real enterprise applications from scratch — one with React and one with Angular — both secured by Azure Entra ID (formerly Azure AD). Both projects shared the same challenge: multi-tenant authentication, role-based access control, and Microsoft Graph integration. In this article, I will share a hands-on comparison of the two approaches, based on what I actually shipped to production.
If you're an architect or lead dev about to choose a front-end stack for a corporate app under Azure Entra ID, this article is for you.
The battlefield: two real projects
Both projects are line-of-business applications with real users, real tenants, and real security requirements. Here's the common ground:
- Single-tenant Azure Entra ID app registration (with a specific tenant GUID)
- Redirect-based authentication flow (no popup)
- Token caching in
localStorage - Role-based access control at route and component level
- Multi-tenancy with a custom
X-Tenant-Idheader - Microsoft Graph API integration (user profiles, people pickers)
The React app uses @azure/msal-react (v1.5) with @azure/msal-browser (v2.38).
The Angular app uses @azure/msal-angular (v2.5) with @azure/msal-browser (v2.14).
Both rely on the same underlying MSAL.js v2 core under the hood.
1. MSAL configuration
In both frameworks, the first step is creating a PublicClientApplication instance. The configuration is virtually identical.
React — authProvider.ts
import { PublicClientApplication } from "@azure/msal-browser";
const envConfig = window.__env;
const config = {
auth: {
authority: `${envConfig.authority}${envConfig.tenantId}`,
clientId: envConfig.clientId,
redirectUri: envConfig.redirectUri,
postLogoutRedirectUri: envConfig.postLogoutRedirectUri,
validateAuthority: true,
navigateToLoginRequestUrl: true
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
export const authProvider = new PublicClientApplication(config);
In React, you typically create the MSAL instance in a dedicated file and export it as a singleton. This instance is then passed to the
<MsalProvider> component. Simple, explicit, manual.
Angular — app.module.ts (factory)
export function MSALInstanceFactory(
environmentService: EnvironmentService
): IPublicClientApplication {
return new PublicClientApplication({
auth: {
clientId: environmentService.clientId,
authority: `${environmentService.authority}${environmentService.tenantId}/`,
redirectUri: `${window.location.protocol}//${window.location.host}/`,
postLogoutRedirectUri: environmentService.post_logout_redirect_uri
},
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: isIE
}
});
}
In Angular, the instance is created via a factory function registered as a provider through the MSAL_INSTANCE injection token.
This plugs into Angular's dependency injection system — more ceremony, but also more testable and consistent with the framework's conventions.
Verdict: The configuration itself is identical (same object shape, same library). The difference is purely how the instance is wired into the framework. React is more explicit (manual export), Angular is more structured (DI factory).
2. Wrapping the app with authentication
Both frameworks need a "wrapper" that provides the MSAL context to the entire application. Here the approaches diverge significantly.
React — index.tsx
import { MsalProvider, MsalAuthenticationTemplate } from '@azure/msal-react';
import { InteractionType } from '@azure/msal-browser';
import { authProvider } from 'services/auth/authProvider';
root.render(
<Provider store={store}>
<MsalProvider instance={authProvider}>
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}>
<App />
</MsalAuthenticationTemplate>
</MsalProvider>
</Provider>
);
In React, you nest JSX components: <MsalProvider> provides the MSAL context, and <MsalAuthenticationTemplate>
blocks rendering until the user is authenticated. If they're not signed in, it triggers a redirect automatically.
This is the "component-as-infrastructure" pattern typical of React.
Angular — app.module.ts + app.component
// app.module.ts — providers
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true },
{ provide: MSAL_INSTANCE, useFactory: MSALInstanceFactory,
deps: [EnvironmentService] },
{ provide: MSAL_GUARD_CONFIG, useFactory: MSALGuardConfigFactory,
deps: [EnvironmentService] },
{ provide: MSAL_INTERCEPTOR_CONFIG, useFactory: MSALInterceptorConfigFactory,
deps: [EnvironmentService] },
MsalService,
MsalGuard,
MsalBroadcastService
],
bootstrap: [AppComponent, MsalRedirectComponent]
In Angular, the setup is spread across the module configuration. You register injection tokens, factories, services, and interceptors.
The MsalRedirectComponent is bootstrapped alongside the AppComponent to handle redirect callbacks.
It's more verbose, but the separation of concerns is arguably cleaner: configuration, interception, guards, and components each live in their own layer.
Verdict: React's declarative JSX wrapper is easier to read at first glance. Angular's module-based approach is more structured and scales better in large teams where responsibilities are split between "infrastructure" and "feature" developers.
3. Attaching tokens to API calls
This is where the biggest architectural difference appears: how bearer tokens are attached to HTTP requests.
React — manual token acquisition
// api-helper.ts
import { authProvider } from "./auth/authProvider";
export default class ApiHelper {
getIdToken = async () => {
const accounts = authProvider.getAllAccounts();
if (accounts.length > 0) {
const request = { scopes: configScopes, account: accounts[0] };
const idToken = await authProvider.acquireTokenSilent(request)
.then((response) => response.idToken)
.catch(error => { console.log(error); return null; });
return idToken;
}
};
init = async () => {
this.api_token = await this.getIdToken();
this.headers = {
Accept: "application/json",
"Content-Type": "application/json",
...(tenantId ? { "X-Tenant-Id": tenantId } : {})
};
if (this.api_token) {
this.headers.Authorization = `Bearer ${this.api_token}`;
}
};
// Every HTTP method calls init() before each request
}
In React, there is no built-in interceptor. You must manually call acquireTokenSilent() before each request and attach the
Authorization header yourself. This gives you full control — but it also means you need to remember to do it everywhere.
In my project, I centralized this in an ApiHelper class, but the responsibility is entirely yours.
Angular — MsalInterceptor with protectedResourceMap
export function MSALInterceptorConfigFactory(
environmentService: EnvironmentService
): MsalInterceptorConfiguration {
const protectedResourceMap = new Map<string, Array<string>>();
protectedResourceMap.set(
'https://graph.microsoft.com/v1.0/me', ['user.read']
);
protectedResourceMap.set(
`${environmentService.apiUrl}/`,
[`api://${environmentService.clientId}/user_impersonation`]
);
return {
interactionType: InteractionType.Redirect,
protectedResourceMap
};
}
In Angular, you configure a protectedResourceMap that maps URL patterns to the scopes they need.
The MsalInterceptor — an Angular HttpInterceptor — automatically matches every outgoing HttpClient request
against this map and calls acquireTokenSilent() behind the scenes. If the silent acquisition fails, it falls back to a redirect.
You never write a single line of token acquisition code in your services.
Verdict: This is Angular's biggest win. The MsalInterceptor + protectedResourceMap pattern is incredibly elegant.
You declare which endpoints need which scopes, and the framework handles everything. In React, you need to build this plumbing yourself.
For a large enterprise app with dozens of API endpoints, the Angular approach saves a significant amount of code and eliminates an entire category of bugs.
4. Route protection
Both frameworks support guarding routes to prevent unauthenticated or unauthorized access — but the mechanisms differ.
React — custom RouteGuard component
const RouteGuard = ({ component, path, grantedRoles, roles }) => {
const isRoleGranted = !grantedRoles
? true
: Habilitations.Utils.hasRoles(roles, grantedRoles);
useEffect(() => {
if (roles.length > 0 && isRoleGranted === false) {
dispatch(notifyMessage({
message: "Access denied", type: "warning"
}));
setTimeout(() => { history.push("/main/home"); }, 500);
}
}, [isRoleGranted, roles]);
return isRoleGranted
? <Route path={path} component={component} />
: null;
};
In React, there is no built-in route guard concept. You write a custom wrapper component that checks roles and conditionally renders the route or redirects the user. It works, but it's entirely custom code that you must design, test, and maintain.
Angular — MsalGuard + custom AuthGuard
// app-routing.module.ts
const routes: Routes = [
{
path: "sales",
loadChildren: () => import("./views/sales/sales.module")
.then(m => m.SalesModule),
canActivate: [MsalGuard, AuthGuard],
data: { roles: ["Sales.Viewer", "Sales.Writer", "Sales.Manager"] }
}
];
// auth.guard.ts
export class AuthGuard {
async canActivate(route: ActivatedRouteSnapshot) {
await this.rolesService
.EnsureUserRolesAreLoadedAsync(
this.authService.instance.getAllAccounts()[0].localAccountId
);
if (this.rolesService.hasRole(['Admin.All'])) return true;
return this.rolesService.hasRole(route.data.roles);
}
}
Angular has a first-class route guard system. MsalGuard handles authentication (redirecting to login if needed),
and you can chain additional guards like a custom AuthGuard for role-based authorization.
The roles are declared as route data — clean, declarative, and easy to audit.
Verdict: Angular's guard system is more mature and standardized. In React, you get flexibility but must build the pattern from scratch. For enterprise apps with complex authorization matrices, Angular's approach is less error-prone.
5. Component-level access control
Beyond routes, both apps need to show or hide UI elements based on the user's role.
React — wrapper components
// WithUserRoles.tsx
const WithUserRoles = ({ children, grantedRoles }) => {
const roles = useContext(ContextUserRole);
return Habilitations.Utils.hasRoles(roles, grantedRoles)
? <>{children}</>
: null;
};
// Usage
<WithUserRoles grantedRoles={["Sales.Writer"]}>
<button>Create transaction</button>
</WithUserRoles>
Angular — structural directive
// access-control.directive.ts
@Directive({ selector: "[accessControl]" })
export class AccessControlDirective implements OnInit {
@Input("roles") roles: Array<string>;
async ngOnInit() {
this.elementRef.nativeElement.style.display = "none";
await this.rolesService.EnsureUserRolesAreLoadedAsync(/*...*/);
this.elementRef.nativeElement.style.display =
this.rolesService.hasRole(this.roles)
? this.previousDisplay : "none";
}
}
// Usage
<button accessControl [roles]="['Sales.Writer']">
Create transaction
</button>
Both approaches work, but Angular's structural directive is more idiomatic. You just slap an attribute on any element. React's wrapper component approach works fine too — it's just a different mental model (composition vs. decoration).
6. Bonus: Microsoft Graph Toolkit (MGT)
The @microsoft/mgt library is a real game-changer for enterprise apps.
It provides ready-to-use web components that connect directly to Microsoft Graph — user profiles, people pickers, agenda views, and more.
I used it in both projects, and the experience was excellent in both.
React — MGT with SimpleProvider
import { Providers, SimpleProvider, ProviderState }
from '@microsoft/mgt-element';
import { authProvider } from 'services/auth/authProvider';
// Bridge MSAL tokens to MGT
Providers.globalProvider = new SimpleProvider(
async (scopes) => {
const accounts = authProvider.getAllAccounts();
const request = { scopes, account: accounts[0] };
const response = await authProvider
.acquireTokenSilent(request);
return response.accessToken;
}
);
Providers.globalProvider.setState(ProviderState.SignedIn);
In React, MGT needs a SimpleProvider bridge that delegates token acquisition to your existing MSAL instance.
Once set up, you can use @microsoft/mgt-react components anywhere:
import { Person, PeoplePicker } from "@microsoft/mgt-react";
import { ViewType } from "@microsoft/mgt-element";
// Show current user's avatar and name
<Person personQuery="me" view={ViewType.oneline} />
// Azure AD people picker for user search
<PeoplePicker selectionMode="single"
selectionChanged={handleSelection} />
Angular — MGT with Msal2Provider
import { Providers } from '@microsoft/mgt-element';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider';
import { TemplateHelper } from '@microsoft/mgt-element';
// In AppComponent constructor or ngOnInit
Providers.globalProvider = new Msal2Provider({
clientId: this.environmentService.clientId,
authority: `${this.environmentService.authority}` +
`${this.environmentService.tenantId}`
});
// Avoid conflict with Angular's {{ }} syntax
TemplateHelper.setBindingSyntax('[[', ']]');
In Angular, MGT provides a dedicated Msal2Provider that handles everything internally — no manual token bridging needed.
However, since Angular uses {{ }} for its own template binding, you need to change MGT's binding syntax to [[ ]].
You also need to declare CUSTOM_ELEMENTS_SCHEMA in your module since MGT components are web components:
// app.module.ts
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
// ...
})
Then in your templates:
<mgt-person person-query="me" view="twolines">
<template>
<div data-if="personImage">
<img class="avatar" src="[[person.image]]" />
</div>
<h4 class="name">[[person.displayName]]</h4>
</template>
</mgt-person>
Some component examples MGT gives you for free:
<mgt-person>/<Person>— Displays a user's name, email, and photo from Azure AD<mgt-people-picker>/<PeoplePicker>— A search-as-you-type component to find users in your organization's directory<mgt-login>/<Login>— A sign-in button with user flyout<mgt-agenda>— Displays the user's upcoming calendar events<mgt-tasks>— Displays and manages the user's Planner tasks
Building any of these from scratch would take days. MGT gives them to you in a single HTML tag.
I found the <PeoplePicker> especially useful — in both apps, it powers user search for onboarding and role assignment workflows.
Head-to-head comparison
Here's a summary table of the key differences I experienced:
| Feature | React (MSAL React) | Angular (MSAL Angular) |
|---|---|---|
| MSAL Configuration | Manual singleton export | Factory + DI token |
| Auth wrapper | JSX components (<MsalProvider>) |
Module providers + MsalRedirectComponent bootstrap |
| Token for API calls | Manual acquireTokenSilent() |
Automatic via MsalInterceptor |
| Route protection | Custom component | Built-in MsalGuard + canActivate |
| Component access control | Wrapper component | Structural directive |
| MGT integration | SimpleProvider bridge (manual) |
Msal2Provider (turnkey) |
| Boilerplate | Less initial setup, more per-feature | More initial setup, less per-feature |
My honest take
After building and shipping both, here's my honest assessment:
Angular wins on enterprise authentication. The MsalInterceptor alone is worth the price of admission.
Declaring a protectedResourceMap once and never thinking about tokens again is a massive productivity gain.
The built-in guard system, the dependency injection, the interceptor chain — everything is designed for exactly this kind of enterprise use case.
React wins on flexibility and simplicity. If your auth needs are simple (one API, one scope), the manual approach is perfectly fine.
React's hook-based API (useMsal, useIsAuthenticated, useMsalAuthentication) is pleasant to work with.
And the ecosystem gives you more choices in how you structure things.
For a complex enterprise application under Azure Entra ID — with multiple protected APIs, role-based access, multi-tenancy, and Graph integration — I would lean towards Angular. Not because React can't do it (it obviously can), but because Angular's opinionated structure reduces the surface area for mistakes. When you have 20+ route guards, 5 different API scopes, and a team of 10 developers, conventions beat flexibility.
That said, whichever framework you choose, invest in Microsoft Graph Toolkit (@microsoft/mgt). It works beautifully in both,
and it will save you weeks of development on user-facing components.
Useful links
- @azure/msal-react on npm
- @azure/msal-angular on npm
- @microsoft/mgt on npm
- Microsoft Tutorial: React SPA with MSAL
- Microsoft Tutorial: Angular SPA with MSAL
- Microsoft Graph Toolkit documentation
Feel free to share your own experience in the comments — I'd love to hear which stack you picked and why!