Поделиться через


Пакетное согласие для приложений идентификатора Microsoft Entra

В этой статье описывается, как настроить групповое согласие для приложений Microsoft Entra ID.

Симптомы

У вас есть пользовательское клиентское приложение и пользовательское приложение API, и вы создаете регистрации приложений для обоих приложений в идентификаторе Microsoft Entra. Вы настраиваете пакетное согласие для обоих приложений. В этом сценарии при попытке входа в любое приложение может появиться одно из следующих сообщений об ошибках:

  • AADSTS70000: запрос был отклонен, так как одна или несколько запрошенных областей действия неавторизованы или срок их действия истек. Пользователь должен сначала войти и предоставить клиентскому приложению доступ к запрошенной области.

  • AADSTS650052: Приложение пытается получить доступ к службе \"{app_id}\" (\"app_name\"), для которой у вашей организации %\"{organization}\" отсутствует учетная запись. Чтобы создать необходимую учетную запись службы, обратитесь к ИТ-администратору для проверки конфигурации подписок на услуги или предоставления разрешения для приложения.

Решение

Шаг 1. Настройка knownClientApplications для регистрации приложения API

Добавьте идентификатор клиентского пользовательского приложения в свойство регистрации пользовательского приложения API knownClientApplications. Дополнительные сведения см. в атрибуте knownClientApplications.

Шаг 2. Настройка разрешений API

Убедитесь, что:

  • Все необходимые разрешения API правильно настроены как для регистрации пользовательского клиента, так и для пользовательских приложений API.
  • Регистрация пользовательского клиентского приложения включает разрешения API, определенные в регистрации пользовательского приложения API.

Шаг 3. Запрос на вход

Ваш запрос на аутентификацию должен использовать область действия .default для Microsoft Graph. Для учетных записей Майкрософт область должна быть для пользовательского API.

Пример запроса учетных записей Майкрософт и рабочих или учебных учетных записей

https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?response_type=code
&Client_id=72333f42-5078-4212-abb2-e4f9521ec76a
&redirect_uri=https://localhost
&scope=openid profile offline_access app_uri_id1/.default
&prompt=consent

Замечание

Клиент, как представляется, не имеет разрешения для API. Это условие ожидается, так как клиент указан как knownClientApplication.

Пример запроса только для рабочих или учебных учетных записей

GET https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?response_type=code
&client_id=72333f42-5078-4212-abb2-e4f9521ec76a
&redirect_uri=https://localhost
&scope=openid profile offline_access User.Read https://graph.microsoft.com/.default
&prompt=consent

Реализация с помощью MSAL.NET

String[] consentScope = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/.default" };
var loginResult = await clientApp.AcquireTokenInteractive(consentScope)
    .WithAccount(account)
	 .WithPrompt(Prompt.Consent)
      .ExecuteAsync();

Распространение согласия для новых служебных принципалов и разрешений может потребовать некоторое время. Приложение должно успешно обрабатывать эту задержку.

Получение токенов для нескольких ресурсов

Если клиентское приложение должно получить маркеры для другого ресурса, например Microsoft Graph, необходимо реализовать логику для обработки потенциальных задержек после предоставления пользователям согласия на приложение. Вот несколько рекомендаций.

  • Используйте область применения .default, когда запрашиваете токены.
  • Отслеживайте приобретенные разрешения до тех пор, пока не будет возвращено требуемое.
  • Добавьте задержку, если результат все еще не имеет требуемого объема.

В настоящее время, если AcquireTokenSilent не удается, MSAL требует успешного интерактивного подтверждения личности, прежде чем разрешить получение другого тихого токена. Это ограничение применяется даже в том случае, если допустимый маркер обновления доступен.

Ниже приведен пример кода, использующего логику повторных попыток:

    public static async Task<AuthenticationResult> GetTokenAfterConsentAsync(string[] resourceScopes)
        {
            AuthenticationResult result = null;
            int retryCount = 0;

            int index = resourceScopes[0].LastIndexOf("/");

            string resource = String.Empty;

            // Determine resource of scope
            if (index < 0)
            {
                resource = "https://graph.microsoft.com";
            }
            else
            {
                resource = resourceScopes[0].Substring(0, index);
            }

            string[] defaultScope = { $"{resource}/.default" };

            string[] acquiredScopes = { "" };
            string[] scopes = defaultScope;
            
            while (!acquiredScopes.Contains(resourceScopes[0]) && retryCount <= 15)
            {
                try
                {
                    result = await clientApp.AcquireTokenSilent(scopes, CurrentAccount).WithForceRefresh(true).ExecuteAsync();
                    acquiredScopes = result.Scopes.ToArray();
                    if (acquiredScopes.Contains(resourceScopes[0])) continue;
                }
                catch (Exception e)
                { }

                // Switch scopes to pass to MSAL on next loop. This tricks MSAL to force AcquireTokenSilent after failure. This also resolves intermittent cachine issue in ESTS
                scopes = scopes == resourceScopes ? defaultScope : resourceScopes;
                retryCount++;

                // Obvisouly something went wrong
                if(retryCount==15)
                {
                    throw new Exception();
                }

                // MSA tokens do not return scope in expected format when .default is used
                int i = 0;
                foreach(var acquiredScope in acquiredScopes)
                {
                    if(acquiredScope.IndexOf('/')==0) acquiredScopes[i].Replace("/", $"{resource}/");
                    i++;
                }

                Thread.Sleep(2000);
            }

            return result;
        }

