3 July 2014
Tokenized Access and Invited Use in Meteor JS

So you have this spiffy new killer app that you spent way too many of your precious waking hours writing, and even dreamt about in your non-waking hours. When the users come, you want them to create accounts so you know who they are. Maybe you want to send them your newsletter, rich with valuable information they never knew was out there, and maybe send the newsletter to all their friends, too. Maybe you are going to watch what they do so you can learn how they work and streamline the app to their particular way of working. Maybe you are going to collect this information and mine the data for novel user trends so you know what to develop next…

But maybe you don’t care about that junk and just want users to just use your app without accounts and logging in and all that craziness. Maybe they’ll tell and invite their friends if they find it useful. Maybe everyone will love it and love you for creating it. But you don’t really care about all that, or who they are, or what they’re doing as long as they’re productively using the app.

Even though you aren’t creating passworded accounts for your users, you still want to them to be adequately protected. They are your still users and they have identities worth securing. You don’t want someone else snaking into the system as one of your users and being inflammatory. That’s not right. Sigh. There needs to be a happy spot where users can count on their safety without having to create another account on another system.

Fortunately there is a decent way to do this. To demonstrate, I’m going to assume something straightforward like a multiuser chat to be the focus of an app I’ll build up, however I’m not going to detail any of that. But in this scenario, the system would maintain lots of multiuser chats, and let participants in a chat invite others to the chat. Lots of chatters, lots of chats, all chatting it up. But the actual app could be multiuser anything. No, I’m just going to detail the creation and invitation mechanism and leave the app itself up to you. I’m also not going to spend time styling anything. This is about the mechanism, not the page layouts.

So let’s make it work in Meteor.

The Instigator

Someone needs to kick things off, and we’ll call her the instigator. She’ll enter her email and click the start button. An email will be sent to her using the address she provided, and will include a link enabling her to access the new chatroom. When she clicks the link she will end up in the room and it will know it was her. From there she’ll be able to add others to the chat.

We’re going to remember that she instigated the chatroom. Technically, we really don’t have to in this case because all chatters should effectively be equal. But we do remember her in the chat just in case we ever needed to know who started things.

First a little bookkeeping. Create a new Meteor project and add the few meteor packages we’ll be using: coffeescript, iron:router, and email. You can delete the standard factory out-of-the-box files and start things running, Meteor will keep integrating new code as we add it.

We’ll start off with two collections

# collections/chats.coffee

Meteor.chats = new Meteor.Collection "chats"
# collections/tokens.coffee

Meteor.tokens = new Meteor.Collection "tokens"

Meteor.chats will hold the collections of all the chats that have been created, and the chats will include the email of the instigator as well as all the chatters. Meteor.tokens will give us our connection from a link to a chatroom-participant.

I’m not going to worry about issues like removing autopublish and using pub/sub. Again, I’m focusing on providing a working solution for the user-without-account problem. A full implementation would remove autopublish, use pub/sub, set allows, and otherwise secure the application appropriately.

We’ll next define a route to the home page, so when the instigator goes to the site, she’ll be routed there to create a chatroom.

# routes.coffee

Router.map ->
  @route 'home',
    path: '/'

What’s on the home page? Probably some pretty layout and picture of happy people chatting together or something. You know, much the same look as most webapp sites of this day and age. But that’s not important to our discussion; the important thing is that the interface includes the field the instigator can enter her email into and a button to press to make everything begin.

# client/views/home/home.html

<template name="home">
  <p>A new chatroom will be started with you as the instigator.</p>
    <label>Your Email</label>
    <input id="email">
  <p>An email will be sent to you to take you to the room.</p>
  <button id="start">Create the Room</button>

In Meteor, pretty much everything on the page is contained in a template to provide a reference point for routing and the code behind it. So now we need the code to handle the button getting pressed.

# client/views/home/home.coffee

  'click #start': ->
    email = $('#email').val()

Template.home.waitForMail = ->
  Router.go "waitForMail"

When the start button in the home template is clicked, the email address will get pulled from the email input field and the startRoom function will be called on the server. When the server finishes, the waitForEmail function will be called, which will route to the waitForMail route. Note we rent doing any validation. Again, mechanism over finished app.

