Search code examples
c#asp.net-corecookiesdotnet-httpclientblazor-webassembly

HttpClient doesn't include cookies with requests in Blazor Webassembly app


I have a Blazor Webassembly app with a user service that is designed to hit an API to retrieve a user's detailed info. The service looks like this:

public class UserDataService : IUserDataService
{
    public readonly HttpClient _HttpClient;

    public UserDataService(HttpClient httpClientDI)
    {
        _HttpClient = httpClientDI;
    }

    public async Task<User> GetUserInfo()
    {
        try
        {
            return await _HttpClient.GetFromJsonAsync<User>("api/users/MyUserInfo");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            throw;
        }
    }
}

The API is specifically designed to read an encrypted cookie from the client request. This cookie contains the user's email address, and is used by the user info service to retrieve a more detailed set of user information.

[HttpGet("MyUserInfo")]
public User MyUserInfo()
{
    var myCookie = HttpContext.Request.Cookies.FirstOrDefault(c => c.Key == "MyCookie");

    var userMask = JsonConvert.DeserializeObject<AuthUserMask>(Protector.Unprotect(myCookie.Value));

    var user = UserService.Find(userMask.Email).FirstOrDefault();

    return user;
}

I'm able to verify that the cookie is there in the browser when I run the web app, but when the app makes the request to the API the cookie is not included. In fact the request doesn't include any cookies from the client at all.

enter image description here

I'm completely new to Blazor and I'm not sure what if any conventions exist for this type of scenario, but at the moment I'm just trying to get this new web app to work with our existing service. Is there a way to ensure the cookies are included? What could I be doing wrong?

Thanks in advance for the help.

EDIT

Here's the code that's creating the cookie. It's part of a larger method that verifies the user is authenticated, but this is the relevant part:

{
    var userJson = JsonConvert.SerializeObject(new AuthUserMask()
    {
        Email = user.Email,
        isActive = user.IsActive
    });

    var protectedContents = Protector.Protect(userJson);

    HttpContext.Response.Cookies.Append("MyCookie", protectedContents, new CookieOptions()
    {
        SameSite = SameSiteMode.None,
        Secure = true,
        Path = "/",
        Expires = DateTime.Now.AddMinutes(60)
    });

    HttpContext.Response.Redirect(returnUrl);
}

EDIT 2

Tried the following out in the UserDataService to see what would happen:

public async Task<User> GetUserInfo()
{
    try
    {
        _HttpClient.DefaultRequestHeaders.Add("Test", "ABC123");
        return await _HttpClient.GetFromJsonAsync<User>("api/users/MyUserInfo");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
}

Unfortunately the result is the same - the RequestCookieCollection is completely empty when it hits the API.


Solution

  • Based on some of @Mihaimyh's insights I was able to get this to work using a custom delegating handler on the user data service. It is registered thusly:

    builder.Services.AddHttpClient<IUserDataService, UserDataService>(client => client.BaseAddress = new Uri("https://localhost:44336/"))
                    .AddHttpMessageHandler<CustomDelegatingHandler>();
    

    Internally it uses JSInterop to run a Javascript function to retrieve the cookie, which it then attaches to all outgoing requests that use the SendAsync() method:

    public class CustomDelegatingHandler : DelegatingHandler
    {
        private IJSRuntime JSRuntime;
    
        public CustomDelegatingHandler(IJSRuntime jSRuntime) : base()
        {
            JSRuntime = jSRuntime;
        }
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var cookie = await JSRuntime.InvokeAsync<string>("blazorExtensions.GetCookie", new[] { "MyCookie" });
            Debug.WriteLine($"My cookie: {cookie}");
            request.Headers.Add("MyCookie", $"{cookie}");
            return await base.SendAsync(request, cancellationToken);
        }
    }
    

    The Javascript function looks like this (lifted almost verbatim from W3Schools):

    window.blazorExtensions = { 
        GetCookie: function (cname) {
            var name = cname + "=";
            var decodedCookie = decodeURIComponent(document.cookie);
            var ca = decodedCookie.split(';');
            for (var i = 0; i < ca.length; i++) {
                var c = ca[i];
                while (c.charAt(0) == ' ') {
                    c = c.substring(1);
                }
                if (c.indexOf(name) == 0) {
                    return c.substring(name.length, c.length);
                }
            }
            return "";
        }
    }
    

    I've also modified things on the service end to look for the cookie in the headers instead of the cookie collection. Now, instead of this...

    var myCookie = HttpContext.Request.Cookies.FirstOrDefault(c => c.Key == "MyCookie");
    

    ...I've done this:

    HttpContext.Request.Headers.TryGetValue("MyCookie", out var myCookie);
    

    I admittedly have no idea how this tracks with the conventions for such things in Blazor apps, but it appears to be working well enough for our purposes. Thanks again everyone for all your help.