Сведения о пользовательском API, использующего поток On-behalf-of

Аналогично клиентскому приложению, когда пользовательский API пытается получить маркеры для другого ресурса с помощью потока on-Behalf-Of (OBO), он может завершиться ошибкой сразу после согласия. Чтобы устранить эту проблему, можно реализовать логику повторных попыток и отслеживание областей, как показано в следующем примере кода:

while (result == null && retryCount >= 6)
            {
                UserAssertion assertion = new UserAssertion(accessToken);
                try
                {
                    result = await apiMsalClient.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync();
                    
                }
                catch { }

                retryCount++;

                if (result == null)
                {
                    Thread.Sleep(1000 * retryCount * 2);
                }
            }

If (result==null) return new HttpStatusCodeResult(HttpStatusCode.Forbidden, "Need Consent");

Если все повторные попытки завершаются ошибкой, верните сообщение об ошибке и укажите клиенту начать полный процесс согласия.

Пример клиентского кода, предполагающий, что ваш API выбрасывает исключение 403

HttpResponseMessage apiResult = null;
apiResult = await MockApiCall(result.AccessToken);

if(apiResult.StatusCode==HttpStatusCode.Forbidden)
{
  var authResult = await clientApp.AcquireTokenInteractive(apiDefaultScope)
    .WithAccount(account)
    .WithPrompt(Prompt.Consent)
    .ExecuteAsync();
  CurrentAccount = authResult.Account;

  // Retry API call
  apiResult = await MockApiCall(result.AccessToken); 
}         

Рекомендации и ожидаемое поведение

В идеале вы создадите отдельный поток, который выполняет следующие действия:

  • Руководство пользователей по процессу согласия
  • Настройка приложения и API в арендаторе или учетной записи Microsoft
  • Завершает процесс получения согласия на одном шаге, который отделен от авторизации

Если вы не отделяете этот поток и вместо этого объединяете его с интерфейсом входа в приложение, процесс может стать запутанным. Пользователи могут столкнуться с несколькими запросами на согласие. Чтобы улучшить работу, попробуйте добавить сообщение в приложение, чтобы сообщить пользователям о том, что им может потребоваться предоставить согласие несколько раз:

  • Для учетных записей Майкрософт ожидается по крайней мере два запроса согласия: один для клиентского приложения и один для API.
  • Как правило, для рабочих или учебных учетных записей требуется только один запрос на согласие.

Ниже приведен полный пример кода, демонстрирующий гладкое взаимодействие с пользователем. Этот код поддерживает все типы учетных записей и запрашивает согласие только при необходимости.

string[] msGraphScopes = { "User.Read", "Mail.Send", "Calendar.Read" }
String[] apiScopes = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/access_as_user" };
String[] msGraphDefaultScope = { "https://graph.microsoft.com/.default" };
String[] apiDefaultScope = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/.default" };

var accounts = await clientApp.GetAccountsAsync();
IAccount account = accounts.FirstOrDefault();

AuthenticationResult msGraphTokenResult = null;
AuthenticationResult apiTokenResult = null;

try
{
	msGraphTokenResult = await clientApp.AcquireTokenSilent(msGraphScopes, account).ExecuteAsync();
	apiTokenResult = await clientApp.AcquireTokenSilent(apiScopes, account).ExecuteAsync();
}
catch (Exception e1)
{
	
	string catch1Message = e1.Message;
	string catch2Message = String.Empty;

	try
	{
        // First possible consent experience
		var result = await clientApp.AcquireTokenInteractive(apiScopes)
		  .WithExtraScopesToConsent(msGraphScopes)
		  .WithAccount(account)
		  .ExecuteAsync();
		CurrentAccount = result.Account;
		msGraphTokenResult = await clientApp.AcquireTokenSilent(msGraphScopes, CurrentAccount).ExecuteAsync();
		apiTokenResult = await clientApp.AcquireTokenSilent(apiScopes, CurrentAccount).ExecuteAsync();
	}
	catch(Exception e2)
	{
		catch2Message = e2.Message;
	};

	if(catch1Message.Contains("AADSTS650052") || catch2Message.Contains("AADSTS650052") || catch1Message.Contains("AADSTS70000") || catch2Message.Contains("AADSTS70000"))
	{
        // Second possible consent experience
		var result = await clientApp.AcquireTokenInteractive(apiDefaultScope)
			.WithAccount(account)
			.WithPrompt(Prompt.Consent)
			.ExecuteAsync();
		CurrentAccount = result.Account;
		msGraphTokenResult = await GetTokenAfterConsentAsync(msGraphScopes);
		apiTokenResult = await GetTokenAfterConsentAsync(apiScopes);
	}
}

// Call API

apiResult = await MockApiCall(apiTokenResult.AccessToken);
var contentMessage = await apiResult.Content.ReadAsStringAsync();

if(apiResult.StatusCode==HttpStatusCode.Forbidden)
{
	var result = await clientApp.AcquireTokenInteractive(apiDefaultScope)
		.WithAccount(account)
		.WithPrompt(Prompt.Consent)
		.ExecuteAsync();
	CurrentAccount = result.Account;

	// Retry API call
	apiResult = await MockApiCall(result.AccessToken);
}

Свяжитесь с нами для получения помощи

Если у вас есть вопросы или нужна помощь, создайте запрос на поддержку или обратитесь к поддержке сообщества Azure. Вы также можете отправить отзыв о продукте в сообщество отзывов Azure.