Вопрос или проблема
Я пытаюсь реализовать аутентификацию вложенного приложения, чтобы переключить свой надстройку Outlook от использования устаревших токенов Exchange. Мне удается получить действительный токен графического API, но только с некоторыми недостатками пользовательского опыта при использовании браузера.
При использовании Chrome, если пользователь выходит из системы, а затем снова входит, когда он пытается снова использовать надстройку, он не получает токен тихо и вместо этого получает быстрое всплывающее окно, которое автоматически закрывается, и он получает токен. На последующих электронных письмах токен получается тихо.
Это нормальное поведение? Кажется, что в этом случае они должны иметь возможность получить токен тихо, так как они уже дали согласие на необходимые разрешения. (Это также может привести к тому, что они вообще не смогут получить токен, если браузер блокирует это быстрое всплывающее окно. Это больше проблема с Firefox, где по какой-то причине мы видим 2 всплывающих окна вместо 1 и иногда получаем всплывающее окно, замороженное открытым и пустым, не уверены, связано ли это с чем-то или нет). Рассматриваем, стоит ли нам обратить внимание на метод, не основанный на NAA, чтобы избежать этих проблем.
Я не вижу этого быстрого всплывающего окна при использовании Outlook Desktop для Mac, там все работает прекрасно.
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: "[email protected]" // TODO genericify
}
const request = {
loginHint: "[email protected]",
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 () => {
// Код здесь получает ewsToken, который мы в настоящее время используем
// Попробуйте получить токен графического API
let graphApiToken = null;
let graphApiError = null;
try {
const account = msalInstance.getActiveAccount();
if (!account) {
throw Error("Нет активной учетной записи");
}
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("Не удалось получить токен графического API тихо");
console.error(graphApiError.message);
}
// Если мы не получили токен графического API тихо и не получили ewsToken, мы постараемся более громко
// получить токен графического API
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("Всплывающее окно заблокировано браузером. Пожалуйста, разрешите всплывающие окна и попробуйте снова.");
}
}
} else {
console.error("взаимодействие уже в процессе");
}
try {
const account = msalInstance.getActiveAccount();
if (!account) {
throw Error("Нет активной учетной записи");
}
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("Не удалось получить токен графического API");
}
if (!ewsToken && !graphApiToken) {
onError({
context: 'Невозможно обработать это сообщение.',
error:
ewsError ??
graphApiError ??
new Error(
'Нет поддержки API для получения токена обратного вызова в этой среде.'
),
});
return;
}
}
useEffect(() => {
if (!!msalInstance) {
doAction();
}
}, [msalInstance]);
return <Root>{// некоторые компоненты здесь}</Root>;
}
Ответ или решение
Проблема с авто-закрывающимися всплывающими окнами при использовании вложенной аутентификации приложений (NAA)
Ваша проблема с Nested App Authentication (NAA) и всплывающими окнами в браузере касается внедрения сторонних токенов для вашего надстройки Outlook. Разобраться в данной ситуации можно, проанализировав как поведение приложений в различных браузерах, так и методы реализации аутентификации в рамках вашей архитектуры.
Текущая ситуация
Вы успешно получаете действительный токен Graph API при использовании приложения в браузере Chrome. Проблема возникновения всплывающего окна, которое быстро закрывается, связана с тем, что в определённых ситуациях пользователь не может получить токен тихо (silently), что приводит к всплывающему окну аутентификации.
Важно отметить, что поведение, когда всплывающее окно автоматически закрывается, действительно может встречаться в браузерах, и оно может зависеть от управления сессиями и аутентификации в конкретном браузере. На это влияют такие факторы, как кэширование, наличие активного сеанса и предыдущие условия согласия на доступ.
Аутентификация и разрешения
Ваша проблема заключается в том, что при повторной аутентификации (логировании) пользователю необходимо повторно вводить свои данные. Это может быть связано с тем, что браузер очищает определенные сеансовые данные, что мешает молчаливой аутентификации. Ваша текущая конфигурация, вероятно, уже содержит все необходимые разрешения, но отсутствие активной учетной записи может приводить к необходимости повторного открытия окна.
Важное замечание: при использовании Firefox вы сталкиваетесь с двумя всплывающими окнами, а иногда и с зависанием одного из них. Это может быть связано с различиями в обработке JavaScript и анимации в этом браузере.
Стратегии для решения проблемы
-
Проверка состояния аутентификации: Перед попыткой получения токена попытайтесь проверить состояние активной учетной записи, как у вас уже реализовано в коде:
const account = msalInstance.getActiveAccount(); if (!account) { throw Error("No active account found"); }
-
Изменение метода аутентификации: Рассмотрите возможность перехода на более надежные способы аутентификации, если восприятие пользователя по-прежнему остается негативным. Например, использование эгименов (refresh tokens) может улучшить пользовательский опыт.
-
Обработка ошибок: Следует детализировать ваши обработчики ошибок для каждого этапа аутентификации. Убедитесь, что механизмы обработки ошибок хорошо документированы, особенно для случаев, когда возникает ошибка всплывающего окна:
if (popupError.errorCode === "popup_window_error") { throw new Error("Popup blocked by browser. Please enable popups and try again."); }
-
Тестирование различных браузеров: Проводите тестирование на различных браузерах, чтобы понять, как различные политики безопасности могут влиять на поведение приложения.
-
Исключения для надстройки: В некоторых браузерах можно добавить исключения для обработки пользователей, что даст возможность им запускать вашу надстройку без блокировок всплывающих окон.
-
Альтернативные способы аутентификации: Если проблемы продолжаются, возможно, следует рассмотреть переход на альтернатива NAA, как, например, OAuth 2.0 с явной аутентификацией, которая даст больший контроль над поведением и потоком аутентификации.
Заключение
Хотя использование NAA может привести к ряду трудностей в браузере, реализация вышеперечисленных стратегий, а также тщательная работа с ответами в кодовой базе могут помочь улучшить пользовательский опыт. Как всегда, важно проводить тестирование с конечными пользователями и принимать во внимание их отзывы для дальнейшей оптимизации работы вашего приложения.