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

Created on 22 Sep 2020  路  31Comments  路  Source: AzureAD/microsoft-identity-web

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

enhancement fixed

Most helpful comment

@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, 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.

All 31 comments

Is the [AuthorizeForScopes(Scopes = new[] { "https://ccbcc.sharepoint.com/AllSites.Read" })]
works like [Authorize] Attribute by checking the header about the token scope to verify the scope exist, otherwise prompt user to a consent page?

@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

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%20id_token&scope=https%3A%2F%2Fccbcc.sharepoint.com%2FAllSites.Read%20openid%20offline_access%20profile&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%20id_token&scope=https%3A%2F%2Fccbcc.sharepoint.com%2FAllSites.Read%20openid%20offline_access%20profile&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 did you have a look at this article: Enable Cross-Origin Requests (CORS) in ASP.NET Core ?

@jmprieur Yes I did. I tried and actually it did not work. Then I realized that I am solving a different problem.
In short:
Does [AuthorizeForScope(Scope="xxx")] covers ajax call into the action method scenario?

[AuthorizeForScope(Scope="xxx")] is good for redirect to the prompt page for user consent when user click on the action link.
but in my case, I use an ajax call to the same action, the page does not redirect(refresh)in browser so user cannot involve the consent page directly hence ajax is redirected and calling into different origin. (localhost to remote origin of https://login.microsoftonline.com/xxx/oauth2/v2.0/authorize?xxx)
So change my code of any CORS thing won't help. The remote origin is not emitting cors header for localhost. Even it does, there will be hard time to bring up the consent within ajax...

It seems [AuthorizeForScope(Scope="xxx")] need to return 200 and some custom header instead of 302 if the request header contains X-Requested-With: XMLHttpRequest so ajax has a chance to check the success result and issue a reload if there is the custom header indicating redirect url for consent prompt?
something like this:
$(document).ajaxSuccess(function(event, request, settings) {
if (request.getResponseHeader('REQUIRES_AUTH') === '1') {
var redirecturl = request.getResponseHeader('AUTH_URL')
window.location =redirecturl;
//Here 'AUTH_URL' = 'https://login.microsoftonline.com/xxx/oauth2/v2.0/authorize?xxx'; set in //[AuthorizeForScope(Scope="xxx")]
}
});

Please let me know if I misunderstood anything.

Thanks,

@jmprieur In the mean time, I have to create another controller action without the [AuthorizeForScope(Scope="xxx")] for the ajax call and return the a 200 with custom data containg redirect url by using ConfidentialClientApplicationBuilder.CreateWithApplicationOptions to construct the authorization endpoint url so the ajax can use the window.location to bring up the consent prompt...

@jmprieur I did the following and now my ajax call is working(kind of ...). Basically I need to remove the [AuthorizeForScopes] attribute.
This way it will return regular view or url for authorization prompt/consent depending on the token scope and ajax can handle both(but not for page).

If the [AuthorizeForScopes] attribute can incorporate this feature would be great.

    [Authorize]
    [HttpGet]
    [Route("RecognitionTile")]
    //[AuthorizeForScopes(Scopes = new[] { "https://ccbcc.sharepoint.com/AllSites.Read" })]
    public async Task<IActionResult> RecognitionTile()
    {
        try
        {
            var aToken = await GetAccessTokenforResource("https://ccbcc.sharepoint.com/AllSites.Read");
        }
        catch(MicrosoftIdentityWebChallengeUserException ex)
        {
            var callbackpath = new PathString("/signin-oidc");
            var request = Request;
            var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, callbackpath);
            var app = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(_applicationOptions).WithRedirectUri(currentUri).Build();
            var appp = app.GetAuthorizationRequestUrl(new string[] { "https://ccbcc.sharepoint.com/AllSites.Read" }).WithLoginHint(User.Identity.Name);
            var authuri = await appp.ExecuteAsync();
            return Unauthorized(new { authurl = authuri.AbsoluteUri });
        }
        return ViewComponent("RecognitionTile");
    }

Hi, @jmprieur After looked at [authorize] source code and [authorizeforscopes] source code
is it possible to do the following like authorizeattribute class is doing:
///


/// A delegate assigned to this property will be invoked when the related method is called.
///

