AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C
MIT License
672 stars 208 forks source link

Should clear session auth cookie if cache is missing account #13

Open jennyf19 opened 4 years ago

jennyf19 commented 4 years ago

from @onovotny and copied from https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/issues/240

In the Microsoft.Identity.Web library, the system should automatically clear (RejectPrincipal()) the auth cookie if the corresponding account entry is missing from the token cache. This can happen if the cache expired, if it was a memory cache and the server bounced, etc.

The issue is that the system is now in an inconsistent state, where the user is considered logged-in, but any operations to call API's won't succeed because there's no token cache, no refresh token, etc.

I've worked around this here: https://github.com/dotnet-foundation/membership

In order to do so, I had to make some changes to ITokenAquisition and the MsalAbstractCacheProvider's GetCacheKey method.

ITokenAcquisition needed a method to check for a user's token cache: https://github.com/dotnet-foundation/membership/blob/97b75e30e50aab76bfa5a21f1ab88bf31ae66da4/Microsoft.Identity.Web/TokenAcquisition.cs#L406-L426

In there, it takes the CookieValidatePrincipalContext to get the incoming ClaimsPrincpal as HtttpContext.User is not yet set at that point. It stores it in the HttpContext.Items via the StoreCookieValidatePrincipalContext extension method (used later by the GetCacheKey method so it can derive the appropriate key): https://github.com/dotnet-foundation/membership/blob/97b75e30e50aab76bfa5a21f1ab88bf31ae66da4/Microsoft.Identity.Web/TokenCacheProviders/MsalAbstractTokenCacheProvider.cs#L68-L69

Finally, the CookieAuthenticationOptions needs to be configured to check for and reject the incoming principal (this could/should be moved into the main IdentityPlatform AddMsal extension methods): https://github.com/dotnet-foundation/membership/blob/97b75e30e50aab76bfa5a21f1ab88bf31ae66da4/Membership/Startup.cs#L110-L123

I can submit these changes as PR if you're in agreement with these changes.

jrmcdona commented 4 years ago

@jennyf19 are you PR'ing this one?

jrmcdona commented 4 years ago

@onovotny do you recommend we put this into our own solution? WIll this go to PR?

jennyf19 commented 4 years ago

@jrmcdona we are still deciding what is the best thing to do. feel free to provide input.

JosephAspey commented 4 years ago

Any update on this on, is it being PR'd?

franva commented 4 years ago

hitting the same issue, do we have any update?

jennyf19 commented 4 years ago

@jmprieur can you respond? thx.

chris5287 commented 4 years ago

Any update here as this seems to be a big blocker for my Blazor server side app atm

jennyf19 commented 4 years ago

@jmprieur fyi

xbdg commented 4 years ago

Same here, working on a server-side Blazor project with Microsoft Identity Web - is there an update or does anyone have a sample workflow of how to workaround this issue ?

mslucasMMID commented 4 years ago

I had the same issue with blazor in the past but not in the last month. During development I could bypass this error by starting an inprivate browser session and close all inprivate sessions before starting a new one.

At the moment I don't have this issue and I don't know if this helps in it. This part of my code it stupid copy/paste without knowing why it works

` // Token acquisition service based on MSAL.NET // and chosen token cache implementation services.Configure(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => false; options.MinimumSameSitePolicy = SameSiteMode.Strict; // Handling SameSite cookie according to https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1 options.HandleSameSiteCookieCompatibility(); });

        services.AddOptions();

        services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddSignIn(x =>
                {
                    Configuration.Bind("AzureAD", x);
                    x.UseTokenLifetime = true;
                    x.GetClaimsFromUserInfoEndpoint = true;
                    x.Events.OnTokenValidated = async ctx =>
                    {

// do something }; }, y => { Configuration.Bind("AzureAD", y); });

        // Token acquisition service based on MSAL.NET
        // and chosen token cache implementation
        services.AddWebAppCallsProtectedWebApi(Configuration,
                new[] {Constants.ScopeUserRead})
            .AddInMemoryTokenCaches();

`

wmgdev commented 4 years ago

