Partager via


Consentement groupé pour les applications Microsoft Entra ID

Cet article explique comment configurer le consentement groupé pour les applications Microsoft Entra ID.

Symptômes

Vous disposez d’une application cliente personnalisée et d’une application API personnalisée, et vous créez des inscriptions d’applications pour les deux applications dans Microsoft Entra ID. Vous configurez le consentement groupé pour les deux applications. Dans ce scénario, vous pouvez recevoir l’un des messages d’erreur suivants lorsque vous essayez de vous connecter à l’une ou l’autre application :

  • AADSTS70000 : la demande a été refusée, car une ou plusieurs étendues demandées ne sont pas autorisées ou expirées. L’utilisateur doit d’abord se connecter et accorder à l’application cliente l’accès à l’étendue demandée.

  • AADSTS650052 : l’application tente d’accéder à un service\"{app_id}\"(\"app_name\ ») pour laquelle votre organisation %\"{organization}\ » ne dispose pas d’un principal de service. Contactez votre administrateur informatique pour passer en revue la configuration de vos abonnements de service ou donner votre consentement à l’application pour créer le principal de service requis.

Solution

Étape 1 : Configurer knownClientApplications pour l’inscription de l’application API

Ajoutez l’ID d’application cliente personnalisée à la propriété de l’inscription d’application knownClientApplications API personnalisée. Pour plus d’informations, consultez l’attribut knownClientApplications.

Étape 2 : Configurer les autorisations d’API

Assurez-vous que :

  • Toutes les autorisations d'API requises sont correctement configurées sur les inscriptions du client personnalisé et de l'application API personnalisée.
  • L’inscription d’application cliente personnalisée inclut les autorisations d’API définies dans l’inscription d’application API personnalisée.

Étape 3 : Demande de connexion

Votre demande d’authentification doit utiliser l’étendue .default de Microsoft Graph. Pour les comptes Microsoft, l’étendue doit être pour l’API personnalisée.

Exemple de demande pour les comptes Microsoft et les comptes professionnels ou scolaires

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

Remarque

Le client semble manquer d’autorisation pour l’API. Cette condition est attendue, car le client est répertorié en tant que knownClientApplication.

Exemple de demande de comptes professionnels ou scolaires uniquement

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

Implémentation à l’aide de MSAL.NET

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

La propagation du consentement pour les nouveaux principaux de service et les autorisations peut nécessiter un certain temps. Votre application doit gérer correctement ce délai.

Acquérir des jetons pour plusieurs ressources

Si votre application cliente doit acquérir des jetons pour une autre ressource, telle que Microsoft Graph, vous devez implémenter une logique pour gérer les retards potentiels une fois que les utilisateurs ont consenti à l’application. Voici quelques recommandations :

  • Utilisez l’étendue .default lorsque vous demandez des jetons.
  • Assurez le suivi des périmètres acquis jusqu'à obtenir celui requis.
  • Ajoutez un délai si le résultat n’a toujours pas l’étendue requise.

Actuellement, si AcquireTokenSilent échoue, MSAL requiert une authentification interactive réussie avant de permettre une nouvelle acquisition silencieuse de jeton. Cette restriction s’applique même si un jeton d’actualisation valide est disponible.

Voici un exemple de code qui utilise la logique de nouvelle tentative :

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

À propos de l’API personnalisée qui utilise le flux On-behalf-of

Comme pour l’application cliente, lorsque votre API personnalisée tente d’acquérir des jetons pour une autre ressource à l’aide du flux on-Behalf-Of (OBO), il peut échouer immédiatement après le consentement. Pour résoudre ce problème, vous pouvez implémenter la logique de nouvelle tentative et le suivi de l’étendue, comme dans l’exemple de code suivant :

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

Si toutes les nouvelles tentatives échouent, retournez un message d’erreur, puis demandez au client de démarrer un processus de consentement complet.

Exemple de code client qui suppose que votre API lève une version 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); 
}         

Recommandations et comportement attendu

Dans l’idéal, vous créez un flux distinct qui effectue les actions suivantes :

  • Guide les utilisateurs dans le processus de consentement
  • Provisionne votre application et votre API dans leur tenant ou compte Microsoft
  • Achève le processus de consentement en une seule étape distincte de la connexion

Si vous ne séparez pas ce flux et que vous le combinez plutôt avec l’expérience de connexion de votre application, le processus peut devenir déroutant. Les utilisateurs peuvent rencontrer plusieurs invites de consentement. Pour améliorer l’expérience, envisagez d’ajouter un message dans votre application pour informer les utilisateurs qu’ils peuvent être invités à donner leur consentement plusieurs fois :

  • Pour les comptes Microsoft, attendez-vous à au moins deux invites de consentement : une pour l’application cliente et une pour l’API.
  • En règle générale, pour les comptes professionnels ou scolaires, une seule invite de consentement est requise.

Voici un exemple de code de bout en bout qui illustre une expérience utilisateur fluide. Ce code prend en charge tous les types de comptes et ne sollicite le consentement que si nécessaire.

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

Contactez-nous pour obtenir de l’aide

Si vous avez des questions ou avez besoin d’aide, créez une demande de support ou demandez le support de la communauté Azure. Vous pouvez également soumettre des commentaires sur les produits à la communauté de commentaires Azure.