authorization with socket.io - sockets

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
});

Related

Why doesn't Accounts.createUser create a user in MongoDB?

I'm using accounts-password package to manage my user accounts. I tried 2 ways to create account using Accounts.createUser() function.
1st way: Calling Accounts.createUser() from the client
register.js:
Template.register.events({
"submit form"(event){
event.preventDefault();
const email = event.target.email.value;
const password = event.target.password.value;
Accounts.createUser({
email: email,
password: password
});
}
});
2nd way: Calling Accounts.createUser() from the server method and calling that method from the client. Got the hint after going through: Meteor: Accounts.createUser() doesn't create user
register.js:
Template.register.events({
"submit form"(event){
event.preventDefault();
const email = event.target.email.value;
const password = event.target.password.value;
Meteor.call('createNewUser', email, password);
}
});
methods.js:(on server)
Meteor.methods({
'createNewUser'(email, password){
Accounts.createUser({
email: email,
password: password
});
}
});
In both the cases, no new collection is created in MongoDB. Neither is any old collection updated. My connection strings are proper. Why is this happening?
However, when I use the following on the server, a document is created:
Accounts.users = new Mongo.Collection("profiles", {
_preventAutopublish: true, _driver: dbConn
});
Meteor.users = Accounts.users;
I don't know why a new collection has to be created for this to work. Isn't accounts-password package supposed to create a collection by itself?
if you are just keen on getting a basic version up and running try adding the default {{>loginButtons}} to your html. This will also make sure the javascript is executed as it should be. Building the js manually only makes sense if you need more customizability.
Have you added the accounts-base package? Accounts.createUser comes from this package. (docs)
In the linked example you gave, the OP had set forbidClientAccountCreation: true in the Accounts.config(). That prevents the Accounts.createUser function from working on the client (which is useful in applications where users need an invitation to register). That is why the answers recommended to create the user account server side and isn't applicable to your application.
As a side note, in your second example you are passing an unencrypted password from the client to the server which is considered dangerous.
You can encrypt it before sending it to the server method like so:
const password = Accounts._hashPassword( event.target.password.value );
_driver: dbConn
That line is really concerning. Are you trying to manage your own database connection?
There's nothing wrong with the code you wrote, it's not your problem. Accounts.createUser is the correct method to call.
If you need to use a different database than the one initialized by default by running the meteor command from terminal, look at the documentation on using MONGO_URL.

Firebase verifyIdToken + NodeJS Express Authentication design

Problem:
Due to legislation I have to store personal information within the EU (Social security number). Therefore I can't store this information in Firebase since there is no guarantee of geographical datacenter location when using Google's cloud services.
My proposed solution:
Having a Redis key value store with the sensitive information that can be accessed via a simple REST api where user authentication would be achieved using the users ID token, sent via HTTP Headers.
Firebase allows for verification of a user via the verifyIdToken method in the NodeJS library. This would allow me to check if the user ID matches any user id in my /admin end point of my Firebase. (Or I could hardcode the userIDs that would be allowed into the server since there aren't that many.)
So, the flow of the request would be as follows:
User signs in client side using the Firebase SDK.
Whenever the user needs access to the sensitive information it first gets the user's ID token
let currentUser = FIRAuth.auth()?.currentUser
currentUser?.getTokenForcingRefresh(true) {idToken, error in
if let error = error {
return
}
let headers = [
"X-FBUser-Token":idToken
]
//build request here to https://myServer.com/myEndpoint
}
Then server side we would retrieve the request
app.get('/myEndpoint', function(req, res) {
let idToken = req.get('X-FBUser-Token')
verifyToken(idToken, function(isAdmin){
if (isAdmin) {
//Fetch the key value pair and send it back to the client here
}
})
})
function verifyToken(idToken, cb) {
firebase.auth().verifyIdToken(idToken).then(function(decodedToken) {
var uid = decodedToken.sub;
firebase.database().ref('admins/' + uid).on('value', function (snap){
cb(snap.val() !== null)
})
}).catch(function(error) {
// Handle error
});
}
And then the client would receive back the response and deal with it. Everything done over HTTPS ofcourse.
Note: I know that the code above is rather crude and would need some refinement, but hopefully you get the concept
My questions:
First of all, is this a safe way of doing things?
Is there a better, more straight forward approach?

Sails.js socket.io general security

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.

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 send final response from findOne() callback?

I have a User controller that has a create method that checks the database for email and username uniqueness before creating the user (this is to work-around a bug in the mongodb adpater for SailsJS that doesn't honour the unique attribute flag - version 0.10.5).
The code looks like the following:
User.find({ email: req.body.email }, function (err, user) {
if(user) {
return res.badRequest('Unique email constraint. Email is already used.');
}
});
User.create(req.body).exec(function (err, user) {
// Code to catch and manage err or new user
}
What I expect is that if the email already exists in the database (mongodb), to send a 400 using res.badRequest(), then execution to end.
What happens is that the response is sent, but then control moves to User.create() - execution doesn't end. I suspect that return res.badRequest is returning control back to the calling function (User.findOne), and execution continues from there.
I tried using res.badRequest().end() but that leaves the client hanging (there is no response), and using res.end() after the return res.badRequest() generated 'header send' errors.
How do I have execution of this request end if an existing email is found?
First of all, your findOne is here a find. That's not related to your problem, but it is slightly confusing, and you should ensure you are getting data in the format you expect.
As for finishing the request after marking it bad, I have not used sails, but I was able to end execution in the past by using res.send(). EDIT: after looking at the docs, it seems this is done for you by .badRequest(), so ignore that part.
That said, even THAT is not actually your problem. Your problem is that you start an asynchronous User.find(), and then you immediately start running User.create() (also asynchronously), and so your request doesn't get marked bad until after you have already attempted to create a new user.
What you need to do is one of two things:
Use promises (NOTE: this is how it works for Mongoose; Sails may be different) to only run User.create() after User.find() has completed. e.g;
var userQuery = User.findOne({ email: req.body.email }).exec();
userQuery.addBack(function(err, user) {
if(!!user) res.badRequest('...');
else create_user();
});
Put your user creation logic inside of your findOne block. e.g.;
User.findOne({ email: req.body.email }, function(err, user) {
if (user) { // or perhaps you want if (!err)
User.create(...);
} else {
// handle error
}
});
Personally, I would advise that you use promises (especially later, when you have long chains of requests happening one on top of the other), but take your pick.