Clunky workaround for Blazor Server Apps.

https://github.com/wmgdev/BlazorGraphApi

chris5287 commented 4 years ago

Any guidance update for Blazor server-side?

damienbod commented 4 years ago

Feature request: Request new access token instead of throwing exception

See https://github.com/AzureAD/microsoft-identity-web/issues/588 for details of setup.

It would really help if this (default in-memory cache with API requests) worked after application restarts. It is a really bad dev experience having to clear out the cache before you can test when you have a valid session but no token. Would it be possible to add a feature where if the access token is missing and the identity session is present and valid, then the application gets a new access token instead of throwing an exception. This would help loads

Greetings Damien

jmprieur commented 4 years ago

@damienbod. Unless I mis-understand, I'm confused. We already have this feature: The authorizeForScopes attribute and the MicrosoftIdentityConsentAndConditionalAccessHandler class.

Did you read this: https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access ?

What else would you have in mind? would you think that we don't even need to give a chance to the the developer to handle the exception?

damienbod commented 4 years ago

@jmprieur Thanks for your answer :) Now Im confused. At the moment when I run the application for the first time everything works fine. I use in-memory cache. When I stop my application and then start it again, it no longer works and returns an exception. I would like that no exception is thrown and that on the second start with in-memory cache it just works. (ie gets a new access token)

Is this possible? I don't see this configuration in the doc, maybe I missed something.

With the default in-memory implementation, I have to delete the cookies after every start to test. This is in my opinion not good.

I also think that as in-memory is the default, I should be able to test (stop-start, stop-start) without error.

Greetings Damien

chris5287 commented 4 years ago

This is behaviour we see with Blazor server as well if the connection is lost between the browser and server (eg: connectivity or user refreshes page after a while)

jdaless commented 3 years ago

@damienbod. Unless I mis-understand, I'm confused. We already have this feature: The authorizeForScopes attribute and the MicrosoftIdentityConsentAndConditionalAccessHandler class.

Did you read this: https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access ?

What else would you have in mind? would you think that we don't even need to give a chance to the the developer to handle the exception?

I just ran into this and fixed it with the ConsentHandler, though it would be nice to be able to handle it in a service instead of every time the service is used in a blazor page. Is there a way to do that now?

jmprieur commented 3 years ago

@jdaless : the service needs to communicate with the user, and that can only be through the app. Which is why the flow is a bit convoluted

jmprieur commented 3 years ago

@jennyf19 @clairernovotny

Here is the design I propose for this issue:

Add a new method .WithForceLoginWhenEmptyCache() on the MicrosoftIdentityAppCallsWebApiAuthenticationBuilder to force the users to login when there is a session cookie, but no account in the cache (for instance because the cache is an in memory token cache and the application was restarted).

This would be an opt-in method, used like this:

  services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
                        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                           .WithForceLoginWhenEmptyCache()
                           .AddMicrosoftGraph(Configuration.GetSection("GraphBeta"))
                           .AddInMemoryTokenCaches();

It's implementation could be something like the following:

public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder WithForceLoginWhenEmptyCache()
{
 Services.Configure<CookieAuthenticationOptions>(cookieScheme, options => options.Events = new RejectSessionCookieWhenAccountNotInCacheEvents());
 return this;
}

and with

 internal class RejectSessionCookieWhenAccountNotInCacheEvents : CookieAuthenticationEvents
    {
        public async override Task ValidatePrincipal(CookieValidatePrincipalContext context)
        {
            try
            {
                var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
                string token = await tokenAcquisition.GetAccessTokenForUserAsync(
                    scopes: new[] { "profile" },
                    user: context.Principal);
            }
            catch (MicrosoftIdentityWebChallengeUserException ex)
               when (AccountDoesNotExitInTokenCache(ex))
            {
                context.RejectPrincipal();
            }
        }

        /// <summary>
        /// Is the exception thrown because there is no account in the token cache?
        /// </summary>
        /// <param name="ex">Exception thrown by <see cref="ITokenAcquisition"/>.GetTokenForXX methods.</param>
        /// <returns>A boolean telling if the exception was about not having an account in the cache</returns>
        private static bool AccountDoesNotExitInTokenCache(MicrosoftIdentityWebChallengeUserException ex)
        {
            return ex.InnerException is MsalUiRequiredException
                                      && (ex.InnerException as MsalUiRequiredException).ErrorCode == "user_null";
        }
    }

