Securing Blazor WebAssembly Apps

Hi Paul,
Thank you for appreciating my article :slight_smile:

Regarding your request, does your rule throw an exception as in this example?
As far as I know, the error message should be returned to the client.

“There was an error signing in” message is the builtin error message from Blazor WASM.

Did you ever solve this @paulkiddybytes ? I am getting the same error.
.cc @andrea.chiarelli

1 Like

Hi folks!
Thx for this manual.
I faced one problem that was described here: [Blazor][Wasm] Set oidc Authentication Options in local storage · Issue #20574 · dotnet/aspnetcore · GitHub
The problem is that the oidc response is saved in the session storage by default and I haven’t find a way to configure it. I have to authenticate every time I open a new tab.
Did smbd face this issue too?

Hi @itbeard,
Welcome to the Auth0 Community.

As mentioned in the issue thread, storing the ID token in the local storage is not secure. Actually, even storing it in the session storage is not so secure.
You can learn more on token storage on the browser by reading the following docs:

Regarding the specific issue, it seems they solved the issue with a silent authentication approach. Did you try with an updated version of Blazor?

Hello!
Thx for your reply.
I use the last version of Blazor that included in .NET5.
Authentication works while I refresh the page, but non when I one page in the new tab, unfortunately.
Looks like the hidden iframe approach does not work in my case for some reason …

Nope. I’m stuck with the process of logging out. I’ve tried all kinds of stuff. The logout URL doesn’t return properly and the browser seems to cache the logged in user. Simply no way to properly and reliably logout a user. Doesn’t help that the whole stack seems really buggy.

Any luck in this topic? I have the same issue

I am using this example, my code below.

The token.Value is very small, like it is just an token code. How do I get an access token that includes “aud”,“iss”, etc?

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {

        try
        {
            var accessToken = await this._tokenProvider.RequestAccessToken(new AccessTokenRequestOptions());
            if (accessToken.TryGetToken(out AccessToken token))
            {
                request.Headers.Add("Authorization", $"Bearer {token.Value}");
                return await base.SendAsync(request, cancellationToken);

            }
            else
            {
                throw new InvalidOperationException("Unable to get token.");

            }

        }
        catch (System.Exception e)
        {
            Console.WriteLine(e.ToString());
            throw;
        }
    }

I can see the calls in fiddler to:
https://tjb.auth0.com/authorize?client_id=XXX
https://tjb.auth0.com/oauth/token
https://tjb.auth0.com/userinfo

and they are all successful…

and the https://tjb.auth0.com/oauth/token response includes a json payload with the access_token and the id_token. I need the value of id_token. How do I get the value of id_token?

Hey folks, this blog post has been revamped to .NET 5!

2 Likes

Woooohoo great news!

Hi @Tim_Bassett, Did you get the solution for that? I am also facing the same issue.
I have configured everything to make HTTP requests with auth token but my API calls were failing due to an invalid token. access_token is attaching to the requests instead of id_token. id_token is actual JWT.

We ended up getting rid of all the manual manipulation of the Token by using the BaseAddressAuthorizationMessageHandler. We also had CORS issues, so inevitably getting everything onto the same domain (both the web and the api) got get rid of the CORS issues, which yielded the opportunity to just use the BaseAddressAuthorizationMessageHandler. Using the BaseAddressAuthorizationMessageHandler got us out of the manually handling the token business, and then everything just worked.

We do use the AuthorizationMessageHandler to do a bit of manual token handling for development against the localhost api (different port). I believe using the AddHttpClient along with the other pattern inside the delegate got us home on that front…

                        builder.Services.AddHttpClient<TClient>(client => builder.Configuration.Bind("HttpClient", client))
                        .AddHttpMessageHandler<AuthorizationMessageHandler>();
                    builder.Services.AddTransient<AuthorizationMessageHandler>(sp =>
                    {
                        // 👇 Get required services from DI.
                        var provider = sp.GetRequiredService<IAccessTokenProvider>();
                        var naviManager = sp.GetRequiredService<NavigationManager>();

                        // 👇 Create a new "AuthorizationMessageHandler" instance,
                        //    and return it after configuring it.
                        var handler = new AuthorizationMessageHandler(provider, naviManager);
                        handler.ConfigureHandler(authorizedUrls: new[] {builder.Configuration["HttpClient:BaseAddress"]});
                        return handler;
                    });

I am not sure whether it is an audience issue or some other issue. Because I have verified the login call back URL and noticed both the tokens (access_token and id_token) are coming but only access token mapping to HTTP requests.
image

Hi @Tim_Bassett, I have tried with your code but no luck. Still access_token passing as Bearer token in request headers.

