Since Koa is not based on connect like Express, there are some adjustment to be done in order to use PassportJs.
Here are the major step in order to use both together. I'm basing the code examples on an existing project available on Github, see Koa-Passport-Example. Note that this is a small example, if you want a full example see Koa-React-Full-Example.
Introduction
In order to use authentication, we are going to need a few libraries. Please not that a few of these libraries are unstable. You have to keep in mind that we're already on a bleeding-edge nodejs version in order to use Koa, and thus it's kind of normal that the libs are not all stable yet.
- Sessions: We need a library to handle Koa Session... There are a few avaible on NPM, but we are going to use koa-sess which is a basic session handler that can accept different session stores.
- Passport: Passport doesn't really work well with Koa since it's not based on connect. Luckily for us, there is a wrapper that has been written and that is available: koa-passport. We're also going to need a strategy, to keep it simple we'll use passport-local.
- Database lib: We need a library in order to be able to save users, you have the choice of pretty much any library, but I'm personally using Mongoosejs, using the 3.9 branch to get promise support.
- Hashing lib: This is actually not directly needed, since we could use node.crypto lib, but I really like bcrypt to hash passwords. So we'll be using bcrypt on a special branch to get 0.11 support since it's not out yet!
- Renderer: We could serve the html statically but, since we might need a renderer later, let's bring it right away, it's not much overhead anyway. We'll be using co-views as renderer and for templater we'll be using Whiskers since it's one of the most lightweight libs.
- Tests: Test is at the base of good development. For tests we are going to use SuperTest, Mocha and Should.
- Generator wrapper: Since not all the libraries support function generators, we need a lib to be able to wrap them. We are going to use co.
File hierarchy
First lets see the project hierarchy
├── config // config files
├── lib // different addons that are not 3rd party
├── package.json
├── server.js // main file
├── src // MVC
│ ├── controllers
│ ├── models
│ └── views
└── test // Tests
Package.json
{
"name": "koa-passport-example",
"private": true,
"version": "0.0.1",
"description": "Full example using Koa and Passport",
"main": "server.js",
"scripts": {
"test": "NODE_ENV=test ./node_modules/.bin/mocha --harmony --reporter spec ./test/test-*.js"
},
"author": "Hugo Dozois <hugo@dozoisch.com>",
"license": "MIT",
"engines": {
"node": ">=0.11.9"
},
"dependencies": {
"bcrypt": "rvagg/node.bcrypt.js#nan",
"co": "3.0.x",
"co-views": "0.2.x",
"koa": "0.8.x",
"koa-bodyparser": "1.0.x",
"koa-passport": "0.4.x",
"koa-router": "3.1.x",
"koa-sess": "0.4.x",
"mongoose": "3.9.x",
"passport-local": "1.0.x",
"whiskers": "0.3.x"
},
"devDependencies": {
"mocha": "1.20.x",
"should": "4.0.x",
"supertest": "0.13.x"
}
}
Application
Creating the user model
We are going to use a really basic user schema and add a few functions to it. The first function we need is a pre-save hook in order to hash the clear text password with bcrypt. After that we need a functions that checks if the password provided by the form matches the one of a user.
We can create generator functions directly for pretty much any function on the schema. Only exception is with hooks on the schema since mongoose doesn't support generators on these yet, we need to wrap them with co.
var bcrypt = require('../../lib/bcrypt_thunk'); // version that supports yields
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
const SALT_WORK_FACTOR = 10;
var UserSchema = new Schema({
username: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true },
},
{
toJSON : {
transform: function (doc, ret, options) {
delete ret.password;
}
}
});
UserSchema.pre('save', function (done) {
// only hash the password if it has been modified (or is new)
if (!this.isModified('password')) {
return done();
}
co(function*() {
try {
var salt = yield bcrypt.genSalt();
var hash = yield bcrypt.hash(this.password, salt);
this.password = hash;
done();
}
catch (err) {
done(err);
}
}).call(this, done);
});
UserSchema.methods.comparePassword = function *(candidatePassword) {
return yield bcrypt.compare(candidatePassword, this.password);
};
UserSchema.statics.matchUser = function *(username, password) {
var user = yield this.findOne({ 'username': username.toLowerCase() }).exec();
if (!user) throw new Error('User not found');
if (yield user.comparePassword(password))
return user;
throw new Error('Password does not match');
};
// Model creation
mongoose.model('User', UserSchema);
Configuring the Koa Server
The config file we will be using has a pretty simple schema. It needs to return at least the following fields:
{
"app": {
"root": "rootpath-tofolder/",
"port": 3000,
"env": "development",
"keys": ["secret-keys-for-sessions"]
},
"mongo": {
"url": "mongodb://localhost/koapassport_dev"
}
}
One of the most important field is the app,keys, without these, koa-sess wont work.
PassportJs
First thing we need to do is to load and configure passport. This is done using the file config/passport.js
The config is pretty straight forward, we can use pretty much all the defaults of passport local strategy. Only thing, since our model functions are using generators, we need to wrap the function with co.
var LocalStrategy = require('passport-local').Strategy;
var User = require('mongoose').model('User');
var co = require('co');
// Basic function that calls our model static function to
// check if the password we got matches the existing one
function AuthLocalUser(username, password, done) {
co(function *() {
try {
return yield User.matchUser(username, password);
} catch (ex) {
return null;
}
})(done);
};
var serialize = function (user, done) {
done(null, user._id);
};
var deserialize = function (id, done) {
User.findById(id, done);
};
// Config of passport
module.exports = function (passport, config) {
passport.serializeUser(serialize);
passport.deserializeUser(deserialize);
passport.use(new LocalStrategy(AuthLocalUser));
};
Koa
The configuration of Koa is pretty simple. Note that
var session = require('koa-sess');
var views = require('co-views');
var bodyParser = require('koa-bodyparser');
module.exports = function (app, config, passport) {
// Configure de keys used to the session
// Without that it wont work!
app.keys = config.app.keys;
// Configure the cookie name of the browser
app.use(session({
key: 'koapassportexample.sid',
}));
app.use(bodyParser());
// Note how we added passport AFTER koa-sess
app.use(passport.initialize());
app.use(passport.session());
// renderer
app.use(function *(next) {
this.render = views('src/views', {
map: { html: 'whiskers' },
});
yield next;
});
};
Routes
var router = require('koa-router');
var indexController = require('../src/controllers/index');
var authController = require('../src/controllers/auth');
var secured = function *(next) {
if (this.isAuthenticated()) {
yield next;
} else {
this.status = 401;
}
};
module.exports = function (app, passport) {
// register functions
app.use(router(app));
app.get('/login', authController.login);
app.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login?error=local'
}));
// Just to be able to create user to test our app
app.get('/user/:username/:password', authController.createUser);
app.get('/', function *() {
if (this.isAuthenticated()) {
yield indexController.index.apply(this);
} else {
this.redirect('/login');
}
});
// Note that to secure a url you can use the secured fonction as a middleware just like this
// app.get('/url', secured, myfunction);
};
Creating controllers
As you might have seen from the routing file, we're going to need 2 controllers.
Auth controller
This is pretty straight forward. The action login renders the login view. We've added a dev utility function. The create user takes 2 parameters /user/:username/:password and creates a user that we can then use to test the auth.
exports.login = function *() {
this.body = yield this.render('auth');
};
exports.createUser = function *() {
var User = require('mongoose').model('User');
try {
var user = new User({ username: this.params.username, password: this.params.password });
user = yield user.save();
this.redirect('/login?usercreated=1');
} catch (err) {
this.redirect('/login?usercreated=0');
}
}
Index controller (secured zone)
Really simple
exports.index = function *() {
this.body = yield this.render('index');
};
Views
We're just missing views now.
The src/views/auth.html is a simple form with a username and password field.
<!DOCTYPE html>
<html>
<head>
<title>Koa Passport Demo</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<div class="container">
<h1>React Koa Demo | Connexion</h1>
<form class="form" role="form" action="/login" method="post">
<div >
<label for="input-username">Username</label>
<input id="input-username" type="text" placeholder="connexion" name="username">
</div>
<div>
<label for="input-password">Password</label>
<input id="input-password" type="password" placeholder="password" name="password">
</div>
<button type="submit">Connexion</button>
</form>
</div>
</body>
</html>
And the src/views/index.html is a simple view saying we are in the secure zone
<!DOCTYPE html>
<html>
<head>
<title>Koa Passport Demo</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<div class="container">
<h1>React Koa Demo | Secured Area</h1>
<div>Welcome to the secured area</div>
</div>
</body>
</html>
Tests
Now with tests. The case we have on /
is simple. If the user is not authenticated, we redirect to /login
. If he is we render the page.
I've created 2 middlewares to help doing the tests. I won't write them completely here just check the github repo!
var should = require('should');
var app = require('../server');
var request = require('supertest').agent(app.listen());
var databaseHelper = require('./middlewares/database');
var authHelper = require('./middlewares/authenticator');
// support for es6 generators
var co = require('co');
describe('Index', function () {
before(function (done) {
co(function *() {
yield authHelper.createUser();
})(done);
});
describe('Anonymous calls', function () {
it('should return 302 to /login', function (done) {
request.get('/')
.expect(302)
.end(function (err, res) {
if(err) return done(err);
res.headers.location.should.equal('/login');
done();
});
});
});
describe('Auth calls', function () {
before(function (done) {
authHelper.signAgent(request, done);
});
it('should render the page', function (done) {
request.get('/')
.expect(200)
.end(done);
});
});
after(function(done) { databaseHelper.dropDatabase(done)});
});
And there you go, you now have a fully fonctionnal basic applications with a secured zone.
To try it, first run npm i
, then you can wether run the tests with npm test
or try it yourself npm start
. You will need to go to localhost:3000/user/:username/:password
to create the first user. You can then try to log him in. Go to localhost:3000
, you should be redirected to /login
where you can enter the credentials of the newly created user. You will then be redirected to the secured zone!