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

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.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 3
  • Comments: 22

Most upvoted comments

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

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.

@jmprieur let me ask this question to some folks, I’m not an expert in HttpClientFactory, the dev who worked on it is no longer on the team, and is now part of dotnet/runtime, so I’ll need to familiarize myself a bit with it before I can understand if there’s something going on.

@henriksen: a possible idea (to try) might be to inject MicrosoftIdentityConsentAndConditionalAccessHandler in the constructor of you delegating handler, and use the .User member when calling GetAccessTokenForUserAsync()