7

I am using the Visual Studio 2019 and .Net Core 3.0.0-preview-7 with the standard Blazor Client, Server and Shared templates.

In the application our server side WebApi application will always require a JWT token to be present in the header for authorization.

From looking at the following

Make HTTP requests using IHttpClientFactory in ASP.NET Core

I created the following handler;

public class JwtTokenHeaderHandler : DelegatingHandler
{
    private readonly ILocalStorageService _localStorage;

    public JwtTokenHeaderHandler(ILocalStorageService localStorage)
    {
        _localStorage = localStorage;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("bearer"))
        {
            var savedToken = await _localStorage.GetItemAsync<string>("authToken");

            if (!string.IsNullOrWhiteSpace(savedToken))
            {
                request.Headers.Add("bearer", savedToken);
            }
        }

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

Where I use Blazored.LocalStorage to get the saved token from localstorage and add it to the header.

Now, at this point I am not sure what to do as if I add the following to the Blazor.Client Startup.cs;

services.AddTransient<JwtTokenHeaderHandler>();
services.AddHttpClient("JwtTokenHandler")
    .AddHttpMessageHandler<JwtTokenHeaderHandler>();

I get the error message;

'IServiceCollection' does not contain a definition for 'AddHttpClient' and no accessible extension method 'AddHttpClient' accepting a first argument of type 'IServiceCollection' could be found (are you missing a using directive or an assembly reference?)

Can anyone direct me to what I am doing wrong here?

H H
  • 263,252
  • 30
  • 330
  • 514
Matthew Flynn
  • 3,661
  • 7
  • 40
  • 98
  • @HenkHolterman I said this is in the `Blazor.Client` project? I want to add the JWT to the header of all calls I make to the server side controls. – Matthew Flynn Aug 22 '19 at 12:36
  • The Hosted model/template is Blazor client-side. Plus a 'normal' Web Api project. – H H Aug 22 '19 at 12:51
  • @Matthew Flynn, you'd better call your app client-side Blazor. – enet Aug 22 '19 at 12:52
  • @HenkHolterman ok, so on the 'normal' webapi I have `[Authorize]` attributes on the controllers, authorisation is standard 'bearer' jwt in the message header. this is working ok, Now on the blazor client side app when it makes a call to get some data etc to the WebApi I just want to intercept the Post, Get etc and add the Jwt stored in localstorage to the header of the request – Matthew Flynn Aug 22 '19 at 13:03
  • I think this url will help you https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-3.1 – Arun Singh Jul 31 '20 at 18:29

4 Answers4

5

I found a really good tutorial and sample demonstrating this (complete with roles/policy based claims): https://chrissainty.com/securing-your-blazor-apps-authentication-with-clientside-blazor-using-webapi-aspnet-core-identity/

Here is an extract below, setting the default request headers on the default http client (through DI). All calls to your web api will then include the bearer token:

public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;

    public ApiAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
    }
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var savedToken = await _localStorage.GetItemAsync<string>("authToken");

        if (string.IsNullOrWhiteSpace(savedToken))
        {
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }

        // **************    Set JWT header       ****************
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", savedToken);
        // *******************************************************

        return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));
    }
    // ...
}
Obliterator
  • 71
  • 1
  • 5
4

@Matthew Flynn, currently you can't use IHttpClientFactory on client-side Blazor.

And you don't have to derive from HttpMessageHandler (DelegatingHandler). It has already been done by Blazor. The following is n extension class to extend the functionality of the HttpClient service to enable the ability to add the Jwt token to the header of the request message...

ServiceExtensions.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Security.Claims;
    using System.Text;
    using System.Text.Json.Serialization;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Components;
    using Microsoft.Extensions.DependencyInjection;

 public static class ServiceExtensions
    {    
     public static async Task<T> GetJsonAsync<T>(this HttpClient httpClient, string url, AuthenticationHeaderValue authorization)
            {
                var request = new HttpRequestMessage(HttpMethod.Get, url);
                request.Headers.Authorization = authorization;

                var response = await httpClient.SendAsync(request);
                var responseBytes = await response.Content.ReadAsByteArrayAsync();
                return JsonSerializer.Parse<T>(responseBytes, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
            }
}

The following show how to call an endpoint on your Web Api, passing the Jwt token which is read from the localStorage. (incidentally, none of these versions is secured with data protection)

Index.razor

@page "/"

@inject ILocalStorageService localStorage
@inject HttpClient Http

<div class="mdc-card main-content-card">
    <h1 class="@MdcTypography.H4">Hello, world!</h1>

    Welcome to your new app.
</div>

// Razor content to display emloyees come here.....

@code {
Employee[] employees;

    protected override async Task OnInitAsync()
    {
        var token = await localStorage.GetTokenAsync();
        employees = await Http.GetJsonAsync<Employee[]>(
            "api/employees",
            new AuthenticationHeaderValue("Bearer", token));
    }
}

Hope this works... If not, and you can't solve thr errors, come here and tell the community about it...

enet
  • 41,195
  • 5
  • 76
  • 113
  • I agree it looks like Blazor restrict this as it adds it own HttpClient defaults to work with webassembly (I did have a gitu issue link but cant find it). I suppose extension methods are the best work around for now, I was just hoping I could avoid intercept each call without having to do this. :( – Matthew Flynn Aug 22 '19 at 14:09
  • Not many know this, but the HttpClient service on client-side Blazor is not the actual or real HttpClient we've known for years. It is actually an implementation of the JavaScript Fetch Api... – enet Aug 22 '19 at 14:54
  • Follow up on this, can I abstract the token from localstorage within the extension method? I was trying to make a HttpClient wrapper to reduce the code on the component side that does add the authorization header and token value all in one "PostJsonAsync()" call – kabuto178 Apr 01 '20 at 11:21
3

The following adds X-CSRF-TOKEN header to http requests:

public class CustomHttpMessageHandler : DelegatingHandler
{
    private readonly IJSRuntime _js;
    public CustomHttpMessageHandler(IJSRuntime js)
    {
        _js = js;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var afrt = await _js.InvokeAsync<string>("getCookie", ".AFRT");
        request.Headers.Add("X-CSRF-TOKEN", afrt);
        return await base.SendAsync(request, cancellationToken);
    }
}

In Program.cs configure like below:

builder.Services.AddScoped<CustomHttpMessageHandler>();
builder.Services.AddHttpClient("ApiClient", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<CustomHttpMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient"));

You need to install Microsoft.Extensions.Http package to your blazor webassembly client.

synergetic
  • 7,756
  • 8
  • 65
  • 106
0

You need the NuGet package Microsoft.Extensions.Http which contains the AddHttpClient method. Install it with the following command: Install-Package Microsoft.Extensions.Http -Version 3.0.0-preview7.19362.4

As it seems, this NuGet package is automatically provided in server-side blazor but has to be installed seperately in client-side blazor.

Pascal R.
  • 2,024
  • 1
  • 21
  • 35
  • 1
    Correct. It's a big(gish) package and the wasm runtime has to be trimmed as much as possible. – H H Aug 22 '19 at 12:48
  • 1
    @HenkHolterman from the links I found it looks like this is not compatible yet with wasm, as the blazor clientside sets up its own httpclient, overriding this seems to break it. Installing this to the client project and then running it currently causing a runtime exception. – Matthew Flynn Aug 22 '19 at 14:15
  • @MatthewFlynn - look at the exact name, Blazor has its own HttpClient package. – H H Aug 22 '19 at 14:21