Search code examples
angularazure-active-directoryoffice365asp.net-core-webapiazure-ad-msal

Azure AD Auth, Angular & .NET API with Graph: MSAL.UiRequiredException Fix?


I'm faced with the following situation: I have two app registrations in Azure AD - an Angular 17 web app and a REST API with .NET 6. I've set up authentication/authorization through Azure AD. The Angular app calls the API with an access token through MSAL, and everything works as intended.

Recently, I extended my API with a Graph client to draft an Outlook email and return a link that Angular can use to open Outlook and display the email.

The user can then review, manually adjust, and send the email. For this, I extended my API app registration in Azure with the "Mail.ReadWrite" scope and implemented it in C# as follows:

  string clientId = _configuration["AzureAd:ClientId"];
  string clientSecret = _configuration["AzureAd:ClientSecret"];
  string tenantId = _configuration["AzureAd:TenantId"];

  string[] scopes = { "Mail.ReadWrite" };

  var options = new OnBehalfOfCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud };
  var onBehalfOfCredential = new OnBehalfOfCredential(tenantId, clientId, clientSecret, userAccessToken, options);

      var graphClient = new GraphServiceClient(onBehalfOfCredential, scopes); 
    
      var requestBody = new Message
      {
          Subject = $"Test",
          Body = new ItemBody
          {
              ContentType = BodyType.Html,
              Content = "Sehr geehrte Damen und Herren,<br/><br/>" +
                        "Test"
          },
          ToRecipients = new List<Recipient>
          {
              new() { EmailAddress = new EmailAddress { Address = sendDocumentsRequest.Receiver } }
          },
          From = new Recipient { EmailAddress = new  EmailAddress { Address = sendDocumentsRequest.Sender } }
      };
    
      // 1. Step: Create E-Mail in drafts folder
      var resultMessage = await graphClient.Me.Messages.PostAsync(requestBody);

and this works perfectly for me.

However, all other users receive the following error message:

2024-03-20 08:28:50.578 +01:00 [ERR] Error while processing the documents in Session ID XXXXX Azure.Identity.AuthenticationFailedException: OnBehalfOfCredential authentication failed: AADSTS65001: The user or administrator has not consented to use the application with ID 'XXXXXX' named 'API_DEV'. Send an interactive authorization request for this user and resource. ---> MSAL.NetCore.4.59.0.0.MsalUiRequiredException: ErrorCode: invalid_grant Microsoft.Identity.Client.MsalUiRequiredException: AADSTS65001: The user or administrator has not consented to use the application with ID 'XXXX' named 'XXXX'. Send an interactive authorization request for this user and resource. Microsoft.Identity.Client.Internal.Requests.RequestBase.HandleTokenRefreshErrorAsync(MsalServiceException e, MsalAccessTokenCacheItem cachedAccessTokenItem) at Microsoft.Identity.Client.Internal.Requests.OnBehalfOfRequest.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.ApiConfig.Executors.ConfidentialClientExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenOnBehalfOfParameters onBehalfOfParameters, CancellationToken cancellationToken) at Azure.Identity.AbstractAcquireTokenParameterBuilderExtensions.ExecuteAsync[T](AbstractAcquireTokenParameterBuilder`1 builder, Boolean async, CancellationToken cancellationToken) at Azure.Identity.MsalConfidentialClient.AcquireTokenOnBehalfOfCoreAsync(String[] scopes, String tenantId, UserAssertion userAssertionValue, Boolean enableCae, Boolean async, CancellationToken cancellationToken) at Azure.Identity.MsalConfidentialClient.AcquireTokenOnBehalfOfAsync(String[] scopes, String tenantId, UserAssertion userAssertionValue, Boolean enableCae, Boolean async, CancellationToken cancellationToken) at Azure.Identity.OnBehalfOfCredential.GetTokenInternalAsync(TokenRequestContext requestContext, Boolean async, CancellationToken cancellationToken) StatusCode: 400

They don't get a consent prompt. I'm unsure if I already confirmed this consent in a unit test. The users who use the application are assigned to groups associated with the application. It appears the issue is with MSAL.NetCore.4.59.0.0.MsalUiRequiredException. How could I program the call differently? How should I address this issue?

enter image description here

enter image description here

=> AdminConsentRequired = no => Microsoft documentation learn.microsoft.com/de-de/graph/permissions-reference


Solution

  • The error usually occurs if you missed granting consent to the added permission in either API or Client applications.

    Initially, I too got same error when I ran same code by passing token generated without granting consent like this:

    enter image description here

    To resolve the error, make sure to grant consent to the added permissions either in App registration or in Enterprise application where admin will review the permissions and grant accordingly.

    In my case, I granted admin consent to added permission in API app like this:

    enter image description here

    When I ran below code in my environment after granting consent, I got response like this:

    using Azure.Identity;
    using Microsoft.Graph;
    using Microsoft.Graph.Me.SendMail;
    using Microsoft.Graph.Models;
    
    namespace GraphSendMail
    {
        class Program
        {
    
            static async Task Main(string[] args)
            {
                try
                {
                    string clientId = "appId";
                    string clientSecret = "secret";
                    string tenantId = "tenantId";
                    string userAccessToken = "token"; // Provide user access token here
    
                    string[] scopes = { "Mail.ReadWrite" };
    
                    var options = new OnBehalfOfCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud };
                    var onBehalfOfCredential = new OnBehalfOfCredential(tenantId, clientId, clientSecret, userAccessToken, options);
    
                    var graphClient = new GraphServiceClient(onBehalfOfCredential, scopes);
    
                    var requestBody = new SendMailPostRequestBody
                    {
                        Message = new Message
                        {
                            Subject = "Test",
                            Body = new ItemBody
                            {
                                ContentType = BodyType.Html,
                                Content = "Sehr geehrte Damen und Herren,<br/><br/>" +
                                          "Test"
                            },
                            ToRecipients = new List<Recipient>
                            {
                                new Recipient
                                {
                                    EmailAddress = new EmailAddress
                                    {
                                        Address = "[email protected]" // Provide recipient email here
                                    }
                                }
                            },
                            CcRecipients = new List<Recipient>
                            {
                                new Recipient
                                {
                                    EmailAddress = new EmailAddress
                                    {
                                        Address = "[email protected]" // Provide cc email here
                                    }
                                }
                            }
                                                },
                        SaveToSentItems = true
                    };
    
                    await graphClient.Me.SendMail.PostAsync(requestBody);
    
                    Console.WriteLine("Email successfully sent.");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"An error occurred: {ex.Message}");
                }
            }
        }
    }
    

    Response:

    enter image description here

    To confirm that, I checked the same in user's Outlook where mail sent successfully like this:

    enter image description here

    If you prefer Administrator to authorize all users by reviewing the permissions, you can check it in Enterprise application like this:

    enter image description here