Setting aside the server for a moment, we’ll add a route for the wait,

# routes.coffee (con't)

@route 'waitForMail',
  path: 'waitForMail'

And the html to put up a friendly message.

# client/views/home/waitForMail.html

<template name="waitForMail">
  <p>Go to your email and wait for the chatroom invitation</p>

Now, back to the server. We need to set up the startRoom method that will be called and send the email.

# server/chat.coffee

  startRoom: (instigatorEmail) ->
    chat_id = Meteor.chats.insert
  instigatorEmail: instigatorEmail
  participants: [ { email: instigatorEmail } ]
    token = randomString()
      chat_id: chat_id
      token: token
      email: instigatorEmail
    sendInstigatorMail instigatorEmail, token

randomString = ->
  chars = "0123456789abcdef"
  length = 60
  string = ""
  for i in [0..length-1]
    num = Math.floor(Math.random() * chars.length)
    string += chars.substring num, num+1

sendInstigatorMail = (instigatorEmail, token) ->
  url = Meteor.absoluteUrl()+"chatter/"+token;
    to: instigatorEmail,
    from: "ChatNow@chat.meteor.com",
    subject: "You've created a chatroom",
    text: "To connect, go to #{url} in your browser."

There are only a few moving parts. The room is created with the instigator as the only chatter, a random token value (60 hex digits long) is created, the token is associated with the instigator and the chat, and a message is sent out.

A mail service like mailgun can be set up quite easily to send the mails. If you sign up and register your chat app, you’d add a line to server to set up the email processor when the server starts:

# server/views/home.coffee (con't)

Meteor.startup ->
  process.env.MAIL_URL =
    'smtp://postmaster%40<mailgun token>.mailgun.org:<password>@smtp.mailgun.org:587'

for instance.

Here’s where the magic begins to happen.

When the instigator opens the email, it contains the embedded link for the chatroom, the url that was constructed in the sendInstigatorMail function on the server. A click on that link heads back to the app to open the chatter route. We need to make sure it’s there:

# routes.coffee (con't)

@route 'chatter',
  path: 'chatter/:_token',
  action: ->
    token = Meteor.tokens.findOne {token: @params._token}
    Session.set 'chat_id', token.chat_id
    Session.set 'chatterEmail', token.email
    Router.go '/chat'

@route 'chat',
  path: 'chat'

We actually established two routes. The chatter route pulls the token that holds the id of the chat and the email of the instigator using the token part of the url and loads them into session variables. It then goes directly to the chat route.

Why the jump from chatter to chat? To hide the token and keep access to the chatter’s identity invisible. Instead of http://…/chatter/{secret-token} being shown in the chatter’s browser’s location bar, only http://…/chat is shown. The token is buried in the chatter’s email message, and that’s the only place it’s visible. If someone else got into the chatter’s email they could log into the chat system and spoof the chatter - but if someone got into the chatter’s email, the chatter is likely have much bigger problems on their mind. We just do our best to make sure the chatter’s access is hidden from the world.

Other Chatters

So, we’re now headed to the chat page itself. We’ll make it kind of a session-long snap chat where you can send invites to others. Let’s break it down.

# client/views/chat/chat.html

<template name="chat">
  &lbrace;&lbrace;#with chat}}
    &lbrace;&lbrace;> chatRecord}}
    &lbrace;&lbrace;> chatMessage}}
    &lbrace;&lbrace;> chatters}}

<template name="chatRecord">
    <textarea id="chatRecord">

<template name="chatMessage">
    <input id="chatMessage">
    <button id="send">Send</button>

<template name="chatters">
    &lbrace;&lbrace;#each participants}}
    &lbrace;&lbrace;> inviteChatter}}

<template name="inviteChatter">
  <input id="chatterEmail">
  <button id="invite">Invite</button>

There are three pieces the main chat page is built from: the chat record, the chat message, and the chatters. The chat record contains the messages that have been exchanged since we got there. No history before that. We could record it, but this app isn’t going to. You can build it differently if you’d like. The chat message includes a place for you to type a message to be added to the chat record when you click the send button. The chatters contains a reactive list of chatters - it grows as more are invited - and a piece to invite other chatters. The invitation includes a place to add an email and an invite button.

