Building a Slack Clone in Meteor.js (Part 3): Authentication and Security

Free Course

Build Your First Node.js Website

Node is a powerful tool to get JavaScript on the server. Use Node to build a great website.

This is the third of a five-part series on building a Slack clone using Meteor. The aim of these tutorials are not just for you to blindly follow instructions, but it’s our hope that you’ll understand the thought process and reasoning behind the architecture.

Right now, everyone is anonymous (everyone is scotch!), and everyone talks on the same channel. Let’s do something about that.

User Accounts

As with everything Meteor – there’s a package for that. The accounts-base and accounts-password packages provide the capabilities for login, logout, account creation, email validation, password recovery. There’s also the accounts-ui and accounts-ui-unstyled which provides you with a login / register form that you can just drop into your application.

Using these packages, user data would be stored in the users collection, so you must make sure your collection names do not clash with this.

Let’s install the packages.


$ meteor add accounts-base accounts-password accounts-ui

And lets drop in the login form template, provided by the accounts-ui package, into our template.


{{> loginButtons}}

We can see that these packages created a users collection in our database.

An empty users collection

And now let’s create a dummy user.

Creating a dummy user using the UI provided by the accounts-ui package

And now we look back into our collection, we find an entry.

The user entry as seen by Robomongo

And here it is in BSON format.


{
    "_id": "e6AN2jGt9T9jrcwDt",
    createdAt": ISODate("2015-05-24T15:48:21.547Z"),
    "services": {
        "password": {
            "bcrypt": "$2a$10$6CDufWjV.OD/mQIv0MOxkukXzWgE0UZVGLAUnBF8vRHkjRKl4NoLy"
        },
        "resume": {
            "loginTokens": []
        }
    },
    "emails": [
        {
            "address": "dan@danyll.com",
            "verified": false
        }
    ]
}

Let’s go through each field to get a better understanding.

The _id field is a unique ID used to identify the document. You can provide this value, but if not, Meteor will generate a random alphanumeric string (e.g. e6AN2jGt9T9jrcwDt). You can retrieve the _id of a user by running Meteor.userId().

The createdAt field stores an ISODate data type of when the user was created. This is not a valid JSON datatype, and is one of the reasons Mongo uses BSON instead of JSON.

The services field contains data that are required for the account service providers. Right now, it is an object with a password field, because we are using the accounts-password package. Later on when we add the accounts-github package, you’ll see the github field being written inside the services property. The services.resume property keeps track of all the login sessions the user has.

The emails field specifies all the emails that belongs to a user. In the Accounts system, each email can only belong to one user.

Configuring the Accounts UI

At the moment, a user can sign up with just an email and password. But mimicking Slack, we want each user to have a username as well as an email address. This is easily done by configuring Accounts.ui. You may put this inside any client-side code.


Accounts.ui.config({
    passwordSignupFields: 'USERNAME_AND_EMAIL'
});

And now when we create a new user, we see the username field in the document.

An additional 'username' field shows up

Apart from USERNAME_AND_EMAIL, there’s also the USERNAME_AND_OPTIONAL_EMAIL, USERNAME_ONLY and EMAIL_ONLY options. EMAIL_ONLY is the default.

The accounts-base package will check the username field to ensure it’s unique, as well as check the email has not already been used by another user, before inserting into the users collection.

The accounts-ui package provides the login form, as well as calling methods to register/login our users. But for production applications, you might want to create your own UI and set your own account management logic. To do that, you must call the Accounts API and Passwords API yourself. For example, you’d manually call Accounts.createUser() to create the user, and customize the creation logic with Accounts.onCreateUser()

Third-Party Integration

How many usernames and passwords must you keep on top of your head? Even with passwords managers like LastPass, keeping track of another username / password set for our Slack clone might deter new users from signing up.

So Meteor also provide package such as accounts-google, accounts-facebook, accounts-twitter so you can log in with your existing Google, Facebook and Twitter accounts. One less thing to remember!

GitHub

