question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Bug] TokenAcquisitionalways fails with MicrosoftIdentityWebChallengeUserException when called from a DelegatingHandler, but only on an Azure Web App.

See original GitHub issue

Which version of Microsoft Identity Web are you using? Microsoft Identity Web 0.3.0-preview

Where is the issue?

  • Web app
    • Sign-in users
    • 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)
    • Calling protected web APIs from a Server side Blazor app.

Is this a new or an existing app? New Blazor part of existing ASP.NET Core MVC app, but reproducible in a small, plain Blazor app.

Repro This is a wierd one. We have existing typed HttpClients and are using a DelegatingHandler to call .GetAccessTokenForUserAsync and add the auth token from AAD on all requests. This works fine when injecting the client into, and using it from a MVC controller, but it fails with a MicrosoftIdentityWebChallengeUserException every time when used from Blazor Server side and running on an Azure Web App, causing the page to go in an endless loop with the application page redirecting to AAD and the AAD authorization page redirecting back. Running locally, it works fine in both cases.

If the token acquisition is included in the actual typed client, and not in the delegating handler, it works fine in all cases. It’s just when called from a DelegatingHandler the call fails.

TestClient.cs - two versions of the typed client,

    // This works every time, everywhere.     
    public interface ITestClientWithAuthIncluded
    {
        Task<string> GetProfileAsync();
    }

    public class TestClientWithAuthIncluded : ITestClientWithAuthIncluded
    {
        private readonly HttpClient _httpClient;
        private readonly ITokenAcquisition _tokenAcquisition;

        public TestClientWithAuthIncluded(HttpClient httpClient, ITokenAcquisition tokenAcquisition)
        {
            _httpClient = httpClient;
            _tokenAcquisition = tokenAcquisition;
        }

        public async Task<string> GetProfileAsync()
        {
            var uri = "https://graph.microsoft.com/v1.0/me";

            var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "User.Read" }).ConfigureAwait(false);

            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            var responseString = await _httpClient.GetStringAsync(uri);

            return responseString;
        }
    }


    // This does not work in Blazor on Azure. 
    public interface ITestClientWithoutAuth
    {
        Task<string> GetProfileAsync();
    }

    public class TestClientWithoutAuth : ITestClientWithoutAuth
    {
        private readonly HttpClient _httpClient;

        public TestClientWithoutAuth(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<string> GetProfileAsync()
        {
            var uri = "https://graph.microsoft.com/v1.0/me";

            var responseString = await _httpClient.GetStringAsync(uri);

            return responseString;
        }
    }

AuthHandler.cs

    internal class AuthHandler : DelegatingHandler
    {
        private readonly ITokenAcquisition _tokenAcquisition;

        public AuthHandler(ITokenAcquisition tokenAcquisition)
        {
            _tokenAcquisition = tokenAcquisition;
        }

        public string Scope { get; set; }

        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] {Scope}).ConfigureAwait(false); 
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            return await base.SendAsync(request, cancellationToken);
        }
    }

Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            IdentityModelEventSource.ShowPII = true;

            services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
                .EnableTokenAcquisitionToCallDownstreamApi()
                .AddInMemoryTokenCaches();

            services.AddHttpClient<ITestClientWithAuthIncluded, TestClientWithAuthIncluded>();

            services.AddTransient<AuthHandler>();
            services.AddHttpClient<ITestClientWithoutAuth, TestClientWithoutAuth>()
                .AddHttpMessageHandler(c =>
                {
                    var handler = c.GetService<AuthHandler>();
                    handler.Scope = "User.Read";
                    return handler;
                });

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

            services.AddRazorPages();
            services.AddServerSideBlazor()
                .AddMicrosoftIdentityConsentHandler();
        }

index.razor

@page "/"
@using Microsoft.Identity.Web
@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
@inject ITestClientWithAuthIncluded TestClientWithAuth
@inject ITestClientWithoutAuth TestClientWithoutAuth

<h1>Hello, world!</h1>

<p>
    @Content
</p>

@code {

    private string Content;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            // This will work every time
            //Content = await TestClientWithAuth.GetProfileAsync();
            
            // This always thows MicrosoftIdentityWebChallengeUserException when running on Azure, works fine locally (throws once and then it's okay). 
            Content = await TestClientWithoutAuth.GetProfileAsync();
        }
        catch (Exception ex)
        {
            Content = ex.ToString();
            ConsentHandler.HandleException(ex);
        }
    }

}

