Initializing HubConnection from TestServer in ASP.NET Core SignalR - asp.net-core

Is is possible to initialize HubConnection from Microsoft.AspNetCore.TestHost.TestServer?
The example below throws HttpRequestException(Not Found) exception at await hubConnection.StartAsync();
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.TestHost;
using Xunit;
namespace FunctionalTests
{
public class PubSubScenarios
{
[Fact]
public async Task SignalRHubTest_Foo()
{
var webHostBuilder = WebHost.CreateDefaultBuilder().UseStartup<Startup>();
using (var testServer = new TestServer(webHostBuilder))
{
var hubConnection = await StartConnectionAsync(testServer.BaseAddress);
}
}
private static async Task<HubConnection> StartConnectionAsync(Uri baseUri)
{
var hubConnection = new HubConnectionBuilder()
.WithUrl($"http://{baseUri.Host}/fooHub")
.WithConsoleLogger()
.Build();
await hubConnection.StartAsync();
return hubConnection;
}
}
}

You need to call testServer.CreateHandler() and pass the HttpMessageHandler to WithMessageHandler:
[Fact]
public async Task SignalRHubTest_Foo()
{
var webHostBuilder = WebHost.CreateDefaultBuilder().UseStartup<Startup>();
using (var testServer = new TestServer(webHostBuilder))
{
var hubConnection = await StartConnectionAsync(testServer.CreateHandler());
}
}
private static async Task<HubConnection> StartConnectionAsync(HttpMessageHandler handler)
{
var hubConnection = new HubConnectionBuilder()
.WithUrl($"http://test/fooHub", options =>
{
options.Transports = HttpTransportType.LongPolling;
options.HttpMessageHandlerFactory = _ => handler;
})
.Build();
await hubConnection.StartAsync();
return hubConnection;
}
This won't work for websockets though (I opened an issue for this here https://github.com/aspnet/SignalR/issues/1595

Related

calling graph from asp .net core signalr hub

I am trying to get groups from AzureAD by calling graph in the hub in the OnConnectedAsync() to add the current user to groups.
When calling my graph service from a controller it works just fine and returns me the data I want, but when calling it from inside the hub I get an error:
IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. No account or login hint was passed to the AcquireTokenSilent call.
Any idea?
Here is my graph service :
public class GraphService : IGraphService
{
private readonly ITokenAcquisition _tokenAcquisition;
private readonly IHttpClientFactory _httpClientFactory;
public GraphService(ITokenAcquisition tokenAcquisition,
IHttpClientFactory clientFactory)
{
_httpClientFactory = clientFactory;
_tokenAcquisition = tokenAcquisition;
}
public async Task<IEnumerable<Group>> GetUserGroupsAsync()
{
var graphClient = await GetGraphClient();
var memberShipCollection = await graphClient
.Me
.MemberOf
.Request()
.GetAsync();
return memberShipCollection
.OfType<Group>();
}
private async Task<GraphServiceClient> GetGraphClient()
{
var token = await _tokenAcquisition
.GetAccessTokenForUserAsync(new string[] { "https://graph.microsoft.com/.default" });
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri("https://graph.microsoft.com/v1.0");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var graphClient = new GraphServiceClient(client)
{
AuthenticationProvider = new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
})
};
graphClient.BaseUrl = "https://graph.microsoft.com/v1.0";
return graphClient;
}
}
The hub :
[Authorize]
public class NotificationHub : Hub
{
ILogger<NotificationHub> _logger;
private readonly IGraphService _graphService;
public NotificationHub(ILogger<NotificationHub> logger,
IGraphService graphService)
{
_logger = logger;
_graphService = graphService;
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
var userDynamicGroups = await GetUserDynamicGroupsAsync();
//foreach (var group in userDynamicGroups)
// await Groups.AddToGroupAsync(Context.ConnectionId, group.DisplayName);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
//var userDynamicGroups = await GetUserDynamicGroupsAsync();
//foreach (var group in userDynamicGroups)
// await Groups.RemoveFromGroupAsync(Context.ConnectionId, group.DisplayName);
}
private async Task<IEnumerable<Group>> GetUserDynamicGroupsAsync()
{
var AllUserGroups = await _graphService.GetUserGroupsAsync();
return AllUserGroups.Where(g => g.DisplayName.Contains("ONE_"));
}
}
The part related to auth in my startup:
public static IServiceCollection AddInternalAuthentification(this IServiceCollection services, IConfiguration configuration)
{
services.AddMicrosoftIdentityWebApiAuthentication(configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
Func<MessageReceivedContext, Task> existingOnMessageReceivedHandler = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await existingOnMessageReceivedHandler(context);
StringValues accessToken = context.Request.Query["access_token"];
PathString path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/Notify"))
context.Token = accessToken;
};
});
return services;
}

Configure OData Test Server

