Sails.js socket.io general security - sockets

Using sails sockets.
From a browser I can get all 'tasks' where the user id is 1.
I can now listen for the 'task' event and look for 'created' in the verb to get new tasks and add them to the list.
However I get events from ALL created tasks regardless of user. This seems to be me as a major security issue. All someone needs to do jump into the console and set up a listener to get notified whenever any user creates a new task.
I had a look around for sometime but can't find any posts on the topic.
Being new to this kind of thing - can someone be kind enough to help out?
What is the best practise for dealing with lists over socket.io in Sails?
Cheers!

This should be what you're looking for; it prevents you from subscribing to all existing tasks on the client side. It only subscribes if you're logged in and only to tasks that belong to you. Keep in mind that this is just a first-step in implementing a secure REST API for your app - but it should get you started.
In your client-side app you'd write:
socket.on('connect', function socketConnected()
{
// This subscribes the user to all tasks that belong to him and only him.
socket.get('/task/subscribe', null, function response(data, jwres)
{
// We don’t really care about the response.
});
// This 1.) creates a new task and 2.) subscribes the user to that task.
// If the 'rest' blueprint is on, POSTing to /task gets redirected to TaskController.create automatically by sails.
// If it's not on, you write "socket.get('/task/create' ..."
socket.post('/task', {name : 'MyNewTask'}, function response(data, jwres)
{
// Add the created task inside of 'data' to your client side app.
});
})
Then in TaskController.js you would write:
subscribe : function(req, res)
{
// Is the user logged in?
if(!req.session.user)
{
return res.badRequest();
}
// Find all tasks that belong to the currently logged in user.
Task.find({userID : req.session.user.id}, findUsersCB(err, tasks)
{
// Subscribe the user to all of his tasks.
Task.subscribe(req.socket, tasks);
// Send user's tasks back to the client.
res.json(tasks);
});
}
create : function(req, res)
{
//Is the user logged in?
if(!req.session.user)
{
return res.badRequest();
}
var taskToBeCreated =
{
name : req.param('name'),
userID : req.session.user.id;
};
// Attempt to create the given task.
Task.create(taskToBeCreated, function createTaskCB(err, createdTask)
{
// Subscribe the user to the newly-created task.
Task.subscribe(req.socket, createdTask);
// Send user's task back to the client.
res.json(task);
});
}
I haven't shown an example for the 'update' and 'destroy' actions but the idea is the same for both.

Related

How to add auth endpoints to existing Sails V1 project?

I have an existing Sails V1 project that was generated as an empty app (it uses a React front-end). I'd now like to add in the auth endpoints that would have been created if the app had been generated as a web app. Is that possible?
Yes, it is possible.
You need to hook up the policies and related actions. Your best bet, I would say, is to generate a new project, with the front-end included, and see how that is set up. It utilizes the policy-middleware to call the policy-actions.
module.exports.policies = {
'*': 'is-logged-in',
// Bypass the `is-logged-in` policy for:
'entrance/*': true,
'account/logout': true,
'view-homepage-or-redirect': true,
'deliver-contact-form-message': true,
};
Here you see that the policy.js in the /config folder, calls is-logged-in for all controllers by default. You also see that there is some exceptions added below.
is-logged-in is the file /api/policies/is-logged-in.js:
module.exports = async function (req, res, proceed) {
// If `req.me` is set, then we know that this request originated
// from a logged-in user.
if (req.me) {
return proceed();
}
// Otherwise, this request did not come from a logged-in user.
return res.unauthorized();
};
This is the part that does the check for the logged-in status of the user. You can see that it uses the req.me part, which is set up in the api/hooks/custom/index.js. Here it loads the user from the database and makes the logged in users data available on the req object.
If you don't have, or want to use, this hook, you can exchange req.me with req.session.userId, assuming that you set the userId on the session-object on your login-handler. Example from Sails-code:
....
var userRecord = await User.findOne({
emailAddress: inputs.emailAddress.toLowerCase(),
});
// check user exist
....
// check password
....
//check remember-me
....
// Modify the active session instance.
this.req.session.userId = userRecord.id;
// Send success response (this is where the session actually gets persisted)
return exits.success();
....
I hope this gets you on the right path, at least in terms of where to dig deeper.

Sails pubsub how to subscribe to a model instance?

