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

Problem Using Identity Web with Blazor Server Client App #442

Closed
16 tasks
DigitalPZ opened this issue Dec 4, 2020 · 21 comments
Closed
16 tasks

Problem Using Identity Web with Blazor Server Client App #442

DigitalPZ opened this issue Dec 4, 2020 · 21 comments
Labels
Answered question Further information is requested

Comments

@DigitalPZ
Copy link

Please provide us with the following information:

This issue is for a: (mark with an x)

- [ X] bug report -> please search issues before submitting
- [ ] feature request
- [ ] documentation issue or request
- [ ] regression (a behavior that used to work and stopped in a new release)

The issue was found for the following scenario:

Please add an 'x' for the scenario(s) where you found an issue

  1. Web app that signs in users
    1. with a work and school account in your organization: 1-WebApp-OIDC/1-1-MyOrg
    2. with any work and school account: /1-WebApp-OIDC/1-2-AnyOrg
    3. with any work or school account or Microsoft personal account: 1-WebApp-OIDC/1-3-AnyOrgOrPersonal
    4. with users in National or sovereign clouds 1-WebApp-OIDC/1-4-Sovereign
    5. with B2C users 1-WebApp-OIDC/1-5-B2C
  2. Web app that calls Microsoft Graph
    1. Calling graph with the Microsoft Graph SDK: 2-WebApp-graph-user/2-1-Call-MSGraph
    2. With specific token caches: 2-WebApp-graph-user/2-2-TokenCache
    3. Calling Microsoft Graph in national clouds: 2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph
  3. Web app calling several APIs 3-WebApp-multi-APIs
  4. Web app calling your own Web API
    1. [X ] with a work and school account in your organization: 4-WebApp-your-API/4-1-MyOrg
    2. with B2C users: 4-WebApp-your-API/4-2-B2C
    3. with any work and school account: 4-WebApp-your-API/4-3-AnyOrg
  5. Web app restricting users
    1. by Roles: 5-WebApp-AuthZ/5-1-Roles
    2. by Groups: 5-WebApp-AuthZ/5-2-Groups
  6. Deployment to Azure
  7. Other (please describe)

Repro-ing the issue

Repro steps

Generate new Blazor Server app to use as client and Asp.Net Core Web API using the templates shipped with Visual Studio 16.8.2. Choose Authentication with work or school account in both cases. Add the client secret to the client app appsettings.json file and add code as prescribed in the wiki in this repo to enable token retrieval.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi() .AddInMemoryTokenCaches();

Add additional consent handler code to Blazor page as per instructions found in this wiki.
` try
{
forecasts = await ForecastService.GetForecastAsync();

    }
    catch (Exception ex)
    {
        ConsentHandler.HandleException(ex);
    }

`

Retrieve token and call API with the following code:
string[] scopes = new[] { _config["API:Scope"] }; var token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await _httpClient.GetAsync($"{_config["API:BaseAddress"] }/WeatherForecast");

Navigate to Blazor page that calls the service that calls the API.

Expected behavior

Get asked for consent
Get asked to login and provide credentials
API gets called

Actual behavior

Consent is asked for and given
Dialog pops up asking which work or school account to use and when I choose it, it loops back to the same dialog asking the same question. I placed a breakpoint on the controller method in the API and it is never reached.

Possible Solution

Additional context/ Error codes / Screenshots

Any log messages given by the failure

Add any other context about the problem here, such as logs.

OS and Version?

Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?)
Windows 10 - latest production release

Versions

of ASP.NET Core, of MSAL.NET
latest stable Nuget packages Identity.Web is 1.3

Attempting to troubleshooting yourself:

Mention any other details that might be useful

If I remove the [Authorize] attribute from the API controller the authentication loop does not occur.


Thanks! We'll be in touch soon.

@Shama-K
Copy link
Contributor

Shama-K commented Dec 4, 2020

@DigitalPZ, Can you please verify if the method where you catch exception and handle consent have AuthorizeForScopes attribute?
You can also follow Blazor server tutorial and let us know if it solves the issue.

@DigitalPZ
Copy link
Author

I did not have the AuthorizeForScopes attribute on that method but I have now added it based on the server tutorial you linked to above and I still get the authentication looping_.

@Shama-K
Copy link
Contributor

Shama-K commented Dec 4, 2020

Can you check if you have added API permissions in Blazor server app on Azure portal?
Registration steps are mentioned in the Readme.
If the registration is correct then please share the config files and fiddler trace.

@DigitalPZ
Copy link
Author

The Blazor server app is registered as per the instructions as is the API app. The Blazor app has the extra item in appsettings for the client secret. I also made sure that the app registration for the Blazor app was given API permissions to the API scope that was registered. I have the Fiddler trace. What config files do you need and where can I send it all to?

