So, I am trying to implement a simple custom authorization implementation using SingalR in .NET 6. I am not using JWTs. Rather I am using something like a PAT for authorization.
I have tried the following in my .NET Server Hub application:
Program.cs (relevant SignalR code)
builder.Services.AddSignalR(o =>
{
o.EnableDetailedErrors = true;
o.ClientTimeoutInterval = TimeSpan.FromSeconds(5);
});
...
app.UseAuthentication();
app.UseAuthorization();
...
// SingalR Hub
app.MapHub<ServerHub>("/serverhub", options =>
{
options.Transports = HttpTransportType.LongPolling;
});
My custom Authorization Attribute (HubAuthorizationAttribute.cs):
using System.Security.Claims;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Hub.API.Authorization
{
public class HubAuthorizationAttribute : Microsoft.AspNet.SignalR.AuthorizeAttribute
{
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
var incomingClientId = request.QueryString.Where(k => k.Key.Equals("clientId")).Select(v => v.Value);
var user = request.User.Identity.Name;
return base.AuthorizeHubConnection(hubDescriptor, request);
}
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
Console.WriteLine("Inside AuthorizeHubMethodInvokation");
return base.AuthorizeHubMethodInvocation(hubIncomingInvokerContext, appliesToMethod);
}
}
}
Then, in my ServerHub.cs file I added [HubAuthorization] above the Hub like this:
[HubAuthorization]
public sealed class ServerHub: Hub
{
...Removed for brevity
}
Then, my .NET client for SignalR looks like this:
var hubConnection = new HubConnectionBuilder()
.WithUrl($"https://localhost:63929/agenthub?clientid={clientId}", options =>
{
//options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.LongPolling;
})
.WithAutomaticReconnect()
.ConfigureLogging(logging =>
{
logging.AddConsole();
// This will set ALL logging to Debug level
logging.SetMinimumLevel(LogLevel.Debug);
})
.Build();
//Signal R Registration
hubConnection.On<bool>("AgentRegistrationStatusReceived", (isReachable) => {
if (isReachable)
Console.WriteLine("(+) Agent is registered!!");
else
Console.WriteLine("(+) Agent wasn't able to be registered!");
});
// SignalR GetState Request from API Server
hubConnection.On<string, Guid>("GetState", async (callbackMethod, requestId) =>
{
Console.WriteLine("Receiving GetState request from server");
bool isAgentAvailable = await LongPollingTest.GetAgentState();
Console.WriteLine("Sending GetState response to server");
await hubConnection.InvokeAsync(callbackMethod, requestId, isAgentAvailable);
});
hubConnection.StartAsync().Wait();
My overridden AuthorizeHubConnection is never called.
Am I missing something fundamental?
Thanks.
Related
I've created a new ASP.NET 6 web app.
I want to periodically broadcast a message through a SignalR hub, from the server.
How can I access the hub from the server? Other answers suggest using GlobalHost but it belongs to a deprecated version of SignalR
Example code from web app Program.cs:
app.MapHub<SiteLayoutHub>("hubs/site");
app.Run();
Task.Factory.StartNew(async () =>
{
var hub = GetSiteLayoutHub(); // How can I get this hub?
while (true)
{
var uiState = GetUIState();
await hub.SendUIUpdateMessage(uiState);
Thread.Sleep(500);
}
});
SiteLayoutHub.cs:
public class SiteLayoutHub : Hub
{
public async Task SendUIUpdateMessage(UIState uiState)
{
await Clients.All.SendAsync("UIUpdateMessage", uiState);
}
}
These are all of the pieces required:
using Microsoft.AspNetCore.SignalR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
builder.Services.AddHostedService<UIUpdateService>();
var app = builder.Build();
app.MapHub<SiteLayoutHub>("hubs/site");
app.Run();
public class SiteLayoutHub : Hub { }
public class UIUpdateService : BackgroundService
{
private readonly IHubContext<SiteLayoutHub> _hubContext;
public UIUpdateService(IHubContext<SiteLayoutHub> hubContext)
{
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
var uiState = GetUiState();
await _hubContext.Clients.All.SendAsync("UIState", uiState);
}
}
private object GetUiState()
{
throw new NotImplementedException();
}
}
I am taking over a legacy SOAP project where I need to replace the SOAP Service with a .NET Core solution. We cannot have the SOAP client change, so we cannot look at REST, GRPC, etc. I have looked at SoapCore and CoreWCF and have both working with the SOAP header username/password auth demo, however, I'm going to use CoreWCF for the time being.
The existing SOAP service uses custom http status code responses, for example, 202 is returned after the service has been processed and in some situations, where a SOAP Fault occurs. I realize this is incorrect, however, we need to maintain the existing business logic.
My questions are:
How can I configure my service to respond with a http status code 202 after the service is completed or when a certain condition is met? IsOneWay=True OperationContract will not work as this returns immediately.
How would I configure a SOAP Fault to respond with a custom http status code?
There are many old SO posts mentioning WebOperationContext, however, I cannot seem to access this within my service. OperationContext doesn't seem to have control of the HttpStatusCode. Maybe I'm missing something.
i.e.:
WebOperationContext ctx = WebOperationContext.Current;
ctx.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.BadRequest;
Here's my sample project breakdown:
Program.cs
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;
namespace CoreWcf.Samples.Http
{
public class Program
{
public const int HTTP_PORT = 8088;
public const int HTTPS_PORT = 8443;
static void Main(string[] args)
{
IWebHost host = CreateWebHostBuilder(args).Build();
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel(options =>
{
options.ListenLocalhost(HTTP_PORT);
options.ListenLocalhost(HTTPS_PORT, listenOptions =>
{
listenOptions.UseHttps();
if (Debugger.IsAttached)
{
listenOptions.UseConnectionLogging();
}
});
})
.UseStartup<BasicHttpBindingStartup>();
}
}
Startup.cs
using CoreWCF;
using CoreWCF.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace CoreWcf.Samples.Http
{
public class BasicHttpBindingStartup
{
public void ConfigureServices(IServiceCollection services)
{
//Enable CoreWCF Services, with metadata (WSDL) support
services.AddServiceModelServices()
.AddServiceModelMetadata();
}
public void Configure(IApplicationBuilder app)
{
var wsHttpBindingWithCredential = new BasicHttpBinding(CoreWCF.Channels.BasicHttpSecurityMode.TransportWithMessageCredential);
wsHttpBindingWithCredential.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName;
app.UseServiceModel(builder =>
{
// Add the Demo Service
builder.AddService<DemoService>(serviceOptions =>
{
// Set a base address for all bindings to the service, and WSDL discovery
serviceOptions.BaseAddresses.Add(new Uri($"http://localhost:{Program.HTTP_PORT}/DemoService"));
serviceOptions.BaseAddresses.Add(new Uri($"https://localhost:{Program.HTTPS_PORT}/DemoService"));
})
// Add BasicHttpBinding endpoint
.AddServiceEndpoint<DemoService, IDemo>(wsHttpBindingWithCredential, "/wsHttpUserPassword", ep => { ep.Name = "AuthenticatedDemoEP"; });
builder.ConfigureServiceHostBase<DemoService>(CustomUserNamePasswordValidator.AddToHost);
// Configure WSDL to be available over http & https
var serviceMetadataBehavior = app.ApplicationServices.GetRequiredService<CoreWCF.Description.ServiceMetadataBehavior>();
serviceMetadataBehavior.HttpGetEnabled = serviceMetadataBehavior.HttpsGetEnabled = true;
});
}
}
}
IDemo.cs (Service Interface):
using CoreWCF;
namespace CoreWcf.Samples.Http
{
// Define a service contract.
[ServiceContract]
public interface IDemo
{
//[OperationContract(IsOneWay = true)]
[OperationContract]
string DemoRequest(string tagid, string readerid, string datetime);
}
}
Demo.cs (Service):
using CoreWCF.Channels;
using Microsoft.AspNetCore.Http;
using System.Net;
namespace CoreWcf.Samples.Http
{
public class DemoService : IDemo
{
public string DemoRequest(string tagid, string readerid, string datetime)
{
return $"Received tagid: {tagid}; readerid: {readerid}; datetime: {datetime}";
}
}
}
CustomUserNamePasswordValidator.cs:
using CoreWCF;
using System.Threading.Tasks;
namespace CoreWcf.Samples.Http
{
internal class CustomUserNamePasswordValidator : CoreWCF.IdentityModel.Selectors.UserNamePasswordValidator
{
public override ValueTask ValidateAsync(string userName, string password)
{
bool valid = userName.ToLowerInvariant().EndsWith("valid")
&& password.ToLowerInvariant().EndsWith("valid");
if (!valid)
{
throw new FaultException("Unknown Username or Incorrect Password");
}
return new ValueTask();
}
public static void AddToHost(ServiceHostBase host)
{
var srvCredentials = new CoreWCF.Description.ServiceCredentials();
srvCredentials.UserNameAuthentication.UserNamePasswordValidationMode = CoreWCF.Security.UserNamePasswordValidationMode.Custom;
srvCredentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNamePasswordValidator();
host.Description.Behaviors.Add(srvCredentials);
}
}
}
Many thanks in advance for any assistance.
cheers.
OutgoingResponse StatusCode is where you set the response code, but it is not an integer value.
If you still want to use it, try ASP.NET compatibility mode. The deployment must be in IIS.
I am writing integration test using XUnit and my web api code is also in C# NET 6 and EF Core.
When I debug it, it can reach the web api and its service layer. But when it reaches EF Core context query example private Message? GetMessage() => _myContext.Messages.OrderBy(m => m.CreatedUtc).FirstOrDefault();, it breaks at Program.cs.
This is the code for TestingWebAppFactory class
public class TestingWebAppFactory<TEntryPoint> : WebApplicationFactory<Program> where TEntryPoint : Program
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<MyContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddDbContext<MyContext>(options =>
{
options.UseInMemoryDatabase("myinmemorydb");
});
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
using (var appContext = scope.ServiceProvider.GetRequiredService<MyContext>())
{
try
{
appContext.Database.EnsureCreated();
}
catch (Exception ex)
{
//Log errors or do anything you think it's needed
throw;
}
}
});
}
}
and this is my code in Xunit
public class MyServiceTest : IClassFixture<TestingWebAppFactory<Program>>
{
private readonly HttpClient _client;
public MyServiceTest(TestingWebAppFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task WhenAValidMessagePosted_ThenShouldReturn()
{
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
source.CancelAfter(TimeSpan.FromSeconds(5));
var response = await _client.GetAsync("https://localhost:xxxx/api/service/message/post?cronExpresson=0");
}
}
We have been using MassTransit with a single RabbitMq transport that is internal to our services. We have a new RabbitMq server that is public that we also want to connect to for certain events, so naturally we want to use the Multibus feature.
The connections are successful, and messages seem to publish fine, but our old RequestClient consumers no longer appear to be working on the original bus, and I am not sure why. The error thrown says MassTransit.RequestTimeoutException: Timeout waiting for response. Multibus IBuses should start on their own, correct?
Here is what it looks like in Startup.cs ConfigureServices (ICorrespondenceInternalBus and ICorrespondenceExternalBus both inherit from IBus):
...
//First bus
services.AddMassTransit<ICorrespondenceInternalBus>(c =>
{
c.AddConsumersFromNamespaceContaining(GetType());
ConfigureAdditionalMassTransitServices(c);
c.UsingRabbitMq((context, cfg) =>
{
cfg.Host(new Uri($"rabbitmq://{rabbitMqServerName}:/"),
h =>
{
h.Username("guest");
h.Password("guest");
});
cfg.ConfigureEndpoints(context, new CorrespondenceSystemEndpointNameFormatter());
cfg.UseMessageRetry(retryConfig => retryConfig.Interval(5, TimeSpan.FromMilliseconds(250)));
cfg.UseHealthCheck(context);
});
});
services.AddMassTransitHostedService();
...
//second bus
services.AddMassTransit<ICorrespondenceExternalBus>(c =>
{
c.UsingRabbitMq((context, cfg) =>
{
cfg.Host(rabbitMqServerName, port, virtualHost, h =>
{
h.Username(username);
h.Password(password);
if (useSsl)
h.UseSsl(s => s.Protocol = SslProtocols.Tls12);
});
cfg.MessageTopology.SetEntityNameFormatter(new CorrespondenceSystemExternalEntityNameFormatter());
cfg.UseHealthCheck(context);
});
});
In the above, both of the buses register and the Exchanges in rabbitmq appear to receive published messages. The part that is not working is consuming messages from RequestClients.
Here is how the RequestClients are being registered:
protected override void ConfigureAdditionalMassTransitServices(
IServiceCollectionConfigurator<ICorrespondenceInternalBus> configurator)
{
configurator.AddRequestClient<ICheckForDuplicateQuery>();
}
The RequestHandler in action:
public class Handler : IRequestHandler<Command, Dto>
{
private readonly IRequestClient<ICheckForDuplicateQuery> _duplicateCheckClient;
public Handler(IRequestClient<ICheckForDuplicateQuery> duplicateCheckClient)
{
_duplicateCheckClient = duplicateCheckClient;
}
public async Task<Dto> Handle(Command request, CancellationToken cancellationToken)
{
var duplicateQuery = new Query();
var duplicateCheckResult = await _duplicateCheckClient.GetResponse<ICheckForDuplicateQueryResult>(duplicateQuery, cancellationToken, TimeSpan.FromSeconds(10));
if (duplicateCheckResult.Message.IsDuplicate)
return new DuplicateDto(duplicateCheckResult.Message.CorrelationIds.First());
...
}
}
And finally the consumer:
public class CheckForDuplicateQueryHandler : IConsumer<ICheckForDuplicateQuery>
{
...
public async Task Consume(ConsumeContext<ICheckForDuplicateQuery> context)
{
if (context is null)
throw new ArgumentNullException(nameof(context));
...
await context.RespondAsync(new Result()).ConfigureAwait(false);
}
private class Result : ICheckForDuplicateQueryResult
{
...
}
}
The consumer never enters and the request client times out.
For comparison, here is what everything looked like before we attempted Multibus when the RequestClients worked fine (the consumer and request client logic are exactly the same, only the Startup.cs is different:
Previous (single bus) Startup.cs:
services.AddMassTransit(c =>
{
c.AddConsumersFromNamespaceContaining(GetType());
ConfigureAdditionalMassTransitServices(c);
c.AddBus(provider => Bus.Factory.CreateUsingRabbitMq(sbc =>
{
sbc.Host(new Uri($"rabbitmq://{rabbitMqServerName}:/"),
h =>
{
h.Username("guest");
h.Password("guest");
});
sbc.ConfigureEndpoints(provider, new CorrespondenceSystemEndpointNameFormatter());
sbc.UseMessageRetry(cfg => cfg.Interval(5, TimeSpan.FromMilliseconds(250)));
}));
});
HealthChecksBuilder.AddRabbitMqHealthcheck(rabbitMqServerName);
...
public virtual void Configure(IApplicationBuilder app, IHostEnvironment env, IHostApplicationLifetime appLifetime, IBusControl bus)
{
...
appLifetime.ApplicationStarted.Register(bus.Start);
appLifetime.ApplicationStopping.Register(bus.Stop);
}
...
//registering the RequestClients previously:
protected override void ConfigureAdditionalMassTransitServices(IServiceCollectionBusConfigurator configurator)
{
configurator.AddRequestClient<ICheckForDuplicateQuery>();
}
Thanks in advance for any help! If you need to see more code snippets I'm glad to provide them, I was trying to keep it concise with only what is needed/affected in the changes.
I have confirmed that the request client should be using the correct bus instance, depending upon where it was configured in this unit test commit.
So, I'm not sure why you aren't seeing the same behavior.
I've tried using MassTransit to publish a message to a topic named events in an Azure Service Bus. I have problems configuring MassTransit to use my predefined topic events, instead it creates a new topic named by the namespace/classname for the message type. So I wonder how to specify which topic to use instead of creating a new one.
This is the code I've tested with:
using System;
using System.Threading.Tasks;
using MassTransit;
using MassTransit.AzureServiceBusTransport;
using Microsoft.ServiceBus;
namespace PublisherNameSpace
{
public class Publisher
{
public static async Task PublishMessage()
{
var topic = "events";
var bus = Bus.Factory.CreateUsingAzureServiceBus(
cfg =>
{
var azureServiceBusHost = cfg.Host(new Uri("sb://<busname>.servicebus.windows.net"), host =>
{
host.OperationTimeout = TimeSpan.FromSeconds(5);
host.TokenProvider =
TokenProvider.CreateSharedAccessSignatureTokenProvider(
"RootManageSharedAccessKey",
"<key>"
);
});
cfg.ReceiveEndpoint(azureServiceBusHost, topic, e =>
{
e.Consumer<TestConsumer>();
});
});
await bus.Publish<TestConsumer>(new TestMessage { TestString = "testing" });
}
}
public class TestConsumer : IConsumer<TestMessage>
{
public Task Consume(ConsumeContext<TestMessage> context)
{
return Console.Out.WriteAsync("Consuming message");
}
}
public class TestMessage
{
public string TestString { get; set; }
}
}
The accepted answer clears up the subscription side:
cfg.SubscriptionEndpoint(
host,
"sub-1",
"my-topic-1",
e =>
{
e.ConfigureConsumer<TestConsumer>(provider);
});
For those wondering how to get the bus configuration right on the publish side, it should look like:
cfg.Message<TestMessage>(x =>
{
x.SetEntityName("my-topic-1");
});
You can then call publish on the bus:
await bus.Publish<TestMessage>(message);
Thanks to #ChrisPatterson for pointing this out to me!
If you want to consume from a specific topic, create a subscription endpoint instead of a receive endpoint, and specify the topic and subscription name in the configuration.
The simplest form is shown in the unit tests:
https://github.com/MassTransit/MassTransit/blob/develop/tests/MassTransit.Azure.ServiceBus.Core.Tests/Subscription_Specs.cs
I was able to send to an Azure Service Bus Topic using the _sendEndpointProvider.GetSendEndpoint(new Uri("topic:shape")); where... "shape" is the topic name.
public class MassTransitController : ControllerBase
{
private readonly ILogger<MassTransitController> _logger;
private readonly ISendEndpointProvider _sendEndpointProvider;
public MassTransitController(ILogger<MassTransitController> logger, ISendEndpointProvider sendEndpointProvider)
{
_logger = logger;
_sendEndpointProvider = sendEndpointProvider;
}
[HttpGet]
public async Task<IActionResult> Get()
{
try
{
var randomType = new Random();
var randomColor = new Random();
var shape = new Shape();
shape.ShapeId = Guid.NewGuid();
shape.Color = ShapeType.ShapeColors[randomColor.Next(ShapeType.ShapeColors.Count)];
shape.Type = ShapeType.ShapeTypes[randomType.Next(ShapeType.ShapeTypes.Count)];
var endpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("topic:shape"));
await endpoint.Send(shape);
return Ok(shape);
}
catch (Exception ex)
{
throw ex;
}
}
}
I also was able to get a .NET 5 Worker Consumer working with code like this... where the subscription "sub-all" would catch all shapes.. I'm going to make a blog post / git repo of this.
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddMassTransit(x =>
{
x.UsingAzureServiceBus((context, cfg) =>
{
cfg.Host("Endpoint=sb://******");
cfg.SubscriptionEndpoint(
"sub-all",
"shape",
e =>
{
e.Handler<Shape>(async context =>
{
await Console.Out.WriteLineAsync($"Shape Received: {context.Message.Type}");
});
e.MaxDeliveryCount = 15;
});
});
});
services.AddMassTransitHostedService();
});