Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] How to handle the MsalUiRequiredException for incremental consent with AJAX calls #603

Closed
7 tasks
creativebrother opened this issue Sep 22, 2020 · 31 comments
Assignees
Labels
enhancement New feature or request fixed
Milestone

Comments

@creativebrother
Copy link
Contributor

creativebrother commented Sep 22, 2020

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

@creativebrother
Copy link
Contributor Author

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?

@jmprieur
Copy link
Collaborator

@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

@jennyf19 jennyf19 added answered question Further information is requested labels Sep 22, 2020
@jennyf19 jennyf19 changed the title [Bug] [Question] How to handle the MsalUiRequiredException for incremental consent Sep 22, 2020
@creativebrother
Copy link
Contributor Author

creativebrother commented Sep 23, 2020

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.

@jmprieur
Copy link
Collaborator

jmprieur commented Sep 23, 2020

@creativebrother did you have a look at this article: Enable Cross-Origin Requests (CORS) in ASP.NET Core ?

@creativebrother
Copy link
Contributor Author

creativebrother commented Sep 23, 2020

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

@creativebrother
Copy link
Contributor Author

creativebrother commented Sep 23, 2020

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

@creativebrother
Copy link
Contributor Author

creativebrother commented Sep 23, 2020

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

@creativebrother
Copy link
Contributor Author

creativebrother commented Sep 24, 2020

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<RedirectContext, 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);
                }**       

@creativebrother
Copy link
Contributor Author

creativebrother commented Sep 24, 2020

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

@jmprieur
Copy link
Collaborator

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

@Tratcher
Copy link

@creativebrother
Copy link
Contributor Author

creativebrother commented Sep 25, 2020

@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()

@creativebrother
Copy link
Contributor Author

creativebrother commented Sep 25, 2020

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

@creativebrother
Copy link
Contributor Author

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

@jmprieur
Copy link
Collaborator

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

@ITRON-kpalmberg
Copy link

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

@creativebrother
Copy link
Contributor Author

creativebrother commented Oct 1, 2020

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

@creativebrother
Copy link
Contributor Author

creativebrother commented Oct 4, 2020

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

@creativebrother
Copy link
Contributor Author

creativebrother commented Oct 4, 2020

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

@ITRON-kpalmberg
Copy link

ITRON-kpalmberg commented Oct 5, 2020

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

@jmprieur
Copy link
Collaborator

jmprieur commented Oct 5, 2020

See also Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2#264 (comment)

@jmprieur jmprieur changed the title [Question] How to handle the MsalUiRequiredException for incremental consent [Question] How to handle the MsalUiRequiredException for incremental consent with AJAX calls Oct 5, 2020
@jmprieur jmprieur added enhancement New feature or request and removed answered question Further information is requested labels Oct 5, 2020
@jmprieur jmprieur added this to the [6] Support new scenarios milestone Oct 5, 2020
@jmprieur
Copy link
Collaborator

jmprieur commented Oct 5, 2020

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

@creativebrother
Copy link
Contributor Author

@jmprieur Sure! Will do that.

@creativebrother
Copy link
Contributor Author

creativebrother commented Oct 5, 2020

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

@jennyf19
Copy link
Collaborator

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

@jmprieur
Copy link
Collaborator

cc: @OnAzureCloud9

@ITRON-kpalmberg
Copy link

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
Copy link
Collaborator

No, it shouldn't @OnAzureCloud9 : the startup.cs is pretty standard now.

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)

@ITRON-kpalmberg
Copy link

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

@ITRON-kpalmberg
Copy link

@jmprieur looks like we still need to add some custom options like @creativebrother showed here? #603 (comment)

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.

@ITRON-kpalmberg
Copy link

ITRON-kpalmberg commented Oct 25, 2020

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request fixed
Projects
None yet
Development

No branches or pull requests

5 participants