@mohsinonxrm/dataverse-sdk-auth-msal-browser
v1.0.0
Published
> MSAL Browser authentication provider for Dataverse SDK. Implements `AccessTokenProvider` using `@azure/msal-browser` for Single Page Applications (SPAs). Supports popup and redirect authentication flows with automatic silent token refresh.
Downloads
12
Readme
@mohsinonxrm/dataverse-sdk-auth-msal-browser
MSAL Browser authentication provider for Dataverse SDK. Implements
AccessTokenProviderusing@azure/msal-browserfor Single Page Applications (SPAs). Supports popup and redirect authentication flows with automatic silent token refresh.
Features
- ✅ Popup & Redirect Flows - Choose interaction type: popup windows or full-page redirects
- ✅ Silent Token Acquisition - Automatic token refresh without user interaction (MSAL handles internally)
- ✅ React Integration - Works seamlessly with
@azure/msal-reactand React apps - ✅ Multi-Account Support - Handle multiple signed-in accounts with active account tracking
- ✅ TypeScript-first - Fully typed with comprehensive IntelliSense
- ✅ Conditional Access - Support for claims challenges and step-up authentication
- ✅ Browser Storage - SessionStorage or LocalStorage for token caching
Installation
pnpm add @mohsinonxrm/dataverse-sdk-auth-msal-browser @mohsinonxrm/dataverse-sdk-core @azure/msal-browserPeer dependencies:
@azure/msal-browser(^3.0.0)@mohsinonxrm/dataverse-sdk-core(workspace dependency)
Quick Start
1. Basic Setup with Popup Flow (Recommended)
Best for: Most SPA scenarios, React apps, Vue apps, Angular apps
Popup flow shows login in a popup window, keeping the main app page loaded.
import { PublicClientApplication } from "@azure/msal-browser";
import { MsalBrowserTokenProvider } from "@mohsinonxrm/dataverse-sdk-auth-msal-browser";
import { DataverseClient } from "@mohsinonxrm/dataverse-sdk-core";
// Step 1: Configure MSAL
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, // Must match app registration
},
cache: {
cacheLocation: "sessionStorage", // 'sessionStorage' or 'localStorage'
storeAuthStateInCookie: false, // Set to true for IE11/Edge legacy
},
};
// Step 2: Initialize MSAL (required before use)
const pca = new PublicClientApplication(msalConfig);
await pca.initialize();
// Step 3: Create token provider with popup interaction
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://your-org.crm.dynamics.com/.default"],
interactionType: "popup", // Default: popup
});
// Step 4: Create Dataverse client
const client = new DataverseClient({
baseUrl: "https://your-org.crm.dynamics.com",
tokenProvider,
});
// Step 5: Use the client - first call triggers popup login automatically
const accounts = await client.api("/accounts").select("name", "accountnumber").top(10).get();
console.log(`Retrieved ${accounts.value.length} accounts`);How it works:
- First API call triggers
getToken() - Provider attempts silent token acquisition
- If no cached token, popup window opens for login
- User authenticates in popup
- Token cached for future requests
- Subsequent calls use cached token (automatic refresh)
2. Redirect Flow
Best for: Mobile browsers where popups may be blocked, full-page authentication UX
Redirect flow navigates the entire page to login, then redirects back to your app.
import { PublicClientApplication } from "@azure/msal-browser";
import { MsalBrowserTokenProvider } from "@mohsinonxrm/dataverse-sdk-auth-msal-browser";
import { DataverseClient } from "@mohsinonxrm/dataverse-sdk-core";
const msalConfig = {
auth: {
clientId: "your-client-id",
authority: "https://login.microsoftonline.com/your-tenant-id",
redirectUri: window.location.origin,
},
cache: {
cacheLocation: "sessionStorage",
},
};
const pca = new PublicClientApplication(msalConfig);
await pca.initialize();
// IMPORTANT: Handle redirect promise after page loads
const tokenResponse = await pca.handleRedirectPromise();
if (tokenResponse) {
console.log("User signed in via redirect:", tokenResponse.account.username);
pca.setActiveAccount(tokenResponse.account);
}
// Create token provider with redirect interaction
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://your-org.crm.dynamics.com/.default"],
interactionType: "redirect",
});
const client = new DataverseClient({
baseUrl: "https://your-org.crm.dynamics.com",
tokenProvider,
});
// First API call will redirect entire page to login
// After login, user is redirected back to redirectUri
// handleRedirectPromise() captures the result
const accounts = await client.api("/accounts").top(10).get();Redirect flow lifecycle:
- App loads →
handleRedirectPromise()checks for redirect result - If no auth → API call triggers
acquireTokenRedirect() - Page redirects to Microsoft login
- User authenticates
- Page redirects back to
redirectUri handleRedirectPromise()resolves with tokens- Token cached, app continues
3. Pre-authenticated Setup (Recommended for Production)
Best for: Apps with dedicated login page/flow, explicit auth control
Login explicitly before creating the Dataverse client.
import { PublicClientApplication } from "@azure/msal-browser";
import { MsalBrowserTokenProvider } from "@mohsinonxrm/dataverse-sdk-auth-msal-browser";
import { DataverseClient } from "@mohsinonxrm/dataverse-sdk-core";
const pca = new PublicClientApplication(msalConfig);
await pca.initialize();
// Explicit login via popup
const loginResponse = await pca.loginPopup({
scopes: ["https://your-org.crm.dynamics.com/.default"],
prompt: "select_account", // Force account picker
});
// Set as active account
pca.setActiveAccount(loginResponse.account);
console.log(`Signed in as: ${loginResponse.account.username}`);
// Create token provider with specific account
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://your-org.crm.dynamics.com/.default"],
account: loginResponse.account, // Use specific account
interactionType: "popup",
});
const client = new DataverseClient({
baseUrl: "https://your-org.crm.dynamics.com",
tokenProvider,
});
// All API calls use silent token acquisition (no popup after initial login)
const whoami = await client.api("/WhoAmI").get();
console.log(`User ID: ${whoami.UserId}`);React Integration
With @azure/msal-react (Recommended)
The @azure/msal-react library provides React-specific hooks and components for MSAL authentication.
import React from 'react';
import { MsalProvider, useMsal, AuthenticatedTemplate, UnauthenticatedTemplate } from '@azure/msal-react';
import { PublicClientApplication } from '@azure/msal-browser';
import { MsalBrowserTokenProvider } from '@mohsinonxrm/dataverse-sdk-auth-msal-browser';
import { DataverseClient } from '@mohsinonxrm/dataverse-sdk-core';
// MSAL configuration
const msalConfig = {
auth: {
clientId: process.env.REACT_APP_CLIENT_ID!,
authority: `https://login.microsoftonline.com/${process.env.REACT_APP_TENANT_ID}`,
redirectUri: window.location.origin,
},
cache: {
cacheLocation: 'sessionStorage',
},
};
const pca = new PublicClientApplication(msalConfig);
await pca.initialize(); // Initialize before rendering
// App wrapper with MsalProvider
export function App() {
return (
<MsalProvider instance={pca}>
<AuthenticatedTemplate>
<DataverseApp />
</AuthenticatedTemplate>
<UnauthenticatedTemplate>
<LoginButton />
</UnauthenticatedTemplate>
</MsalProvider>
);
}
// Login button component
function LoginButton() {
const { instance } = useMsal();
const handleLogin = () => {
instance.loginPopup({
scopes: [process.env.REACT_APP_DATAVERSE_SCOPE!],
});
};
return <button onClick={handleLogin}>Sign In</button>;
}
// Component using Dataverse client
function DataverseApp() {
const { instance, accounts } = useMsal();
const [client, setClient] = React.useState<DataverseClient | null>(null);
const [accounts, setAccounts] = React.useState([]);
React.useEffect(() => {
if (accounts.length > 0) {
// Create token provider with first account
const tokenProvider = new MsalBrowserTokenProvider(instance, {
scopes: [process.env.REACT_APP_DATAVERSE_SCOPE!],
account: accounts[0],
interactionType: 'popup',
});
const dataverseClient = new DataverseClient({
baseUrl: process.env.REACT_APP_DATAVERSE_URL!,
tokenProvider,
});
setClient(dataverseClient);
}
}, [instance, accounts]);
React.useEffect(() => {
if (!client) return;
// Fetch accounts from Dataverse
client.api('/accounts')
.select('name', 'accountnumber')
.top(10)
.get()
.then(result => setAccounts(result.value))
.catch(console.error);
}, [client]);
if (!client) {
return <div>Initializing...</div>;
}
return (
<div>
<h1>Dataverse Accounts</h1>
<ul>
{accounts.map(acc => (
<li key={acc.accountid}>{acc.name}</li>
))}
</ul>
</div>
);
}Custom React Hook for Dataverse Client
Create a reusable hook for the Dataverse client.
import { useMsal } from '@azure/msal-react';
import { DataverseClient } from '@mohsinonxrm/dataverse-sdk-core';
import { MsalBrowserTokenProvider } from '@mohsinonxrm/dataverse-sdk-auth-msal-browser';
import { useMemo } from 'react';
export function useDataverseClient(): DataverseClient | null {
const { instance, accounts } = useMsal();
return useMemo(() => {
if (accounts.length === 0) {
return null;
}
const tokenProvider = new MsalBrowserTokenProvider(instance, {
scopes: [process.env.REACT_APP_DATAVERSE_SCOPE!],
account: accounts[0],
interactionType: 'popup',
});
return new DataverseClient({
baseUrl: process.env.REACT_APP_DATAVERSE_URL!,
tokenProvider,
});
}, [instance, accounts]);
}
// Usage in component
function AccountList() {
const client = useDataverseClient();
const [accounts, setAccounts] = React.useState([]);
React.useEffect(() => {
if (!client) return;
client.api('/accounts')
.select('name', 'accountnumber')
.top(10)
.get()
.then(result => setAccounts(result.value))
.catch(console.error);
}, [client]);
if (!client) {
return <div>Not authenticated</div>;
}
return (
<div>
{accounts.map(acc => (
<div key={acc.accountid}>{acc.name}</div>
))}
</div>
);
} account: accounts[0],
interactionType: 'popup',
});
return new DataverseClient({
baseUrl: process.env.REACT_APP_DATAVERSE_URL!,
tokenProvider,
});}, [instance, accounts]); }
// Usage in component function AccountList() { const client = useDataverseClient(); const [accounts, setAccounts] = React.useState([]);
React.useEffect(() => { if (!client) return;
client.api('/accounts').top(10).get()
.then(setAccounts)
.catch(console.error);}, [client]);
if (!client) { return Not authenticated; }
return {/_ Render accounts _/}; }
## Account Management
### Multi-Account Scenarios
Handle multiple signed-in accounts in your SPA.
```typescript
const pca = new PublicClientApplication(msalConfig);
await pca.initialize();
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ['https://your-org.crm.dynamics.com/.default'],
});
// Get all cached accounts
const accounts = tokenProvider.getAccounts();
console.log(`Found ${accounts.length} cached account(s)`);
accounts.forEach(account => {
console.log(`- ${account.username} (${account.name})`);
});
// Switch to a different account
if (accounts.length > 1) {
tokenProvider.setActiveAccount(accounts[1]);
console.log(`Switched to account: ${accounts[1].username}`);
}
// Get currently active account
const activeAccount = tokenProvider.getActiveAccount();
if (activeAccount) {
console.log(`Active account: ${activeAccount.username}`);
} else {
console.log('No active account set');
}
// Use specific account for token provider
const specificTokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ['https://your-org.crm.dynamics.com/.default'],
account: accounts[0], // Use first account explicitly
});Sign Out
Sign out the current user or a specific account.
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://your-org.crm.dynamics.com/.default"],
interactionType: "popup", // Determines sign-out method
});
// Sign out current active account (popup)
await tokenProvider.signOut();
console.log("User signed out via popup");
// Sign out specific account
const accounts = tokenProvider.getAccounts();
if (accounts.length > 0) {
await tokenProvider.signOut(accounts[0]);
}
// Redirect-based sign out
const redirectTokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://your-org.crm.dynamics.com/.default"],
interactionType: "redirect",
});
await redirectTokenProvider.signOut();
// Page redirects to Microsoft logout, then back to app
// Sign out and clear all accounts
const allAccounts = pca.getAllAccounts();
for (const account of allAccounts) {
await pca.logoutPopup({ account });
}Sign out behavior:
interactionType: 'popup'→logoutPopup()(popup window)interactionType: 'redirect'→logoutRedirect()(full page redirect)- Clears tokens from browser cache
- Ends session with Microsoft identity platform
API Reference
Class: MsalBrowserTokenProvider
Implements AccessTokenProvider interface from @mohsinonxrm/dataverse-sdk-core.
Constructor
constructor(
publicClientApplication: IPublicClientApplication,
options: MsalBrowserTokenProviderOptions
)Parameters:
publicClientApplication- Initialized MSALPublicClientApplicationorIPublicClientApplication- Must call
await pca.initialize()before passing to constructor
- Must call
options- Configuration options (see below)
Throws:
DataverseInvalidRequestErrorif parameters are invalid or missing
Methods
async getToken(scopes?, options?): Promise<string>
Acquires access token using silent flow first, falls back to interactive (popup/redirect) if needed.
// Use configured scopes
const token = await tokenProvider.getToken();
// Override scopes for this request
const token = await tokenProvider.getToken(["https://org2.crm.dynamics.com/.default"]);
// With additional options
const token = await tokenProvider.getToken(undefined, {
claims: '{"access_token":{"acrs":{"essential":true,"values":["c1"]}}}',
correlationId: crypto.randomUUID(),
});getAccounts(): AccountInfo[]
Returns all accounts cached by MSAL.
const accounts = tokenProvider.getAccounts();
console.log(`Found ${accounts.length} account(s)`);setActiveAccount(account: AccountInfo): void
Sets the active account for MSAL operations.
const accounts = tokenProvider.getAccounts();
tokenProvider.setActiveAccount(accounts[0]);getActiveAccount(): AccountInfo | null
Gets the currently active account.
const activeAccount = tokenProvider.getActiveAccount();
if (activeAccount) {
console.log(`Active: ${activeAccount.username}`);
}async signOut(account?: AccountInfo): Promise<void>
Signs out a user using popup or redirect (based on interactionType).
// Sign out active account
await tokenProvider.signOut();
// Sign out specific account
const accounts = tokenProvider.getAccounts();
await tokenProvider.signOut(accounts[0]);Configuration Options
MsalBrowserTokenProviderOptions
interface MsalBrowserTokenProviderOptions {
scopes: string[]; // Required
account?: AccountInfo; // Optional
interactionType?: "popup" | "redirect"; // Default: 'popup'
claims?: string; // Optional
correlationId?: string; // Optional
}| Property | Type | Required | Default | Description |
| ----------------- | ----------------------- | -------- | ------------- | ----------------------------------------- |
| scopes | string[] | ✅ Yes | - | OAuth scopes for Dataverse access |
| account | AccountInfo | ❌ No | First account | Specific account for silent auth |
| interactionType | 'popup' \| 'redirect' | ❌ No | 'popup' | How to handle interactive auth |
| claims | string | ❌ No | - | JSON string for Conditional Access claims |
| correlationId | string | ❌ No | - | Correlation ID for tracing |
Common Scopes
| Scope Pattern | Description | Example |
| --------------------------------------------------- | ------------------------------------- | ----------------------------------------------------- |
| https://<org>.crm.dynamics.com/.default | All permissions from app registration | https://contoso.crm.dynamics.com/.default |
| https://<org>.crm.dynamics.com/user_impersonation | Act on behalf of signed-in user | https://contoso.crm.dynamics.com/user_impersonation |
Recommendation: Use
.defaultscope for most scenarios. It requests all permissions granted to your app registration.
Advanced Scenarios
Conditional Access & Claims Challenges
Handle Conditional Access policies that require additional claims or step-up authentication.
// Scenario: Dataverse protected by Conditional Access requiring device compliance
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://your-org.crm.dynamics.com/.default"],
claims: JSON.stringify({
access_token: {
acrs: {
essential: true,
values: ["c1"], // Compliance claim
},
},
}),
});
// Or pass claims at token acquisition time (dynamic claims challenge)
try {
await client.api("/accounts").get();
} catch (error) {
// If Dataverse returns 401 with claims challenge
if (error.status === 401 && error.headers["www-authenticate"]) {
const claimsChallenge = parseClaimsFromHeader(error.headers["www-authenticate"]);
const token = await tokenProvider.getToken(undefined, {
claims: claimsChallenge,
});
// Retry request with new token
}
}Custom Correlation IDs for Tracing
Add correlation IDs for distributed tracing and debugging.
// Set correlation ID in provider options
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://your-org.crm.dynamics.com/.default"],
correlationId: crypto.randomUUID(),
});
// Or per-request correlation ID
const token = await tokenProvider.getToken(undefined, {
correlationId: "my-request-correlation-id-123",
});Scope Override Per Request
Use different scopes for specific requests (multi-org scenarios).
// Default provider for org1
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://org1.crm.dynamics.com/.default"],
});
const client1 = new DataverseClient({
baseUrl: "https://org1.crm.dynamics.com",
tokenProvider,
});
// Override scopes for org2 access
const org2Token = await tokenProvider.getToken(["https://org2.crm.dynamics.com/.default"]);
// Use org2Token manually or create separate client
const client2 = new DataverseClient({
baseUrl: "https://org2.crm.dynamics.com",
tokenProvider: new MsalBrowserTokenProvider(pca, {
scopes: ["https://org2.crm.dynamics.com/.default"],
}),
});Error Handling
import { DataverseAuthenticationError } from "@mohsinonxrm/dataverse-sdk-core";
try {
const client = new DataverseClient({
baseUrl: "https://your-org.crm.dynamics.com",
tokenProvider,
});
const accounts = await client.api("/accounts").get();
} catch (error) {
if (error instanceof DataverseAuthenticationError) {
console.error("Authentication failed:", error.message);
// Handle auth error (e.g., show login button)
} else {
console.error("API call failed:", error);
}
}Browser Compatibility
- ✅ Chrome 90+
- ✅ Edge 90+
- ✅ Firefox 88+
- ✅ Safari 14+
Note: Requires browsers with native
fetchandPromisesupport. For older browsers, use polyfills.
Storage Options
Session Storage (Recommended)
const msalConfig = {
cache: {
cacheLocation: "sessionStorage", // Tokens cleared on tab close
storeAuthStateInCookie: false,
},
};Local Storage (Persistent)
const msalConfig = {
cache: {
cacheLocation: "localStorage", // Tokens persist across browser sessions
storeAuthStateInCookie: false,
},
};Cookies (IE 11 Support)
const msalConfig = {
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true, // For IE 11 and Edge Legacy
},
};Security Best Practices
- ✅ Use sessionStorage for cache in most scenarios
- ✅ Set restrictive CORS policies on your web server
- ✅ Use HTTPS in production (required by MSAL)
- ✅ Configure redirect URI to specific pages, not wildcards
- ✅ Enable PKCE (automatic in MSAL Browser)
- ✅ Validate tokens server-side for sensitive operations
- ❌ Don't store tokens in localStorage if avoidable
- ❌ Don't expose client secrets in browser code (use public client)
Troubleshooting
Popup blocked by browser
Problem: Login popup is blocked
Solution:
- Ensure login is triggered by user action (button click)
- Or switch to redirect flow
const tokenProvider = new MsalBrowserTokenProvider(pca, {
scopes: ["https://org.crm.dynamics.com/.default"],
interactionType: "redirect", // Use redirect instead
});"Interaction required" errors
Problem: Silent token acquisition fails
Solution: This is normal behavior. The provider automatically falls back to popup/redirect.
CORS errors
Problem: No 'Access-Control-Allow-Origin' header
Solution: Dataverse automatically allows CORS from your app's redirect URI origin. Ensure:
- Redirect URI is correctly registered
- Requests originate from that URI
- Origin header is sent by browser
Token expiration
Problem: API calls fail after some time
Solution: Token refresh is automatic. If seeing errors:
// Force new token
const newToken = await tokenProvider.getToken(
["https://org.crm.dynamics.com/.default"],
{ forceRefresh: true } // Note: Not supported by default, handle manually
);Related Packages
- @mohsinonxrm/dataverse-sdk-core - Core SDK (required)
- @mohsinonxrm/dataverse-sdk-auth-msal-node - MSAL for Node.js apps
- @mohsinonxrm/dataverse-sdk-auth-azure-identity - Azure Identity for managed identity scenarios
- @azure/msal-browser - MSAL Browser library (peer dependency)
- @azure/msal-react - React integration for MSAL
Examples
See complete examples in:
- samples/spa-fluentui-v9 - Full React SPA with Fluent UI
- samples/node-cli-devicecode - CLI authentication (uses msal-node)
License
GNU AGPL v3.0
