Search code examples
c#.net-coreazure-ad-b2cazure-ad-msalblazor-webassembly

Blazor Standalone WASM Unable to get Access Token with MSAL


After fighting this for 2 days, around 6h invested, I finally decided to ask for help.

I have a standalone Blazor WASM app with MSAL Authentication, after the login is successful and it tries to get an Access Token I get the error:

blazor.webassembly.js:1 info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[1]
      Authorization was successful.
blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: An exception occurred executing JS interop: The JSON value could not be converted to System.DateTimeOffset. Path: $.token.expires | LineNumber: 0 | BytePositionInLine: 73.. See InnerException for more details.
Microsoft.JSInterop.JSException: An exception occurred executing JS interop: The JSON value could not be converted to System.DateTimeOffset. Path: $.token.expires | LineNumber: 0 | BytePositionInLine: 73.. See InnerException for more details.
 ---> System.Text.Json.JsonException: The JSON value could not be converted to System.DateTimeOffset. Path: $.token.expires | LineNumber: 0 | BytePositionInLine: 73.
 ---> System.InvalidOperationException: Cannot get the value of a token type 'Null' as a string.
   at System.Text.Json.Utf8JsonReader.TryGetDateTimeOffset(DateTimeOffset& value)
   at System.Text.Json.Utf8JsonReader.GetDateTimeOffset()

This error only shows after I login.

My setup is running on .NET 5.0, the Authentication provider is an Azure B2C tenant, I have the redirect URIs configured correctly as "Single-page application" and permissions granted to "offline_access" and "openid".

Here is my Program.cs

public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            // Authenticate requests to Function API
            builder.Services.AddScoped<APIFunctionAuthorizationMessageHandler>();
            
            //builder.Services.AddHttpClient("MyAPI", 
            //    client => client.BaseAddress = new Uri("<https://my_api_uri>"))
            //  .AddHttpMessageHandler<APIFunctionAuthorizationMessageHandler>();

            builder.Services.AddMudServices();

            builder.Services.AddMsalAuthentication(options =>
            {
                // Configure your authentication provider options here.
                // For more information, see https://aka.ms/blazor-standalone-auth
                builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);

                options.ProviderOptions.LoginMode = "redirect";
                options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
                options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
            });

            await builder.Build().RunAsync();
        }
    }

I've intentionally commented out the HTTPClient link to the AuthorizationMessageHandler. The "AzureAD" configuration has the Authority, ClientId and ValidateAuthority which is set to true.

public class APIFunctionAuthorizationMessageHandler : AuthorizationMessageHandler
    {
        public APIFunctionAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager)
        : base(provider, navigationManager)
        {
            ConfigureHandler(
                authorizedUrls: new[] { "<https://my_api_uri>" });
                //scopes: new[] { "FunctionAPI.Read" });
        }
    }

I've tried defining the scopes such as openid or custom API scope and now without. No difference.

Then to cause the exception, all I'm doing is something as simple as:

@code {
    private string AccessTokenValue;

    protected override async Task OnInitializedAsync()
    {
        var accessTokenResult = await TokenProvider.RequestAccessToken();
        AccessTokenValue = string.Empty;

        if (accessTokenResult.TryGetToken(out var token))
        {
            AccessTokenValue = token.Value;
        }
    }
}

The final objective is to use something like this:

   try {
      var httpClient = ClientFactory.CreateClient("MyAPI");
      var resp = await httpClient.GetFromJsonAsync<APIResponse>("api/Function1");
      FunctionResponse = resp.Value;
      Console.WriteLine("Fetched " + FunctionResponse);
   }
   catch (AccessTokenNotAvailableException exception)
   {
      exception.Redirect();
   }

But the same error is returned, to what it seems before this even runs. This code is OnInitializedAsync() of the Blazor Component also.

Any ideas or suggestions are welcome. I'm stuck and getting a bit desperate.

I suspect that the access token is not being requested or returned from Azure AD B2C, but that assume that is the AuthorizationMessageHandler job.

Any welcome is greatly appreciated.

Thanks.


Solution

  • Found the issue.

    After doing some debugging on the JavaScript side, file AuthenticationService.js, method "async getTokenCore(e)" on line 171 after prettified, I've confirmed that in fact the Access Token was not being returned and only the IdToken.

    From reading this document regarding requesting Access Token to Azure AD B2C, it mentioned that depending on the scopes you define, it will change what it returns back to you.

    The Scope "openid" tells it you need an IdToken, then "offline_access" tells it you need a refresh token and lastly there is a nifty trick where you can define the scope to the App Id and it will return an Access token. More details here: https://learn.microsoft.com/en-us/azure/active-directory-b2c/access-tokens#openid-connect-scopes

    So I've changed my code in Program.cs, builder.Services.AddMsalAuthentication step.

    Now it looks like this:

    builder.Services.AddMsalAuthentication(options =>
                {
                    // Configure your authentication provider options here.
                    // For more information, see https://aka.ms/blazor-standalone-auth
                    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    
                    options.ProviderOptions.LoginMode = "redirect";
                    options.ProviderOptions.DefaultAccessTokenScopes.Add("00000000-0000-0000-0000-000000000000");
                    //options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
                    //options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
                });
    

    Instead of "00000000-0000-0000-0000-000000000000", I've set the actual App ID I'm using on this Blazor App.

    Now the error is not happening and the Access Token returned.

    Thanks.