Trying to implement a request/response scenario with MassTransit.RabbitMQ version 4.0.1.1378-develop, a ASP.NET Core 2 Web application and a .NET Core 2 Console Project.
Below is the code that creates bus and request client in my ASP.NET Core 2.0 application:
var bus = Bus.Factory.CreateUsingRabbitMq(sbc =>
sbc.Host(new Uri("rabbitmq://localhost"), h =>
{
h.Username("guest");
h.Password("guest");
})
);
services.AddSingleton<IPublishEndpoint>(bus);
services.AddSingleton<IBusControl>(bus);
var requestTimeout = TimeSpan.FromSeconds(10);
var address = new Uri("rabbitmq://localhost/myqueue");
services.AddScoped<IRequestClient<SubmitOrder, OrderSubmitResult>>(ctx => new MessageRequestClient<SubmitOrder, OrderSubmitResult>(bus, address, requestTimeout));
Then using the IRequestClient<SubmitOrder, OrderSubmitResult> that's been injected into my controller, I send the request as below:
private readonly IRequestClient<SubmitOrder, OrderSubmitResult> _client;
public async Task<ActionResult> SendRequest(string id)
{
var result = await _client.Request(new SubmitOrder());
...
}
Finally, the ways I have tried to send response to the client:
var bus = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
var host = cfg.Host(new Uri("rabbitmq://localhost"), h =>
{
h.Username("guest");
h.Password("guest");
});
cfg.ReceiveEndpoint(host, "myqueue", ep =>
{
ep.Handler<SubmitOrder>(context => context.RespondAsync(new OrderSubmitResult()));
// async version
//ep.Handler<SubmitOrder>(async context =>
//{
// await context.RespondAsync(new OrderSubmitResult());
//});
// with a Consumer
//ep.Consumer<SubmitOrderConsumer>();
// with a single Consumer
//ep.Instance(new SubmitOrderConsumer());
});
});
bus.Start();
I get the request in my Bus, and send appropriate response. But I always get RequestTimeoutException in the client side and none of the above approaches worked.
Update:
Code is OK. I forgot to start the bus (bus.start) on client side :/
#Chris: Thanks for adding a full working sample in a short time. I suggest adding services.AddSingleton<IBusControl>(bus); and then change Startup.cs's Configure method to below:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime, IBusControl bus)
{
applicationLifetime.ApplicationStopping.Register(() => bus.Stop(TimeSpan.FromSeconds(10)));
....
To handle a graceful shutdown.
I've built a sample to show how to do this properly, including the use of ASP.NET Core 2, both a Web Application and a Console Application.
https://github.com/MassTransit/Sample-DotNetCore-Request
Enjoy!
Related
I'm attempting to convert an old WCF service to an ASP.NET Core Web API, making use of the CoreWCF package. A key feature of this existing service is that it's being self hosted by an other application and is able to gracefully start & stop by button presses done by the users on a WPF UI.
This service should be accessed via an HTTPS endpoint and this is where I'm struggling. I have been able to succesfully access the API using regular HTTP, but I'm guessing I'm missing something or it's got something to do with my certificates. I've tried both using the default certificate by passing the listeningOption.UseHTTPS() but when I go to the URL, it'll return an error 500. I've also tried a self-signed certificate and passing it in the afformentioned method, but it keeps returning the same error.
The code for configuring everything looks like this:
Configuring the webhost:
private void CreateWebHostBuilder(){
host = WebHost.CreateDefaultBuilder()
.UseKestrel(options =>
{
options.AllowSynchronousIO = true;
options.ListenLocalhost(Startup.PORT_NR,
lOptions => lOptions.UseHttps("{absolute path}", "{password}"));
);
})
.ConfigureLogging(logging => { logging.SetMinimumLevel(LogLevel.Warning); })
.UseSetting(WebHostDefaults.DetailedErrorsKey, "true")
.UseShutdownTimeout(TimeSpan.FromSeconds(1))
.UseStartup<Startup>()
.Build();
}
Inside the Startup class:
Configuring the IApplicationBuilder:
public void Configure(IApplicationBuilder app){
app.UseServiceModel(builder =>
{
// Add the Echo Service
builder.AddService<EchoService>()
// Add service web endpoint
.AddServiceWebEndpoint<EchoService, IEchoService>(
WEB_API_PATH,behavior => { behavior.HelpEnabled = true;}
);
});
app.UseMiddleware<SwaggerMiddleware>();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseHttpsRedirection();
}
Configuring the services:
public void ConfigureServices(IServiceCollection services){
services.AddServiceModelWebServices()
.AddHostedService<EchoService>()
.AddSingleton(new SwaggerOptions())
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate();
services.AddHttpsRedirection(options =>
{
options.HttpsPort = HTTPS_PORT_NR;
});
}
Since this started as a WPF application, I don't have an appsettings.json file to add, and I wouldn't really know what to add in it if this were needed.
So if anyone has any suggestions or tutorial reference, be sure to let me know, any help is welcome.
So I eventually figured out what was missing. I simply needed to add another endpoint, that would listen to https requests like so:
public void Configure(IApplicationBuilder app){
app.UseServiceModel(builder =>
{
// Add the Echo Service
builder.AddService<EchoService>()
// Add service web endpoint
.AddServiceWebEndpoint<EchoService, IEchoService>(
WEB_API_PATH,behavior => { behavior.HelpEnabled = true;}
);
.AddServiceWebEndpoint<EchoService, IEchoService>(new WebHttpBinding
{
Security = new WebHttpSecurity { Mode = WebHttpSecurityMode.Transport },
WEB_API_PATH,behavior => { behavior.HelpEnabled = true;}
);
});
// remainder of code.
}
I am trying to set up Azure Signal R Service. Currently, I can get it working if I send a message to ALL clients but if I try and use Groups it doesn't send any messages. I suspect it has to do with the Azure signal R Service. I am running this locally atm
in My Web Api Startup.cs I have the following:
services.AddSignalR()
.AddAzureSignalR(options => options.Endpoints = new[]
{
new ServiceEndpoint(Configuration.GetConnectionString("AzureSignalRConnection"))
});
and
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapHub<SignalRHub>("/SingalRHub`enter code here`");
});
Then In my Hub:
public class SignalRHub: Hub
{
public async Task Join(long deviceId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, deviceId);
}
public async Task ReadTime(int deviceId)
{
await Clients.Group(deviceId).SendAsync($"ReadResponse", DateTime.Now.ToString());
}
}
On my client side: (Blazor wasm app)
private async Task InitiaizeSignalR()
{
//Intialize signal R
_hubConnection = new HubConnectionBuilder()
.WithUrl(_navigationManager.ToAbsoluteUri("/SignalRHub"))
.Build();
_hubConnection.On<string>("ReadResponse", (message) =>
{
Console.WriteLine($"Read Response: {message}");
});
//Connect to hub
await _hubConnection.StartAsync();
//Register to get messages for this DeviceId
await _hubConnection.SendAsync("Join", DeviceId);
}
Now the stange thing is that when I call this on my client, I dono get any response. However If i change it to:
await Clients.All.SendAsync($"ReadResponse", DateTime.Now.ToString());
Then my clinet will get the message?
Im new to `webassembly blazor, Im spend too much time trying to figure out what's wrong here but I couldnt manage.
I have the following scenario:
Asp.net API registered and protected in Azure AD
Expose API with Scope AcessApi with status enabled
A Client application is added to authorized client applications
Token configuration both are checked Access Token and ID Token
And a client app that will call the API, developed in webassembly blazor
client app is registered in Azure AD
Client API permissions has delegated permission to use my client API
with correct scope AccessApi.
I tested the API using swagger interface, it forces user to authenticate first before accessing the API.
I tested using curl and grabbed the token from swagger interface and works perfectly fine.
curl -X GET "http://localhost:9400/api/getdata" -H "accept: text/plain" -H "Authorization: Bearer XX"
However, when my client application trying to access the API, a sign-in page pop-up for credentials, I could see the Token ID at browser bar being retrieved and while calling the API the app logs error not authorized
program class of the client application:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
//builder.Logging.SetMinimumLevel(LogLevel.Debug);
////builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient("AccessApi",
client => client.BaseAddress = new Uri("http://localhost:9400"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("AccessApi"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
});
await builder.Build().RunAsync();
}
in CustomAuthorizationMessageHandler class I have defined:
private static string scope = #"api://xxx-35fc2470889f/AccessApi";
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "http://localhost:9400" },
}
In appsettings.json a defined the client id of the API and tenant id without scopes since they are been defined in the CustomAuthorizationMessageHandlerclass:
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/<tenant_id>",
"ClientId": "<clientid>",
"CallbackPath": "/signin-oidc",
"ValidateAuthority": "true"
}
}
After a successful login via Azure AD, I call to fetch data from the API here
protected override async Task OnInitializedAsync()
{
...
try
{
responseBody = await Http.GetStringAsync("/api/getdata"); # use base URL of the API
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
the console logs
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[1]
Authorization was successful.
info: System.Net.Http.HttpClient.AccessApi.ClientHandler[100]
Sending HTTP request GET http://localhost:9400/api/getdata
:9400/customer-manager/api/getdata:1 Failed to load resource: the server responded with a status of 401 (Unauthorized)
What could be wrong here?
Is there a way how to print the return token?
Update
I tested the API using Postman where auth Grant type is Implicit, after successful login, I store token on variable and passed in the header as Bearer the API return 401 Unauthroized. I decoded the token it contains the right scope AccessApi , with the correct clientId. what could be wrong here ?
If you want to call Microsoft graph and your custom API in one blazor webassembly project, we can implement it by creating different HTTP client to call different API
For example
Register a server API app
Register an AAD app for the Server API app
Expose an API
Register a client app
Register a client app
Enable Implicit grant flow
Add API permissions. ( API app permissions)
Configure API app
Please add the following code in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder => builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod());
});
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Authority += "/v2.0";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuers = new[] {
$"https://sts.windows.net/{Configuration["AzureAD:TenantId"]}/",
$"https://login.microsoftonline.com/{Configuration["AzureAD:TenantId"]}/v2.0"
},
RoleClaimType = "roles",
// The web API accepts as audiences both the Client ID (options.Audience) and api://{ClientID}.
ValidAudiences = new[]
{
options.Audience,
$"api://{options.Audience}"
}
};
});
....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.OAuthClientId(Configuration["Swagger:ClientId"]);
c.OAuthScopeSeparator(" ");
c.OAuthAppName("Protected Api");
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Configure Client APP
Create custom AuthorizationMessageHandler for Graph API and custom API
// custom API
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "<your web API url>" },
scopes: new[] { "the API app scope" });
}
}
Add the following code to the program.cs
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
// register HTTP client to call our own api
builder.Services.AddHttpClient("MyAPI", client => client.BaseAddress = new Uri("<your web API url>"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("<the API app scope>");
});
await builder.Build().RunAsync();
}
}
Call the api
#inject IHttpClientFactory _clientFactory
var httpClient = _clientFactory.CreateClient("<the client name you register>");
await apiClient.GetStringAsync("path");
Finally I found the issue was on the server side ASP.net core where I was validating the token in ConfigureServices at startup class:
// For token parameters validation
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.Audience = "<xx>"; // Application id
o.Authority = "https://login.microsoftonline.com/<xx>"; // Tenant ID
//Token validation
o.TokenValidationParameters = new TokenValidationParameters {ValidateIssuerSigningKey = false, ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true};
});
I had to disable Issuer since the token is coming from a different application.
I would love to see an example on how to add authentication to a SignalR hub connection using the WebAssembly flavor of Blazor. My dotnet version is 3.1.300.
I can follow these steps to get an open, unauthenticated SignalR connection working: https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr-blazor-webassembly?view=aspnetcore-3.1&tabs=visual-studio
All the tutorials I find seem older or are for a server-hosted type, and don't use the built-in template.
I have added authentication to the rest of the back-end, using the appropriate template and these instructions, including the database:
https://learn.microsoft.com/en-us/aspnet/core/security/blazor/?view=aspnetcore-3.1
But every time I add [Authenticate] to the chat hub, I get an error returned. Is there any way, extending the first tutorial, that we can authenticate the hub that is created there? It would be great to hitch on to the built-in ASP.NET system, but I am fine just passing a token in as an additional parameter and doing it myself, if that is best. In that case I would need to learn how to get the token out of the Blazor WebAssembly, and then look it up somewhere on the server. This seems wrong, but it would basically fill my needs, as an alternative.
There are all sorts of half-solutions out there, or designed for an older version, but nothing to build off the stock tutorial that MS presents.
Update:
Following the hints in this news release https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-preview-2-release-now-available/, I now can get a token from inside the razor page, and inject it into the header. I guess this is good?? But then how do I get it and make use of it on the server?
Here is a snippet of the razor code:
protected override async Task OnInitializedAsync()
{
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(UriHelper.BaseUri);
var tokenResult = await AuthenticationService.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
hubConnection = new HubConnectionBuilder()
.WithUrl(UriHelper.ToAbsoluteUri("/chatHub"), options =>
{
options.AccessTokenProvider = () => Task.FromResult(token.Value);
})
.Build();
}
}
Update 2:
I tried the tip in here: https://github.com/dotnet/aspnetcore/issues/18697
And changed my code to:
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chatHub?access_token=" + token.Value))
.Build();
But no joy.
I've come across the same issue.
My solution was 2-sided: I had to fix something in the fronend and in the backend.
Blazor
In your connection builder you should add the AccessTokenProvider:
string accessToken = "eyYourToken";
connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/hub/chat", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token.Value);
})
.Build();
options.AccessTokenProvider is of type Func<Task<string>>, thus you can also perform async operations here. Should that be required.
Doing solely this, should allow SignalR to work.
Backend
However! You might still see an error when SignalR attempts to create a WebSocket connection. This is because you are likely using IdentityServer on the backend and this does not support Jwt tokens from query strings. Unfortunately SignalR attempts to authorize websocket requests by a query string parameter called access_token.
Add this code to your startup:
.AddJwtBearer("Bearer", options =>
{
// other configurations omitted for brevity
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs"))) // Ensure that this path is the same as yours!
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
edit 1: Clarified the usage of the Blazor SignalR code
In my case (Blazor WebAssembly, hosted on ASP.NET Core 5.0 using JWT Bearer Token Auth), I had to add the following:
Blazor WASM Client
When building the connection (in my case: in the constructor of some service proxy class), use IAccessTokenProvider and configure the AccessTokenProvider option like so:
public ServiceProxy(HttpClient httpClient, IAccessTokenProvider tokenProvider) {
HubConnection = new HubConnectionBuilder()
.WithUrl(
new Uri(httpClient.BaseAddress, "/hubs/service"),
options => {
options.AccessTokenProvider = async () => {
var result = await tokenProvider.RequestAccessToken();
if (result.TryGetToken(out var token)) {
return token.Value;
}
else {
return string.Empty;
}
};
})
.WithAutomaticReconnect() // optional
.Build();
}
ASP.NET Core Server
Add the following to Startup.ConfigureServices:
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => {
// store user's "name" claim in User.Identity.Name
options.TokenValidationParameters.NameClaimType = "name";
// pass JWT bearer token to SignalR connection context
// (from https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0)
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
var accessToken = context.Request.Query["access_token"];
// If the request is for on of our SignalR hubs ...
if (!string.IsNullOrEmpty(accessToken) &&
(context.HttpContext.Request.Path.StartsWithSegments("/hubs/service"))) {
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
This is my solution and works
[Inject] HttpClient httpClient { get; set; }
[Inject] IAccessTokenProvider tokenProvider { get; set; }
HubConnection hubConnection { get; set; }
(...)
private async Task ConnectToNotificationHub()
{
string url = httpClient.BaseAddress.ToString() + "notificationhub";
var tokenResult = await tokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
hubConnection = new HubConnectionBuilder().WithUrl(url, options =>
{
options.Headers.Add("Authorization", $"Bearer {token.Value}");
}).Build();
await hubConnection.StartAsync();
hubConnection.Closed += async (s) =>
{
await hubConnection.StartAsync();
};
hubConnection.On<string>("notification", m =>
{
string msg = m;
});
}
}
I'm trying to connect to a SignalR service from my blazor webassembly client but this fails I think on CORS. This is the code in my razor file.
m_connection = new HubConnectionBuilder()
.WithUrl(myMircoServiceUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult(userService.Token);
})
.WithAutomaticReconnect()
.Build();
await m_connection.StartAsync();
Then in the webassembly logging I see the following error:
Access to fetch at 'xxxx/negotiate?negotiateVersion=1' from origin 'http://localhost:5010' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I added the following CORS policy in my Blazor server configuration and something similar in the microservice config:
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBlazorDebugging();
}
else
{
app.UseExceptionHandler(#"/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors(policy => policy
.WithOrigins("http://localhost:5010")
.AllowAnyHeader()
.AllowAnyMethod());
app.UseClientSideBlazorFiles<Client.Program>();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToClientSideBlazor<Client.Program>(#"index.html");
});
Anybody got any idea what might be wrong?
Update 1
I now see the following error in the Chrome console:
dotnet.js:1 WebSocket connection to 'ws://localhost:5000/hubs/posts?id=9Jxs0DhP924zgw_eIeE9Lg' failed: HTTP Authentication failed; no valid credentials available
Update 2
I removed the [Authorize] attribute from the SignalR hub and now it connects. And I can send messages to the hub. Problem is there is a reason for this attribute, because I don't want that people can subscribe to messages that are not for them
Update 3
Still no progress. Looking at pulling out the authentication to a seperate microservice using IdentityServer4. Last status is I have the following startup routines:
Microservice: gist.github.com/njannink/15595b77ffe1c0593be1a555fa37f83f
Blazor server: gist.github.com/njannink/7302a888110e24d199ea45b66da4f26b
Blazor client: gist.github.com/njannink/add2568cbf48c8b3c070ccd4f28fd127
I've got the same errors with CORS and afterwards Websocket.
In my case the fallback longPolling was used as why the connection worked but the console logged the error HTTP Authentication failed; no valid credentials available.
If you use Identity Server JWT the following code solved the error for my case.
(The Code is from the Microsoft SignalR Documentation - Authentication and authorization in ASP.NET Core SignalR - Identity Server JWT authentication)
services.AddAuthentication()
.AddIdentityServerJwt();
// insert:
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
ConfigureJwtBearerOptions>());
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
public void PostConfigure(string name, JwtBearerOptions options)
{
var originalOnMessageReceived = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await originalOnMessageReceived(context);
if (string.IsNullOrEmpty(context.Token))
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
}
};
}
}
Important: Your Route has to start with hubs for the Options to trigger!
(see Line path.StartsWithSegments("/hubs")))
app.UseEndpoints(e =>
{
...
e.MapHub<ChatHub>("hubs/chat");
});
In my case, ASP.NET Core 2.2 I have an API from which I want to be able to use SignalR from the API to connect to my client application.
I have Projects for
Web API
IdentityServer4
MVC Client
With ASP.NET Core Identity as the for user management
In order for your user to be authenticated you need to implement a IUserIdProvider like this
public class IdBasedUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
//TODO: Implement USERID Mapper Here
//throw new NotImplementedException();
//return whatever you want to map/identify the user by here. Either ID/Email
return connection.User.FindFirst("sub").Value;
}
}
With this I make sure I am pushing along the ID/Email to a method I am calling either from the Server or Client. Although I can always use the .User on the HubContext and it works fine.
In my Web API Startup.cs file I came up with
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(cfg =>
{
cfg.AddDefaultPolicy(policy =>
{
policy.WithOrigins(Configuration.GetSection("AuthServer:DomainBaseUrl").Get<string[]>())
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.SetIsOriginAllowed((_) => true)
.SetIsOriginAllowedToAllowWildcardSubdomains();
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, UserManager<AppUser> userManager,
RoleManager<IdentityRole> roleManager){
app.UseCors();
}
NOTE
Configuration.GetSection("AuthServer:DomainBaseUrl").Get() retrieves the list of domains to allow CORS for from a config file.
And I did this configuration in My Client App COnfigureService Method
services.AddCors(cfg =>
{
cfg.AddDefaultPolicy(policy => {
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.SetIsOriginAllowed((host) => true);
policy.AllowAnyOrigin();
});
});
I hope this helps your situation.
The best solution is indeed as Ismail Umer described using a seperate authentication service using something like IdentityServer4. And use this service in all other services. This is something I will do in a next iteration.
As short term solution I temporary moved the blazor server part into my api service and use a dual authentication method (JWT header or cookie).
var key = Encoding.UTF8.GetBytes(m_configuration[#"SecurityKey"]);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = #"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true
};
})
.AddCookie();
// TODO: For time being support dual authorization. At later stage split in various micro-services and use IdentityServer4 for Auth
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
This is problem with Microsoft.AspNetCore.SignalR.Client 3.1.3.
You can read about it here in comments.
You can wait for update or temporarly fix this issue:
Disable negotiation
Set WebSocket transport explicitly
Modify query url
Add OnMessageReceived handler
Client side:
var token = await GetAccessToken();
var hubConnection = new HubConnectionBuilder()
.WithUrl($"/notification?access_token={token}", options =>
{
options.SkipNegotiation = true;
options.Transports = HttpTransportType.WebSockets;
options.AccessTokenProvider = GetAccessToken;
})
.Build();
Server side:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
// ...
})
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/notification", System.StringComparison.InvariantCulture)))
{
context.Token = accessToken;
}
return Task.CompletedTask;
},
};
});
}