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-Policy headers. 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:

ContextTop-level siteBroadcastChannel partition
Your app in the iframehostplatform.comhostplatform.com
Popup opened by the iframeyourapp.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.

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

  1. 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);
        });
    }
}
  1. Add the /redirect route in your routing configuration. The redirect route must be outside the MsalGuard, and the redirect page should not make API calls that would trigger MsalInterceptor (or otherwise invoke MSAL APIs):
import { RedirectComponent } from "./redirect/redirect.component";

const routes: Routes = [
    { path: "redirect", component: RedirectComponent },
    // ... your other routes
];
  1. Ensure the build includes the component. No angular.json assets change is needed when using an Angular route component — the Angular CLI bundles the component automatically. If you prefer a static redirect.html instead 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.

  1. Create redirect.html in your project root (next to index.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>
  1. Update vite.config.ts to 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.

  1. 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>
  1. 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);
});
  1. 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/)

  1. 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>;
}
  1. Exclude the redirect page from MsalProvider in _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/)

  1. 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>;
}
  1. Exclude the redirect route from MsalProvider in your root layout. If your app/layout.js wraps children in MsalProvider, 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.

Additional Resources