image

Sorry, I didn’t fully comprehend what you were trying to accomplish…

In our backend (asp.net) we are using middleware to get the token from the access token.

Some of this is voodoo to me, but I believe the “spell” is in this:

            string domain = $"https://{this.Configuration["Auth0:Domain"]}/";
        services
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.Authority = domain;
                options.Audience = this.Configuration["Auth0:Audience"];
                // If the access token does not have a `sub` claim, `User.Identity.Name` will be `null`. Map it to a different claim by setting the NameClaimType below.
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = ClaimTypes.NameIdentifier
                };
            });

I think what we ended up with is mostly based on Auth0 ASP.NET Core Web API SDK Quickstarts: Authorization

1 Like

Thanks for sharing that with the rest of community!

@Tim_Bassett I don’t know am I suppose to do that. Because the same API is used by two different Angular applications. It may effect the existing application. Is there any way to do that from blazor client side application by implementing Custom Authorization Message Handler?
Anyhow I will give a try this option too.

@Tim_Bassett I have resolved it on my own trails by implementing a few custom handlers. I have taken a AuthorizationMessageHandler.cs class file and added it in my wasm client project with CustomAuthorizationMessageHandler.cs this name and modified the SendAsync method with my own logic to get identity token from the session storage and adding that token in the request authorization header.

Code Snippet: CustomAuthorizationMessageHandler.cs

public class CustomAuthorizationMessageHandler : DelegatingHandler
{
    private readonly IAccessTokenProvider _provider;
    private readonly NavigationManager _navigation;
    private readonly IJSRuntime jsRuntime;
    private AuthenticationHeaderValue _cachedHeader;
    private Uri[] _authorizedUris;
    private AccessTokenRequestOptions _tokenOptions;
    private CachedAuthSettings authSettings;
    private CachedOidcUser _user;
    private DateTime? _tokenExpiresAt;



    /// <summary>
    /// Initializes a new instance of <see cref="AuthorizationMessageHandler"/>.
    /// </summary>
    /// <param name="provider">The <see cref="IAccessTokenProvider"/> to use for provisioning tokens.</param>
    /// <param name="navigation">The <see cref="NavigationManager"/> to use for performing redirections.</param>
    public CustomAuthorizationMessageHandler(
        IAccessTokenProvider provider,
        NavigationManager navigation,
        IJSRuntime jsRuntime)
    {
        _provider = provider;
        _navigation = navigation;
        this.jsRuntime = jsRuntime;
    }

    /// <inheritdoc />
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.Now;
        var utcDefaultDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);

        if (_authorizedUris == null)
        {
            throw new InvalidOperationException($"The '{nameof(AuthorizationMessageHandler)}' is not configured. " +
                $"Call '{nameof(AuthorizationMessageHandler.ConfigureHandler)}' and provide a list of endpoint urls to attach the token to.");
        }

        if (_authorizedUris.Any(uri => uri.IsBaseOf(request.RequestUri)))
        {
            if (_user == null || _tokenExpiresAt == null || now >= _tokenExpiresAt?.AddMinutes(-5))
            {
                string key = "Microsoft.AspNetCore.Components.WebAssembly.Authentication.CachedAuthSettings";
                string authSettingsRAW = await jsRuntime.InvokeAsync<string>("sessionStorage.getItem", key);
                authSettings = JsonSerializer.Deserialize<CachedAuthSettings>(authSettingsRAW);
                string userRAW = await jsRuntime.InvokeAsync<string>("sessionStorage.getItem", authSettings?.OidcUserKey);
                var cachedUser = JsonSerializer.Deserialize<CachedOidcUser>(userRAW);

                if (!string.IsNullOrEmpty(cachedUser?.IdentityToken))
                {
                    var handler = new JwtSecurityTokenHandler();
                    var decodedValue = handler.ReadJwtToken(cachedUser?.IdentityToken);
                    _tokenExpiresAt = decodedValue.ValidTo.ToLocalTime();
                    _user = cachedUser;
                    _cachedHeader = new AuthenticationHeaderValue("Bearer", _user.IdentityToken);
                }
                else
                {
                    var tokenResult = _tokenOptions != null ?
                        await _provider.RequestAccessToken(_tokenOptions) :
                        await _provider.RequestAccessToken();

                    throw new AccessTokenNotAvailableException(_navigation, tokenResult, _tokenOptions?.Scopes);
                }
            }

            // We don't try to handle 401s and retry the request with a new token automatically since that would mean we need to copy the request
            // headers and buffer the body and we expect that the user instead handles the 401s. (Also, we can't really handle all 401s as we might
            // not be able to provision a token without user interaction).
            request.Headers.Authorization = _cachedHeader;
        }

        return await base.SendAsync(request, cancellationToken);
    }

    /// <summary>
    /// Configures this handler to authorize outbound HTTP requests using an access token. The access token is only attached if at least one of
    /// <paramref name="authorizedUrls" /> is a base of <see cref="HttpRequestMessage.RequestUri" />.
    /// </summary>
    /// <param name="authorizedUrls">The base addresses of endpoint URLs to which the token will be attached.</param>
    /// <param name="scopes">The list of scopes to use when requesting an access token.</param>
    /// <param name="returnUrl">The return URL to use in case there is an issue provisioning the token and a redirection to the
    /// identity provider is necessary.
    /// </param>
    /// <returns>This <see cref="AuthorizationMessageHandler"/>.</returns>
    public CustomAuthorizationMessageHandler ConfigureHandler(
        IEnumerable<string> authorizedUrls,
        IEnumerable<string> scopes = null,
        string returnUrl = null)
    {
        if (_authorizedUris != null)
        {
            throw new InvalidOperationException("Handler already configured.");
        }

        if (authorizedUrls == null)
        {
            throw new ArgumentNullException(nameof(authorizedUrls));
        }

        var uris = authorizedUrls.Select(uri => new Uri(uri, UriKind.Absolute)).ToArray();
        if (uris.Length == 0)
        {
            throw new ArgumentException("At least one URL must be configured.", nameof(authorizedUrls));
        }

        _authorizedUris = uris;
        var scopesList = scopes?.ToArray();
        if (scopesList != null || returnUrl != null)
        {
            _tokenOptions = new AccessTokenRequestOptions
            {
                Scopes = scopesList,
                ReturnUrl = returnUrl
            };
        }

        return this;
    }
}