@Shama-K
Copy link
Contributor

Shama-K commented Dec 8, 2020

Contact to share fiddler trace: v-lnushama At microsoft.com
Please use a test tenant and a test user account as Fiddler might capture your credentials.

@DigitalPZ
Copy link
Author

Contact to share fiddler trace: v-lnushama At microsoft.com
Please use a test tenant and a test user account as Fiddler might capture your credentials.

Email sent

@DigitalPZ
Copy link
Author

Shama, is the v- part of your email address? The email I sent you was rejected as address not found.

@Shama-K
Copy link
Contributor

Shama-K commented Dec 8, 2020

Shama, is the v- part of your email address? The email I sent you was rejected as address not found.

yes, it's "v-lnushama".

@DigitalPZ
Copy link
Author

I resent it, I thought the l was a capital i.

@Shama-K
Copy link
Contributor

Shama-K commented Dec 10, 2020

@DigitalPZ, Could you please check Startup.cs in your project and see if scope is passed in EnableTokenAcquisitionToCallDownstreamApi. Here is the code for your reference Startup.cs.
Please let me know if it solved your problem.

@DigitalPZ
Copy link
Author

I checked and I was not passing the scope in there. I was passing the scope in the GetAccessTokenForUserAsync call. I left it in there and added it in the startup call. I revoked the admin consent in the portal and re-tested and it now works. Why does it need to be in both places? I left it out because I saw documentation that said you could leave it out of startup and ask for it when needed.

@jmprieur jmprieur added Answered question Further information is requested labels Dec 10, 2020
@jmprieur
Copy link
Contributor

Which documentation is this, @DigitalPZ ?

@DigitalPZ
Copy link
Author

Which documentation is this, @DigitalPZ ?

Sorry I don't remember where I saw it, I went through so many different pieces of documentation trying to solve this.

@LockTar
Copy link

LockTar commented Dec 28, 2020

Hi,
I have the same issue as @DigitalPZ.

I have all the tooling (visual studio 2019) and nuget packages on the latest version.
I have a new blazor server app. An API that I want to call (in the future I want to call multiple API's). So I have the following code:

Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
                .EnableTokenAcquisitionToCallDownstreamApi()
                //.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "api://HiddenGuid/User.Read" })
                .AddInMemoryTokenCaches();
            services.AddControllersWithViews()
                .AddMicrosoftIdentityUI();

            services.AddAuthorization(options =>
            {
                // By default, all incoming requests will be authorized according to the default policy
                options.FallbackPolicy = options.DefaultPolicy;
            });

            services.AddBootstrapCss();
            services.AddHttpClient();

            services.AddRazorPages();
            services.AddServerSideBlazor()
                .AddMicrosoftIdentityConsentHandler();
                  
            services.AddTransient<UserService>();
        }

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });
        }

UserService.cs

public async Task<User> GetCustomer(CustomerDetailsViewModel model, PrincipalSecurityContext securityContext)
        {
            var scopes = new string[] { "api://HiddenGuid/User.Read" };
            await SetAuthorizationAsync(securityContext, scopes);

            var request = new HttpRequestMessage(HttpMethod.Get, $"v1/customers/{model.Id}");
            var response = await Client.SendAsync(request);

            try
            {
                response.EnsureSuccessStatusCode();

                return JsonSerializer.Deserialize<User>(await response.Content.ReadAsStringAsync(), options);
            }
            catch (HttpRequestException ex)
            {
                throw await HandleError(response, ex);
            }
        }

private async Task SetAuthorizationAsync(PrincipalSecurityContext securityContext, string[] scopes = null)
        {
            string accessToken = null;
            
            switch (securityContext)
            {
                case PrincipalSecurityContext.UserRole:
                    throw new NotImplementedException("Unknown security context");
                    break;
                case PrincipalSecurityContext.UserScope:                    
                    accessToken = await _tokenAcquisitionService.GetAccessTokenForUserAsync(scopes);
                    break;
                case PrincipalSecurityContext.ApplicationRole:
                    accessToken = await _tokenAcquisitionService.GetAccessTokenForAppAsync("api://HiddenGuid/.default");
                    break;
                default:
                    throw new NotImplementedException("Unknown security context");
            }
            
            Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        }

CustomerDetails.razor

@page "/customer-details"

@using Microsoft.Identity.Web