Alternatively AccountDoesNotExitInTokenCache could be a bool property surfaced on MicrosoftIdentityWebChallengeUserException

jennyf19 commented 3 years ago

@jmprieur one would lose CAE, incremental consent, etc... correct?

clairernovotny commented 3 years ago

@jennyf19 how do you lose anything given that the token cache is empty? Either way, a redirect to the IDP is required, isn't it?

jennyf19 commented 3 years ago

@clairernovotny do you control the tenant in which your app runs? or is it up to the customers? if the latter, then tenant admin can enable some conditional access policies that will trigger a multi-factor authentication even if you have pre-authorization. By not using the [AuthorizeForScopes] attribute you don't allow your app to process this kind of scenario. you'll have to handle it yourself, by challenging the user (if you are in the web app part of your app), or reply with a WWW-Authenticate header (if you are in the web API part of your app). Ms.Id.Web supports these scenarios for you, if you use the exception filter attribute. Otherwise you are on your own.

clairernovotny commented 3 years ago

I control the tenant where the app runs, it's not up to the customers. There's no chance CA will take effect here.

jennyf19 commented 3 years ago

@jmprieur i think we should document this specific scenario, but not take any product changes.

clairernovotny commented 3 years ago

@jennyf19 there's no workaround today as the GetAccessTokenForUserAsync is internal. If that was external, I could code around it. I should also note that this is part of an ADAL migration and I don't have time to rewrite the app around Identity Web's new concepts. I'm sure I'm not the only one in this boat. For new apps, it's easier to work with the new model, but it needs to be easy to get off of ADAL given its imminent deprecation.

Adding things like support for incremental consent and CA are "nice to have," and important, but apps migrating from ADAL already don't support this, so they're no worse off. They need a path forward off of the deprecated library. They can then gradually add support for the missing functionality.

jmprieur commented 3 years ago

@clairernovotny : I provided the work around and tested it.

In Startup.cs:

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
                        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                           .AddMicrosoftGraph(Configuration.GetSection("GraphBeta"))
                           .AddInMemoryTokenCaches();

Services.Configure<CookieAuthenticationOptions>(cookieScheme, options => options.Events = new RejectSessionCookieWhenAccountNotInCacheEvents());

And then (inspired by what you had)

internal class RejectSessionCookieWhenAccountNotInCacheEvents : CookieAuthenticationEvents
    {
        public async override Task ValidatePrincipal(CookieValidatePrincipalContext context)
        {
            try
            {
                var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
                string token = await tokenAcquisition.GetAccessTokenForUserAsync(
                    scopes: new[] { "profile" },
                    user: context.Principal);
            }
            catch (MicrosoftIdentityWebChallengeUserException ex)
               when (AccountDoesNotExitInTokenCache(ex))
            {
                context.RejectPrincipal();
            }
        }

        /// <summary>
        /// Is the exception thrown because there is no account in the token cache?
        /// </summary>
        /// <param name="ex">Exception thrown by <see cref="ITokenAcquisition"/>.GetTokenForXX methods.</param>
        /// <returns>A boolean telling if the exception was about not having an account in the cache</returns>
        private static bool AccountDoesNotExitInTokenCache(MicrosoftIdentityWebChallengeUserException ex)
        {
            return ex.InnerException is MsalUiRequiredException
                                      && (ex.InnerException as MsalUiRequiredException).ErrorCode == "user_null";
        }
    }
jennyf19 commented 3 years ago

@clairernovotny GetAccessTokenForUserAsync is public. If this is a 1P migration, from ADAL to MSAL, we have a procedure in place to assist with migration. Please ping one of us and we'll send you the link to the Teams channel. ADAL did not support incremental consent, that's a v2 concept, but it does support CA.