For comparison, this works fine in all cases. HomeController.cs

    [Authorize]
    [AuthorizeForScopes(Scopes = new string[] { "User.Read" })]
    public class HomeController : Controller
    {
        private readonly ITestClientWithoutAuth _testClient;

        public HomeController(ITestClientWithoutAuth testClient)
        {
            _testClient = testClient;
        }

        [Route("Home/Index")]

        public async Task<IActionResult> Index()
        {
            var result = await _testClient.GetProfileAsync();
            return Ok(result);
        }
    }

Expected behavior When using the HTTP client, it should throw MicrosoftIdentityWebChallengeUserException once, redirect to AAD, authorize, reload the page and the HTTP client should get a token.

Actual behavior The .GetAccessTokenForUserAsync call throws a MicrosoftIdentityWebChallengeUserException every time it is called in a delegating handler from a Blazor page, when running on Azure.

Possible solution No idea. Told ya it was an interesting one…

Additional context / logs / screenshots I have a minimal repo here: https://github.com/henriksen/BlazorAuthRepo that includes the code mentioned above and consistently reproduces the error in our environment. It is based on the standard Blazor template and modified for Microsoft.Identity.Web 0.3.0. Add the correct tenantId and clientId in appSettings.json, it also expects a AzureAd:ClientSecret as a user secret (or in the appSettings.json file).

The sample repo uses Graph API for simplicity, but we’re seeing the same problem calling our own APIs using our own defined scopes.

The app is set up to run on server, if changed to ServerPrerendered the Blazor page will flash the correct data once (from the pre-render), try to refresh, get the exception, redirect and then enter the infinite loop.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:3
  • Comments:22

github_iconTop GitHub Comments

2reactions
jmprieurcommented, Aug 28, 2020

Thanks for trying, @henriksen That’s definitively one for @javiercn, then

1reaction
EdAlexandercommented, Sep 11, 2020

Very nice work Glenn! Hopefully the default tooling catches up in Blazor and things will get back to simple.

EAC Partners/317.762.3331

COVID-19: As always, EAC Partners is available to help your staff whether they are working at home or in the office. Remote assistance for your employees can be performed over the phone, through Microsoft Teams and/or Quick Assist on Windows 10.Together we can keep your workforce efficient through this health emergency.

From: Glenn F. Henriksenmailto:notifications@github.com Sent: Friday, September 11, 2020 12:42 PM To: AzureAD/microsoft-identity-webmailto:microsoft-identity-web@noreply.github.com Cc: Edward Alexandermailto:ed@eacpartners.com; Mentionmailto:mention@noreply.github.com Subject: Re: [AzureAD/microsoft-identity-web] [Bug] TokenAcquisitionalways fails with MicrosoftIdentityWebChallengeUserException when called from a DelegatingHandler, but only on an Azure Web App. (#516)

@EdAlexanderhttps://github.com/EdAlexander The workaround is basically using ITokenAccessor anywhere but in a DelegateHandler 😄 What I did was make a new Typed Client that just reuse the DTOs and Query objects from the generated clients and then does the token aquisition and HTTP calls itself. I have an example here: https://gist.github.com/henriksen/fe8846ffb4a4373a95403597b285ed18 The BaseService does the generic heavy lifting and the UserService specifies the path to call and passes parameters in and results out. Hope you find it useful.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/AzureAD/microsoft-identity-web/issues/516#issuecomment-691199977, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ADI6FERKMOLOKK37TPISNITSFJHPNANCNFSM4QOBBHDQ.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Add support for DelegatingHandler · Issue #1131
[Bug] TokenAcquisitionalways fails with MicrosoftIdentityWebChallengeUserException when called from a DelegatingHandler, but only on an ...
Read more >
Stack Overflow
In my case I encountered MicrosoftIdentityWebChallengeUserException while working on a .Net Core 3.1 Web App. My code in controller action:
Read more >
Troubleshoot domain and TLS/SSL certificates - Azure App ...
When you set up a domain or TLS/SSL certificate for your web apps in Azure App Service, you might encounter the following common...
Read more >
Troubleshooting intermittent outbound connection errors in ...
This article helps you troubleshoot intermittent connection errors and related performance issues in Azure App Service.
Read more >
Developer guidance for Azure Active Directory Conditional ...
Developer guidance and scenarios for Azure AD Conditional Access and Microsoft identity platform.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found