Trying to set-up unit / integration tests for some extensions I am writing for the OdataQueryOptions class. I am using .net core 3.1.
In order to create the SUT instance - I need a HttpRequest. Which I creating using the WebApplicationFactory
public class TestingWebApplicationFactoryFixture : WebApplicationFactory<TestStartUp>
{
protected override IHostBuilder CreateHostBuilder()
{
var builder = Host.CreateDefaultBuilder();
builder.ConfigureWebHost(hostBuilder =>
{
hostBuilder.ConfigureServices(services =>
{
services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddOData();
}).Configure(app =>
{
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection();
routeBuilder.Select().Expand().OrderBy().Filter().MaxTop(int.MaxValue);
});
});
});
return builder;
}
I arrange the test to use the TestServer to produce the HttpContext. The OdataQueryContext and HttpRequest is then used to instantiate the OdataQueryOptions object.
const string path = "/?$filter=SalesOrderID eq 43659";
var httpContext = await _testingWebApplicationFactoryFixture.Server.SendAsync(context =>
{
context.Request.Method = HttpMethods.Get;
context.Request.Path = path;
});
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.AddEntityType(typeof(Customer));
var model = modelBuilder.GetEdmModel();
var odataQueryContext = new ODataQueryContext(model, typeof(Customer), new ODataPath());
var sut = new ODataQueryOptions<Customer>(odataQueryContext, httpContext.Request);
I am getting an exception during the instantiation of the object:
System.ArgumentNullException
Value cannot be null. (Parameter 'provider')
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T]
(IServiceProvider provider)
at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.CreateRequestScope(HttpRequest request,
String routeName)
at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.CreateRequestContainer(HttpRequest
request, String routeName)
at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.GetRequestContainer(HttpRequest request)
at Microsoft.AspNet.OData.Query.ODataQueryOptions..ctor(ODataQueryContext context, HttpRequest
request)
at Microsoft.AspNet.OData.Query.ODataQueryOptions`1..ctor(ODataQueryContext context, HttpRequest
request)
Digging into the actual method that is throwing - it is because the IServiceProvider is null. Shouldn't this be handled by the host?
UPDATE:
I modified the test method a bit so that I eliminate the WebApplicationFactory class.
Instead I create a TestServer with an IWebHostBuilder:
private IWebHostBuilder GetBuilder()
{
var webHostBuilder = new WebHostBuilder();
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddOData();
}).Configure(app =>
{
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection();
routeBuilder.Select().Expand().OrderBy().Filter().MaxTop(int.MaxValue);
});
});
return webHostBuilder;
}
And then create the TestServer:
[Fact]
public async Task QueryGenerator_Generate_SomeExpress_ShouldProduce()
{
const string path = "/?$filter=SalesOrderID eq 43659";
var testServer = new TestServer(GetBuilder());
var httpContext = await testServer.SendAsync(context =>
{
context.Request.Method = HttpMethods.Get;
context.Request.Path = path;
});
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.AddEntityType(typeof(Customer));
var model = modelBuilder.GetEdmModel();
var odataQueryContext = new ODataQueryContext(model, typeof(Customer), new ODataPath());
var sut = new ODataQueryOptions<Customer>(odataQueryContext, httpContext.Request);
}
I get the same exception. Why is the IServiceProvider null?
Never got a solution to using TestServer, but I found a work around. At the end of the day I needed the OdataQueryOptions class generated by the framework. So I created an IClassFixture<> in Xunit to manually get it created.
public class OdataQueryOptionFixture
{
public IServiceProvider Provider { get; private set; }
private IEdmModel _edmModel;
public OdataQueryOptionFixture()
{
SetupFixture();
}
public ODataQueryOptions<T> CreateODataQueryOptions<T>(HttpRequest request)
where T : class
{
var odataQueryContext = CreateOdataQueryContext<T>();
var odataQueryOptions = new ODataQueryOptions<T>(odataQueryContext, request);
return odataQueryOptions;
}
private ODataQueryContext CreateOdataQueryContext<T>()
where T : class
{
var odataQueryContext = new ODataQueryContext(_edmModel, typeof(T), new ODataPath());
return odataQueryContext;
}
private void SetupFixture()
{
var collection = new ServiceCollection();
collection.AddOData();
collection.AddTransient<ODataUriResolver>();
collection.AddTransient<ODataQueryValidator>();
Provider = collection.BuildServiceProvider();
ConfigureRoutes();
BuildModel();
}
private void ConfigureRoutes()
{
var routeBuilder = new RouteBuilder(Mock.Of<IApplicationBuilder>(x => x.ApplicationServices == Provider));
routeBuilder.Select().Expand().OrderBy().Filter().MaxTop(int.MaxValue).Count();
routeBuilder.EnableDependencyInjection();
}
private void BuildModel()
{
var edmContext = new AdventureWorksEdmContext();
_edmModel = edmContext.BuildModel();
}
Using the class fixture in a test class to construct the OdataQueryOptions
private QueryOptionsBuilder<Customer> GetSut(HttpRequest request)
{
var odataQueryOptions = _odataQueryOptionFixture.CreateODataQueryOptions<Customer>(request);
var odataQuerySettings = new ODataQuerySettings();
var odataValidationSettings = new ODataValidationSettings();
var customerExpandBinder = new CustomerExpandBinder(odataValidationSettings, odataQueryOptions.SelectExpand);
var customerOrderByBinder = new CustomerOrderByBinder(odataValidationSettings, odataQueryOptions.OrderBy);
var customerSelectBinder = new CustomerSelectBinder(odataValidationSettings, odataQueryOptions.SelectExpand);
var customerCompositeBinder = new CustomerCompositeBinder(customerExpandBinder, customerOrderByBinder, customerSelectBinder);
return new QueryOptionsBuilder<Customer>(customerCompositeBinder, odataQuerySettings);
}
TestServer would have been easier - but this gets the job done.

Create an instance of IHubContext<MyClass> in a webjob with Simple Injector

I'm using .NET Core 2.2 and SignalR Core and I need to inject IHubContext<MyClass> via Simple Injector in my Webjob.
It works perfectly in my web app but when I'm trying to reach my service via web job, it's complaining about lacking injection of IHubContext<IHubContext<BroadcastHub>>
I need a way to register it via Simple Injector
This is my Configuration in Program.cs file in my Webjob
using AutoMapper;
using Gateway.BLL.BaseClasses;
using Gateway.BLL.Config;
using Gateway.BLL.Services;
using Gateway.BLL.Services.Interfaces;
using Gateway.BLL.SignalR;
using Gateway.Model.MappingProfiles;
using Gateway.Repository;
using Gateway.Repository.Interfaces;
using Gateway.Repository.Repositories;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SimpleInjector;
using System;
using System.IO;
using System.Net.Http;
namespace Gateway.WebJob
{
class Program
{
private static void Main()
{
var container = new Container();
DbContextOptionsBuilder ob = new DbContextOptionsBuilder();
var config = new MapperConfiguration
(cfg =>
{
cfg.AddProfile(new ModelMappingProfile());
}
);
var mapper = config.CreateMapper();
var loggerFactory = new LoggerFactory();
ServiceCollection sr = new ServiceCollection();
sr.AddSignalR();
var serviceProvider = sr.AddHttpClient().BuildServiceProvider();
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
// Duplicate here any configuration sources you use.
configurationBuilder.AddJsonFile("appsettings.json");
IConfiguration configuration = configurationBuilder.Build();
var medchartApiConfiguration = new MedchartApiConfiguration();
configuration.Bind("MedchartApiConfiguration", medchartApiConfiguration);
var serviceBusConfiguration = new ServiceBusConfiguration();
configuration.Bind("ServiceBusConfiguration", serviceBusConfiguration);
ob = ob.UseSqlServer(configuration["ConnectionString:GatewayDB"]);
IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions());
ConfigureServices(sr);
var builder = new HostBuilder();
builder.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddServiceBus(o =>
{
o.MessageHandlerOptions.AutoComplete = true;
o.MessageHandlerOptions.MaxConcurrentCalls = 10;
o.ConnectionString = "Endpoint=sb://gatewayqueue.servicebus.windows.net/;SharedAccessKeyName=admin;SharedAccessKey=Wd2YwCEJT2g3q4ykvdOIU2251YD5FizCn5aCuumzdz4=";
}).AddSignalR();
});
builder.ConfigureLogging((context, b) =>
{
b.AddConsole();
string instrumentationKey = context.Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
if (!string.IsNullOrEmpty(instrumentationKey))
{
b.AddApplicationInsightsWebJobs(o => o.InstrumentationKey = instrumentationKey);
}
});
builder.ConfigureServices((hostContext, services) => {
//services.AddHttpClient();
//hostContext.Configuration.Bind("MedchartApiConfiguration", medchartApiConfiguration);
//services.AddSingleton(medchartApiConfiguration);
services.AddSingleton(container);
services.AddScoped<JobActivator.ScopeDisposable>();
services.AddScoped<IJobActivator, JobActivator>();
});
container.Register<IPatientService, PatientService>();
container.Register<IPatientRepository, PatientRepository>();
container.Register<IProviderService, ProviderService>();
container.Register<IPatientGroupProviderRepository, PatientGroupProviderRepository>();
container.Register<IPatientGroupRepository, PatientGroupRepository>();
container.Register<IConsentRepository, ConsentRepository>();
container.Register<IHttpClientWrapper, HttpClientWrapper>();
container.Register<IMedchartService, MedchartService>();
container.Register<IGroupRepository, GroupRepository>();
container.Register<IReportRepository, ReportRepository>();
container.Register<IProviderRepository, ProviderRepository>();
container.RegisterSingleton(httpClientFactory);
container.RegisterSingleton(memoryCache);
container.RegisterSingleton(medchartApiConfiguration);
container.RegisterSingleton(serviceBusConfiguration);
container.Register<ILoggerFactory>(() => loggerFactory, Lifestyle.Singleton);
container.RegisterSingleton(configuration);
container.RegisterSingleton(typeof(ILogger<PatientRepository>), typeof(Logger<PatientRepository>));
container.RegisterSingleton(typeof(ILogger<PatientService>), typeof(Logger<PatientService>));
container.RegisterSingleton(typeof(ILogger<HttpClientWrapper>), typeof(Logger<HttpClientWrapper>));
container.RegisterSingleton(typeof(ILogger<MedchartService>), typeof(Logger<MedchartService>));
container.RegisterSingleton(typeof(ILogger<ProviderService>), typeof(Logger<ProviderService>));
container.RegisterSingleton(typeof(ILogger<ProviderRepository>), typeof(Logger<ProviderRepository>));
container.RegisterSingleton(typeof(ILogger<ReportRepository>), typeof(Logger<ReportRepository>));
container.RegisterSingleton(mapper);
container.Register<GatewayDBContext>(() => {
var options = ob.Options;
return new GatewayDBContext(options);
});
var host = builder.Build();
using (host)
{
host.Run();
}
}
private static IConfiguration Configuration { get; set; }
private static void ConfigureServices(IServiceCollection services)
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
services.AddSingleton(Configuration);
services.AddTransient<Functions, Functions>();
services.AddLogging(builder => builder.AddConsole());
}
}
}
This is my simplified Service that is using SignalR: (this is another project that webjob will use it
using Microsoft.AspNetCore.SignalR;
using Gateway.BLL.SignalR;
// namespace Gateway.BLL.Services
public class PatientService : HttpClientWrapper, IPatientService
{
private readonly IHubContext<BroadcastHub> _hubContext;
public PatientService(IHubContext<BroadcastHub> hubContext)
: base(logger,httpClientFactory,medchartConfig)
{
_hubContext = hubContext;
}
public async Task<OutputHandler<IEnumerable<PatientEnrollmentParams>>>
CreatePatientAsync(List<PatientEnrollmentParams> patients,
CancellationToken ct)
{
var result = new OutputHandler<IEnumerable<PatientEnrollmentParams>>();
await _hubContext.Clients.All.SendAsync("BroadcastMessage");
return result;
}
}
this is my webjob that will call PatientService in another project
using Gateway.BLL.Config;
using Gateway.BLL.Processors;
using Gateway.BLL.Queues;
using Gateway.BLL.Services;
using Gateway.Model.Queues;
using Microsoft.Azure.ServiceBus;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using SimpleInjector;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Gateway.WebJob
{
public class Functions
{
private Container _container;
public Functions(Container container)
{
_container = container;
}
public async Task ProcessEnrollmentStatus([ServiceBusTrigger("%ServiceBusConfiguration:EnrollmentMessage:QueueName%")]string message, ILogger log)
{
var _patientService = _container.GetInstance<IPatientService>();
GetEnrollmentStatusTaskProcessor processor = new GetEnrollmentStatusTaskProcessor(_patientService);
EnrollmentStatusTask data = JsonConvert.DeserializeObject<EnrollmentStatusTask>(message);
await processor.Process(data);
}
public async Task ProcessConsentRequestStatus([ServiceBusTrigger("%ServiceBusConfiguration:ConsentRequestMessage:QueueName%")]string message, ILogger log)
{
var _patientService = _container.GetInstance<IPatientService>();
GetConsentRequestTaskProcessor processor = new GetConsentRequestTaskProcessor(_patientService);
ConsentRequestTask data = JsonConvert.DeserializeObject<ConsentRequestTask>(message);
await processor.Process(data);
}
}
}
and this is the process method that will call patientService:
using Gateway.BLL.Services;
using Gateway.Model.Queues;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Gateway.BLL.Processors
{
public class GetEnrollmentStatusTaskProcessor : IProcessor<EnrollmentStatusTask>
{
private IPatientService _patientService;
public GetEnrollmentStatusTaskProcessor(IPatientService patientService)
{
_patientService = patientService;
}
public async Task<bool> Process(EnrollmentStatusTask data)
{
bool updated = await _patientService.UpdatePatientEnrollmentStatus(data.PatientId, data.PatientMedchartId.ToString(), data.GroupId);
return updated;
}
}
}
I need to register IHubContext<MyClass> in my webjob in program.cs but I'm not able to register it via the following ways:
hubContext = serviceProvider.GetService<IHubContext<BroadcastHub>>();
container.RegisterSingleton(hubContext);
or this way
container.Register<IHubContext<BroadcastHub>>(Lifestyle.Singleton);
Update 2019-12-02:
I was able to resolve IHubContext but now i'm receiving e new issue. this is my function class:
using Gateway.BLL.Config;
using Gateway.BLL.Processors;
using Gateway.BLL.Queues;
using Gateway.BLL.Services;
using Gateway.Model.Queues;
using Microsoft.Azure.ServiceBus;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using SimpleInjector;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Gateway.WebJob
{
public class Functions
{
private Container _container;
public Functions(Container container)
{
_container = container;
}
public async Task ProcessEnrollmentStatus([ServiceBusTrigger("%ServiceBusConfiguration:EnrollmentMessage:QueueName%")]string message, ILogger log)
{
var _patientService = _container.GetInstance<IPatientService>();
GetEnrollmentStatusTaskProcessor processor = new GetEnrollmentStatusTaskProcessor(_patientService);
EnrollmentStatusTask data = JsonConvert.DeserializeObject<EnrollmentStatusTask>(message);
await processor.Process(data);
}
public async Task ProcessConsentRequestStatus([ServiceBusTrigger("%ServiceBusConfiguration:ConsentRequestMessage:QueueName%")]string message, ILogger log)
{
var _patientService = _container.GetInstance<IPatientService>();
GetConsentRequestTaskProcessor processor = new GetConsentRequestTaskProcessor(_patientService);
ConsentRequestTask data = JsonConvert.DeserializeObject<ConsentRequestTask>(message);
await processor.Process(data);
}
}
}
and this is my progrm.cs class after all updates:
using AutoMapper;
using Gateway.BLL.BaseClasses;
using Gateway.BLL.Config;
using Gateway.BLL.Services;
using Gateway.BLL.Services.Interfaces;
using Gateway.BLL.SignalR;
using Gateway.Model.MappingProfiles;
using Gateway.Repository;
using Gateway.Repository.Interfaces;
using Gateway.Repository.Repositories;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SimpleInjector;
using System;
using System.IO;
using System.Net.Http;
namespace Gateway.WebJob
{
class Program
{
private static void Main()
{
var container = new Container();
DbContextOptionsBuilder ob = new DbContextOptionsBuilder();
var config = new MapperConfiguration
(cfg =>
{
cfg.AddProfile(new ModelMappingProfile());
}
);
var mapper = config.CreateMapper();
var loggerFactory = new LoggerFactory();
ServiceCollection sr = new ServiceCollection();
sr.AddLogging();
sr.AddSignalR();
sr.AddDbContextPool<GatewayDBContext>(options => { /*options */ });
sr.AddSimpleInjector(container, options =>
{
options.AddLogging();
//options.CrossWire<ILoggerFactory>();
});
sr.BuildServiceProvider(validateScopes: true).UseSimpleInjector(container);
var serviceProvider = sr.AddHttpClient().BuildServiceProvider();
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
// Duplicate here any configuration sources you use.
configurationBuilder.AddJsonFile("appsettings.json");
IConfiguration configuration = configurationBuilder.Build();
var medchartApiConfiguration = new MedchartApiConfiguration();
configuration.Bind("MedchartApiConfiguration", medchartApiConfiguration);
var serviceBusConfiguration = new ServiceBusConfiguration();
configuration.Bind("ServiceBusConfiguration", serviceBusConfiguration);
ob = ob.UseSqlServer(configuration["ConnectionString:GatewayDB"]);
IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions());
ConfigureServices(sr);
var builder = new HostBuilder();
builder.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddServiceBus(o =>
{
o.MessageHandlerOptions.AutoComplete = true;
o.MessageHandlerOptions.MaxConcurrentCalls = 10;
o.ConnectionString = "Endpoint=sb://gatewayqueue.servicebus.windows.net/;SharedAccessKeyName=admin;SharedAccessKey=Wd2YwCEJT2g3q4ykvdOIU2251YD5FizCn5aCuumzdz4=";
});
});
builder.ConfigureLogging((context, b) =>
{
b.AddConsole();
b.Services.AddLogging();
string instrumentationKey = context.Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
if (!string.IsNullOrEmpty(instrumentationKey))
{
b.AddApplicationInsightsWebJobs(o => o.InstrumentationKey = instrumentationKey);
}
});
builder.ConfigureServices((hostContext, services) =>
{
//services.AddHttpClient();
//hostContext.Configuration.Bind("MedchartApiConfiguration", medchartApiConfiguration);
//services.AddSingleton(medchartApiConfiguration);
//services.AddSingleton(container);
//services.AddScoped<JobActivator.ScopeDisposable>();
//services.AddScoped<IJobActivator, JobActivator>();
});
container.Register<IPatientService, PatientService>();
container.Register<IPatientRepository, PatientRepository>();
container.Register<IProviderService, ProviderService>();
container.Register<IPatientGroupProviderRepository, PatientGroupProviderRepository>();
container.Register<IPatientGroupRepository, PatientGroupRepository>();
container.Register<IConsentRepository, ConsentRepository>();
container.Register<IHttpClientWrapper, HttpClientWrapper>();
container.Register<IMedchartService, MedchartService>();
container.Register<IGroupRepository, GroupRepository>();
container.Register<IReportRepository, ReportRepository>();
container.Register<IProviderRepository, ProviderRepository>();
container.RegisterSingleton(httpClientFactory);
container.RegisterSingleton(memoryCache);
container.RegisterSingleton(medchartApiConfiguration);
container.RegisterSingleton(serviceBusConfiguration);
//container.Register<ILoggerFactory>(() => loggerFactory, Lifestyle.Singleton);
container.RegisterSingleton(configuration);
container.RegisterSingleton(typeof(ILogger<PatientRepository>), typeof(Logger<PatientRepository>));
container.RegisterSingleton(typeof(ILogger<PatientService>), typeof(Logger<PatientService>));
container.RegisterSingleton(typeof(ILogger<HttpClientWrapper>), typeof(Logger<HttpClientWrapper>));
container.RegisterSingleton(typeof(ILogger<MedchartService>), typeof(Logger<MedchartService>));
container.RegisterSingleton(typeof(ILogger<ProviderService>), typeof(Logger<ProviderService>));
container.RegisterSingleton(typeof(ILogger<ProviderRepository>), typeof(Logger<ProviderRepository>));
container.RegisterSingleton(typeof(ILogger<ReportRepository>), typeof(Logger<ReportRepository>));
container.RegisterSingleton(mapper);
//container.Register<GatewayDBContext>(() =>
//{
// var options = ob.Options;
// return new GatewayDBContext(options);
//});
container.Verify();
var host = builder.Build();
using (host)
{
host.Run();
}
}
private static IConfiguration Configuration { get; set; }
private static void ConfigureServices(IServiceCollection services)
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
services.AddSingleton(Configuration);
services.AddTransient<Functions, Functions>();
services.AddLogging(builder => builder.AddConsole());
}
}
}
Now I'm getting this error that it means the container inside the function class is not resolved:
fail: Host.Results[0]
System.InvalidOperationException: Unable to resolve service for type 'SimpleInjector.Container' while attempting to activate 'Gateway.WebJob.Functions'.
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
at lambda_method(Closure , IServiceProvider , Object[] )
at Microsoft.Azure.WebJobs.Host.Executors.DefaultJobActivator.CreateInstance[T](IServiceProvider serviceProvider) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\DefaultJobActivator.cs:line 37
at Microsoft.Azure.WebJobs.Host.Executors.FunctionInvoker`2.CreateInstance(IFunctionInstanceEx functionInstance) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionInvoker.cs:line 44
at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ParameterHelper.Initialize() in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 846
at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.TryExecuteAsyncCore(IFunctionInstanceEx functionInstance, CancellationToken cancellationToken) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 117
I just tried this out myself, but as long as "auto cross wiring" is enabled in the Simple Injector integration (which is the default), you should be able to get IHubContext<T> implementations injected without having to do anything.
Here's an example startup class:
public class Startup
{
private readonly Container container = new Container();
public Startup(IConfiguration configuration) => Configuration = configuration;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// default asp stuff here
// add signalr
services.AddSignalR();
// add simple injector (enables auto cross wiring)
services.AddSimpleInjector(this.container, options =>
{
options.AddAspNetCore().AddControllerActivation();
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSimpleInjector(container);
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
And here's a hub context injected into a controller that is created by Simple Injector:
public class HomeController : Controller
{
private readonly IHubContext<MyHub> context;
public HomeController(IHubContext<MyHub> context, Container container)
{
this.context = context;
}
public IActionResult Index()
{
return View();
}
}

Razor Pages .NET Core 2.1 Integration Testing post authentication

I am looking for some guidance...
I'm currently looking at trying to write some integration tests for a Razor Pages app in .net core 2.1, the pages I'm wanting to test are post authentication but I'm not sure about the best way of approaching it. The docs seem to suggest creating a CustomWebApplicationFactory, but apart from that I've got a little bit lost as how I can fake/mock an authenticated user/request, using basic cookie based authentication.
I've seen that there is an open GitHub issue against the Microsoft docs (here is the actual GitHub issue), there was a mentioned solution using IdentityServer4 but I’m just looking how to do this just using cookie based authentication.
Has anyone got any guidance they may be able to suggest?
Thanks in advance
My Code so far is:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseMySql(connectionString);
options.EnableSensitiveDataLogging();
});
services.AddLogging(builder =>
{
builder.AddSeq();
});
services.ConfigureAuthentication();
services.ConfigureRouting();
}
}
ConfigureAuthentication.cs
namespace MyCarparks.Configuration.Startup
{
public static partial class ConfigurationExtensions
{
public static IServiceCollection ConfigureAuthentication(this IServiceCollection services)
{
services.AddIdentity<MyCarparksUser, IdentityRole>(cfg =>
{
//cfg.SignIn.RequireConfirmedEmail = true;
})
.AddDefaultUI()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
options.AllowAreas = true;
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
options.Conventions.AuthorizeFolder("/Sites");
});
return services;
}
}
}
Integration Tests
PageTests.cs
namespace MyCarparks.Web.IntegrationTests
{
public class PageTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly CustomWebApplicationFactory<Startup> factory;
public PageTests(CustomWebApplicationFactory<Startup> webApplicationFactory)
{
factory = webApplicationFactory;
}
[Fact]
public async Task SitesReturnsSuccessAndCorrectContentTypeAndSummary()
{
var siteId = Guid.NewGuid();
var site = new Site { Id = siteId, Address = "Test site address" };
var mockSite = new Mock<ISitesRepository>();
mockSite.Setup(s => s.GetSiteById(It.IsAny<Guid>())).ReturnsAsync(site);
// Arrange
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("http://localhost:44318/sites/sitedetails?siteId=" + siteId);
// Assert
response.EnsureSuccessStatusCode();
response.Content.Headers.ContentType.ToString()
.Should().Be("text/html; charset=utf-8");
var responseString = await response.Content.ReadAsStringAsync();
responseString.Should().Contain("Site Details - MyCarparks");
}
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseStartup<Startup>();
}
}
}
For implement your requirement, you could try code below which creates the client with the authentication cookies.
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
});
base.ConfigureWebHost(builder);
}
public new HttpClient CreateClient()
{
var cookieContainer = new CookieContainer();
var uri = new Uri("https://localhost:44344/Identity/Account/Login");
var httpClientHandler = new HttpClientHandler
{
CookieContainer = cookieContainer
};
HttpClient httpClient = new HttpClient(httpClientHandler);
var verificationToken = GetVerificationToken(httpClient, "https://localhost:44344/Identity/Account/Login");
var contentToSend = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("Email", "test#outlook.com"),
new KeyValuePair<string, string>("Password", "1qaz#WSX"),
new KeyValuePair<string, string>("__RequestVerificationToken", verificationToken),
});
var response = httpClient.PostAsync("https://localhost:44344/Identity/Account/Login", contentToSend).Result;
var cookies = cookieContainer.GetCookies(new Uri("https://localhost:44344/Identity/Account/Login"));
cookieContainer.Add(cookies);
var client = new HttpClient(httpClientHandler);
return client;
}
private string GetVerificationToken(HttpClient client, string url)
{
HttpResponseMessage response = client.GetAsync(url).Result;
var verificationToken =response.Content.ReadAsStringAsync().Result;
if (verificationToken != null && verificationToken.Length > 0)
{
verificationToken = verificationToken.Substring(verificationToken.IndexOf("__RequestVerificationToken"));
verificationToken = verificationToken.Substring(verificationToken.IndexOf("value=\"") + 7);
verificationToken = verificationToken.Substring(0, verificationToken.IndexOf("\""));
}
return verificationToken;
}
}
Following Chris Pratt suggestion, but for Razor Pages .NET Core 3.1 I use a previous request to authenticate against login endpoint (which is also another razor page), and grab the cookie from the response. Then I add the same cookie as part of the http request, and voila, it's an authenticated request.
This is a piece of code that uses an HttpClient and AngleSharp, as the official microsoft documentation, to test a razor page. So I reuse it to grab the cookie from the response.
private async Task<string> GetAuthenticationCookie()
{
var formName = nameof(LoginModel.LoginForm); //this is the bounded model for the login page
var dto =
new Dictionary<string, string>
{
[$"{formName}.Username"] = "foo",
[$"{formName}.Password"] = "bar",
};
var page = HttpClient.GetAsync("/login").GetAwaiter().GetResult();
var content = HtmlHelpers.GetDocumentAsync(page).GetAwaiter().GetResult();
//this is the AndleSharp
var authResult =
await HttpClient
.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='login-form']"),
(IHtmlButtonElement)content.QuerySelector("form[id='login-form']")
.QuerySelector("button"),
dto);
_ = authResult.Headers.TryGetValues("Set-Cookie", out var values);
return values.First();
}
Then that value can be reused and passed with the new http request.
//in my test, the cookie is a given (from the given-when-then approach) pre-requirement
protected void Given()
{
var cookie = GetAuthenticationCookie().GetAwaiter().GetResult();
//The http client is a property that comes from the TestServer, when creating a client http for tests as usual. Only this time I set the auth cookie to it
HttpClient.DefaultRequestHeaders.Add("Set-Cookie", cookie);
var page = await HttpClient.GetAsync($"/admin/protectedPage");
//this will be a 200 OK because it's an authenticated request with whatever claims and identity the /login page applied
}