public Func, Task> OnRedirectToReturnUrl { get; set; } = context =>
{
if (IsAjaxRequest(context.Request))
{
context.Response.Headers["Location"] = context.RedirectUri;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};

    private static bool IsAjaxRequest(HttpRequest request)
    {
        return string.Equals(request.Query["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal) ||
            string.Equals(request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal);
    }

in AuthorizeForScopeAttribute:

                AuthenticationProperties properties = IncrementalConsentAndConditionalAccessHelper.BuildAuthenticationProperties(
                    incrementalConsentScopes,
                    msalUiRequiredException,
                    context.HttpContext.User,
                    UserFlow);

                **var httprequest = context.HttpContext.Request;
                if(IsAjaxRequest(httprequest))
                {
                    context.Result = ?
                }
                else
                {
                    context.Result = new ChallengeResult(properties);
                }**

@jmprieur Or is it possible to do it in the Startup.cs

        services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.Events = new CookieAuthenticationEvents
                {
                    OnRedirectToLogin = context =>
                    {
                        if (IsAjaxRequest(context.HttpContext.Request))
                        {
                            context.Response.Headers["Location"] = context.RedirectUri;
                            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                        }
                        else
                        {
                            context.Response.Redirect(context.RedirectUri);
                        }

                        return Task.FromResult(0);
                    }
                };
            })
            .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
            .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
            .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
            .AddDistributedTokenCaches();

@creativebrother if this works for you, this looks good for me, but I'm not an expert
Adding @tratcher for his input

@Tratcher Hi, Even thought it is using cookie authentication, but it is not behaving like it does when the ChallengeResult rendered the redirect result. It is still producing 302 instead of 401.

When my ajax call the action method decorated with [AuthorizeForScopes(Scopes = new[] { "https://ccbcc.sharepoint.com/AllSites.Read" })], its response is NOT 401 but is still a 302 redirect(ChallengeResult) that caused the CORS issue because ajax is silently redirecting to Azure Authorization EndPoint.

Is it possible in the AuthorizeForScopesAttribute to return ChallengeResult(302) or UnauthorizedObjectResult(/Here goes with the redirectUrl for Authorization URL/) based on whether the request is ajax or not ?

I did try this, but can not get a redirect authorization url exactly like from login redirect is creating.

I tried modify the attribute this way at the end of the AuthorizeForScopesAttribute class:

                AuthenticationProperties properties = IncrementalConsentAndConditionalAccessHelper.BuildAuthenticationProperties(
                    incrementalConsentScopes,
                    msalUiRequiredException,
                    context.HttpContext.User,
                    UserFlow);

                var request = context.HttpContext.Request;
                if (IsAjaxRequest(request))
                {
                    IConfiguration configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
                    var applicationOptions = configuration.GetSection("AzureAd").Get<ConfidentialClientApplicationOptions>();
                    var callbackpath = new PathString("/signin-oidc");
                    var loginhint = properties.Parameters["login_hint"].ToString();
                    var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, callbackpath);
                    var app = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(applicationOptions).WithRedirectUri(currentUri).Build();
                    var appp = app.GetAuthorizationRequestUrl(incrementalConsentScopes).WithLoginHint(loginhint);
                    var authuri = appp.ExecuteAsync().Result;
                    context.Result = new UnauthorizedObjectResult(new { authurl = authuri.AbsoluteUri });
                }
                else
                    context.Result = new ChallengeResult(properties);

it does redirect to the authorization endpoint, can scope prompt and everything, but on the call back, I got this error:
Exception: OpenIdConnectAuthenticationHandler: message.State is null or empty.
Unknown location

Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()

@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?

I have altered the AuthorizeForScopesAttribute a bit to let it return a cookie based ChallengeResult within the OnException override
var request = context.HttpContext.Request;
if (IsAjaxRequest(request))
context.Result = new ChallengeResult(CookieAuthenticationDefaults.AuthenticationScheme, properties);
else
context.Result = new ChallengeResult(properties);

Now the ajax call will get 401 instead of 302 from OpenIdConnectDefaults.AuthenticationScheme as @Tratcher have suggested.
However, there are two problems now:

1 The response header gives
location: https://localhost:44354/Account/Login?ReturnUrl=%2FHome%2FRecognitionTile%3F_%3D1601010828886
which is not what I want, we want to challenge the OpenId, not local Cookie Authentication here, this is not useful for
incremental consent since it cannot carry scopes at all, we just go back to the initial login again...
2 And there is a problem here even for cookie authentication though if you examine the Account controller Route, it is missing
Area "MicrosoftIdentity" here and will 404. I reported similar issue before. It was access is denied 403, and it was missing Area
"MicrosoftIdentity". I had to do the following to workaround:
services.Configure(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.AccessDeniedPath = new PathString("/MicrosoftIdentity/Account/AccessDenied");
.....
}

@creativebrother : to enable the MicrosoftIdentity area, you need to add the 'Microsoft.Identity.Web.UI' NuGet package, and enable controllers in the startup.cs:

