Dumas.DED
Dumas.DED

Reputation: 626

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.

Upvotes: 11

Views: 10707

Answers (4)

Nicola Biada
Nicola Biada

Reputation: 2800

Using Blazor .net 6 style in Program.cs you need the following code:

builder.Services
    .AddTransient<CookieHandler>()
    .AddScoped(sp => sp
        .GetRequiredService<IHttpClientFactory>()
        .CreateClient("API"))
    .AddHttpClient("API", client => client.BaseAddress = new Uri(apiAddress)).AddHttpMessageHandler<CookieHandler>();

then you need the handler described by @murat_yuceer like:

namespace Client.Extensions
{
    public class CookieHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

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

You don't need (and you shouldn't) specify the cookie. The correct cookie will be sent for you, just add BrowserRequestCredentials.Include in the message.

On the server side, where you have your APIs, you need to set CORS allowing credentials.

Using .net 6 syntax you should already have in Program.cs:

app.UseCors(x => x.
  .AllowAnyHeader()
  .AllowAnyMethod()
  .AllowAnyOrigin()
);

but you need also AllowCredentials()

If you add AllowCredentials you obtain the following runtime error:

System.InvalidOperationException: 'The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time. Configure the CORS policy by listing individual origins if credentials needs to be supported.'

So you need to specify the allowed origins, or a wildcard like this:

app.UseCors(x => x
    .AllowAnyHeader()
    .AllowAnyMethod()
    //.AllowAnyOrigin()
    .SetIsOriginAllowed(origin => true)
    .AllowCredentials()
);

And now all should works as expected.

Upvotes: 8

murat_yuceer
murat_yuceer

Reputation: 27

Add this

public class CookieHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

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

Upvotes: 2

Dumas.DED
Dumas.DED

Reputation: 626

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.

Upvotes: 1

Mihaimyh
Mihaimyh

Reputation: 1410

This is what I did in a test Blazor WebAssembly AspNet Hosted app:

FetchData.razor

@page "/fetchdata"
@using BlazorApp3.Shared
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        Http.DefaultRequestHeaders.Add("key", "someValue");
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }

}

Notice Http.DefaultRequestHeaders.Add("key", "someValue");

On the server side, on WeatherForecastController I am looking in the request headers for the key, and if is present I am trying to get the value:

using BlazorApp3.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

namespace BlazorApp3.Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        // The Web API will only accept tokens 1) for users, and 2) having the access_as_user scope for this API
        private static readonly string[] scopeRequiredByApi = new string[] { "user_impersonation" };

        private static readonly string[] Summaries = new[]
                {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            if (HttpContext.Request.Headers.ContainsKey("key"))
            {
                var success = HttpContext.Request.Headers.TryGetValue("key", out var headervalue);

                if (success)
                {
                    _logger.LogInformation(headervalue.ToString());
                }
            }

            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

Debug screenshot

I am able to get the value on the http request headers.

If you need to create a cookie, you must use JsInterop, more details here How do I create a cookie client side using blazor.

Upvotes: 2

Related Questions