Redirect Bridge
June 3, 2026 · View on GitHub
This guide provides framework-specific instructions for setting up the redirect bridge page introduced in MSAL Browser v5. For background on why the redirect bridge is needed, see the v4 to v5 migration guide.
Important: The redirect bridge page must NOT be served with
Cross-Origin-Opener-Policyheaders. The bridge page is an intermediary that receives the authentication response after the IdP completes the OAuth flow. If COOP headers are set on the bridge page, the browser performs a browsing context group swap that severs the communication channel back to the main application — reintroducing the exact problem the bridge is designed to solve.
Important
After updating your redirectUri to point to the new redirect bridge page,
you MUST also update the redirect URI in your
Entra ID app registration.
The URI must match exactly — including path, protocol, and port.
Failure to update the app registration will result in redirect_uri_mismatch errors.
Warning
If the redirect bridge is not set up, all authentication flows that rely
on a popup or hidden iframe will stop working. ssoSilent,
acquireTokenPopup, and loginPopup depend on the redirect bridge to
receive the authentication response from the identity provider.
acquireTokenSilent is also affected when the refresh token is expired and
MSAL falls back to acquiring a new token in a hidden iframe (the same
mechanism used by ssoSilent). Without the redirect bridge, the popup or
iframe cannot communicate the response back to the main application window.
Redirect flows (loginRedirect / acquireTokenRedirect) can work
without the redirect bridge only if your redirectUri points to a page
that directly processes the authentication response (for example, using
handleRedirectPromise as in MSAL v4). However, when following the v5
guidance in this document—where redirectUri is set to the redirect bridge
page that calls broadcastResponseToMainFrame()—those redirect flows will
also fail if the bridge page is missing or not implemented correctly.
Caution
Do NOT host the redirect bridge page or any of its assets (including the bridge JavaScript module) on a third-party CDN or other third-party origin. The redirect bridge receives the raw authentication response — including authorization codes and tokens — directly from the identity provider. Hosting any part of the bridge on a third-party origin creates a supply-chain and token-theft risk: a compromised bridge asset could intercept the authentication response before it reaches your application. Always bundle the redirect bridge with your application and serve the page and its assets from the same origin as your application.
Because the bridge page carries sensitive authentication material in the URL,
it must not be cached by any intermediary. Serve the redirect bridge page
with Cache-Control: no-store to prevent CDNs, reverse proxies, and browser
disk caches from retaining the page — and the authorization codes or tokens
in its URL — after the response has been processed.
Note: In Safari Private Browsing, privacy protections or content-blocking features may restrict requests to some third-party domains and can interfere with CDN-hosted scripts. If the redirect bridge is hosted on a CDN, this can lead to authentication failures that may be difficult to diagnose. Bundling the redirect bridge with your application and serving it from the same origin helps avoid this class of issue.
Logout and the Redirect Bridge
logoutPopup() — redirect bridge is required
When using logoutPopup(), the postLogoutRedirectUri must point to a page that implements the redirect bridge (calls broadcastResponseToMainFrame()). After ESTS completes the sign-out, it redirects the popup to the postLogoutRedirectUri. The redirect bridge on that page broadcasts the response back to the main window and closes the popup. Without the bridge, the popup will remain open after logout because COOP headers prevent the main window from closing it directly.
The simplest configuration is to set postLogoutRedirectUri to the same value as redirectUri:
const msalConfig = {
auth: {
clientId: "{your-client-id}",
redirectUri: "/redirect",
postLogoutRedirectUri: "/redirect", // Must host the redirect bridge
},
};
Important
If your postLogoutRedirectUri differs from your redirectUri, you must
ensure both pages implement the redirect bridge and both URIs are
registered in your Entra ID app registration
as redirect URIs.
Note: For applications that support personal Microsoft accounts (MSA),
the postLogoutRedirectUri must be registered as a redirect URI in your
app registration. If it is not registered, MSA will not redirect back after
sign-out and the user will see a generic "close this tab" page instead. See
Send a sign-out request
for details.
logoutRedirect() — redirect bridge is optional
For logoutRedirect(), hosting the redirect bridge on the postLogoutRedirectUri page is optional. The redirect flow navigates the entire browser window, so there is no popup to close. If the redirect bridge is present on the postLogoutRedirectUri page, it will navigate the user back to the origin page. If not, ESTS will redirect the user directly to the postLogoutRedirectUri page and the user stays there.
Cross-Origin Iframe Limitation
The redirect bridge relies on the BroadcastChannel API to send the authentication response from the popup back to the main application window. Starting with Chromium 115+ (Chrome, Edge, and other Chromium-based browsers), third-party storage partitioning is enforced, which means BroadcastChannel is partitioned by top-level site, not just by origin. Other browsers are expected to adopt similar partitioning in the future.
This causes popup-based flows to fail when your application runs inside a cross-origin iframe:
| Context | Top-level site | BroadcastChannel partition |
|---|---|---|
| Your app in the iframe | hostplatform.com | hostplatform.com |
| Popup opened by the iframe | yourapp.com (popup is its own top-level context) | yourapp.com |
Although both the iframe and the popup share the same origin (yourapp.com), the popup is in a different storage partition. The redirect bridge sends the response on a BroadcastChannel in the popup's partition, but the iframe is listening on a channel in the host platform's partition — the message never arrives, and the authentication flow times out.
Important
This is a browser-level constraint — not a bug in MSAL. There is no secure client-side workaround that avoids this partitioning while preserving the security guarantees of the redirect bridge.
Recommended alternatives for cross-origin iframe scenarios
Option 1: Nested App Authentication (recommended)
If the host platform supports it, use Nested App Authentication (NAA).
With NAA, the iframe delegates authentication entirely to the host application
via createNestablePublicClientApplication. The host authenticates the user at
the top level (where there are no partitioning issues) and provides tokens to
the nested app through the NAA bridge. This avoids popup and silent flows from
within the iframe altogether.
import { createNestablePublicClientApplication } from "@azure/msal-browser";
const pca = await createNestablePublicClientApplication({
auth: {
clientId: "your-client-id",
authority: "https://login.microsoftonline.com/your-tenant-id",
},
});
NAA is supported out of the box by Microsoft host platforms such as Teams,
Outlook, and Microsoft 365. For custom host platforms, the host must
implement the NAA bridge protocol.
If the NAA bridge is not available, createNestablePublicClientApplication
automatically falls back to a standard PublicClientApplication.
Option 2: Redirect flow within the iframe (fallback)
If NAA is not available, use loginRedirect() / acquireTokenRedirect()
instead of popup APIs. The redirect flow happens entirely within the iframe's
browsing context and does not use BroadcastChannel, so the redirect bridge
partition mismatch does not apply. You will need to set
system.allowRedirectInIframe: true:
const msalConfig = {
auth: {
clientId: "your-client-id",
authority: "https://login.microsoftonline.com/your-tenant-id",
redirectUri: "/redirect",
},
system: {
allowRedirectInIframe: true,
},
};
Note
Redirect flows in iframes require the identity provider to allow rendering in
an iframe. Azure AD / Microsoft Entra ID will refuse to render interactive
prompts (credential entry, consent, etc.) inside an iframe and will return an
X-FRAME-OPTIONS: DENY error. This means redirect flows in iframes are
primarily supported for Azure AD B2C with the
embedded sign-in experience,
or for non-interactive redirect-in-iframe scenarios where no user interaction is required. This is distinct from MSAL's silent token renewal APIs (ssoSilent / acquireTokenSilent), which use a hidden iframe with prompt=none and are subject to third-party cookie and tracking-prevention restrictions.
For more information on running MSAL in iframes, see Using MSAL in iframed apps.
Page Title
Always set a meaningful <title> on your redirect bridge page. Without an explicit title, the browser displays the raw redirect URL — which contains authorization codes or tokens — as the tab title and browser history entry.
A good title should:
- Indicate that authentication is in progress (e.g., "Signing in" or "Signing in - MyApp")
- Be static HTML (not set dynamically after JavaScript loads) so it appears immediately
<head>
<title>Signing in</title>
</head>
This improves:
- User experience — Users see a meaningful tab title instead of a long URL with query parameters
- Browser history — History entries show a descriptive name rather than the raw redirect URL
- Accessibility — Screen readers announce the page title when focus moves to the window or tab
Note
MSAL overwrites document.title to "Microsoft Authentication" at runtime during popup, iframe, and redirect processing. A static <title> prevents the tab/history entry from briefly showing the raw redirect URL before JavaScript executes.
Angular
- Create the redirect bridge component (
src/app/redirect/redirect.component.ts):
import { Component, OnInit } from "@angular/core";
import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge";
@Component({
selector: "app-redirect",
standalone: true,
template: "<p>Processing authentication...</p>",
})
export class RedirectComponent implements OnInit {
ngOnInit(): void {
broadcastResponseToMainFrame().catch((error: Error) => {
console.error("Error broadcasting response to main frame:", error);
});
}
}
- Add the
/redirectroute in your routing configuration. The redirect route must be outside theMsalGuard, and the redirect page should not make API calls that would triggerMsalInterceptor(or otherwise invoke MSAL APIs):
import { RedirectComponent } from "./redirect/redirect.component";
const routes: Routes = [
{ path: "redirect", component: RedirectComponent },
// ... your other routes
];
- Ensure the build includes the component. No
angular.jsonassets change is needed when using an Angular route component — the Angular CLI bundles the component automatically. If you prefer a staticredirect.htmlinstead of a routed component, add it to the assets array:
// angular.json
{
"projects": {
"your-app": {
"architect": {
"build": {
"options": {
"assets": [
{ "glob": "**/*", "input": "public" },
"src/redirect.html" // ← Add redirect bridge page
]
}
}
}
}
}
}
Sample: See angular-standalone-sample and angular-modules-sample.
Vite
Vite requires a multi-page configuration so that redirect.html is included as a separate entry point in the build output.
- Create
redirect.htmlin your project root (next toindex.html):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Signing in</title>
</head>
<body>
<p>Processing authentication...</p>
<script type="module">
import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge";
broadcastResponseToMainFrame().catch((error) => {
console.error("Error broadcasting response:", error);
});
</script>
</body>
</html>
- Update
vite.config.tsto add the redirect page as a second entry:
import { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
redirect: resolve(__dirname, "redirect.html"), // ← Redirect bridge entry
},
},
},
});
During development (vite dev), the redirect page is automatically served at /redirect.html. In production builds, Rollup will emit both index.html and redirect.html in the output directory.
Sample: See the react-router-sample, typescript-sample, and b2c-sample.
Webpack
Webpack requires a dedicated entry point and an HtmlWebpackPlugin instance for the redirect page.
- Create
src/redirect.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Signing in</title>
</head>
<body>
<p>Processing authentication...</p>
<!-- The redirect script bundle will be injected by HtmlWebpackPlugin (see redirect.js entry). -->
</body>
</html>
- Create
src/redirect.js(entry point for Webpack):
import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge";
broadcastResponseToMainFrame().catch((error) => {
console.error("Error broadcasting response:", error);
});
- Update
webpack.config.js:
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: {
main: "./src/index.js",
redirect: "./src/redirect.js", // ← Redirect bridge entry
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
template: "./src/index.html",
chunks: ["main"],
}),
new HtmlWebpackPlugin({
filename: "redirect.html",
template: "./src/redirect.html",
chunks: ["redirect"], // ← Only include the redirect chunk
}),
],
};
Next.js
Next.js pages automatically become routes, so the redirect bridge is a page component. The setup differs between the Pages Router and the App Router.
Pages Router (pages/)
- Create
pages/redirect.js:
import { useEffect } from "react";
import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge";
export default function Redirect() {
useEffect(() => {
broadcastResponseToMainFrame().catch((error) => {
console.error("Error broadcasting response to main frame:", error);
});
}, []);
return <p>Processing authentication...</p>;
}
- Exclude the redirect page from
MsalProviderin_app.js:
// pages/_app.js
import { useRouter } from "next/router";
import { MsalProvider } from "@azure/msal-react";
function MyApp({ Component, pageProps }) {
const router = useRouter();
// The redirect page must NOT be wrapped in MsalProvider
if (router.pathname === "/redirect") {
return <Component {...pageProps} />;
}
return (
<MsalProvider instance={msalInstance}>
<Component {...pageProps} />
</MsalProvider>
);
}
App Router (app/)
- Create
app/redirect/page.js— this must be a Client Component ("use client"):
"use client";
import { useEffect } from "react";
import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge";
export default function Redirect() {
useEffect(() => {
broadcastResponseToMainFrame().catch((error) => {
console.error("Error broadcasting response to main frame:", error);
});
}, []);
return <p>Processing authentication...</p>;
}
- Exclude the redirect route from
MsalProviderin your root layout. If yourapp/layout.jswraps children inMsalProvider, create a separate layout for the redirect route that skips it:
// app/redirect/layout.js — no MsalProvider wrapper
export default function RedirectLayout({ children }) {
return <>{children}</>;
}
This prevents MSAL from processing the auth response hash before broadcastResponseToMainFrame() runs.
No next.config.js changes are needed for either router — Next.js serves pages automatically.
Sample: See the nextjs-sample for a Pages Router example.
Express.js / Node.js Backend
When using Express.js (or any Node.js backend serving static files), configure the server to serve the redirect page without COOP headers:
const express = require("express");
const path = require("path");
const app = express();
// Serve the redirect bridge page WITHOUT COOP headers
app.get("/redirect", (req, res) => {
res.sendFile(path.join(__dirname, "public", "redirect.html"));
});
// Set COOP headers for all other routes
app.use((req, res, next) => {
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
next();
});
app.use(express.static(path.join(__dirname, "public")));
Sample: See the HybridSample.