I am struggling to receive pubsub events in my client. The client store (reflux) gets the data from a project using its id. As I understand it this automatically subscribes the Sails socket for realtime events (from version 0.10), but I don't see it happening.
Here's my client store getting data from sails
(this is ES6 syntax)
onLoadProject(id) {
var url = '/api/projects/' + id;
io.socket.get(url, (p, jwres) => {
console.log('loaded project', id);
this.project = p;
this.trigger(p);
});
io.socket.on("project", function(event){
console.log('realtime event', event);
});
},
Then I created a test "touch" action in my project controller, just to have the modifiedAt field updated.
touch: function(req, res){
var id = req.param('id');
Project.findOne(id)
.then(function(project) {
if (!project) throw new Error('No project with id ' + id);
return Project.update({id: id}, {touched: project.touched+1});
})
.then(function(){
// this should not be required right?
return Project.publishUpdate(id);
})
.done(function() {
sails.log('touched ok');
res.ok();
}, function(e) {
sails.log("touch failed", e.message, e.stack);
res.serverError(e.message);
});
}
This doesn't trigger any realtime event in my client code. I also added a manual Project.publishUpdate(), but this shouldn't be required right?
What am I missing?
-------- edit ----------
There was a complication a result of my model touched attribute, since I set it to 'number' instead of 'integer' and the ORM exception wasn't caught by the promise error handling without a catch() part. So the code above works, hurray! But the realtime events are received for every instance of Project.
So let me rephrase my question:
How can I subscribe the client socket to an instance instead of a model? I could check the id on the client side and retrieve the updated instance data but that seems inefficient since every client receives a notification about every project even though they only should care about a single one.
----- edit again ------
So nevermind. The reason I was getting updates from every instance is simply because at the start of my application I triggered a findAll to get a list of available projects. As a result my socket got subscribed for all of them. The workaround would be to either initiate that call via plain http instead of a socket, or use a separate controller action for retrieving the list (therefor bypassing the blueprint route). I picked the second option because in my case it's silly to fetch all project data prior to picking one.
So to answer my own question. The reason I was getting updates from every instance is simply because at the start of my application I triggered a findAll to get a list of available projects. As a result my socket got subscribed for all of them.
The workaround would be to either initiate that call via plain http instead of a socket, or use a separate controller action for retrieving the list (therefor bypassing the blueprint route). I picked the second option because in my case it's silly to fetch all resources data prior to selecting one.
Here's the function I used to list all resources, where I filter part of the data which is not relevant for browsing the list initially.
list: function(req, res) {
Project.find()
.then(function(projects) {
var keys = [
'id',
'name',
'createdAt',
'updatedAt',
'author',
'description',
];
return projects.map(function(project){
return _.pick(project, keys);
});
})
.catch(function (e){
res.serverError(e.message);
})
.done(function(list){
res.json(list);
}, function(e) {
res.serverError(e.message);
});
},
Note that when the user loads a resource (project in my case) and then switches to another resource, the client is will be subscribed to both resources. I believe it requires a request to an action where you unsubscribe the socket explicitly to prevent this. In my case this isn't such a problem, but I plan to solve that later.
I hope this is helpful to someone.

publishing user relevant data

I have created a simple, minimalistic diary app.
On the client, I use
Meteor.subscribe('entries', Meteor.userId());
to subscribe to the entries created by the user (stored in a mongodb collection). I pass the users ID to the publish function (on the server):
Meteor.publish('entries', function(userID) {
return Entries.find({userId: userID});
});
After login, Meteor.userId() isn't falsy anymore, because it's a reactive data source. However, the relevant data is not being published. I fixed that by auto-running the subscribe function:
Tracker.autorun(function() {
Meteor.subscribe('entries', Meteor.userId());
});
It works, but I feel it's a bad solution.
So here comes the question:
How should one publish user-relevant data in general? There must be a better way to do this, than passing the users ID to the publish-function. Also, isn't it insecure?
By the way, would love to hear some feedback on the app
You don't need to pass the userId from the subscription. Inside the publish function you can use this.userId to get the current user. You can also just return an empty array if the user is not logged in.
Meteor.publish("entries", function () {
if (!this.userId) return [];
return Entries.find({ userId: this.userId });
});

How to get added record (not just the id) through publishAdd()-notification?