Each third-party integration is a little different, but instructions should be clear. Here, we’ll show you how to integrate with GitHub. Feel free to try it out with other services like Twitter or Facebook!

Let’s add the accounts-github package.


$ meteor add accounts-github

For your application to use GitHub for logging users in, it must be registered to an account. So go to your application and open up the login UI. Click the red button and follow the instructions set it up.

meteor-github

github-new-oauth

And now we can login using our GitHub account. The data required for this is stored under the github property of services.


...
"services": {
    "github": {
        "id": 3571481,
        "accessToken": "2D58E69740CAD0FC1721970A1FF6A140A1A76A41",
        "email": "dan@danyll.com",
        "username": "d4nyll"
    }
    ...
},
...

Once the user is logged in, you will also see that reflected in the GitHub Applications tab.

github-slack-oauth

Confirmation Email

When you sign up to most online services these days, you get a confirmation email.

We can enable this through Accounts.config (note that it’s not Accountsa.ui.config).

/server/accounts.js


Accounts.config({
    sendVerificationEmail: true
});

This will internally call the Accounts.sendVerificationEmail() on accounts creation.

You must call Accounts.config only on the server, since all mail are sent from the server. Specifying it in client code means no emails will be sent.

But to send the email, Meteor needs to be hooked up to a mail server. To do that we need the email package provided by the core.


$ meteor add email

The email package reads the MAIL_URL environment variable for the address of the SMTP server. If you use meteor deploy, the app will automatically uses an account provided by Mailgun to send your emails.

If no MAIL_URL variable is set, the mail that would have been sent is output to the console.


I20150525-19:38:08.837(8)? ====== BEGIN MAIL #0 ======
I20150525-19:38:08.839(8)? To: dan@danyll.com
I20150525-19:38:08.839(8)? Subject: How to verify email address on localhost:3000
I20150525-19:38:08.840(8)? Content-Type: text/plain; charset=utf-8
I20150525-19:38:08.840(8)? Content-Transfer-Encoding: quoted-printable
I20150525-19:38:08.840(8)? 
I20150525-19:38:08.840(8)? Hello,
I20150525-19:38:08.840(8)? 
I20150525-19:38:08.840(8)? To verify your account email, simply click the link below.
I20150525-19:38:08.840(8)? 
I20150525-19:38:08.840(8)? http://localhost:3000/#/verify-email/LooL9TcQj9qhCONfDwLLpCNNtrNs-FDeSsz6efqTrA-
I20150525-19:38:08.840(8)? 
I20150525-19:38:08.840(8)? Thanks.
I20150525-19:38:08.840(8)? 
I20150525-19:38:08.841(8)? ====== END MAIL #0 ======
I20150525-19:38:08.839(8)? (Mail not sent; to enable sending, set the MAIL_URL environment variable.)
I20150525-19:38:08.839(8)? MIME-Version: 1.0
I20150525-19:38:08.839(8)? From: "Meteor Accounts" 

Notice the verification link provided – http://localhost:3000/#/verify-email/LooL9TcQj9qhCONfDwLLpCNNtrNs-FDeSsz6efqTrA- If we go to that URL, it will run Accounts.onEmailVerificationLink(), which verifies our email address.

meteor-email-verified

We can check this on the database.

meteor-email-verified-robo

Mandrill

But we want to actually send the email! We must set up our own SMTP server, but we can also use services like Mailgun or Mandrill. Here, we will use Mandrill, but the logic is the same. So, let’s set up an account on Mandrill.

mandrill-signup

You’ll be provided with some details about the SMTP server. Note that the value for MAIL_URL has the syntax smtp://USERNAME:PASSWORD@HOST:PORT/. Here we have all the information apart from the PASSWORD:


smtp://dan@danyll.com:PASSWORD@smtp.mandrillapp.com:587/

Click on + Add API Key and a new key will be generated for you.

mandrill-no-key

mandrill-new-key

So now let’s update our MAIL_URL environment variable.

/server/mail.js