clairernovotny commented 3 years ago

I didn't realize that was public, thanks. Documenting it would be helpful, for sure somewhere in the wiki where it'll be seen.

This isn't 1P, it's for the .NET Foundation's code signing service, so like 1.5P. Not sure if that counts?

jennyf19 commented 3 years ago

sounds good.

the migration is for web apps/web APIs right now, so if that makes sense, feel free to join.

clairernovotny commented 3 years ago

That's what it is, it's a combination web app and api app. It could very much benefit from an updated security review too to ensure it's doing the right things with the new code. what's the right way to get started?

jmprieur commented 3 years ago

@jmprieur i think we should document this specific scenario, but not take any product changes.

@jennyf19 : I've added a small paragraph in the existing article on how to handle the empty cache, incremental consent, conditional access, etc: https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access#clearing-the-authentication-cookie-instead

jennyf19 commented 2 years ago

closing as documented by @jmprieur above.

kelps commented 2 years ago

I was trying this workaround here, but I noticed that the code that was supposed to validate and reject the principal isn't always called. What happens in my case is that I still get the exception when I try to call the GetAccessTokenForUserAsync.

This is very problematic, since I need this code the call a downtream API with the user principal. On new sign-ins, it works. After a while it starts to fail and I have to force the user to sign-in again to fix. I even put a workaround link in my page that calls the sign in code again and comes back to retry the API call.

My version of the RejectSessionCookieWhenAccountNotInCacheEvents class has only 1 difference from the workaround proposed here by @jmprieur. My code reads the required scopes from the app settings instead of using a fixed value. Everything else in this class is identical.

But I'm registering the class using the extension method services.ConfigurApplicationCookie instead of using services.Configure. Could that be the problem?

My case runs like this: Azure Functions behind APIM, ASPNET Core app (site). Both in .NET6. Azure AD sign-in (Office 365). The site calls the functions (behind APIM) from the server side code. The client side code only calls back to a "reverse proxy" api endpoint in the site. This proxy api is responsible for getting the auth token and make the actual call to the Azure Functions as the user. We decided to do this way in order to increase our security and create firewall rules between the Functions and the Site.

@clairernovotny and @jennyf19: did the workaround work for you?

clairernovotny commented 2 years ago

I'm having similar issues, not sure why. The ValidatePrincipal code isn't even being called in the Coookie auth event handler.

clairernovotny commented 2 years ago

This needs to be re-opened @jennyf19 @jmprieur

kelps commented 2 years ago

I'm having similar issues, not sure why. The ValidatePrincipal code isn't even being called in the Coookie auth event handler.

Exactly what happens to me. I was ready to accept that maybe it was being called some times, but not when I was debugging it, but I placed some logs in the method, and the logs NEVER registered.

Could there be something else we could do? Maybe a different workaround?

kelps commented 2 years ago

Any news on this? I just spent a few hours trying to debug this without success. From what I glimpsed, AuthenticationMiddleware runs a foreach on Schemes.GetRequestHandlerSchemesAsync(), but the Cookie handler is not returned, even though it is in the schemes array. That seems to be why the ValidatePrincipal code is never called.

