microsoft-identity-web: [Question] How to handle the MsalUiRequiredException for incremental consent with AJAX calls

Which version of Microsoft Identity Web are you using? v0.4.0-preview Where is the issue?

  • Web app
    • Sign-in users
    • [x ] Sign-in users and call web APIs
  • Web API
    • Protected web APIs (validating tokens)
    • Protected web APIs (validating scopes)
    • Protected web APIs call downstream web APIs
  • Token cache serialization
    • In-memory caches
    • Session caches
    • Distributed caches
  • Other (please describe)

Is this a new or an existing app? c. This is a new app or an experiment.

Repro

in startup.cs configureservices:
string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
            services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
                .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
                .AddDistributedTokenCaches();

            services.AddControllersWithViews(options =>
            {
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                options.Filters.Add(new AuthorizeFilter(policy));
            }).AddMicrosoftIdentityUI();
in appsettings.json:
  "DownstreamApi": {
    /*
     'Scopes' contains space separated scopes of the Web API you want to call. This can be:
      - a scope for a V2 application (for instance api:b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/access_as_user)
      - a scope corresponding to a V1 application (for instance <App ID URI>/.default, where  <App ID URI> is the
        App ID URI of a legacy v1 Web application
      Applications are registered in the https:portal.azure.com portal.
    */
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": "user.read"
in controller:
       [Authorize]
        [HttpGet]
        [Route("RecognitionTile")]
        [AuthorizeForScopes(Scopes = new[] { "https://ccbcc.sharepoint.com/AllSites.Read" })]
        public ViewComponentResult RecognitionTile()
        {
            return ViewComponent("RecognitionTile");
        }
for token acquistion;
        /// <summary>
        /// Private: Gets and returns an access token for the provided resource.
        /// </summary>
        /// <param name="resource">Resource to obtain access token for</param>
        /// <returns></returns>
        private async Task<string> GetAccessTokenforResource(string resource)
        {
            // Get the access token for the resource.
            string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { resource });
           //resource is actually same as controller scope -- "https://ccbcc.sharepoint.com/AllSites.Read"

Expected behavior Expect the incremental consent prompt to come up when call the controller. Actual behavior When call the tokenAcquisition.GetAccessTokenForUserAsync(new[] { resource }) they exception is thrown: IDW10502 An MsalUiRequiredException was thrown due to a challenge for the user. Inner Exeption: AADSTS65001: The user or administrator has not consented to use the application with ID ‘xxx’ named ‘xxxx’. Send an interactive authorization request for this user and resource.

Possible solution

Additional context / logs / screenshots This code is trying to access sharepoint api with incremental scope. The initial scope cause the initial consent prompt during authentication: image Cleared all the Permissions prior: image After initial consent: image

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 31 (15 by maintainers)

Most upvoted comments

@creativebrother any luck with your AJAX calls after steps taken in this thread? I am facing the same issues.

@OnAzureCloud9 I tried to do the following in startup.cs and it solved the AJAX issue similar as Cookie Authentication scheme mentioned earlier by @Tratcher. Basically it detect AJAX request and return 401 in Response and in Header Location parameter pass the incremental consent page to get Auth Code. The AJAX has a chance to redirect the browser window by window.location = authcode url and avoid CORS errors.

        services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            options.Events = new OpenIdConnectEvents()
            {
                OnRedirectToIdentityProvider = context =>
                {
                    if (IsAjaxRequest(context.Request))
                    {
                        var message = context.ProtocolMessage;
                        var properties = context.Properties;
                        if (!string.IsNullOrEmpty(message.State))
                        {
                            properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
                        }
                        // When redeeming a 'code' for an AccessToken, this value is needed
                        properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri);

                        message.State = options.StateDataFormat.Protect(properties);
                        if (string.IsNullOrEmpty(message.IssuerAddress))
                        {
                            throw new InvalidOperationException(
                                "Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.");
                        }

                        if (options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
                        {
                            var redirectUri = message.CreateAuthenticationRequestUrl();
                            context.Response.Headers["Location"] = redirectUri;
                            context.Response.StatusCode = 401;

                        }
                        else if (options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
                        {
                           // similar just let AJAX bring up a form post to avoid the CORS here too...
                        }
                        context.HandleResponse();
                    }
                    return Task.FromResult(0);
                }
            };
        });

But I have another problem after I introduced this OnRedirectToIdentityProvider OpenIdConnectEvent, which is now causing an infinite loop on this authcode page similar to #573, #531. It seems that the it is interfering with the following method protected override async Task HandleChallengeAsync(AuthenticationProperties properties) within public class OpenIdConnectHandler : RemoteAuthenticationHandler<OpenIdConnectOptions>, IAuthenticationSignOutHandler. The method is processing posted auth code and then within same method redeming access token. Trying reading all kind of document about auth code flow which is up to date with the code is a challenging task alone,

I just upgraded the Microsoft.Identity.Web to the 1.0.0, will look if there is any luck.

For my project to move ahead, I just admin consent them all and removed the incremental consent, sadly.