For instance if you are using Razor pages:

       services.AddRazorPages().AddMvcOptions(options =>
            {
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                options.Filters.Add(new AuthorizeFilter(policy));
            }).AddMicrosoftIdentityUI();

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

@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, 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();                  
                    }
                };
            }); 

@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 how are you handling the AJAX call client side after getting a 401? I setup a similar custom solution but instead I'm returning a 403 (not sure if 401 or 403 is more appropriate here, but really we're doing the same thing). On client side in the AJAX callback function, I check if I get an error and a 403. Then, I'm changing the window location direction to "/MicrosoftIdentity/Account/SignIn" (from the Microsoft.Identity.Web.UI package). This re-signs in the user which refreshes the tokens and then AJAX calls succeed. However, after navigating to the login page, it returns the user back to the homepage after successfully logging in. I tried replacing the redirect URL in a custom call to the login endpoint but that does not work since it's not in the reply URLs for the Azure app registration. So, my best solution is notify the user via alert that their session has expired and they'll be redirected to login. Then it logs them back in and they land back at the home page.

Can you explain how you are handling this client side? Are you able to redirect back to the original page the user was viewing before an MSAL exception occurred from an AJAX call? Are you able to save any state (such as if the user was filling out a form) on the page as well? I'm not happy with forcing the user back to the login screen every time the token needs to be refreshed, but at least I'm able to get my AJAX calls working now.

@creativebrother. Thanks for sharing your learnings.
Do you want to provide a PR (with a test solution?)

@jmprieur Sure! Will do that.

@OnAzureCloud9 For the state info like form data I would think Tempdata, but it perhaps won't work since there are two redirects here. But you can embed your custom data in the OpenIdMessage's state property as exiting auth properties like redirect url etc, since it will be protected when sending to authorize endpoint and on the way back. Specifically I just add a new dictionary item on the AuthenticationProperties.Items and you get it back after authentication.
As for redirect to previous user window location, this should be provided by the challenge itself, if not, an default of "/" is provided to the framework in the RemoteAuthenticationHandler's HandleRequestAsync(). Currently it is providing the "AJAXed URL" which is not fit for my situation because the window then only show partial page designed for the aforementioned AJAX call, I just manually set it in the above mentioned state to whatever URL which contains the AJAX call so the page load all the other info including the ajax part.
In a separate thread you mentioned you issue, I commented about refresh token feature existed in the authentication flow, just wondering why that did not kick in? My understanding is you should not need to manually refresh the token, the MSAL will do it for you, according to the comment on the method AcquireTokenSilent() wrapped by GetAccessTokenForUserAsync() in ITokenAquistion implementation in Mircosoft.Identity.Web.
// If the access token is expired or close to expiration (within
// a 5 minute window), then the cached refresh token (if available) is used to acquire
// a new access token by making a silent network call.

@creativebrother thanks for your contribution on this!
Included in 1.2.0 release.

cc: @OnAzureCloud9

Thanks for the tag @jmprieur. I will try out the 1.2.0 release this weekend with AJAX calls. Do you know if the 1.2.0 release still requires some custom implementation of OpenIdConnect? @creativebrother described that in this post.

@jmprieur great to hear. I will try this out this weekend and report back some results.

@jmprieur looks like we still need to add some custom options like @creativebrother showed here? https://github.com/AzureAD/microsoft-identity-web/issues/603#issuecomment-703316365

I updated my started based on the format linked above with Open ID Connection defaults. When MicrosoftIdentityWebChallengeUserException is thrown it's a very similar issue to before where I receive a CORS error in the browser and no headers or status code information available in the error callback function for the AJAX call.

Looks like I'm having better luck after adding the OnRedirectToIdentityProvider into my app. Is there a reason this isn't in the example linked for Ajax calls? That is what is throwing me off a bit and what made me think I'm doing something wrong.

After adding the OnRedirectToIdentityProvider in Startup file I am getting a 401 now. Still having an issue with it redirecting back to the appropriate page. Looks like in @creativebrother example above he is setting the redirect URI in the OnRedirectToIdentityProvider. Isn't this always going to redirect back to the home page?

To get redirect working properly, I had to update this to use the HTTP context request and fetch the X-ReturnUrl that I'm now passing with my AJAX requests. Then I re-assign that value instead of using "/". I feel like I'm missing something from the 1.2.0 update for Ms.Id.Web:

  • Shouldn't the authorize for scopes attribute now determining that it's an AJAX call and add the appropriate redirect URL to the challenge properties?
  • Do I still need the OnRedirectToIdentityProvider which are missing from the sample or am I missing something there?
Was this page helpful?
0 / 5 - 0 ratings