the order in my code is as follows:

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("Authentication:AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetValue<IEnumerable<string>>("Authentication:FuncoesAzure:Scopes"))
    .AddInMemoryTokenCaches();

builder.Services.ConfigureApplicationCookie(options => {
    options.Cookie.HttpOnly = true;
    options.SlidingExpiration = true;
    options.Events.OnValidatePrincipal = MyAuthenticationAPIHelper.ValidatePrincipal; //this is the code that was provided in the workaround above
});

Without this, I can't safely make my app work as it should. The app needs to call some external HTTP apis using the User's token from AAD, but many times it is not able to do so because the app can only generate the token if the user reauthenticate. I even added a button in one page to test and when this happens the user needs to click to and the SignIn code runs again, but that is really bad. I am almost addind a new middleware "before" the AuthenticationMiddleware to try to do this little dance for me (call the auth handler, check if it can generate the token, if not, try to force a new auth cicle...)

sidediallo commented 2 years ago

@kelps it is necessary to specify the authentication scheme for the event to be triggered.

services.Configure<CookieAuthenticationOptions>(
     CookieAuthenticationDefaults.AuthenticationScheme,
     options =>
     {
            options.Events = new RejectSessionCookieWhenAccountNotInCacheEvents(); //this is the code that was provided in the workaround above
     });

Also, be careful because with each action performed on your web application, the event is launched (which is normal) and a token is generated with the scopes "profile" and the Claims "context.Principal" thanks to the following code (by default it checks in the cache token before generating a new token)

string token = await tokenAcquisition.GetAccessTokenForUserAsync(
                        scopes: new[] { "profile" },
                        user: context.Principal);

Generating the token (several times) with the "profile" scope causes an exception MsalClientException: The cache contains multiple tokens satisfying the requirements. Try to clear token cache. You must therefore ensure that the token is generated only once and if necessary.

h2df commented 2 years ago

I'm new to MSAL and get confused by the exception message MsalClientException: The cache contains multiple tokens satisfying the requirements. Try to clear token cache . Would you mind elaborate a little why this is happening (I've adopted the code provided above and got the same exception), and point to the doc regarding how to clear the token cache? Many thanks!!

jmprieur commented 2 years ago

@bgavrilMS @pmaytak : when can this happen? MsalClientException: The cache contains multiple tokens satisfying the requirements. Try to clear token cache (the rest of the issues is irrelevant for you)

bgavrilMS commented 2 years ago

Hi @jmprieur, this error occurs when MSAL's access token filtering logic returns more than 1 result. We filter user tokens by:

Out of all of these, the scopes might be problematic, because there could be overlapping scopes etc. Note that we compare request scopes to response scopes. MSAL has logic to delete access tokens with overlapping scopes, but maybe we have a bug there?

@h2df @sidediallo Is it possible to enable verbose MSAL logging with PII to understand this better? Feel free to send them directly to bogavril[at]microsoft.com

Or to give exact repro steps, i.e. what scopes are you asking for the first time and what scopes are you asking for the second time when you get the error? And what user is logging in (Personal account or Work School account?). But logs would be better.

sidediallo commented 2 years ago

Hi @bgavrilMS and thank you for your feedback. Here is a snippet of the code that generates the error. Some of this code comes from @jmprieur.

public void ConfigureServices(IServiceCollection services)
{
     services.AddDistributedMemoryCache();

     services.AddOptions();

     services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                  .AddMicrosoftIdentityWebApp(Configuration)
                  .EnableTokenAcquisitionToCallDownstreamApi()
                  .AddInMemoryTokenCaches();

      // MicrosoftIdentityWebChallengeUserException
      services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme,
              options =>
              {
                  options.Events = new RejectSessionCookieWhenAccountNotInCacheEvents();
              });
}

internal class RejectSessionCookieWhenAccountNotInCacheEvents : CookieAuthenticationEvents
{
        public async override Task ValidatePrincipal(CookieValidatePrincipalContext context)
        {
            try
            {
                var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();

                var token = await tokenAcquisition.GetAccessTokenForUserAsync(
                    scopes: new[] { "profile" },
                    user: context.Principal);
            }
            catch (MicrosoftIdentityWebChallengeUserException ex)
                when (AccountDoesNotExitInTokenCache(ex))
            {
                context.RejectPrincipal();
            }
        }

        private bool AccountDoesNotExitInTokenCache(MicrosoftIdentityWebChallengeUserException ex)
        {
            return ex.InnerException is MsalUiRequiredException
                                      && (ex.InnerException as MsalUiRequiredException).ErrorCode == MsalError.UserNullError;
        }
}

I added it to manage the MicrosoftIdentityWebChallengeUserException error which is triggered following the restart of the app services or following a long period of inactivity of the application. FYI, I am trying to modify the code so that:

var token = await tokenAcquisition.GetAccessTokenForUserAsync(
                    scopes: new[] { "profile" },
                    user: context.Principal);

