Game Servers and Couchbase with Node.js - Part 2
Introduction
If you have yet to read Part 1 of this series, I suggest you do as it sets up the basic project layout as well as basic user management and is a prerequisite to Part 2!
In this part of the series, we will be implementing session management and authenticated endpoints (endpoints that require you to be logged in). Lets get started!
Session Management - The Model
The first step towards building our session management endpoints is setting up a model to use from these endpoints for manipulating the database. Unlike the account model, this model does not need referential documents or any complicated logic and thus is fairly simple. The first step is importing the various modules we will need, as well as getting a reference to our database connection from our database module.
var db = require('./../database').mainBucket; var couchbase = require('couchbase'); var uuid = require('uuid');
Next, again similar to our account model, we build a small function for stripping away our database properties. 'type' is the only one we need to remove in this case as well.
function cleanSessionObj(obj) { delete obj.type; return obj; }
Now lets start working on the session model class itself. First comes our blank constructor. I'd like to mention at this point that our model classes are currently entirely static in this example, but a good practice to follow is returning your model classes from the static CRUD operations, we just are not at the point that this would be helpful yet.
function SessionModel() { } module.exports = SessionModel;
Now for our first session model function that will actually do any work.
SessionModel.create = function(uid, callback) { };
And inside that function we need to create our document we will be inserting and its associated key (we only store the uid, and then access the users remaining information directly from their account document through our account model, which you will see later.
var sessDoc = { type: 'session', sid: uuid.v4(), uid: uid }; var sessDocName = 'sess-' + sessDoc.sid;
Then we store our newly created session document to our cluster. You will also notice that we call our sanitization function from above here, as well as setting an expiry value of 60 minutes. This will cause the cluster to 'expire' the session by removing it after that amount of time.
db.add(sessDocName, sessDoc, {expiry: 3600}, function(err, result) { callback(err, cleanSessionObj(sessDoc), result.cas); });
Next we need to build a function on our model that allows us to retrieve session information that we have previous stored. To do this, we generate a key matching the one we would have stored, and then execute a get request, returning the users uid from the session to our callback.
SessionModel.get = function(sid, callback) { var sessDocName = 'sess-' + sid; db.get(sessDocName, function(err, result) { if (err) { return callback(err); } callback(null, result.value.uid); }); };
And here is the finalized accountmodel.js:
var db = require('./../database').mainBucket; var couchbase = require('couchbase'); var uuid = require('uuid'); function cleanSessionObj(obj) { delete obj.type; return obj; } function SessionModel() { } SessionModel.create = function(uid, callback) { var sessDoc = { type: 'session', sid: uuid.v4(), uid: uid }; var sessDocName = 'sess-' + sessDoc.sid; db.add(sessDocName, sessDoc, {expiry: 3600}, function(err, result) { callback(err, cleanSessionObj(sessDoc), result.cas); }); }; SessionModel.get = function(sid, callback) { var sessDocName = 'sess-' + sid; db.get(sessDocName, function(err, result) { if (err) { return callback(err); } callback(null, result.value.uid); }); }; module.exports = SessionModel;
Session Management - Account Lookup
Before we can start writing our request handler itself, we need to build a method that will allow us to lookup a user based on their username. We don't really want users to have to remember their uid's! In part 1 we built our account model in accountmodel.js. Lets go back to that file and add a new method.
AccountModel.getByUsername = function(username, callback) { };
How we handle this method is that we will build a key using the provided username that will search for one of the referential documents we created in `AccountModel.create`. If we are not able to locate this referential document, we assume that the user does not exist and return an error. If the username is able to be located, and we find the referential document, we then execute a `AccountModel.get` to locate the user document itself, and forward the callback through there. This means that calls to `AccountModel.getByUsername` will return the full user object as if you had directly called `AccountModel.get` with the uid.
Here is the whole function:
AccountModel.getByUsername = function(username, callback) { var refdocName = 'username-' + username; db.get(refdocName, function(err, result) { if (err && err.code === couchbase.errors.keyNotFound) { return callback('Username not found'); } else if (err) { return callback(err); } // Extract the UID we found var foundUid = result.value.uid; // Forward to a normal get AccountModel.get(foundUid, callback); }); };
Session Management - Request Handling
The last step of enabling session creation is writing the request handler itself. Thankfully, most of the important logic has gone into the sections above, and our request handler simpler forwards information off to each for processing. First we validate our inputs from the user to make sure everything neccessary was provided. Next we try to locate the account by the provided username. Next we validate that the password matches what the user provided by hashing the provided password and comparing. And finally we create the session with our newely created session model and return the details to the user. You may notice that the session id is not directly provided to the user, but instead is passed in the header. This is for consistency as the header is also used to authenticate each request later.
app.post('/sessions', function(req, res, next) { if (!req.body.username) { return res.send(400, 'Must specify a username'); } if (!req.body.password) { return res.send(400, 'Must specify a password'); } accountModel.getByUsername(req.body.username, function(err, user) { if (err) { return next(err); } if (crypt.sha1(req.body.password) !== user.password) { return res.send(400, 'Passwords do not match'); } sessionModel.create(user.uid, function(err, session) { if (err) { return next(err); } res.setHeader('Authorization', 'Bearer ' + session.sid); // Delete the password for security reasons delete user.password; res.send(user); }); }); });
Now that our session creation exists, lets add a method to authenticate the user on a per-request basis. This will check the users session, for now I have placed this in our app.js, however it may be better put in its own separate file later once routes begin being separated into separate files. To authenticate the user, we check the standard HTTP Authorization header, grab the session id from it and then look this up using our session model. If everything goes as plan, we store the newly found user id in the request for later route handlers.
function authUser(req, res, next) { req.uid = null; if (req.headers.authorization) { var authInfo = req.headers.authorization.split(' '); if (authInfo[0] === 'Bearer') { var sid = authInfo[1]; sessionModel.get(sid, function(err, uid) { if (err) { next('Your session id is invalid'); } else { req.uid = uid; next(); } }); } else { next('Must be authorized to access this endpoint'); } } else { next('Must be authorized to access this endpoint'); } }
Mainly for the purpose of displaying the authUser method in action I implemented a `/me` endpoint that returns the user document. We simply do a get through the account model based on the uid that was stored into the request by the authInfo handler, and return this to the client, of course stripping away the password first.
app.get('/me', authUser, function(req, res, next) { accountModel.get(req.uid, function(err, user) { if (err) { return next(err); } delete user.password; res.send(user); }); });
Finale
At this point you should now be able to create accounts, log into them and request stored information about the user. Here is an example using the user we created in Part 1.
> POST /sessions { "username": "brett19", "password": "success!" } < 200 OK Header(Authorization): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7 > GET /me Header(Authorization): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7 < 200 OK { "uid": "b836d211-425c-47de-9faf-5d0adc078edc", "name": "Brett Lawson", "username": "brett19" }
The full source for this application is available here: https://github.com/brett19/node-gameapi
Enjoy! Brett
Comments