And then I have created a ConfiguredAuthorizationMessageHandler.cs class which is derived from CustomAuthorizationMessageHandler class for setting authorized URLs.
Code Snippet: ConfiguredAuthorizationMessageHandler.cs

public class ConfiguredAuthorizationMessageHandler : CustomAuthorizationMessageHandler
{
    public ConfiguredAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager, IJSRuntime jsRuntime, IConfiguration configuration)
        : base(provider, navigationManager, jsRuntime)
    {
        ConfigureHandler(
           authorizedUrls: new[] { configuration["ApiBaseUrl"] }
           );
    }
}

After that, I have injected those classes into the Program Main method.
Code Snippet: Program.cs

        builder.Services.AddScoped<ConfiguredAuthorizationMessageHandler>();

        builder.Services.AddHttpClient("ServerAPI", client => client.BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"]))
           .AddHttpMessageHandler<ConfiguredAuthorizationMessageHandler>();

        builder.Services.AddScoped<ITestService>(provider =>
                new TestService(provider.GetService<IHttpClientFactory>().CreateClient("ServerAPI")));

I have created two models for serializing session storage JSON data.

Code Snippet: CachedAuthSettings.cs and CachedOidcUser.cs

public class CachedAuthSettings
{
    [JsonPropertyName("authority")]
    public string Authority { get; set; }
    [JsonPropertyName("metadataUrl")]
    public string MetadataUrl { get; set; }
    [JsonPropertyName("client_id")]
    public string ClientId { get; set; }
    [JsonPropertyName("defaultScopes")]
    public IEnumerable<string> DefaultScopes { get; set; }
    [JsonPropertyName("redirect_uri")]
    public string RedirectUri { get; set; }
    [JsonPropertyName("post_logout_redirect_uri")]
    public string PostLogoutRedirectUri { get; set; }
    [JsonPropertyName("response_type")]
    public string ResponseType { get; set; }
    [JsonPropertyName("response_mode")]
    public string ResponseMode { get; set; }
    [JsonPropertyName("scope")]
    public string Scope { get; set; }

    public string OidcUserKey => $"oidc.user:{Authority}:{ClientId}";
}

public class CachedOidcUser
{
    [JsonPropertyName("id_token")]
    public string IdentityToken { get; set; }
    [JsonPropertyName("access_token")]
    public string AccessToken { get; set; }
    [JsonPropertyName("refresh_token")]
    public string RefreshToken { get; set; }
    [JsonPropertyName("token_type")]
    public string TokenType { get; set; }
    [JsonPropertyName("scope")]
    public string Scope { get; set; }
    [JsonPropertyName("expires_at")]
    public int ExpiresAt { get; set; }
}

Reference:

  1. Reading id_token from session storage.
  2. Configuring Authorized URLs
  3. JWT token decoder
  4. WebAssembly.Authentication Repo -Microsoft