@OnAzureCloud9 @Tratcher @jmprieur Finally had sometime to look into the related source code and find a way to correctly handle AJAX call to Action Method deocrated with AuthorizeForScopesAttribute for dynamic consent.

            services.AddOptions<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme).Configure(options =>
            {
                var redirecthandler = options.Events.OnRedirectToIdentityProvider;
                options.Events.OnRedirectToIdentityProvider = async context =>
                {
                    await redirecthandler(context).ConfigureAwait(false); //Need to call this first !!! unlike the original ones in library
                    if (IsAjaxRequest(context.Request))
                    {                        
                        var message = context.ProtocolMessage;
                        var properties = context.Properties;
                        properties.RedirectUri = "/";
                        properties.Items[".redirect"] = "/";
                        if (!string.IsNullOrEmpty(message.State))
                        {
                            properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
                        }                       
                        properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri);

                        message.State = options.StateDataFormat.Protect(properties);
                        if (string.IsNullOrEmpty(message.IssuerAddress))
                        {
                            throw new InvalidOperationException(
                                "Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.");
                        }

                        if (options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
                        {
                            var redirectUri = message.CreateAuthenticationRequestUrl();
                            context.Response.Headers["Location"] = redirectUri;
                            context.Response.StatusCode = 401;

                        }
                        else if (options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
                        {
                           // similar just let AJAX bring up a form post to avoid the CORS here too...
                        }     
                        context.HandleResponse();                  
                    }
                };
            }); 

@jmprieur Sure! Will do that.

@Tratcher After looking at the source code in OpenIdConnectHandler -> HandleChallengeAsync-> HandleChallengeAsyncInternal I need to duplicated the part of the code there in my options.Events.OnRedirectToIdentityProvider handler to short-circuit it to conditionally return and 401 for AJAX type request. Not sure if it is possible to add this logic in the HandleChallengeAsyncInternal or @jmprieur add this redirect logic in options.Events.OnRedirectToIdentityProvider within AddMicrosoftIdentityWebApp extention method? It will greatly help developers on successfully AJAX calling the incremental consent controller actions because it took me quite some time to sort this out after going through lots of scouting in both OpenIdConnect and Microsoft.Identity.Web and MSAL stuff, not to mention the original confusing CORS stuff which triggered it. Thanks,

@creativebrother any luck with your AJAX calls after steps taken in this thread? I am facing the same issues.

@Tratche Actually the problem I am facing is as simple as on web app page using Microsoft.Identity.Web 0.4.0-preview, an ajax call the action method decorated with AuthorizeForScopesAttribute will cause CORS issue with Azure Authorization endpoint because AuthorizeForScopesAttribute is not producing 401 but 302 result because of OpenId Authentication scheme.

If challenge with cookie scheme, it will produce 401 as @Tratcher mentioned. But it is not useful in this case because we want to challenging OpenId Authentication with dynamic scopes to prompt user to consent.

Is there a way to let OpenId scheme authentication handler to emit 401 if it is ajax request, similar to cookie scheme is doing?

Hi, jmprieur Thanks for point out that I should acquire the token in the controller action. Now it seems it is redirecting to the authorization endpoint. The problem is I am usingjquery.ajax call to the controller action decorated with the AuthorizeForScopes attribute. So I have the following cors errors from chrome - see bottom portion. (I tried to remove the custom header from xhr in jquery ajax call to avoid the preflight check) but it still did not work. I thought the ajax is the problem here due to redirect and CORS. If the controller action is invoked by page, then the 302 redirect would be no problem… Could you kindly point out what I am missing?

Chrome error before remove X-Requested-With header : Access to XMLHttpRequest at ‘https://login.microsoftonline.com/xxx/oauth2/v2.0/authorize?client_id=xxx&redirect_uri=https%3A%2F%2Flocalhost%3A44354%2Fsignin-oidc&response_type=code id_token&scope=https%3A%2F%2Fccbcc.sharepoint.com%2FAllSites.Read openid offline_access profile&response_mode=form_post&nonce=xxx&login_hint=xxx&domain_hint=organizations&client_info=1&state=xxx&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0’ (redirected from ‘https://localhost:44354/Home/RecognitionTile?_=1600837060526’) from origin ‘https://localhost:44354’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

Chrome error after remove X-Requested-With header : Access to XMLHttpRequest at ‘https://login.microsoftonline.com/xxx/oauth2/v2.0/authorize?client_id=xxx&redirect_uri=https%3A%2F%2Flocalhost%3A44354%2Fsignin-oidc&response_type=code id_token&scope=https%3A%2F%2Fccbcc.sharepoint.com%2FAllSites.Read openid offline_access profile&response_mode=form_post&nonce=xxx&login_hint=xxx&domain_hint=organizations&client_info=1&state=xxx&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0’ (redirected from ‘https://localhost:44354/Home/RecognitionTile?_=1600837060526’) from origin ‘https://localhost:44354’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

@creativebrother : the AuthorizeForScopes attribute is explained here: https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access

It’s an exception handling attribute (it handles the MsalUiRequiredException) which means that the method by which you acquire the token should be called by your controller action.

You can also place this attribute on the controller/page if all the actions require consent for the same scopes