Forum Discussion

CilansSystem's avatar
CilansSystem
Copper Contributor
May 21, 2026

ssoSilent() not working across Next.js apps — timed_out or account picker on localhost

Hi everyone,

 

I've been stuck on this for a few days and would really appreciate some guidance from anyone who has dealt with cross-app silent SSO using MSAL.js v5.

 

Here's the setup. We have 3 separate Next.js applications all belonging to the same organisation, all registered under a single Azure Entra ID App Registration with the same clientId and tenantId. In production they all live under the same parent domain — app1.contoso.com, app2.contoso.com, app3.contoso.com — so localStorage is shared between them. On localhost we run them on ports 3000, 3001, and 3002.

 

The goal is simple: if a user is already signed into App 1, opening App 2 in a new tab should silently authenticate them without any popup, redirect, or account picker. Just seamless SSO.

 

Here is how I've set up the msalConfig:

 

export const msalConfig: Configuration = {

auth: {

clientId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',

authority: 'https://login.microsoftonline.com/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy',

redirectUri: 'http://localhost:3001/',

postLogoutRedirectUri: '/login',

},

cache: {

cacheLocation: 'localStorage',

storeAuthStateInCookie: true,

},

};

 

export const loginRequest = {

scopes: ['openid', 'profile', 'email', 'User.Read'],

};

 

Inside a component called SsoInitializer that sits inside MsalProvider, I scan localStorage for a sibling app's MSAL account on mount. I check both msal.2.account.keys (MSAL v5 format) and msal.account.keys (older format), extract the username/email as a loginHint, and then call ssoSilent(). If no loginHint is found — which is always the case on localhost since different ports are different origins — I still call ssoSilent() without a hint, expecting it to fall back to the Entra session cookie that was set when the user logged into port 3000.

 

instance.ssoSilent({

...loginRequest,

...(loginHint ? { loginHint } : {}),

redirectUri: `${window.location.origin}/silent-callback.html`,

})

 

The silent-callback.html in /public is just a blank HTML page with no scripts, which I believe is the correct approach based on the docs since MSAL v5 uses postMessage to communicate with the iframe.

 

The Azure app registration has the SPA platform selected, all redirect URIs including the /silent-callback.html variants are registered for all three localhost ports, ID tokens are enabled, and User.Read has admin consent.

 

Now here is the problem.

 

When App 1 is logged in on localhost:3000 and I open App 2 on localhost:3001, ssoSilent() fires but one of two things happens:

 

The first failure is a timed_out error — BrowserAuthError: timed_out from BrowserUtils.ts. The server-telemetry key in localStorage shows redirect_bridge_timeout repeated multiple times with cacheHits of 0. This started happening when I had a CDN import of MSAL inside silent-callback.html trying to call handleRedirectPromise(). The CDN download was too slow for the iframe timeout window, so I removed it.

 

The second failure happens after switching to the blank HTML silent-callback page. The timed_out goes away but now ssoSilent() seems to fall through entirely and the Microsoft "Pick an account" full-page redirect opens — which completely defeats the purpose.

 

I've also tried passing prompt: 'none' explicitly in the ssoSilent request. No change.

 

One important observation from DevTools: the Entra session cookie IS present in the browser. The user is fully signed in on port 3000. Based on my understanding of the docs, ssoSilent() without a loginHint should detect this session cookie and authenticate silently. But it's either timing out or showing the account picker.

 

I have a few specific questions I'm hoping someone can help with:

 

First, is ssoSilent() actually supposed to work without a loginHint using only the Entra session cookie? Or does it require a hint and will always show the account picker if multiple accounts are signed in to the browser?

 

Second, what is the correct content of silent-callback.html for MSAL v5 specifically? The blank page causes redirect_bridge_timeout, but adding MSAL scripts causes a different timeout because they load too slowly. Has the iframe handshake mechanism changed between v1/v2 and v5?

 

Third, is there an officially recommended pattern for cross-app silent SSO when developing on localhost with different ports? In production the same-domain setup handles localStorage sharing fine, but on localhost the browser's same-origin policy makes each port completely isolated, so the sibling token scan always returns null.

 

Fourth, does the redirectUri passed to ssoSilent() need to point to a page that actively runs MSAL code, or is a blank page genuinely sufficient for the iframe to complete its handshake in v5?

 

Using azure/msal-browser 5.6.1, azure/msal-react 3.0.20, Next.js 14 App Router, Chrome on Windows 11, single tenant.

 

Any help or a working example from someone who has done this in MSAL v5 would be hugely appreciated. Thanks in advance.

 

No RepliesBe the first to reply