Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to securely send a message to a specific user

I am using ASP.NET MVC 5 and SignalR. I want to send a message to a specific user. I have followed the method which is explained in this tutorial (also suggested by this answer).

I have overridden IUserIdProvider, to use UserId as connectionId.

public class SignalRUserIdProvider : IUserIdProvider
{
    public string GetUserId(IRequest request)
    {
        // use: UserId as connectionId
        return Convert.ToString(request.User.Identity.GetUserId<int>());
    }
}

And I have made the change to my app Startup to use the above custom provider:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var idProvider = new SignalRUserIdProvider();
        GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
        ConfigureAuth(app);
        app.MapSignalR();
    }
}

Now my client can pass destination ClientID to the hub, and hub will forward the message only to the requested Client, this is my Hub:

[Authorize] 
public class ChatHub : Hub
{
    public void Send(string message, string destClientId)
    {
        Clients.User(destClientId).messageReceived(Context.User.Identity.Name + " says: " + message);
    }
}

This is working perfectly fine. My question is about security and if this is the right approach for a secure website?

According to Introduction to SignalR Security, randomly generated connection id is part of SignalR security:

The server does not process any request from a connection id that does not match the user name. It is unlikely a malicious user could guess a valid request because the malicious user would have to know the user name and the current randomly-generated connection id.

The above approach is replacing the randomly selected connectionId with a fixed UserId... Is there any security issue with the above code?


Note: I am working on an e-commerce website where users need to be able to receive messages even if they are offline (the messages would be stored in DB and they can read once the are online).

like image 872
Hooman Bahreini Avatar asked Sep 06 '18 00:09

Hooman Bahreini


2 Answers

You're pretty well on your way to the right solution. The only trick is that your security should be set up such that you can't spoof someone else's UserId.

For instance, we have exactly the same scenario with SignalR, but we use the UserId claim from a JWT Token to tell who you are. So you would need to know the guy's login credentials if you wanted to receive his messages. You can't just change the UserId in the claims, because then the JWT signature would be invalid and you wouldn't be authenticated or authorized anymore.

So TL;DR: use JWT authentication or something with a one-way signature that prevents tampering with the UserId.

like image 199
Captain Kenpachi Avatar answered Nov 17 '22 02:11

Captain Kenpachi


Updated on

Note: I am working on an e-commerce website where users need to be able to receive messages even if they are offline (the messages would be stored in DB and they can read once the are online).

Do the followings to send message using randomly generated connectionId in secured way:

First add the following classes to track user's connectionIds to understand whether the user is online or offline

public static class ChatHubUserHandler
{
    public static ConcurrentDictionary<string, ChatHubConnectionViewModel> ConnectedIds =
        new ConcurrentDictionary<string, ChatHubConnectionViewModel>(StringComparer.InvariantCultureIgnoreCase);
}

public class ChatHubConnectionViewModel
{
    public string UserName { get; set; }
    public HashSet<string> UserConnectionIds { get; set; }
}

Configure the ChatHub as follows

To make the ChatHub secured add [Authorize] attribute on the ChatHub class.

[Authorize]
public class ChatHub : Hub
{
    private string UserName => Context.User.Identity.Name;
    private string ConnectionId => Context.ConnectionId;

    // Whenever a user will be online randomly generated connectionId for
    // him be stored here.Here I am storing this in Memory, if you want you
    // can store it on database too.
    public override Task OnConnected()
    {

        var user = ChatHubUserHandler.ConnectedIds.GetOrAdd(UserName, _ => new ChatHubConnectionViewModel
        {
            UserName = UserName,
            UserConnectionIds = new HashSet<string>()
        });

        lock (user.UserConnectionIds)
        {
            user.UserConnectionIds.Add(ConnectionId);
        }

        return base.OnConnected();
    }


    // Whenever a user will be offline his connectionId id will be
    // removed from the collection of loggedIn users.

    public override Task OnDisconnected(bool stopCalled)
    {
        ChatHubConnectionViewModel user;
        ChatHubUserHandler.ConnectedIds.TryGetValue(UserName, out user);

        if (user != null)
        {
            lock (user.UserConnectionIds)
            {
                user.UserConnectionIds.RemoveWhere(cid => cid.Equals(ConnectionId));
                if (!user.UserConnectionIds.Any())
                {
                    ChatHubUserHandler.ConnectedIds.TryRemove(UserName, out user);
                }
            }
        }

        return base.OnDisconnected(stopCalled);
    }
}

Now use the following model class to store the message to the database. You can also customize the message class according to your exact need.

public class Message
{

    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long MessageId { get; set; }

    [ForeignKey("Sender")]
    public string SenderId { get; set; }

    [ForeignKey("Receiver")]
    public string ReceiverId { get; set; }

    [Required]
    [DataType(DataType.MultilineText)]
    public string MessageBody { get; set; }
    public DateTime MessageSentAt { get; set; }
    public bool IsRead { get; set; }


    public User Sender { get; set; }
    public User Receiver { get; set; }
}

Then in the Message Controller:

This is just an example code. You can customize the code according to your exact need.

[HttpPost]
public async Task<ActionResult> SendMessage(string messageBody, string receiverAspNetUserId)
{
      string loggedInUserId = User.Identity.GetUserId();
      Message message = new Message()
      {
            SenderId = loggedInUserId,
            ReceiverId = receiverAspNetUserId,
            MessageBody = messageBody,
            MessageSentAt = DateTime.UtcNow
      };

      _dbContext.Messages.Add(message);
      _dbContext.SaveChangesAsync();


      // Check here if the receiver is currently logged in. If logged in,
      // send push notification. Send your desired content as parameter
      // to sendPushNotification method.

      if(ChatHubUserHandler.ConnectedUsers.TryGetValue(receiverAspNetUserId, out ChatHubConnectionViewModel connectedUser))
      {
            List<string> userConnectionIds = connectedUser.UserConnectionIds.ToList();
            if (userConnectionIds.Count > 0)
            {
                var chatHubContext = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
                chatHubContext.Clients.Clients(userConnectionIds).sendPushNotification();
            }
      }

      return Json(true);
}

Now question is what if the message was sent whenever the receiver was offline?

Okay! In this case you can handle the push notification in two ways! calling a ajax method or SignalR Hub method as soon as the receiver is online to bind the notifications. Another method is using partial view for notification area in layout page. I personally prefer using partial view for notification area.

Hope this will help you!

like image 1
TanvirArjel Avatar answered Nov 17 '22 00:11

TanvirArjel