| name | astroapps-client-msal |
| description | Microsoft Authentication Library (MSAL) integration for @astroapps/client with Azure AD/Entra ID authentication. Use when building React apps that need Azure AD authentication with popup or redirect flows. |
@astroapps/client-msal - Microsoft Authentication Library Integration
Overview
@astroapps/client-msal provides Microsoft Authentication Library (MSAL) integration for @astroapps/client. It implements the SecurityService interface using Azure AD/Entra ID authentication with support for popup and redirect flows.
When to use: Use this library when building React applications that need to authenticate users against Microsoft Azure AD (Entra ID) and use Microsoft identity platform.
Package: @astroapps/client-msal
Dependencies: @astroapps/client, @react-typed-forms/core, @azure/msal-browser, @azure/msal-react, React 18+
Published to: npm
Key Concepts
1. useMsalSecurityService Hook
Creates a SecurityService implementation using MSAL for authentication. Handles token acquisition, user login/logout, and maintains authenticated state.
2. wrapWithMsalContext Helper
Wraps a React component with the MSAL MsalProvider context, enabling MSAL hooks throughout the component tree.
3. Authentication Flows
Supports three authentication flows:
- Silent: Acquire tokens silently without user interaction (for refresh)
- Popup: Show login popup window (better UX, less disruptive)
- Redirect: Full page redirect to Microsoft login (more compatible)
4. Token Management
Automatically handles:
- Access token acquisition via
acquireTokenSilent - Token refresh when expired
- Active account management
- Session persistence
Common Patterns
Basic Setup with MSAL Configuration
import { PublicClientApplication } from "@azure/msal-browser";
import { wrapWithMsalContext, useMsalSecurityService } from "@astroapps/client-msal";
import { AppContextProvider } from "@astroapps/client";
// 1. Configure MSAL instance
const msalConfig = {
auth: {
clientId: "your-client-id", // Azure AD app registration client ID
authority: "https://login.microsoftonline.com/your-tenant-id",
redirectUri: window.location.origin, // Or specific redirect URL
},
cache: {
cacheLocation: "sessionStorage", // or "localStorage"
storeAuthStateInCookie: false,
},
};
const msalInstance = new PublicClientApplication(msalConfig);
// 2. Create root layout component
function RootLayout({ children }: { children: React.ReactNode }) {
const security = useMsalSecurityService();
const navigation = useNavigationService(); // Your navigation service
return (
<AppContextProvider value={{ security, navigation }}>
{children}
</AppContextProvider>
);
}
// 3. Wrap with MSAL context
export default wrapWithMsalContext(RootLayout, msalInstance);
Using Custom User Data
import { useMsalSecurityService } from "@astroapps/client-msal";
function App() {
const security = useMsalSecurityService({
// Fetch additional user data after authentication
getUserData: async (fetch) => {
// Use authenticated fetch to get user roles/profile
const response = await fetch("/api/user/profile");
const profile = await response.json();
return {
name: profile.displayName,
email: profile.email,
roles: profile.roles || [],
};
},
});
return (
<AppContextProvider value={{ security }}>
<YourApp />
</AppContextProvider>
);
}
Popup Authentication Flow
import { useMsalSecurityService } from "@astroapps/client-msal";
function App() {
const security = useMsalSecurityService({
// Use popup instead of redirect
popupRequest: {
scopes: ["User.Read", "openid", "profile"],
prompt: "select_account",
},
getUserData: async (fetch) => {
const roles: string[] = [];
// Fetch roles from your API
return { roles };
},
});
return <AppContextProvider value={{ security }}>...</AppContextProvider>;
}
Redirect Authentication Flow
import { useMsalSecurityService } from "@astroapps/client-msal";
function App() {
const security = useMsalSecurityService({
// Use redirect flow (default if popupRequest not provided)
redirectRequest: {
scopes: ["User.Read", "openid", "profile"],
prompt: "select_account",
},
// Redirect to specific page after login
defaultAfterLogin: "/dashboard",
});
return <AppContextProvider value={{ security }}>...</AppContextProvider>;
}
Custom Token Scopes
import { useMsalSecurityService } from "@astroapps/client-msal";
function App() {
const security = useMsalSecurityService({
// Custom scopes for silent token acquisition
silentRequest: {
scopes: [
"api://your-api-id/user.read",
"api://your-api-id/data.write",
],
},
// Custom scopes for popup login
popupRequest: {
scopes: [
"User.Read",
"api://your-api-id/user.read",
"api://your-api-id/data.write",
],
},
});
return <AppContextProvider value={{ security }}>...</AppContextProvider>;
}
Login Component
import { useSecurityService } from "@astroapps/client";
function LoginPage() {
const security = useSecurityService();
const user = security.currentUser.value;
if (user.busy) {
return <div>Checking authentication...</div>;
}
if (user.loggedIn) {
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Email: {user.email}</p>
<p>Roles: {user.roles?.join(", ")}</p>
<button onClick={() => security.logout()}>Logout</button>
</div>
);
}
return (
<div>
<h1>Please Sign In</h1>
<button onClick={() => security.login()}>
Sign in with Microsoft
</button>
</div>
);
}
Protected Route
import { useSecurityService } from "@astroapps/client";
import { useEffect } from "react";
import { useNavigationService } from "@astroapps/client";
function ProtectedPage() {
const security = useSecurityService();
const navigation = useNavigationService();
const user = security.currentUser.value;
useEffect(() => {
if (!user.busy && !user.loggedIn) {
// Save current URL for redirect after login
security.currentUser.fields.afterLoginHref.value = navigation.pathname;
security.login();
}
}, [user.busy, user.loggedIn]);
if (user.busy) {
return <div>Loading...</div>;
}
if (!user.loggedIn) {
return <div>Redirecting to login...</div>;
}
return (
<div>
<h1>Protected Content</h1>
<p>Only authenticated users can see this.</p>
</div>
);
}
Role-Based Access Control
import { useSecurityService } from "@astroapps/client";
function AdminPanel() {
const security = useSecurityService();
const user = security.currentUser.value;
const hasRole = (role: string) => user.roles?.includes(role) ?? false;
if (!user.loggedIn) {
return <div>Please log in</div>;
}
if (!hasRole("Admin")) {
return <div>Access denied. Admin role required.</div>;
}
return (
<div>
<h1>Admin Panel</h1>
{hasRole("SuperAdmin") && (
<button>Super Admin Actions</button>
)}
{hasRole("Moderator") && (
<button>Moderator Actions</button>
)}
</div>
);
}
Authenticated API Calls
import { useSecurityService } from "@astroapps/client";
import { useState } from "react";
function UserProfile() {
const security = useSecurityService();
const [profile, setProfile] = useState<any>(null);
const loadProfile = async () => {
// security.fetch automatically includes Bearer token
const response = await security.fetch("/api/user/profile");
const data = await response.json();
setProfile(data);
};
return (
<div>
<button onClick={loadProfile}>Load Profile</button>
{profile && (
<pre>{JSON.stringify(profile, null, 2)}</pre>
)}
</div>
);
}
Best Practices
1. Initialize MSAL Instance Once
// ✅ DO - Create msalInstance at module level, not in component
const msalInstance = new PublicClientApplication(msalConfig);
export default wrapWithMsalContext(RootLayout, msalInstance);
// ❌ DON'T - Create new instance on each render
function App() {
const msalInstance = new PublicClientApplication(msalConfig); // Re-creates on each render!
// ...
}
2. Use Popup for Better UX
// ✅ DO - Use popup for smoother authentication flow
popupRequest: {
scopes: ["User.Read"],
}
// ⚠️ CAUTION - Redirect interrupts user flow
// Only use redirect if popups are blocked or for compatibility
redirectRequest: {
scopes: ["User.Read"],
}
3. Handle Authentication State Properly
// ✅ DO - Check both busy and loggedIn states
if (user.busy) {
return <LoadingSpinner />;
}
if (!user.loggedIn) {
return <LoginPrompt />;
}
return <AuthenticatedContent />;
// ❌ DON'T - Only check loggedIn
if (!user.loggedIn) {
// Might show login prompt even during authentication check
}
4. Use Minimal Scopes
// ✅ DO - Request only needed scopes
silentRequest: {
scopes: ["User.Read", "api://your-api/.default"],
}
// ❌ DON'T - Request unnecessary scopes
silentRequest: {
scopes: [
"User.Read",
"Mail.Read",
"Calendars.Read",
"Files.ReadWrite.All", // Too many permissions
],
}
5. Store Tokens Securely
// ✅ DO - Use sessionStorage for sensitive apps
cache: {
cacheLocation: "sessionStorage", // Cleared when browser closes
}
// ⚠️ CAUTION - localStorage persists across sessions
cache: {
cacheLocation: "localStorage", // Persistent, less secure
}
Troubleshooting
Common Issues
Issue: "InteractionRequiredAuthError" during silent token acquisition
- Cause: User needs to re-authenticate (consent required, password changed, etc.)
- Solution: Catch the error and call
security.login()to prompt for authentication
Issue: "BrowserAuthError: interaction_in_progress"
- Cause: Multiple simultaneous authentication requests
- Solution: Check
user.busystate before callinglogin()or use MSAL'sinProgresscheck
Issue: Login popup blocked by browser
- Cause: Browser popup blocker preventing login window
- Solution: Use redirect flow instead of popup, or ensure login is triggered by user action
Issue: Tokens not included in API requests
- Cause: Not using
security.fetch()for authenticated requests - Solution: Always use
security.fetch()instead of nativefetch()for authenticated API calls
Issue: "User is not logged in" after successful authentication
- Cause: Active account not set after redirect
- Solution: Ensure MSAL redirect handling is properly configured and
handleRedirectPromiseis called
Issue: Infinite redirect loop
- Cause: Protected page redirecting to itself after authentication
- Solution: Set
afterLoginHrefbefore callinglogin()to redirect to correct page
Issue: Roles not populated in user state
- Cause:
getUserDatanot implemented or not fetching roles - Solution: Implement
getUserDatacallback to fetch user roles from your API
Issue: "AADSTS50058: silent sign-in request sent but no user is signed in"
- Cause: No active account when acquiring token silently
- Solution: Ensure user is logged in before calling
acquireTokenSilent, or handle the error and prompt for login
Issue: Token expires and requests fail
- Cause: Access token expired and silent refresh failed
- Solution: Implement error handling in your API calls to detect 401 errors and re-authenticate
Issue: Different tenant users cannot log in
- Cause: Single-tenant authority configuration
- Solution: Use
https://login.microsoftonline.com/commonfor multi-tenant, or configure allowed tenants
Package Information
- Package:
@astroapps/client-msal - Path:
astrolabe-client-msal/ - Published to: npm
- Version: 3.0.0+