This project is an attempt at demonstrating a simple use-case of authentication and some basic authorization for an application written using the Meteor framework.
-
Re-worked RPC structure and models to make use of dual-sided
Meteor.methods
(client and server). The previous version did not properly implement latency compensation as intended by Meteor. If you used my previous code as a base for learning Meteor, I suggest reviewing the changes so as not to perpetuate incorrect code structure. -
Create separate UserSessions (server only) collection to hold session information. This allows us to log-in from multiple browsers. Logging out clears all sessions for the user, so effectively logs out anyone using that username.
-
Added expires property to user sessions, defaulting to 1 week.
-
When logging in, your login and password are sent down to the server in plaintext through Meteor RPC (Meteor.call). I am open to suggestions here, but as far as I understand it, this is the job of HTTPS;
-
You'll need Meteor 0.3.6 or higher (run
meteor update
if you have an older version);
I've implemented security based on my accumulated experiences, mostly from perusing StackOverflow over the past few years and in writing implementations. As such, this implementation may be totally flawed. This is not at all a dig against SO; it's a dig against my own comfort-level with regards to my security knowledge. I implore any security experts to carefully review this approach and make any suggestions for improvement.
Here's how I've approached security:
-
Client sends login/password (and other user details) in plaintext to the server (ideally via https)
-
Server generates password-hash from password using bcrypt, stores said hash in DB in user's row:
var salt = bcrypt.genSaltSync(10); var hash = bcrypt.hashSync(password, salt);
-
Client sends login/password in plaintext to the server (ideally via https);
-
Server looks up user in DB by login;
-
Server uses bcrypt to check password against password-hash in DB (
bcrypt.compareSync
) -
If correct, server generates a signed token (note the serverKey, which is a unique value you define server-side in code):
var randomToken = CryptoJS.SHA256(Math.random().toString()).toString(); var signature = CryptoJS.HmacMD5(randomToken, serverKey).toString(); var signedToken = randomToken + ":" + signature;
-
Signed token is hashed and stored in server user-session collection;
-
Server sends signed token to client, which it can store in a cookie if it pleases. The signed token itself is not saved on the server, only a hash representation.
-
The client can then use this session token in subsequent requests to identify itself. The mechanism with which it chooses to preserve this session token is based on what the browser has available (cookie, localStorage, etc).
- Client sends session token in plaintext to the server;
- Server verifies the integrity of the session token (signed by server);
- If successful, retrieves user row by hashing the session token, and looking for this same hash in the user-session collection (stored in step 5 above).
- Server can do what it wants with user row, such as allowing/denying access etc.
Follow the usual installation instructions over at https://github.com/meteor/meteor. If you already have meteor, make sure you have at least version 0.3.6.
Userauth isn't setup as a Meteor package. Since it's a proof-of-concept, you'll need to clone or fork the repository and play around/learn how it works, then pull it apart and adapt it for your own application. So go ahead and clone/fork it.
Once you have it installed, make sure to run npm install
in the root of the project folder to have the bcrypt node module compiled and installed.
You don't explicitly need npm for Userauth, but you do need bcrypt. I'm using the public/node_modules trick, but if that doesn't work for you, you will need to use npm to install bcrypt under /usr/local/meteor/lib
.
If you want to see it in action right now, head on over to http://meteor-userauth.herokuapp.com/. Example usernames/passwords can be found under server/bootstrap.js
. To get you started, try the login mathieu and password password.
I'll briefly explain what each folder and file is used for in the project.
This is used mostly to enable deployment to Heroku with regards to managing node module dependencies (bcrypt). But it's also great for installing node modules locally. Simply run npm install
and the dependencies will get installed (and moved to the public/node_modules
folder too).
The "users" and "notes" collections are created here. They are created both on the server and the client.
For each of these collection types, there's a set of Create, Update and Delete functions. They'll do some basic sanity checks (like making sure some required properties are provided), but they won't do any authorization checks. This isn't their job. These are full-access functions. As long as you use them as intended, they'll modify your collections/documents. We do our authorization checks in rpc-endpoints
, detailed next.
The technique used to set these up (i.e. defined both for server and client) allows our application to feel very responsive. This is where the latency compensation technique employed by Meteor is leveraged. When defining methods under Meteor.methods
for both client and server, both of them will run the code. However, the client will run this code in simulation mode (i.e. it's meant to take a best guess at what the server will do in order to show results near instantly). The client will still write to the local database, but the server will decide if it needs to undo that if the server version of the call disagrees.
This comes for free from Meteor, and is a really cool technique.
- In almost all cases, we are passed the
sessionToken
as the first parameter. Subsequent parameters are function-specific. - If we're in simulation mode (i.e. client), we ignore
sessionToken
and grab the current user. If on the server, we retrieve the current user by callingAuth.getUserBySessionToken
. - Then we do our checks. These range from simple to complex, whatever you want to prevent/allow, you do it here. Throwing exceptions for errors is the recommended way to go here.
Self-explanatory: some basic styling for the example application.
This is where you'll find the <head>
and <body>
tags, along with the root template, which I've called main
. You'll notice that for each major template, there will be a corresponding .js file of the same name under the js/
folder. This is my own unique way of organizing my client-side file structure in Meteor projects.
The main
template essentially includes all other templates.
This template shows a simple login form.
This template shows a list of users, and a form to add a new user. A "delete" button will appear next to your user name (once logged in).
This template shows a list of arbitrary "notes", grouped by user. These notes are just example data. They have an is_private
flag that helps demonstrate server-side filtering based on authorization.
There is also a simple form to add a new note.
This template shows an information box with a dismiss button. Messages (good or bad) that come back from RPCs are usually shoved in there. For example, when a login attempt fails, that message goes in the info box.
This file is a stub for now.
Handles sending login info.
Handles displaying/deleting users. Note the __is_session_user
field, which is added dynamically to the "users" collection on the server end.
Handles displaying/deleting notes. Note the __is_owned_by_session_user
field, which is added dynamically to the "notes" collection on the server end.
Handles displaying/dismissing the info box.
These methods are RPC helpers meant to be called from within the various Meteor client functions like Template.xxx.yyy, Template.xxx.events, etc.
They literally match the RPC endpoints on the server one-to-one. I've found this pattern to be the most effective, as it keeps the necessary division between client and server simple and clear. However, I do look forward to the sugar the Meteor team is cooking up in their auth branch.
You'll notice a pattern emerging from one function to the next. It's as simple as doing a Meteor.call to the equivalent RPC endpoint on the server, and the first parameter is always getSessionToken(), which is fetching the session token from, you guessed it, the Session. Subsequent parameters are also passed depending on the function signature, simple as that.
The callback is boilerplate too. As documented by Meteor, first parameter is the error exception (if any), and the result. You'll most likely see an if (!error)
statement with some additional action to perform if necessary, and the else
showing the error in our info box. Easy.
These are helper methods having to do with the session token and cookie. On startup, we initialize the session token by checking for its presence as a cookie (see initializeSessionToken
). If it's there, we use it. Otherwise, consider us logged-out. I use the string "unknown" as to be explicit about the session token not being known.
rememberSessionToken
is called on successful login, which is the only time you'll be getting a session token from the server. After that, the session token is lost. Only a hash of it exists in the DB.
forgetSessionToken
is called on logout.
getSessionToken
is a simple abstraction so that we're not calling Session.get("token")
throughout our own code.
This is where some of the cooler Meteor magic takes place.
We subscribe to publishedUsers
and publishedNotes
, and pass down our session token to these. This will allow the results coming back from the server to be affected by the session token (i.e. the server will know who is asking for users/notes and will be able to filter results).
Additionally, we wrap all of this in an autosubscribe
so as to make the whole thing reactive. Now, whenever the session token changes (log in or log out), the users/notes will get updated.
Intended to be where the server app initializes. We create the Auth
global, which has a few public methods. We are instructing it to use our Users
collection to manipulate user credentials etc, and also which fields to look for on the user documents.
Next, we lock down the users
and notes
collections so as to prevent client from modifying them, say via console.
Finally, we call our bootstrap function, detailed next.
Simply put, if the database is empty, it populates it with some sample data.
Here we define the collections we want to expose to the client. We have the opportunity to massage them before sending them out. Since the sessionToken
is passed down our publish functions, we can choose to filter the collections based on user privileges.
Additionally, using the publishModifiedCursor
extension I adapted from the built-in _publishCursor
, you can add computed fields to the cursor (such as __is_owned_by_session_user
and __is_session_user
, in this case).
bcrypt, SHA256, HMAC, this is where it all happens. The makeAuthenticationManager
function is a maker function with parameters to allow defining the user collection and user document field-names to affect.
CryptoJS library for HMAC MD5.
CryptoJS library for SHA256.
Near identical adaptation of the require
code by Jonathan Kingston, which I found in a StackOverflow answer.
Contains only node_modules/bcrypt/
, which is the NodeJS bcrypt library used to hash passwords on the server. Yes, it's in public/
... but hopefully a better method to package up node modules (like, under server/
) will be devised soon.
- Of course, props to the Meteor team for putting together such a great framework;
- Special thanks to Jonathan Kingston of Britto fame for providing me a launch platform for understanding manual pub-sub in Meteor;
- StackOverflow for help in figuring out security best practices (bcrypt, etc);
- node.bcrypt.js;
- CryptoJS;
- Darren Schnare for some help in trying to debug what turned out to be a bug in Meteor ;-)