10

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.

Dumas.DED
  • 466
  • 5
  • 17
  • 1
    Can you also include the code where the cookie is written or configured? Some settings could be affecting the ability of the browser to properly include it in your requests. – Matt Hensley Sep 10 '20 at 14:37
  • @MattHensley - done and done. Edited the original post to add it. – Dumas.DED Sep 10 '20 at 14:43
  • 1
    It looks like setting your Path to "/" could be the issue. Looking at the docs, it sounds like that will only include your cookie if the request is to the root of the issued domain. Would you mind removing that property in your CookieOptions & see if the problem persists? https://learn.microsoft.com/en-us/dotnet/api/system.web.httpcookie.path?view=netframework-4.8 – Matt Hensley Sep 10 '20 at 14:53
  • @MattHensley - Alas. Tried removing that entirely but the problem is still there. In fact it appears to be defaulting to the same value of "/" anyway. – Dumas.DED Sep 10 '20 at 14:58
  • 1
    If I read correctly you are trying to send this cookie to the server, in this case it shouldn't be a `Request` not a `Response`? Can you try adding `httpclient.DefaultRequestHeaders.Add("MyCookie", "SomeCokkieContent");` – Mihaimyh Sep 10 '20 at 15:01
  • 1
    The plot thickens. The only other thing catching my eye is the Secure = true. When that is set to true, both the UI & the API must be utilizing HTTPS for all communications. You might double check that as well. If that doesn't work, I have upvoted to draw some attention to the question. Best of luck. – Matt Hensley Sep 10 '20 at 15:01
  • Thanks so much, @MattHensley. Really do appreciate it. Both the API and the UI are using HTTPS, so that ought not to be the problem either. The plot thickens indeed. I guess we'll see where it leads. – Dumas.DED Sep 10 '20 at 15:03
  • @Mihaimyh - Gave it a try, but unfortunately the result is the same. The cookie doesn't come through at all. See my edit above for more detail. – Dumas.DED Sep 10 '20 at 15:20
  • 1
    I am a little confused here, you are trying to include something in the header when you are POST-ing to the server or your are looking for something sent by the server in the response headers? `_HttpClient.DefaultRequestHeaders.Add("Test", "ABC123");` should be followed by a POST request, not a GET, if your scope is to send it to the server. – Mihaimyh Sep 10 '20 at 15:24
  • @Mihaimyh - I am not actually POST-ing to the server in this particular case - the `_HttpClient.GetFromJsonAsync("api/users/MyUserInfo")` call is just a GET request to the server for the current user's info. I'm expecting that request to include the cookie from the browser, but on the server side the cookie just isn't showing up at all. – Dumas.DED Sep 10 '20 at 15:28
  • Cookies are not really used or supported for API requests. Use normal JSON payloads and optionally set a Bearer token in the Headers. Plenty of examples laying around for how to do that. – H H Sep 11 '20 at 11:10

4 Answers4

4

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.

Nicola Biada
  • 2,325
  • 1
  • 8
  • 22
2

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.

Mihaimyh
  • 1,262
  • 1
  • 13
  • 35
  • 1
    I've actually tried this out in my code and it does work, at least in that I'm able to include the cookie value in the header. That being said, the `HttpContext.Request.Cookies` collection is still empty. I've also tried using `Http.DefaultRequestHeaders.Add("Set-Cookie", $"myCookie={someValue}")`, but that still doesn't populate the collection. I don't know how much it matters if I can still read the cookie value from the header, but it's puzzling nonetheless. – Dumas.DED Sep 11 '20 at 15:38
1

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.

Dumas.DED
  • 466
  • 5
  • 17
1

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);
    }
}
murat_yuceer
  • 17
  • 1
  • 4