使用Nested App Authentication(NAA)时弹窗自动快速关闭问题咨询
问题描述
我正尝试通过实现Nested App Authentication(NAA),将Outlook插件从legacy exchange tokens迁移出来。目前已能成功获取有效的Graph API令牌,但浏览器端存在糟糕的用户体验问题:
- Chrome浏览器中,用户登出后重新登录,再次使用插件时无法静默获取令牌,会弹出一个快速自动关闭的弹窗,之后才能获取令牌;后续操作可正常静默获取。
- Firefox中问题更严重,会出现2个弹窗,有时弹窗还会冻结空白。
- 若浏览器阻止弹窗,会导致无法获取令牌。
- Mac版Outlook桌面客户端无此问题,一切正常。
疑问:这种弹窗行为是否正常?用户已授权所需权限,理应可以静默获取令牌。我们正考虑改用非NAA方案规避问题。
相关认证代码
MSAL配置代码
export const getMsalConfig = (instance: string): Configuration => { let config = { auth: { clientId: getClientId(instance), authority: "https://login.microsoftonline.com/common", redirectUri: "https://localhost:3000/auth.html", postLogoutRedirectUri: "https://localhost:3000/auth.html", }, cache: { cacheLocation: "localStorage", }, system: {}, }; }
根组件代码
export default function App({ isOfficeInitialized }: Props) { const [msalInstance, setMsalInstance] = React.useState<IPublicClientApplication>(); useEffect(() => { createNestablePublicClientApplication(getMsalConfig(instance)) .then(res => { setMsalInstance(res); }).catch(error => { console.error(error); }); }, [instance]) return ( <> {msalInstance && <MsalProvider instance={msalInstance}> <AppErrorBoundary> <GlobalStyle /> <Root> ... <DoThing> </Root> </AppErrorBoundary> </MsalProvider> }</> ); }
业务组件代码
export default function DoThing({}: Props) { const accountIdentifiers = { username: "foo@example.com" // TODO genericify } const request = { loginHint: "foo@example.com", scopes: ["User.Read", "Mail.ReadWrite.Shared"] } const useMsalResult = useMsal(); const msalInstance = useMsalResult['instance']; // @ts-ignore const inProgress = useMsalResult['inProgress']; // @ts-ignore const isAuthenticated = useIsAuthenticated(); // @ts-ignore const {login, error } = useMsalAuthentication(InteractionType.Silent, request, accountIdentifiers); const doAction = async () => { // Code here gets an ewsToken that we currently use // Try to get a graph API token let graphApiToken = null; let graphApiError = null; try { const account = msalInstance.getActiveAccount(); if (!account) { throw Error("No active account found"); } const tokenRequest = { scopes: ["User.Read", "Mail.ReadWrite.Shared"], account: account }; const tokenResponse = await msalInstance.acquireTokenSilent(tokenRequest); graphApiToken = tokenResponse.accessToken; } catch ($error) { graphApiError = error ?? new Error("Failed to obtain a graph API token silently"); console.error(graphApiError.message); } // If we didn't silently get a graph api token and we didn't get a ewsToken, we'll try to more noisily // acquire a graph api token try { if (!restToken && !graphApiToken) { if (inProgress == InteractionStatus.None) { try { await login(InteractionType.Popup, request); } catch (popupError) { if (popupError.errorCode === "popup_window_error") { throw new Error("Popup blocked by browser. Please enable popups and try again."); } } } else { console.error("interaction already in progress"); } try { const account = msalInstance.getActiveAccount(); if (!account) { throw Error("No active account found"); } const tokenRequest = { scopes: ["User.Read", "Mail.ReadWrite.Shared"], account: account }; const tokenResponse = await msalInstance.acquireTokenSilent(tokenRequest); graphApiToken = tokenResponse.accessToken; } catch ($error) { graphApiError = error; console.error(graphApiError.message); throw graphApiError; } } } catch (error) { graphApiError = error ?? new Error("Failed to obtain a graph API token"); } if (!ewsToken && !graphApiToken) { onError({ context: 'Unable to process this message.', error: ewsError ?? graphApiError ?? new Error( 'There is no API support to obtain a callback token in this environment.' ), }); return; } } useEffect(() => { if (!!msalInstance) { doAction(); } }, [msalInstance]); return <Root>{// some components here}</Root>; }
解答
这种弹窗行为不正常,核心问题出在NAA模式下浏览器的缓存状态、MSAL实例初始化逻辑以及静默令牌获取的时机上。
问题原因分析
- 登出后缓存未完全清理:使用
localStorage作为缓存位置时,登出操作可能未彻底清除MSAL的账户缓存和令牌缓存,导致重新登录后MSAL无法识别有效会话,触发弹窗验证。 - 静默获取时机过早:组件初始化时立即调用
doAction尝试静默获取令牌,但此时MSAL实例可能尚未完成账户加载和会话同步,直接进入错误分支触发弹窗。 - 浏览器第三方上下文限制:Chrome和Firefox对Outlook网页版插件这类第三方上下文的Cookie/存储限制更严格,导致MSAL无法读取缓存的会话信息,被迫发起弹窗验证。桌面客户端无此限制,因此正常。
修复建议
1. 优化缓存清理与会话同步
登出时调用MSAL的logout方法并主动清理缓存:
// 示例登出逻辑 await msalInstance.logout({ account: msalInstance.getActiveAccount(), postLogoutRedirectUri: "https://localhost:3000/auth.html" }); // 清除localStorage中MSAL相关缓存项 localStorage.removeItem(`msal.${getClientId(instance)}.common`);
2. 调整静默获取逻辑与时机
在尝试静默获取前,先确保MSAL已加载有效账户:
const doAction = async () => { // 先加载并设置活跃账户 const accounts = msalInstance.getAllAccounts(); if (accounts.length > 0) { msalInstance.setActiveAccount(accounts[0]); } let graphApiToken = null; let graphApiError = null; try { const account = msalInstance.getActiveAccount(); if (!account) { // 先尝试静默SSO而非直接弹窗 await msalInstance.ssoSilent(request); } const tokenRequest = { scopes: ["User.Read", "Mail.ReadWrite.Shared"], account: msalInstance.getActiveAccount() }; const tokenResponse = await msalInstance.acquireTokenSilent(tokenRequest); graphApiToken = tokenResponse.accessToken; } catch ($error) { // 仅当静默方式完全失败时,再触发弹窗 if ($error instanceof InteractionRequiredAuthError) { try { await msalInstance.acquireTokenPopup(request); const tokenResponse = await msalInstance.acquireTokenSilent({ scopes: ["User.Read", "Mail.ReadWrite.Shared"], account: msalInstance.getActiveAccount() }); graphApiToken = tokenResponse.accessToken; } catch (popupError) { graphApiError = popupError; console.error(graphApiError.message); } } else { graphApiError = $error; console.error(graphApiError.message); } } // 后续业务逻辑... }
3. 调整MSAL配置适配浏览器限制
改用sessionStorage减少跨会话缓存残留,并开启Cookie存储适配第三方上下文:
export const getMsalConfig = (instance: string): Configuration => { return { auth: { clientId: getClientId(instance), authority: "https://login.microsoftonline.com/common", redirectUri: "https://localhost:3000/auth.html", postLogoutRedirectUri: "https://localhost:3000/auth.html", }, cache: { cacheLocation: "sessionStorage", storeAuthStateInCookie: true, // 针对浏览器第三方上下文开启Cookie存储 }, system: { loggerOptions: { loggerCallback: (level, message, containsPii) => { if (containsPii) return; switch (level) { case LogLevel.Error: console.error(message); return; case LogLevel.Info: console.info(message); return; case LogLevel.Verbose: console.debug(message); return; case LogLevel.Warning: console.warn(message); return; } } } }, }; }
4. 避免重复触发认证逻辑
移除useMsalAuthentication的重复使用,直接通过MSAL实例调用方法,减少不必要的交互触发:
// 删除该行代码 // const {login, error } = useMsalAuthentication(InteractionType.Silent, request, accountIdentifiers);
是否需要改用非NAA方案?
如果上述优化后仍无法解决浏览器端的弹窗问题,可以考虑改用**Office.js的Single Sign-On (SSO)**方案,该方案针对Outlook插件场景做了专门优化,依赖Office的身份上下文,避免第三方弹窗问题。但需要注意SSO的权限范围限制,部分Graph API权限可能需要额外的管理员审批。
内容的提问来源于stack exchange,提问作者user1938392
相关产品推荐
相关产品推荐