Ok, so I know that’s a lot of pieces, but good form dictates they should all be in their own file. I have to admit that I don’t do it so much, tending to have multiple templates in the same file. (Stepping up onto the soapbox) Meteor is currently struggling with project layout issues. There is no one perfect arrangement, so breaking things out maximally isn’t terrible - just crowded. There is a better tool out there waiting to be written to manage creating Meteor projects hiding all the file details, but many of the old guard still cling to software in files in file systems as the preferred building blocks for code delivery. I personally, think we can do a lot better than files, especially in a platform as clean as Meteor. (stepping off the soapbox.)

There’s code backing each of these, of course.

# client/views/chat/chat.coffee

Template.chat.chat -> Meteor.chats.findOne Session.get('chat_id')
# client/views/chat/chatRecord.coffee

  messages: ->
    chatRecord = $('#chatRecord')

Template.chatRecord.rendered = ->
  $('#chatRecord').val ""
# client/views/chat/chatMessage.coffee

  'click #send': ->
    email = Session.get 'chatterEmail'
    chatMessage = $('#chatMessage')
    message = chatMessage.val()
    Meteor.call 'addMessage', @_id, email, message
    chatMessage.val ""
# client/views/chat/inviteChatter.coffee

  'click #invite': ->
    chatterEmail = $('#chatterEmail')
    email = chatterEmail.val()
    Meteor.call 'addParticipant', @_id, email
    chatterEmail.val ""

Again, another bunch of small files. The first establishes the chat from the chat id that was stored in the session when the instigator logged in. The second reactively appends any changed message in the chat to the chat record, but clearing everything out when you first arrive. The third sends a chat message entered by the chatter to the server to be set into the chat, pulling the message from the input field when the send button is clicked, and clearing it once sent. Finally, much the same mechanism is used to add chatters, pulling the email when the invite button is clicked, sending it to the server, and clearing it once sent. Note that no corresponding code is needed for the chatters view - it’s update is completely reactive.

Out on the server, we’ve got two more messages to implement, and a mail invitation.

# server/chat.coffee (con't)

  addMessage: (chat_id, email, message) ->
    shout = "#{email}: #{message}\n"
    Meteor.chats.update { chat_id }, { $set: {message: shout} }

  addParticipant: (chat_id, email) ->
    chat = Meteor.chats.findOne
      _id: chatId
      $nin: {participants, email}
    if chat?
      Meteor.chats.update { chat_id },
      	      { $addToSet: {participants: {email: email } }
      token = randomString()
        chat_id: chat_id
        token: token
        email: email
      sendInvitation email, token

sendInvitation = (email, token) ->
  url = Meteor.absoluteUrl()+"chatter/"+token;
    to: email,
    from: "ChatNow@chat.meteor.com",
    subject: "You've been invited to a chatroom",
    text: "To connect, go to #{url} in your browser."

It’s pretty straightforward. For adding a message, the message is decorated and set as the message in the chat. Other chatters will see it appear in their chat record automatically via reactivity.

For adding a participant, if the chat’s participants don’t already include the invitation email, add it to the participants, generate a token for that user, and send an email built on it to the invitee.

And That’s It

If you noticed, the url we’ve built and sent is the same as the instigator’s (with a different personalized token, of course) so when the invitee clicks the link, they’ll be routed to the chatter route and then to chat. And the circle is closed.

Of course, if we wanted to treat the instigator and other chatters differently, we could have routed them to different endpoints to get different views and functionality. We could have also included names with the emails and possibly looked up avatars to display in the interface. There’s all sorts of things we could add. Hopefully you have a few ideas of your own about the changes you might make.

But all this is separate from the notion of joining a system without creating an account and remembering the credentials. Instead it’s just simple access through a tokenized url, personalized to the user and the space.

Watch Out…

There is one subtle bug in the code that has to do with the way changes to the chat are being observed, but it can be factored out by breaking apart the list of chatters and the message. A bigger, more robust treatment will be the subject of a future blog post.