Reputation: 626
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.
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
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
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
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
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();
}
}
}
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