Open GeorgeTTD opened 3 years ago
I think this is what I am asking for but I am not 100% what it is doing. https://github.com/AzureAD/microsoft-identity-web/wiki/1.2.0#ajax-calls-can-now-participate-in-incremental-consent-and-conditional-access is the x-returnurl header on the request the important part here?
@GeorgeTTD you found the right page, and more is explained here. let us know if this is enough and you're able to resolve your issue.
@jennyf19 I checked out the example AjaxCallActionsWithDynamicConsent yesterday and when I run it on my local machine with a breakpoint set before the ajax call is called but the page has loaded in the browser so that the auth cookie is set. I then deleted the auth cookie as to be unauthenticated again. I then continued on from my breakpoint which caused the ajax call to be made. Now from the documentation on that project I am expecting to see a 401 response from the controller but instead I get a 302 redirect to login again which causes the CORS issue. Is this a bug?
@creativebrother do you have any thoughts on this one? thanks.
We have found this solution works for us as we are only calling API Endpoints from a SPA. If you were to call page actions async then you would need a way to recognise AJAX requests (Potentially x-RequestedWith). I still think there is a bug or a documentation change is required as I don't think this scenario is clear enough.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.Events ??= new OpenIdConnectEvents();
options.Events.OnRedirectToIdentityProvider += OnRedirectToIdentityProviderFunc;
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
private async Task OnRedirectToIdentityProviderFunc(RedirectContext context)
{
if (context.Request.Path.Value.StartsWith("/api"))
{
context.Response.StatusCode = 401;
context.HandleResponse();
}
// Don't remove this line
await Task.CompletedTask.ConfigureAwait(false);
}
@jennyf19 @creativebrother Any update on this at all?
not on my end @GeorgeTTD I hope to have time next week to revisit this.
@GeorgeTTD I am experiencing the problem you referenced on 20 Oct (getting 302 instead of 401)
I've tried adapting your OnRedirectToIdentityProviderFunc
method as a workaround but I take it the call to HandleResponse
stops the additional response handling that occurs in the code for the AuthorizeForScopesAttribute
? The response headers (like 'location') aren't set. I can do something like this (below), but then I get an OpenIdConnectAuthenticationHandler
exception: "message.State is null or empty". This event handler also seems to get called twice for one call - I wouldn't know if that's normal or not:
private Task OnRedirectToIdentityProviderFunc(RedirectContext context)
{
if (context.Request.Path.Value.StartsWith("/api"))
{
context.Response.StatusCode = 401;
if (!context.Response.Headers.ContainsKey("Location"))
context.Response.Headers.Add("Location", context.ProtocolMessage.BuildRedirectUrl());
context.HandleResponse();
}
// Don't remove this line
return Task.CompletedTask;
}
Any help @jennyf19 or @creativebrother may be able to provide would be most appreciated!
Failing that, you may have other ideas about how to approach my scenario... I have an app that requests Graph specific scopes like User.ReadBasic.All, Team.ReadBasic.All etc. - I'm using the ASP Core React SPA template, when users login and authenticate for the first time, they can consent to the requested resources just fine. There is a particular part of my app where I am managing/hosting data in SharePoint (files and permissions), so I have attempted to use AuthorizeForScopes
on the relevant Web API endpoints to dynamically request mytenant.sharepoint.com/AllSites.Write
when this feature is engaged. I think this probably would have worked using the current documented approach, until for whatever reason it started returning 302 instead of 401... any tips/ideas?
@creativebrother 's post here worked for me in terms of getting a 401 back using AuthorizeForScopes
https://github.com/AzureAD/microsoft-identity-web/issues/603#issuecomment-703316365
but I was continually prompted for consent as though the token / session cookie wasn't being updated on the client-side or something.
I tried it a different way - based on the docs here, and used the regular [Authorize]
attribute, caught the MsalUiRequiredException
myself and used _tokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeaderAsync(scopes, ex);
to return the www-authenticate
header in my response. That has the consentUrl I need - which I can hackily parse on the client-side, something like:
return fetch(`/api/controller/action`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"x-ReturnUrl": `${window.location.protocol}//${window.location.host}`
},
credentials: 'include',
cache: 'no-cache'
}).then(async (response) => {
if (response.ok) {
return await response.json();
} else {
const wwwAuthHeader = response.headers.get('www-authenticate');
if (wwwAuthHeader) {
const consentUri = wwwAuthHeader.split(/,? /).filter(v => /consentUri/.test(v)).map((v) => v.replace(/consentUri="(.*)"/, '$1'));
if (consentUri.length) {
// I have to manually replace the redirect_uri, as signin-oidc is throwing message.state is null or empty ??
consentUri[0] = consentUri[0].replace(/(?<=[?|&])(redirect_uri=)[^&]+/, `${window.location.protocol}//${window.location.host}`);
window.location.href = consentUri[0];
}
}
}
});
Using either of these methods I get the consent prompts, and if I check in the Azure portal, consent has been given, but again I'm getting loops - unless I reload the site or access MicrosoftIdentity/Account/SignIn
-
EDIT / UPDATE: I cleared application cookies, revoked consent for my test user using powershell and started from scratch - and these methods (above) work fine for me now. The need to manually alter the redirect_uri on the client-side notwithstanding...
Obviously I'd still prefer a more seamless solution that requires less tinkering, but thank you guys for all the info you guys have put up on this issue and others..
@jennyf19 I checked out the example AjaxCallActionsWithDynamicConsent yesterday and when I run it on my local machine with a breakpoint set before the ajax call is called but the page has loaded in the browser so that the auth cookie is set. I then deleted the auth cookie as to be unauthenticated again. I then continued on from my breakpoint which caused the ajax call to be made. Now from the documentation on that project I am expecting to see a 401 response from the controller but instead I get a 302 redirect to login again which causes the CORS issue. Is this a bug?
@jennyf19 This demo still has the same issue as far as i can see What are we expecting as i just get a 302 that is handled by the browser and blocked by CORS which this demo makes no sense? I must be getting confused as surely this is such a common scenario. Page loaded, come back tomorrow try and use any ajax function and nothing would happen for the user. Any help would be grateful thanks
Almost 3 years later, and this is still a major issue.
The Wiki in this repo says that handling the AJAX stuff is easy-peasy. Just do "this." And then you have us look at the AjaxCallActionsWithDynamicConsent test. But it really DOESN'T work. As multiple people have said, it produces a 302 then a CORS issue.
I start with an initial scope of user.read
and try to incrementally ask for calendars.read
. Seems like a VERY easy problem to reproduce.
Can we get any kind of definitive help on this?
What happens is that you perform the ajax request. Auth is expired. Thus the openidmiddleware starts to redirect to the openid provider, for example login.microsoft.com. And this response is returned. However because its an ajax request. all kind of things fails.
What we did is override the RedirectToIdentityProvider
in the openidoptions.
There we do this:
//set our own appredirectionurl. This was needed for use because we host in a application under the main site. e.g: www.mydomain.net/myapp which previously wasn't supported properly.
context.ProtocolMessage.RedirectUri = AppRedirectUrl(context.Request);
if (context.Request.IsAjaxRequest())
{
context.Response.Headers["Auth-Refresh"] = "1";
context.Response.StatusCode = 401;
context.HandleResponse();
}
return Task.CompletedTask;
Then clientside we have an global ajax response error handler. The example is with jquery but you can easily use your own js framework of choice.
//force page refresh when ajax request fails with 401
$(document).ajaxError(function(event, request, settings) {
let authRefresh = request.getResponseHeader('Auth-Refresh');
if (request.status === 401 && authRefresh === "1") {
if ((window.parent !== null) && (window.parent !== undefined))
window.parent.location.reload();
else
location.reload();
}
});
What this does it that it allows us to signal to the browser that there was an auth issue, and we then resolve that by simply reloading the entire page, causing a normal auth login flow.
Documentation related to component
Please check all that apply
Description of the issue
We currently have a hybrid app with a .net core razor page web application and Vue.js front SPA application which calls through to an api hosted by the razor application.
We have an issue whereby users regularly spend more than an hour on one page and the token expires. This is fine when the user is on the razor page side of the application as the user goes through the refresh loop, however when on the SPA side we start getting 302 status codes which are then blocked by the browser when making AJAX requests.
What we need is an example of how we can send 401's on API routes only or handle the token refresh on the SPA side.