Blazor Server GraphServiceClient not authenticated in wrapper library

Viewed 51

I have a Blazor Server application that authenticates users through AAD. I'm using the Microsoft Graph SDK to query user info such as photo, etc., which I can do using DI to get the GraphServiceClient directly within a component in the Blazor Server app.

    [Inject] private GraphServiceClient GraphServiceClient { get; set; }

However, for testing purposes, I've created a wrapper around the GraphServiceClient called IGraphService in another library within the same solution. This is where the problem occurs and the GraphServiceClient fails authentication with:

No account or login hint was passed to the AcquireTokenSilent call

My code is set up as follows:

Program.cs

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
                .EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetValue<string>("Graph:Scopes")?.Split(' '))
                .AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
                .AddInMemoryTokenCaches();

builder.Services.AddScoped<IGraphService, GraphService>();

appsettings.json

"AzureAd": {
    "Instance": "https://login.microsoftonline.com",
    "Domain": "{domain}.onmicrosoft.com",
    "CallbackPath": "/signin-oidc",
    "TenantId": "{tenantId}",
    "ClientId": "{clientId}",
    "ClientSecret": "{clientSecret}"
},
"Graph": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": "user.read user.readbasic.all"
},

The part of the implementation of the GraphService I created to wrap the GraphServiceClient that tries to set up the GraphServiceClient:

return new GraphServiceClient(new DelegateAuthenticationProvider((requestMessage) =>
                {
                    string[] scopes = new[] { "user.read", "user.readbasic.all" };
                    ITokenAcquisition tokenService = _contextAccessor.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
                    var token = tokenService.GetAccessTokenForUserAsync(scopes);     <-- FAILS!!!

                    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Result);
                    return Task.FromResult(0);
                }));

Clearing my cookies, this works fine the first time the app runs, but on restarts, it fails with the same message. Injecting the GraphServiceClient directly into a component works fine too, but I'm trying to wrap it so the components are loosely coupled.

Additionally, if I pass the AccessToken from the Blazor Server project to the wrapper library, it works fine too, but I want to avoid that.

What am I missing? Is my setup not right? Thanks for any help or pointers :)

Update 1

To simplify the issue, I have a component on a page and I can successfully request the user's token in that component, every time.

public sealed partial class HeaderBar
    {
        [Inject] private ITokenAcquisition TokenAcquisition { get; set; } = null!;
        [Inject] private IGraphService GraphService { get; set; } = null!;

        protected override async Task OnInitializedAsync()
        {
            try
            {
                // this works every time!
                string token = await TokenAcquisition.GetAccessTokenForUserAsync(new[] { "user.read", "user.readbasic.all" });

                var photo = await GraphService.GetUserPhoto();
                ...
            }
            catch (Exception ex)
            {
                ...
            }
        }
    }

As you can see, I'm not doing anything with the token, but if I remove that line, the same line within the GraphService fails. It will only succeed and get a token if I make the same call within the component first.

I've tried moving the code to the App.razor, but it fails there too.

1 Answers

Thanks to the following issue, I was able to pinpoint the problem and resolve it: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/issues/281

Problem: When you lose the in-memory cache while your cookies are still alive, the auth calls are skipped and you don't bootstrap the token cache, which throws the MsalUIException described in the original question above. That explains why everything works the first time the app runs and doesn't when it gets restarted.

Some of the solutions describe using an attribute to re-populate the cache. However, in Blazor Server, there is no "Controller", so we can't use the AuthorizeForScopes attribute.

Solution: I've created a page model for the standard _Host.csthml page that will attempt to get a token from the token cache, and if it's not populated, the AuthorizeForScopes attribute will do that for me (Thanks wmgdev!).

[AuthorizeForScopes(Scopes = new[] { "user.read", "user.readbasic.all" })]
public class _Host : PageModel
{
    private readonly ITokenAcquisition _tokenAcquisition;

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

    public async Task OnGetAsync()
    {
        // Get a token. If the token cache is not populated, the `AuthorizeForScopes` attribute will re-authorize, which will populate the cache
        await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "user.read", "user.readbasic.all" });
    }
}
Related