I am currently developing a web application using SignalR Core. I am using an AuthorizeAttribute (without roles) to ensure only authenticated users will connect to our Hub.
For certain methods a specific policy is required. These methods are also decorated with an AuthorizeAttribute containing a specific policy. Here is my Hub code:
[Authorize]
public class CustomerHub : Hub
{
public async Task SimpleMethod()
{
// Do things that don't require additional authorization
await Task.CompletedTask;
}
[Authorize(Policies.IsAdmin)]
public async Task AdvancedMethod()
{
// Do things that require admin authorization
await Task.CompletedTask;
}
public async Task ErrorMethod()
{
await Task.CompletedTask;
throw new NotImplementedException();
}
}
It all works as intended. I am able to connect to the Hub and call the SimpleMethod and the debugger does not step into the AdvancedMethod when I call it with insufficient rights. However when a user is not authorized to call a specific method I would like to inform them about this. But no specific error or message is sent back to the client in this case. I did implement a custom IHubFilter which informs the user of errors, but it turns out the AuthorizeAttribute is evaluated before this HubFilter is invoked.
public class CustomHubExceptionsFilter : IHubFilter
{
public async ValueTask<object?> InvokeMethodAsync(HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next)
{
try
{
var result = await next(invocationContext);
return result;
}
catch (Exception ex)
{
await invocationContext.Hub.Clients.Caller.SendCoreAsync("Error", new object?[] { $"Oops, something went wrong. Technical details: {ex.GetType().Name}" });
throw;
}
}
}
I also tried adding a custom AuthorizationHandler which returns an error when a user is unauthorized. However this causes the connection to be closed which is not a great solution in my opinion.
Is there a away to inform the user using AuthorizeAttributes? I could create a custom IHubFilter which checks whether the user is authorized. But that would require a lot of custom code. Is there a simpler/more native method available?
I suggest you could consider calling the Hub method's ErrorMethod directly inside the custom AuthorizationHandler inside of directly return the exception.
For example:
We need store the connection id inside the request, then inside the AuthorizationHandler we could get it and then use hub method to return the error to the client.
Like this :
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ConnectionRequirement requirement)
{
var connectionIdClaim = context.User.FindFirst("connectionId");
if (connectionIdClaim == null)
{
context.Fail();
return;
}
var connectionId = connectionIdClaim.Value;
await _hubContext.Clients.Client(connectionId).SendAsync("message", "You are not authorized to perform this action.");
context.Fail();
}
Related
i have a xamarin app that is trying to talk to use SignalR in Azure functions.
i have 2 azure functions as per the documentation.
public static class NegotiateFunction
{
[FunctionName("negotiate")]
public static SignalRConnectionInfo GetSignalRInfo(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
[SignalRConnectionInfo(HubName = "chat")] SignalRConnectionInfo connectionInfo)
//, UserId = "{headers.x-ms-client-principal-id}"
{
return connectionInfo;
}
}
and
public static class SendMessageFunction
{
[FunctionName("Send")]
public static Task SendMessage(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]object message,
[SignalR(HubName = "chat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
// var chatObj = (ChatObject)(message);
return signalRMessages.AddAsync(
new SignalRMessage
{
// the message will only be sent to this user ID
// UserId = chatObj.ReciversId,
Target = "Send",
Arguments = new[] { message }
});
}
}
in my xamarin client i am connecting like this.
try
{
_connection = new HubConnectionBuilder()
.WithUrl("http://192.168.1.66:7071/api")
.Build();
_connection.On<string>("Send", (message) =>
{
AppendMessage(message);
});
await _connection.StartAsync();
}
I send message using this code in one of the pages of Xamarin app page.
try
{
await _connection.SendAsync("Send", MessageEntry.Text);
MessageEntry.Text = "";
}
connection code works it hits "negotiate" function properly but when i call SendAsync it does not hit break-point in [FunctionName("Send")] and nothing happens. It doesn't give me any exception as well.
local settings are like this
Update
i also tried Invoke. it didnt worked.
Should i try making a POST call to [FunctionName("Send")] ?
The way SignalR SaaS works in Functions is slightly different to using the NuGet package in a .NET Application.
You can't invoke a function using the SignalR library, as you can see on the attribute in your function, it's expecting a Http trigger so you have to do a POST to this endpoint instead of invoking it as you normally would.
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]
You still want to listen to the Send target as normal.
I have a Web API that gets call by this method:
public async Task<Model> AddModel(string token, Model newModel)
{
HttpContent content = await HttpHelper.Request(token, baseUrl, url, newModel, HttpRequestType.POST);
return await content.ReadAsAsync<Model>();
}
The Web API is successfully called and I can add a Model. This is the Web API method that gets called:
[Route("webapi/models/addModel")]
[HttpPost]
public async Task<ModelDto> AddWithoutResetingDefault(ModelDto newModel)
{
ModelService modelService = new ModelService();
return modelService.AddModel(newModel);
}
The problem is after the successful add, it doesn't return to the calling code anymore (I have a breakpoint that doesn't get hit). There are no console errors in the browser, I enclosed in a try-catch the calling code and the called code but there were no exceptions thrown.
Also, after the first click to add, if I try to refresh the browser, it takes a really long time to reload the browser (I don't know if being async has something to do with it).
(I don't know if being async has something to do with it) - Yes it has
your Api method
public async Task<ModelDto> AddWithoutResetingDefault(ModelDto newModel)
{
ModelService modelService = new ModelService();
return modelService.AddModel(newModel);
}
is marked as Async method, but the code inside is Sync. And that is the problem, if your modelService.AddModel(newModel); is async, then do
return await modelService.AddModel(newModel);
if its not then there is no point in making the AddWithoutResetingDefault
method async, hence remove Aysnc and simply do a sync method like
public ModelDto AddWithoutResetingDefault(ModelDto newModel)
{
ModelService modelService = new ModelService();
return modelService.AddModel(newModel);
}
Is it possible to use the IUserIDProvider instead of ConnectionID when working with Groups? I have already found an answer here, but that concerns the SignalR 1.0 version. I wonder, whether things have changed in 2.0.
So far, I was using the conventional
Groups.Add(Context.ConnectionId, "groupName");
However, it was difficult to keep track of the connected users when their connectionID was changed (the client is a Xamarin Android app and somehow, reconnection always resulted in creation of a new ConnectionID). Thus, when the client is connecting, I have added a header:
public async Task<bool> Login(int waitMilis, string name)
{
var cts = new CancellationTokenSource();
try
{
cts.CancelAfter(waitMilis);
_connection.Headers.Add("userName", name);
await _connection.Start();
return true;
}
catch(Exception ex)
{
CallFailure(ex);
return false;
}
}
And on server side, implemented the IUserIdProvider:
public class MyUserProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
if (request == null)
throw new ArgumentNullException("request");
else if (request.Headers != null && request.Headers["userName"] != null)
return request.Headers["userName"].ToString();
else return null;
}
}
Now, I would like to do something like
Groups.Add("userName", "groupName");
but the Add method does not have an overload for IUserIdProvider. So, is there a possibility to combine the IUserIdProvider and working with Groups, or am I stuck to creating a ConcurrentDictionary and then calling this?
foreach(User user in group.Users)
{
Clients.User(user.Name).SendMessage(message,
group.LastUpdateIndex
);
}
It ruins the whole beauty and simplicity of the SignalR code :-/
Unfortunately, there isn't currently a method like Groups.Add("userName", "groupName"); in SignalR.
I suggest adding users to their appropriate group(s) in OnConnected:
public class MyHub : Hub
{
public override async Task OnConnected()
{
var userName = MyUserHelper.GetUserId(Context.Request);
foreach (var groupName in GroupManager.GetJoinedGroups(userName))
{
await Groups.Add(Context.ConnectionId, groupName);
}
}
// ...
}
If you need to add an already connected user to a group, then you will likely need to send a message to the user using something like Clients.User(userName).joinGroup(groupName). Each client with userName could then call the appropriate hub method to join groupName.
I have the following Api Controller:
[HttpPost]
public User Create(User user)
{
User user = _domain.CreateUser(user);
//set location header to /api/users/{id}
//set status code to 201
//return the created user
}
It seems like we have to depend on Request.CreateResponse(..) and change the signature of the controller so as to return IHttpActionResult.
I do not want to change the method signature as it is very useful for the documentation purpose. I am able to add the Location header using HttpContext.Current.Response... but not able to set the status code.
Anybody has any better idea on this?
Because you are using a custom (other) return type outside of void, HttpResponseMessage, and IHttpActionResult - it's harder to specify the status code. See Action Results in Web API 2.
From Exception Handling in Web API. If you want to stick with not modifying the return type then this might be something you can do to set the status code:
[HttpPost]
public User Create(User user)
{
User user = _domain.CreateUser(user);
//set location header to /api/users/{id}
//set status code to 201
if (user != null)
{
//return the created user
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Created, user);
}
else
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.InternalServerError));
}
}
My goal is:
To use custom headers with my own token to authenticate a user or machine against my signalr service.
We've been using this methodology succesfully under ASP.net WEB API to perform our own custom claims based authentication and authorization.
Our Web Api was as follows:
protected void Application_Start()
{
GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthorizationHeaderHandler());
}
Then we would have a AuthorizationHandler that would overwrite the Thread.CurrentPrincipal = principal; and we would be done.
Within SignalR I have tried to implement:
1. Mark our hub using Authorize
2. Implemented custom authorize atributes
3. Tried A Custom Module. But besides returning true if the correct headers we're send I still do not get the Context.User to change to the claims based principal that we generate.
But never can we get the Context.User to show the actual user that's being used to connect to the hub.
Any suggestions are Welcome.
Main reason why we want to achieve this is because we have a couple of different user/machine types that connect to our system.
Anybody any suggestions.
Finally found the solution.
I added my own owin security middleware allowing me to handle customer header based authentication.
This could be easily expanded allowing you to combine multiple authenitication scheme's within on service.
First Create Custom Authentication Middleware:
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) :
base(next) { }
public override async Task Invoke(IOwinContext context)
{
var request = context.Request;
var value = request.Headers["Phocabby-MachineKey"];
var username = value;
var usernameClaim = new Claim(ClaimTypes.Name, username);
var identity = new ClaimsIdentity(new[] { usernameClaim }, "ApiKey");
var principal = new ClaimsPrincipal(identity);
principal.Identities.First().AddClaim(new Claim("CanGetApiKey", "False"));
principal.Identities.First().AddClaim(new Claim("Cabinet", "True"));
request.User = principal;
await Next.Invoke(context);
}
}
Then register it in the startup class
public void Configuration(IAppBuilder app)
{
app.Use(typeof(AuthenticationMiddleware));
app.MapSignalR();
}