only run if the account is not present in the token cache. The scope I use is always the same and the account used is a professional account. I'm trying to enable MSAL logging with PII and as soon as it's ok I'll send you the logs.

MauRiEEZZZ commented 2 years ago

@sidediallo doe you have any progress yet to implement the GetAccessTokenForUserAsync in the case of an "AccountDoesNotExitInTokenCache" exception?

DerekFoulk commented 2 years ago

This is not constructive in any way, but I'm going to share anyway... This functionality led to a couple of days trying to figure out why my app was losing the token that I expected to be there. I'm building new Blazor Server/ASP.NET Core Web API projects and trying to secure them with Azure B2C. This is my first time doing this, so the documentation wasn't super helpful. I feel like you need to have a grasp on Microsoft Identity before you can decipher the information in the provided docs.

I eventually stumbled upon https://github.com/Azure-Samples/ms-identity-blazor-server/blob/main/WebApp-OIDC/B2C/README.md, which got me 90% of the way there, but I screwed up by wrapping the call to 'GetAccessTokenForUserAsync()' in a try-catch to log whatever the exception was. Today, I finally realized that this exception needed to be left unhandled so that it would bubble up to the 'ConsentHandler.HandleException()' call.

I would just like to echo the sentiments of others: this functionality is very unintuitive. It would be nice if the VS2022 Blazor Server template would use a different cache method for tokens (if that would indeed solve the issue). This functionality nearly pushed me away from Azure AD B2C all together in favor of something like an API key auth model. I'm glad I stuck with it and followed the rabbit hole to the bottom, but still. It shouldn't have been this hard to get Blazor Server querying an API that is protected with Microsoft Identity.

I feel that I wouldn't have encountered this issue if I was using Azure AD (non-B2C). That development experience was always pretty intuitive and easy to build.

I'll shut up. Thanks for listening.

jennyf19 commented 2 years ago

It's actually very constructive and helpful feedback @DerekFoulk Thank you for sharing your experience and thoughts on improvements.

jmprieur commented 2 years ago

Thanks @DerekFoulk for this feedback. Did you create your app from the File | New project experience?

erhan355 commented 2 years ago

@kelps it is necessary to specify the authentication scheme for the event to be triggered.

services.Configure<CookieAuthenticationOptions>(
     CookieAuthenticationDefaults.AuthenticationScheme,
     options =>
     {
            options.Events = new RejectSessionCookieWhenAccountNotInCacheEvents(); //this is the code that was provided in the workaround above
     });

Also, be careful because with each action performed on your web application, the event is launched (which is normal) and a token is generated with the scopes "profile" and the Claims "context.Principal" thanks to the following code (by default it checks in the cache token before generating a new token)

string token = await tokenAcquisition.GetAccessTokenForUserAsync(
                        scopes: new[] { "profile" },
                        user: context.Principal);

Generating the token (several times) with the "profile" scope causes an exception MsalClientException: The cache contains multiple tokens satisfying the requirements. Try to clear token cache. You must therefore ensure that the token is generated only once and if necessary.

This is the same problem i am facing with. How can we ensure that the token is generated only once ? Issue was opened almost 2,5 years ago still not a valid solution other than workaround that causes new issues.

jmprieur commented 2 years ago

@erhan355. Can you please remind me why using [AuthorizeForScopes] attribute does not work for you? https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access ?

erhan355 commented 2 years ago

@erhan355. Can you please remind me why using [AuthorizeForScopes] attribute does not work for you? https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access ?

It is a bit long reply, I tried to sum up my flow briefly. First of all I don't think it is about AuthorizeForScopes attribute. As you already know that if the browser has a cached cookie but corresponding account entry is missing from the token cache , it causes MicrosoftIdentityWebChallengeUserException.

To prevent this , I used your workaround(RejectSessionCookieWhenAccountNotInCacheEvents ) but this time it causes another exception.

My app calls both my protected azure api and graph.When app starts on debugging mode it first hits RejectSessionCookieWhenAccountNotInCacheEvents and context.RejectPrincipal(); is executed image Then hits RejectSessionCookieWhenAccountNotInCacheEvents again and obtain acccess token however access token doesn't include profile scope even though following scopes are requested in url. image