Meteor.startup(function () {
    process.env.MAIL_URL = "smtp://dan@danyll.com:y3Z8TQxpxCiYsJJsCwyV0A@smtp.mandrillapp.com:587/";
};

Or if you want things clear over concise:


Meteor.startup(function () {
  smtp = {
    username: 'dan@danyll.com',
    password: 'y3Z8TQxpxCiYsJJsCwyV0A',
    server:   'smtp.mandrillapp.com',
    port: 587
 };
    
  process.env.MAIL_URL = 'smtp://' + encodeURIComponent(smtp.username) + ':' + encodeURIComponent(smtp.password) + '@' + encodeURIComponent(smtp.server) + ':' + smtp.port;
});

This time, the mail has been sent using Mailgun and to an actual recipient!

email-received

If you don’t like the default email template, you can modify it using Accounts.emailTemplates

Updating Our application

Now that we have the idea of users, let’s assign each message to the user that sent it. We will store the Meteor.userId to get the ID of the current user.


Messages.insert({
  text: $('.input-box_text').val(),
  user: Meteor.userId(),
  timestamp: Date.now()
});

Meteor.userId() will return the current user’s id, and if no user is logged in, returns null.

We are storing our users by ID, but when we display it, we’d really want to display their username. So let’s create a helper function to do that.

Since this function might be used in more than one place, we’d use the Template.registerHelper() function to define a helper function which can be used from all templates.


Template.registerHelper("usernameFromId", function (userId) {
    var user = Meteor.users.findOne({_id: userId});
    if (typeof user === "undefined") {
        return "Anonymous";
    }
    if (typeof user.services.github !== "undefined") {
        return user.services.github.username;
    }
    return user.username;
});

We will also do something similar so the time of the message is stored and displayed.


Template.registerHelper("timestampToTime", function (timestamp) {
    var date = new Date(timestamp);
    var hours = date.getHours();
    var minutes = "0" + date.getMinutes();
    var seconds = "0" + date.getSeconds();
    return hours + ':' + minutes.substr(minutes.length-2) + ':' + seconds.substr(seconds.length-2);
});

We will use the new properties and helper methods in our message template.


    <div class="message">
        <a href="" class="message_profile-pic"></a>
        <a href="" class="message_username">{{usernameFromId user}}</a>
        <span class="message_timestamp">{{timestampToTime timestamp}}</span>
        <span class="message_star"></span>
        <span class="message_content">{{text}}</span>
    </div>

Now when you log in and post a message, your username and the timestamp when the message was posted is displayed.

meteor-slack-username-time

Security

Great! Now we have user data – a big responsibility! We would need to keep this information secure.

When we created our example Meteor application, the autopublish and insecure packages were automatically added. autopublish makes available all the data from all collections, and insecure allows any user to insert into, update, and/or delete any documents.

Why were these two packages included in the first place? Well, imagine I introduced the topic of subscription, publication, allow-deny rules, method calls, all before we were able to send one message! It’d been too much! So Meteor, by design, included those two packages to allow us to start building our application as quickly as possible. But to make our data secure, we must remove both packages.

autopublish

If we run Meteor.users in the console now, we get the records of all the users. Not great.


$ meteor remove autopublish

You’ll see all the messages on screen disappeared, that’s because the data from the messages collection is no longer being sent from the server.

If we run Meteor.users now, we will only see, at most, the _id, username and profile fields. We won’t be able to get other users’ data now.

But we want our messages to show. So let’s publish it now server-side.

/server/publications.js


Meteor.publish('messages', function () {
    return Messages.find();
});

And now subscribe to the publication client-side.

/client/app.js


Meteor.subscribe('messages');

And now our messages are back! But the usernames have gone. So we must also publish certain fields from our users collection too.

/server/publications.js


Meteor.publish("allUsernames", function () {
  return Meteor.users.find({}, {fields: {
    "username": 1,
    "services.github.username": 1
  }});
});

/client/app.js


Meteor.subscribe('allUsernames');
insecure

If you open up your browser’s console, and you run


Messages.find().fetch()

You can actually get the list of all the messages in the collection. What’s more, find the _id of one of the Objects returned and run:


Messages.remove({_id: "tduWf5JMyJbX4w2Qj"})

Where tduWf5JMyJbX4w2Qj is the _id of the Object.

The message has been removed! And not just on your local environment, but for everyone! Of course that’s a big security hole.

Tthe insecure package is just there to help us develop faster by allowing us to alter the server database on the client. Now we are not on training wheels anymore, we can remove it!


$ meteor remove insecure

But now if you try to send a message, this error comes up in the console:


insert failed: Access denied

This is because we have now prevented the user from making changes to the collections. To get control over what actions the user is allowed to perform, we can either use Allow/Deny rules or Meteor methods.

Allow / Deny

We can allow users to interact directly with the collection by setting allow rules.


Messages.allow({
  insert: function (userId, doc) {
    return true;
  }
});

The collection.allow() method specify which modifications are allowed from the client. Here, we are specifying that all inserts from any users are allowed on the Messages collection, by returning true.

All looks innocent, but what we just did allows a user to insert a message masquerading as another user:


Messages.insert({
  text: $('.input-box_text').val(),
  user: "someoneElsesID",
  timestamp: Date.now()
});

So our allow rule should really check that the userId of the user matches the one specified int he document.


Messages.allow({
  insert: function (userId, doc) {
    return (userId && doc.user === userId);
  }
});

As you can see, using allow/deny rules are quick and easy, but are prone to mistakes. Oh, and this also reminds me, I should check that the timestamp given is accurate…but how do we do that? What’s to stop someone claim they posted a message predicting the lottery numbers an hour before the draw?

You can install the matb33:collection-hooks and do something like this:


Messages.before.insert(function (userId, doc) {
  doc.timestamp = Date.now();
});

But this involves another package (Meteor doesn’t support hooks natively). This is why I think using Meteor methods are easier in most cases.

Methods

With Meteor methods, when the client wants to insert into the collection, it asks the server to do it. The server can then do some validation and set its own fields (such as message.timestamp). This saves us having to check whether the timestamp provided by the client is accurate.

/client/input.js


Meteor.call('newMessage', {text: $('.input-box_text').val()});

/server/methods.js


Meteor.methods({
  newMessage: function (message) {
    message.timestamp = Date.now();
    message.user = Meteor.userId();
    Messages.insert(message);
  }
})

All in all, I prefer using Meteor methods over allow/deny rules, mainly for these reasons:

  • Easier to do complicated validation – you explicitly determine what is allowed or not in the code
  • All the code are in one block – no inserts on the client, setting allow / deny rules on both client and server, and hooks on the server
  • Quite subjective, but I find methods less error-prone

Do you agree / disagree / still not sure? Start a discussion in the comments section!

Stubs

But now, you might think, this defeats the purpose of latency compensation and the database everywhere principles of Meteor, since we must now wait for the server to confirm before the message shows up.

If we shut down our application now on the server, users won’t see their messages on the message list.

But we can define a stub – the equivalent of a Meteor method that gets ran on the client-side. Taken from this Stack Overflow answer I provided a few months back.

When the client calls the method, it will execute the stub method on the client-side, which can quickly return a (predicted) response. When the server comes back with the ‘actual’ response, it will replace the response generated by the stub and update other elements according.

I’d usually create a /client/stubs.js to house all the stub methods, but since there are no secret information we need to hide from the client, we can just move our /server/methods.js to, say, /methods.js

/client/stubs.js


Meteor.methods({
  newMessage: function (message) {
    message.timestamp = Date.now();
    message.user = Meteor.userId();
    Messages.insert(message);
  }
});

Now even when the server is down, the messages will still be rendered immediately on the client. And when the server is back up, the messages are still saved.

Daniel Li

I am a full-stack web developer in Hong Kong. Do check out my profile, blog and projects, many of which are open-source! I get a real buzz from hearing words like Meteor and Node, if you do too, throw me a tweet!