Web Api Bearer JWT Token Authentication with Api Key fails on successive calls after successful token authentication

I have created an MVC Core API that authenticates users with an api key. On successful authentication it sends back a JWT token which they use for any subsequent requests.
I can successfully get authenticated with a valid api key and get a token as a response back. Then i can use this token to make a request but the next request fails.
In my real application the consumer is an MVC Core site and until now i hadn't noticed this issue because in every mvc controller action i was calling one api action but now that i have the need to call two api actions one after the other on the same mvc action the second one fails and i cannot understand why.
I have reproduced my issue in a sample web api and console application.
This is the code for the MVC Core API endpoint validating the api key and generating the jwt token:
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using PureHub.Services.Models.Valid8.Authentication;
namespace BugApiJwt.Controllers
{
[Authorize]
[Route("v1/[controller]")]
public class AuthenticationController : ControllerBase
{
[AllowAnonymous]
[HttpPost("[action]")]
public virtual async Task<IActionResult> Token([FromBody] ApiLoginRequest model)
{
if (model != null)
{
if (model.ApiKey == "VdPfwrL+mpRHKgzAIm9js7e/J9AbJshoPgv1nIZiat22R")
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
new Claim(JwtRegisteredClaimNames.Iat,
new DateTimeOffset(DateTime.UtcNow).ToUniversalTime().ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("FTTaIMmkh3awD/4JF0iHgAfNiB6/C/gFeDdrKU/4YG1ZK36o16Ja4wLO+1Qft6yd+heHPRB2uQqXd76p5bXXPQ=="));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "http://localhost:58393/",
audience: "http://localhost:58393/",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: creds);
return Ok(new ApiLoginResponse
{
Token = new JwtSecurityTokenHandler().WriteToken(token),
Expiration = token.ValidTo
});
}
}
return BadRequest();
}
}
}
This is the protected resource:
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BugApiJwt.Controllers
{
[Authorize]
[Route("v1/values")]
public class ValuesController : Controller
{
[HttpGet]
public IEnumerable<string> Get()
{
return new[] { "value1", "value2" };
}
[HttpGet("{id}")]
public string Get(int id)
{
return $"You said: {id}";
}
}
}
And this is my startup:
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace BugApiJwt
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "http://localhost:58393/",
ValidAudience = "http://localhost:58393/",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("FTTaIMmkh3awD/4JF0iHgAfNiB6/C/gFeDdrKU/4YG1ZK36o16Ja4wLO+1Qft6yd+heHPRB2uQqXd76p5bXXPQ==")),
};
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
}
}
And this is the console application i'm testing it with:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace BugApiJwt.Console
{
public class Program
{
private const string ApiKey = "VdPfwrL+mpRHKgzAIm9js7e/J9AbJshoPgv1nIZiat22R";
private const string BaseAddress = "http://localhost:58393/";
private static HttpClient _client = new HttpClient();
private static string _realToken = string.Empty;
private static void Main()
{
_client = new HttpClient
{
BaseAddress = new Uri(BaseAddress)
};
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Works
System.Console.WriteLine("Call GetOne");
var getOne = Get().GetAwaiter().GetResult();
System.Console.WriteLine(getOne);
// Fails
System.Console.WriteLine("Call GetAll");
var getTwo = GetAll().GetAwaiter().GetResult();
System.Console.WriteLine(getTwo);
System.Console.WriteLine("All Finished. Press Enter to exit");
System.Console.ReadLine();
}
private static async Task<string> GetAuthenticationToken()
{
const string resource = "v1/authentication/token";
if (!string.IsNullOrEmpty(_realToken)){return _realToken;}
var loginRequest = new ApiLoginRequest{ApiKey = ApiKey};
var httpResponseMessage = await _client.PostAsync(resource, ObjectToJsonContent(loginRequest)).ConfigureAwait(false);
if (httpResponseMessage.IsSuccessStatusCode)
{
var content = await httpResponseMessage.Content.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<ApiLoginResponse>(content);
_realToken = obj.Token;
return obj.Token;
}
throw new Exception("Token is null");
}
public static async Task<string> Get()
{
var resource = "v1/values/1";
var token = await GetAuthenticationToken();
_client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {token}");
var httpResponseMessage = await _client.GetAsync(resource);
System.Console.WriteLine(httpResponseMessage.RequestMessage.Headers.Authorization);
System.Console.WriteLine(httpResponseMessage.Headers.WwwAuthenticate);
var content = await httpResponseMessage.Content.ReadAsStringAsync();
return content;
}
public static async Task<string> GetAll()
{
var resource = "v1/values";
var token = await GetAuthenticationToken();
_client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {token}");
var httpResponseMessage = await _client.GetAsync(resource);
System.Console.WriteLine(httpResponseMessage.RequestMessage.Headers.Authorization);
System.Console.WriteLine(httpResponseMessage.Headers.WwwAuthenticate);
var content = await httpResponseMessage.Content.ReadAsStringAsync();
return content;
}
private static StringContent ObjectToJsonContent<T>(T objectToPost) where T : class, new()
{
var tJson = JsonConvert.SerializeObject(objectToPost,
Formatting.Indented, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
return new StringContent(tJson, Encoding.UTF8, "application/json");
}
}
public class ApiLoginRequest
{
public string ApiKey { get; set; }
}
public class ApiLoginResponse
{
public string Token { get; set; }
public DateTime Expiration { get; set; }
}
}
Any help on why the second call fails?
The error message shown in the web api output window is:
Bearer was not authenticated. Failure message: No SecurityTokenValidator available for token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIyNmFiNjQzYjFjOTM0MzYwYjI4NDAxMzZjNDIxOTBlZSIsImlhdCI6MTUxMDA2NDg0MywiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIxIiwiR2xvYmFsSWQiOiI2NjVjYWEzYjYxYmY0MWRmOGIzMTVhODY5YzQzMmJkYyIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluaXN0cmF0b3IiLCJuYmYiOjE1MTAwNjQ4NDMsImV4cCI6MTUxMDA2NjY0MywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0NDM2MCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQzNjAifQ.wJ86Ut2dmbDRDCNXU2kWXeQ1pQGkiVtUx7oSyJIZMzc
It doesn't work because this piece of code TryAddWithoutValidation("Authorization", $"Bearer {token}"); adds the token on top of what is already in the authorization header without clearing it first. As a result successive calls add the bearer string with the token in the header which already contains the bearer token.