image

I think Gatewayscope comes to play because of being default scope of downstream api but I can't figure out that why profile scope doesn't present in token.That's weird.

image

After passing RejectSessionCookieWhenAccountNotInCacheEvents without exception it hits index page with following attribute.(Because It is razor page not mvc, attribute is placed on the top of the file) [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/User.Read.All","api://3bc5e4b0-70cc-4dd9-8763-1b754c68344a/GatewayScope", "api://80acf42d-562a-4543-b429-7b93263dfb2d/YakitKartScopes" })]

image

When it is done with OnGet method on my Index page ,header component is called on my layout which fetches current user's photo when it finishes job(successfully obtains access token)

RejectSessionCookieWhenAccountNotInCacheEvents is hit again and an exception is thrown.

image image

MsalClientException: The cache contains multiple tokens satisfying the requirements. Try to clear token cache.

image

By the way despite demanding User.Read scope at GetPhotoAsBase64Async at HeaderViewComponent ,access token with following scopes "scp": "User.Read User.Read.All profile openid email" are received.I think this is because of configured app permissions. image

DerekFoulk commented 2 years ago

@jmprieur Indeed, I did. One important thing that I didn't mention is that I am using VS2022 Preview. Here is my VS2022 "Info" output:

Microsoft Visual Studio Community 2022 Version 17.3.0 Preview 1.1 VisualStudio.17.Preview/17.3.0-pre.1.1+32519.111 Microsoft .NET Framework Version 4.8.04084

Installed Version: Community

.NET Core Debugging with WSL 1.0 .NET Core Debugging with WSL

ADL Tools Service Provider 1.0 This package contains services used by Data Lake tools

ASA Service Provider 1.0

ASP.NET and Web Tools 2019 17.3.122.33185 ASP.NET and Web Tools 2019

Azure App Service Tools v3.0.0 17.3.122.33185 Azure App Service Tools v3.0.0

Azure Data Lake Tools for Visual Studio 2.6.5000.0 Microsoft Azure Data Lake Tools for Visual Studio

Azure Functions and Web Jobs Tools 17.3.122.33185 Azure Functions and Web Jobs Tools

Azure Stream Analytics Tools for Visual Studio 2.6.5000.0 Microsoft Azure Stream Analytics Tools for Visual Studio

C# Tools 4.3.0-1.22254.1+9919d7e7bd753404a5d2328e5e3fb2de635169f3 C# components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Common Azure Tools 1.10 Provides common services for use by Azure Mobile Services and Microsoft Azure Tools.

Microsoft Azure Hive Query Language Service 2.6.5000.0 Language service for Hive query

Microsoft Azure Stream Analytics Language Service 2.6.5000.0 Language service for Azure Stream Analytics

Microsoft Azure Tools for Visual Studio 2.9 Support for Azure Cloud Services projects

Microsoft JVM Debugger 1.0 Provides support for connecting the Visual Studio debugger to JDWP compatible Java Virtual Machines

NuGet Package Manager 6.3.0 NuGet Package Manager in Visual Studio. For more information about NuGet, visit https://docs.nuget.org/

Razor (ASP.NET Core) 17.0.0.2222701+751db1ebea5e6a9ecc7fa57fe447180422afa610 Provides languages services for ASP.NET Core Razor.

SQL Server Data Tools 17.0.62204.01010 Microsoft SQL Server Data Tools

ToolWindowHostedEditor 1.0 Hosting json editor into a tool window

TypeScript Tools 17.0.10420.2001 TypeScript Tools for Microsoft Visual Studio

Visual Basic Tools 4.3.0-1.22254.1+9919d7e7bd753404a5d2328e5e3fb2de635169f3 Visual Basic components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Visual F# Tools 17.1.0-beta.22178.3+6da0245a7ce4bb8483b8d1f2993c8ecaea967ad9 Microsoft Visual F# Tools

Visual Studio IntelliCode 2.2 AI-assisted development for Visual Studio.