How to secure Redis & Socket.IO real-time server so only authenticated users can listen? - node.js

I'm building a web app that has a chat feature. I'm using Laravel 5.4 for the backend and Angular 4 for the front-end.
Everything is working (meaning I can broadcast and recieve) but I'm not at all sure how to secure it. The chat will always be 1 to 1 so its private and has to be secure. Each chat room will have a unique id but somebody could still listen in.
Currently I'm using JWTs for authentication when I make requests from my frontend to my API, but I'm not sure if its possible to implement something similar for this. I know I can pass the token from the frontend using the query option but than I'm unsure how to parse it and I'm also unsure how to verify that it indeed belongs to the user that is trying to access the chat (should I make a request to the API to verify in server.js? That doesn't seem efficient. Is it good enough to compare the user id of the token to the user id that will be passed in the data?)
If anybody has any advice or knows a better way to do it, it would be greatly apperciated
Event that is fired off from Laravel when a new message is posted
class NewMessage implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $data;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct()
{
$this->data = array(
'message'=> 'hi'
);
}
/**
* Get the channels the event should broadcast on.
*
* #return Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('chat');
}
}
server.js (node.js)
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var Redis = require('ioredis');
var redis = new Redis();
redis.psubscribe('private-chat', function(err, count) {
console.log('psubscribe');
});
redis.on('pmessage', function(subscribed, channel, message) {
console.log('pmessage', subscribed, channel, message);
message = JSON.parse(message);
io.emit(channel + ':' + message.event, message.data);
});
http.listen(3000, function(){
console.log('Listening on Port 3000');
});
Frontend component
socket: io.Socket;
this.socket = io.connect("http://webapp.test:3000", { query: this.token });
this.socket.on("private-chat:App\\Events\\NewMessage", (data) =>
{
console.log("Data", data);
});

Try to use SSL (confidentiality)
SSL as we know ensures that the server you're communicating with is actually the webservice you want.
Authentication (To avoid certificate pinning)
You need authentication in your case you are using JWT with HS256, I would strongly recommend you to use RS256 algorythm JWT in this case with private and public keys.
It doesn't matter that nobody else can listen to the conversation between the two of you if you don't know for sure who's on the other end (authentication).
Since I would build the app in the way that if session authentication between two channels its not set then never unfold the content of the data that is being send.
Unique session
As slong as your sevice is authenticated using JWT you send them back an authentication token. This is as simple as a random number or a GUID. This identifier will be required as part of any request to send or receive data on this channel during this session, it will only be accepted on this particular channel, and only as long as this unique session is open.
One more security check: link reply attack
I gave you the tips that I use everyday to seccurely send data back and forth with central banks (so following those tips I think your app should be pretty secure)
UPDATE
How to handle JWT
Create a new middleware Verify JWT token since the middleware in laravel its outter layer of core vendor that means if authentication fails it fails in outter layer not in core.
Group the routes under Verify JWT token middleware!
Create a RedisRepository this could be under App\RedisRepository. This class should be responsible for fetching data from redis.
On middleware decrypt user JWT get the decrypted payload(this might be user UUID or ID).
Fetch user ID from RedisRepository compare with the decrypted payload if positive authentication passes otherwise abort 403 unauthenticated!

Related

Anchor: How to ask the Nodejs backend to co-sign a transaction from the frontend without using Serum's Multisig?

Ideally, I want my Nodejs backend (which secures a keypair) to co-sign a transaction coming from the frontend app (assuming the user will sign the transaction using his / her Solana wallet). The pubkey of the backend will be also included as a signer account to the anchor program.
I can't see any tutorial from Anchor doing this, hopefully it makes sense. I was thinking that the frontend will call a backend API passing down the serialize parameters, on which the backend will sign, and will return a signature to the frontend with its public key.
I have no idea on:
What's the correct Anchor / Solana web3 to use to sign the transaction
How can I add the returning signature (of the backend) to the transaction
I initially don't want to use Multisig with this approach as it will just overcomplicate things. Just need some confirmation from the backend before submitting the transaction.
You probably want to do something like this (backend):
// create the transaction, with user's public key as the feePayer
// Sign the transaction as the backend
// We must partial sign because the transaction still requires the user signature
transaction.partialSign(backendKeypair)
// Serialize the transaction and convert to base64 to return it
const serializedTransaction = transaction.serialize({
// We will need the buyer to sign this transaction after it's returned to them
requireAllSignatures: false
})
const base64 = serializedTransaction.toString('base64')
// return base64 to the frontend
Then from the frontend:
// make sure you're passing user's public key in this request
const response = await fetch(`/your-api/`, {
method: 'POST',
...
})
// assuming your API returns JSON
const json = await response.json()
// Deserialize the transaction from the response
const transaction = Transaction.from(Buffer.from(json.transaction, 'base64'));
// have the user sign transaction using their connected wallet

How to grab remote public key and store in Apollo graphql server periodically?

I have run an Apollo graphql server. I want to verify JWT sent by Firebase client. According to this document we can fetch public keys to verify. It also gives max-age until which we can store the key. I use Auth0-nodejs for JWT verification. My problem is how do I periodically fetch this public key and store in nodejs variable in Apollo server so that I don't have to fetch keys from firebase for every request in the apollo server.
const server = new ApolloServer({
schema, context: ({ req }) => {
const token = req.headers.authorization;
const user = getAuthorizedUser(token);
return { user };
}
});
server.listen();
Here getAuthorizedUser(token) needs to grab the public keys from remote server every time on new request. This may slow down my app. I am not sure how can I periodically fetch data from remote server and use it for every request on my server.
You could just fetch them lazily. Something like:
let keys
let expiresAt = 0
async function getPublicKeys () {
if (expiresAt < Date.now()) {
try {
const { maxAge, publicKeys } = await getKeysFromFirebase()
catch (e) {
// Handle being unable to fetch the keys from Google -- either retry or throw
}
expiresAt = (maxAge * 1000) + Date.now() - arbitraryPadding
keys = publicKeys
}
return keys
}
Barring that, you could have your application refetch them before they expire using setTimeout -- you would just need to ensure that your timer is cleared when the process exits. You could also set up a cron job that would just periodically write the keys to file.

Messages not coming thru to Azure SignalR Service

I'm implementing Azure SignalR service in my ASP.NET Core 2.2 app with React front-end. When I send a message, I'm NOT getting any errors but my messages are not reaching the Azure SignalR service.
To be specific, this is a private chat application so when a message reaches the hub, I only need to send it to participants in that particular chat and NOT to all connections.
When I send a message, it hits my hub but I see no indication that the message is making it to the Azure Service.
For security, I use Auth0 JWT Token authentication. In my hub, I correctly see the authorized user claims so I don't think there's any issues with security. As I mentioned, the fact that I'm able to hit the hub tells me that the frontend and security are working fine.
In the Azure portal however, I see no indication of any messages but if I'm reading the data correctly, I do see 2 client connections which is correct in my tests i.e. two open browsers I'm using for testing. Here's a screen shot:
Here's my Startup.cs code:
public void ConfigureServices(IServiceCollection services)
{
// Omitted for brevity
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtOptions => {
jwtOptions.Authority = authority;
jwtOptions.Audience = audience;
jwtOptions.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// Check to see if the message is coming into chat
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/im")))
{
context.Token = accessToken;
}
return System.Threading.Tasks.Task.CompletedTask;
}
};
});
// Add SignalR
services.AddSignalR(hubOptions => {
hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(10);
}).AddAzureSignalR(Configuration["AzureSignalR:ConnectionString"]);
}
And here's the Configure() method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Omitted for brevity
app.UseSignalRQueryStringAuth();
app.UseAzureSignalR(routes =>
{
routes.MapHub<Hubs.IngridMessaging>("/im");
});
}
Here's the method I use to map a user's connectionId to the userName:
public override async Task OnConnectedAsync()
{
// Get connectionId
var connectionId = Context.ConnectionId;
// Get current userId
var userId = Utils.GetUserId(Context.User);
// Add connection
var connections = await _myServices.AddHubConnection(userId, connectionId);
await Groups.AddToGroupAsync(connectionId, "Online Users");
await base.OnConnectedAsync();
}
Here's one of my hub methods. Please note that I'm aware a user may have multiple connections simultaneously. I just simplified the code here to make it easier to digest. My actual code accounts for users having multiple connections:
[Authorize]
public async Task CreateConversation(Conversation conversation)
{
// Get sender
var user = Context.User;
var connectionId = Context.ConnectionId;
// Send message to all participants of this chat
foreach(var person in conversation.Participants)
{
var userConnectionId = Utils.GetUserConnectionId(user.Id);
await Clients.User(userConnectionId.ToString()).SendAsync("new_conversation", conversation.Message);
}
}
Any idea what I'm doing wrong that prevents messages from reaching the Azure SignalR service?
It might be caused by misspelled method, incorrect method signature, incorrect hub name, duplicate method name on the client, or missing JSON parser on the client, as it might fail silently on the server.
Taken from Calling methods between the client and server silently fails
:
Misspelled method, incorrect method signature, or incorrect hub name
If the name or signature of a called method does not exactly match an appropriate method on the client, the call will fail. Verify that the method name called by the server matches the name of the method on the client. Also, SignalR creates the hub proxy using camel-cased methods, as is appropriate in JavaScript, so a method called SendMessage on the server would be called sendMessage in the client proxy. If you use the HubName attribute in your server-side code, verify that the name used matches the name used to create the hub on the client. If you do not use the HubName attribute, verify that the name of the hub in a JavaScript client is camel-cased, such as chatHub instead of ChatHub.
Duplicate method name on client
Verify that you do not have a duplicate method on the client that differs only by case. If your client application has a method called sendMessage, verify that there isn't also a method called SendMessage as well.
Missing JSON parser on the client
SignalR requires a JSON parser to be present to serialize calls between the server and the client. If your client doesn't have a built-in JSON parser (such as Internet Explorer 7), you'll need to include one in your application.
Update
In response to your comments, I would suggest you try one of the Azure SignalR samples, such as
Get Started with SignalR: a Chat Room Example to see if you get the same behavior.
Hope it helps!

How to share dynamic objects across workers?

I'm trying to make a game, which works on rooms, lobby and such (imagine the chat app, except with additional checks/information storing).
Let's say, I have a module room.js
var EventEmitter = require('events');
class Room extends EventEmitter {
constructor (id, name) {
super();
this.id = id;
this.name = name;
this.users = [];
}
}
Room.prototype.addUser = function (user) {
if(this.users.indexOf(user) === -1) {
this.users.push(user);
this.emit('user_joined', user);
} else {
/* error handling */
}
};
module.exports = {
Room: Room,
byId: function (id) {
// where should I look up?
}
};
How can I get exactly this object (with events)? How can I access events emitted by this object?
In a single instance of node, I would do something like:
var rooms = [];
var room = new Room(1234, 'test room');
room.on('user_joined', console.log);
rooms.push(room);
Also, I don't quite understood how Redis is actually helping (is it replacement of EventEmitter?)
Regards.
EDIT: Would accept PM2 solutions too.
Instead of handling rooms in Node, you can replace them with channels in Redis).
When a new client wants to join in a room, the NodeJS app returns it the ID of this given room (that is to say the name of the channel), then the client suscribes to the selected room (your client is directly connected to Redis.
You can use a Redis Set to manage the list of rooms.
In this scenario, you don't need any event emitter, and your node servers are stateless.
Otherwise, it would mean Redis would be exposed on the Internet (assuming your game is public), so you must activate Redis authentication. A major problem with this solution is that you have to give the server password to all clients, so it's definitely unsecure.
Moreover, Redis' performances allow brute force attacks so exposing it on Internet is not recommended. That's why I think all communications should go through a Node instance, even if Redis is used as a backend.
To solve this, you can use socket.io to open sockets between Node and your clients, and make the Node instances (not the client) subscribe to the Redis channel. When a message is published by Redis, send it to the client through the socket. And add a layer of authentication to ensure only valid clients connect to a given channel.
Event emitter is not required. It's the Redis client which will be an event emitter (like in this example based on ioRedis)

how can I make private chat rooms with sockjs?

I am trying to make a chat system where only two users are able to talk to each other at a time ( much like facebook's chat )
I've tried multiplexing, using mongoDB's _id as the name so every channel is unique.
The problem I'm facing is that I cannot direct a message to a single client connection.
this is the client side code that first sends the message
$scope.sendMessage = function() {
specificChannel.send(message)
$scope.messageText = '';
};
this is the server side receiving the message
specificChannel.on('connection', function (conn) {
conn.on('data', function(message){
conn.write('message')
}
}
When I send a message, to any channel, every channel still receives the message.
How can I make it so that each client only listens to the messages sent to a specific channel?
It appeared that SockJS doesn't support "private" channels. I used the following solution for a similar issue:
var channel_id = 'my-very-private-channel'
var connection = new SockJS('/pubsub', '')
connection.onopen = function(){
connection.send({'method': 'set-channel', 'data': {'channel': channel_id}})
}
Backend solution is specific for every technology stack so I can't give a universal solution here. General idea is the following:
1) Parse the message in "on_message" function to find the requested "method name"
2) If the method is "set-channel" -> set the "self.channel" to this value
3) Broadcast further messages to subscribers with the same channel (I'm using Redis for that, but it also depends on your platform)
Hope it helps!

Resources