Each Sails.js model has the method publishAdd(). This notifies every listener, when a new record was added to a associated model.
This notification does not contain the newly created record. So I have to start another request from the client side to get the new record.
Is there a possibility, that Sails.js sends the new record with the notification, so I can reduce my request count?
Solution
I realized the accepted answer like that:
https://gist.github.com/openscript/7016c5fd8c5053b5e3a3
There's no way to get this record using the default publishAdd method. However, you can override that method and do the child record lookup in your implementation.
You can override publishAdd on a per-model basis by adding a publishAdd method to that model class, or override it for all models by adding the method to the config/models.js file.
I would start by copying the default publishAdd() method and then tweaking as necessary.
I know this is old, but I just had to solve this again, and didn't like the idea of dropping in duplicate code so if someone is looking for alternative, the trick is to update the model of the newly created record with an afterCreate: method.
Say you have a Game that you want to your Players to subscribe to. Games have notifications, a collection of text alerts that you only want players in the game to receive. To do this, subscribe to Game on the client by requesting it. Here I'm getting a particular game by calling game/gameId, then building my page based on what notifications and players are already on the model:
io.socket.get('/game/'+gameId, function(resData, jwres) {
let players = resData.players;
let notifications = resData.notifications;
$.each(players, function (k,v) {
if(v.id!=playerId){
addPartyMember(v);
}
});
$.each(notifications, function (k,v) {
addNotification(v.text);
});
});
Subscribed to game will only give the id's, as we know, but when I add a notification, I have both the Game Id and the notification record, so I can add the following to the Notification model:
afterCreate: function (newlyCreatedRecord, cb) {
Game.publishAdd(newlyCreatedRecord.game,'notifications',newlyCreatedRecord);
cb();}
Since my original socket.get subscribes to a particular game, I can publish only to those subscriber by using Game.publishAdd(). Now back on the client side, listen for the data coming back:
io.socket.on('game', function (event) {
if (event.attribute == 'notifications') {
addNotification(event.added.text);
}
});
The incoming records will look something like this:
{"id":"59fdd1439aee4e031e61f91f",
"verb":"addedTo",
"attribute" :"notifications",
"addedId":"59fef31ba264a60e2a88e5c1",
"added":{"game":"59fdd1439aee4e031e61f91f",
"text":"some messages",
"createdAt":"2017-11-05T11:16:43.488Z",
"updatedAt":"2017-11-05T11:16:43.488Z",
"id":"59fef31ba264a60e2a88e5c1"}}

authorization with socket.io

I'm attempting to determine how best to authorize (in addition to authenticate) a user to perform a specific task using socket.io.
In express, this is fairly straightforward. I first have a login/password form that queries the database to determine if the record exists, and if it does exist, then I attach the User to the req.session data.
exports.session = function(req, res){
User.authenticate(req.body.username, req.body.password, function(err, user){
if (user){
req.session.user = user;
res.redirect('/');
} else {
console.log("authentication failed");
res.render('user/login');
}
});
};
And once I have this, I can use middleware to authorize certain requests. For example,
app.put('/api/users/:userId', m.requiresLogin, m.isUser, api.putUser);
//Middleware
exports.isUser = function(req, res, next){
if (req.session.user._id == req.user._id){
next();
} else {
res.send(403);
}
};
But I'm a bit confused about how to do this using socket.io. Say I have a event listener which alters a user's profile in the database, given that user's profile JSON object.
socket.on('updateProfile', function(data){
// query the database for data.user._id, and update it with the data attribute
// but only do this if the data.user._id is equal to the user trying to do this.
});
Any suggestions as how to achieve this? Can it be done through the session information?
It appears as though you're using Express.
I would highly recommend Express middleware called Passport (https://github.com/jaredhanson/passport).
Using Passport, you can implement any number of strategies to authenticate users (ex. OpenID through Google, Yahoo, Facebook; OAuth through Twitter, Facebook; or local strategies (ex. email registration)).
Finally, to answer your precise question: a project called passport.socketio is amazing in that it plays well with the above authentication strategies and, if you set Express's session, it will play well with that too. (https://github.com/jfromaniello/passport.socketio)
See the documentation here:
https://socket.io/docs/migrating-from-0-9/#authentication-differences
io.set('authorization', function (handshakeData, callback) {
// make sure the handshake data looks good
callback(null, true); // error first, 'authorized' boolean second
});