Compartilhar via


Consentimento agrupado para aplicativos de identidade do Microsoft Entra

Este artigo discute como configurar o consentimento empacotado para aplicativos de ID do Microsoft Entra.

Sintomas

Você tem um aplicativo cliente personalizado e um aplicativo de API personalizado e cria registros de aplicativo para ambos os aplicativos na ID do Microsoft Entra. Você configura o consentimento empacotado para ambos os aplicativos. Nesse cenário, você pode receber uma das seguintes mensagens de erro ao tentar entrar em qualquer aplicativo:

  • AADSTS70000: a solicitação foi negada porque um ou mais escopos solicitados não são autorizados ou expiraram. O usuário deve primeiro entrar e conceder ao aplicativo cliente acesso ao escopo solicitado.

  • AADSTS650052: O aplicativo está tentando acessar um serviço\"{app_id}\"(\"app_name\") para o qual sua organização %\"{organization}\" não tem uma entidade de serviço. Entre em contato com o administrador de TI para examinar a configuração de suas assinaturas de serviço ou consentir com o aplicativo para criar a entidade de serviço necessária.

Solução

Etapa 1: configurar knownClientApplications para o registro do aplicativo de API

Adicione a ID personalizada do aplicativo cliente à propriedade do registro do aplicativo de API personalizado de knownClientApplications. Para obter mais informações, consulte o atributo knownClientApplications.

Etapa 2: Configurar permissões de API

Certifique-se de que:

  • Todas as permissões de API necessárias são configuradas corretamente nos registros personalizados do cliente e do aplicativo de API personalizado.
  • O registro de aplicativo cliente personalizado inclui as permissões de API definidas no registro de aplicativo de API personalizado.

Etapa 3: A solicitação de entrada

Sua solicitação de autenticação deve usar o escopo .default do Microsoft Graph. Para contas da Microsoft, o escopo deve ser para a API personalizada.

Solicitação de exemplo para contas da Microsoft e contas corporativas ou de estudante

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

Observação

O cliente parece não ter permissão para a API. Essa condição é esperada porque o cliente está listado como knownClientApplication.

Solicitação de exemplo somente para contas corporativas ou de estudante

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

Implementação usando MSAL.NET

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

A propagação de consentimento para novas entidades de serviço e permissões pode levar algum tempo para ser concluída. Seu aplicativo deve lidar com esse atraso com êxito.

Adquirir tokens para vários recursos

Se o aplicativo cliente precisar adquirir tokens para outro recurso, como o Microsoft Graph, você deverá implementar a lógica para lidar com possíveis atrasos após o consentimento dos usuários para o aplicativo. Veja algumas recomendações:

  • Use o .default escopo ao pedir tokens.
  • Acompanhe os escopos adquiridos até que o necessário seja retornado.
  • Adicione um atraso se o resultado ainda não tiver o escopo necessário.

Atualmente, se AcquireTokenSilent falhar, a MSAL exigirá uma autenticação interativa bem-sucedida antes de permitir outra aquisição de token silenciosa. Essa restrição se aplica mesmo se um token de atualização válido estiver disponível.

Aqui está um exemplo de código que usa a lógica de repetição:

    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;
        }

Sobre a API personalizada que usa o fluxo em nome de outrem

Semelhante ao aplicativo cliente, quando sua API personalizada tenta adquirir tokens para outro recurso usando o fluxo OBO (On-Behalf-Of), ele pode falhar imediatamente após o consentimento. Para resolver esse problema, você pode implementar a lógica de repetição e o controle de escopo, como no seguinte código de exemplo:

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");

Se todas as novas tentativas falharem, retorne uma mensagem de erro e instrua o cliente a iniciar um processo de consentimento completo.

Exemplo de código do cliente que pressupõe que sua API gera um 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); 
}         

Recomendações e comportamento esperado

O ideal é criar um fluxo separado que execute as seguintes ações:

  • Orienta os usuários por meio do processo de consentimento
  • Provisiona seu aplicativo e API em seu locatário ou conta da Microsoft
  • Conclui o consentimento em uma única etapa, separada do login.

Se você não separar esse fluxo e combiná-lo com a experiência de entrada do aplicativo, o processo poderá se tornar confuso. Os usuários podem encontrar vários prompts de consentimento. Para melhorar a experiência, considere adicionar uma mensagem em seu aplicativo para informar aos usuários que eles podem ser solicitados a consentir mais de uma vez:

  • Para contas da Microsoft, espere pelo menos dois prompts de consentimento: um para o aplicativo cliente e outro para a API.
  • Normalmente, para contas corporativas ou de estudante, apenas um prompt de consentimento é necessário.

Veja a seguir um exemplo de código de ponta a ponta que demonstra uma experiência suave do usuário. Esse código dá suporte a todos os tipos de conta e solicita consentimento somente quando necessário.

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);
}

Entre em contato conosco para obter ajuda

Se você tiver dúvidas ou precisar de ajuda, crie uma solicitação de suporte ou peça ajuda à comunidade de suporte do Azure. Você também pode enviar comentários sobre o produto para a comunidade de comentários do Azure.