@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
@inject UserService UserService
@inject NavigationManager Navigation

    <h3>Customer details</h3>

    @if (hasError)
    {
        <div class="alert alert-danger" role="alert">
            Error! @message
        </div>
    }

    @if (!hasError && !string.IsNullOrWhiteSpace(message))
    {
        <div class="alert alert-info" role="alert">
            Success! @message
        </div>
    }

    <div style="margin-top: 20px; margin-bottom: 20px;">
        <EditForm Model="@model" OnValidSubmit="HandleValidSubmit">
            <DataAnnotationsValidator />
            <ValidationSummary />

            <InputText id="id" @bind-Value="model.Id" placeholder="Id of user" required />

            <button type="submit">Search</button>
        </EditForm>
    </div>

    @if (user == null)
    {
        <p><em>Search for user above...</em></p>
    }
    else
    {
        <UserDetails User="user"></UserDetails>
    }

    @code {
        [CascadingParameter]
        protected PrincipalSecurityContext SecurityContext { get; set; }

        private bool hasError;
        private string message;

        private User user = new User();
        private CustomerDetailsViewModel model = new CustomerDetailsViewModel();

        private void ResetStatus()
        {
            hasError = false;
            message = string.Empty;
        }

        //[AuthorizeForScopes(Scopes = new string[] { "api://HiddenGuid/User.Read" })]
        private async Task HandleValidSubmit()
        {
            ResetStatus();

            try
            {
                user = await UserService.GetCustomer(model, SecurityContext);
                message = $"Found user '{user.DisplayName}'";
            }
            //catch (Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException ex)
            //{
            //    hasError = true;
            //    message = ex.Message;

            //    ConsentHandler.HandleException(ex);
            //}
            //catch (MsalUiRequiredException ex)
            //{
            //    hasError = true;
            //    message = ex.Message;

            //    ConsentHandler.HandleException(ex);
            //}
            catch (Exception ex)
            {
                hasError = true;
                message = ex.Message;

                ConsentHandler.HandleException(ex);
            }
        }
    }


If I call the above code (with SecurityContext PrincipalSecurityContext.UserScope. So accessToken = await _tokenAcquisitionService.GetAccessTokenForUserAsync(scopes);), I get a correct MsalUiRequiredException with the Message "An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.".
If I look at the details of the exception I see this:
AADSTS65001: The user or administrator has not consented to use the application with ID 'HiddenGuid' named 'Userservice-TestApp'. Send an interactive authorization request for this user and resource. Trace ID: 25c128b5-620c-4ee1-8427-6f24799a3b00 Correlation ID: b1ef35e6-a7b4-4b17-86eb-ac403f1a99e4 Timestamp: 2020-12-28 10:31:20Z

In my test app, I of course have the permissions correctly.
The ConsentHandler.HandleException(ex); only refreshes the page of the blazor. I see some kind of redirect in the browser (and then redirect back to the same page???).

Working code as workaround
When I change the code in Startup.cs to .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "api://HiddenGuid/User.Read" }) , I will get the consent screen after logging in. After that, all code works. But that is not the idea behind incremental consent.

@DigitalPZ You say "Consent is asked for and given". So you see a redirect to Azure AD consent page???? I don't see that happen....

@LockTar
Copy link

LockTar commented Dec 28, 2020

Which documentation is this, @DigitalPZ ?

That's the whole idea behind the page Managing incremental consent and conditional access right?
So only ask the permissions you need when you login into the app. Ask more permissions when the users needs it.

@LockTar
Copy link

LockTar commented Dec 28, 2020

One thing extra...

When I use the code:
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "api://HiddenGuid/User.Read" })
start the app, login, give consent, call the api and then I see that it works.

But when I then change the code to:
.EnableTokenAcquisitionToCallDownstreamApi()
start the app (consent already given previous time), I get MsalUIException. The strange page refresh is done, a new token is requested and the call to the api still works....

So conclusion:
When (admin or user) consent is already given, you don't need to give the scope in the startup EnableTokenAcquisitionToCallDownstreamApi following the idea behind incremental consent. So it must be a small bug somewhere in the package? Or our applications?

@Shama-K
Copy link
Contributor

Shama-K commented Dec 29, 2020

@DigitalPZ and @LockTar, It's fixed, here is the related issue raised in Microsoft.Identity.Web.

@LockTar
Copy link

LockTar commented Dec 29, 2020

Hi @Shama-K,
If I track the PR, this fix should be in version 1.4.1 of Microsoft.Identity.Web(.UI) right?
I'm using that version and still have that issue.

@Shama-K
Copy link
Contributor

Shama-K commented Dec 29, 2020

It will be available in 1.4.2 as mentioned in this issue.

@LockTar
Copy link

LockTar commented Feb 9, 2021

@Shama-K My issue about this is solved for me at least. Tested with version 1.5.1 of the Nuget package.

@Shama-K
Copy link
Contributor

Shama-K commented Feb 9, 2021

@LockTar, Thank you for letting us know.
@DigitalPZ, you can also update the package and test it.
Closing this issue. Feel free to reopen if there is any issue.

@Shama-K Shama-K closed this as completed Feb 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Answered question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants