How to use JWT with Seneca, Passport and Express on NodeJS

Ever wondered how to get JWT access working on Seneca running through Express? I certainly was! Here’s a quick tutorial showing you the code I put together after reading lots of other blogs and scratching my head.

Start off by creating a workspace, open a command prompt and create a directory called seneca-jwt-tutorial then move inside it:

$ mkdir seneca-jwt-tutorial
$ cd seneca-jwt-tutorial

 

Next we need to initialise npm, feel free to press <ENTER> for all the questions that pop up:

$ npm init

This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install  --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (seneca-jwt-tutorial) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /Users/dave/Desktop/seneca-jwt-tutorial/package.json:

{
  "name": "seneca-jwt-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes)

 

Then we’ll need to install all the packages we’re going to use, these are:

  • body-parser
  • express
  • express-session
  • jsonwebtoken
  • passport
  • passport-jwt
  • seneca
  • seneca-web
  • seneca-web-adapter-express

Use the following command to install them all together:

$ npm install --save body-parser \
  express express-session \
  jsonwebtoken \
  passport passport-jwt \
  seneca seneca-web seneca-web-adapter-express

 

Right, now it’s time to start adding some code, create repo.js which we’ll use to store our user information:

$ touch repo.js

 

Then copy this code into repo.js, it’s an array and a couple of methods to query the values in the array; in a production environment you’d probably want to replace this with something a little more permanent and a bit more secure:

'use strict'

var storage = [
    { id: 1, username: 'jack', password: 'admin', displayName: 'Jack', email: 'jack@example.com' },
    { id: 2, username: 'jill', password: 'admin', displayName: 'Jill', email: 'jill@example.com' }
]

function findById (id, cb) {
    process.nextTick(() => {
        var idx = id - 1

        if (storage[idx]) {
            cb(null, storage[idx])
        }
        else {
            cb(new Error('User ' + id + ' does not exist'))
        }
    })
}

function findByUsername (username, cb) {
    process.nextTick(() => {
        for (var i = 0, len = storage.length; i < len; i++) {
            var record = storage[i]
    
            if (record.username === username) {
                return cb(null, record)
            }
        }

        return cb(null, null)
    })
}

exports.users = {
    findById: findById,
    findByUsername: findByUsername
}

 

Next we create routes.js which we’ll use to map out our routes and pin them to Seneca action patterns:

$ touch routes.js

 

Then copy this code into routes.js, as you can see we are creating three routes, one insecure telling you to login (home), one to handle the login process (login) and another which is secure (profile):

'use strict'

module.exports = [
    {
        pin: 'role:admin,cmd:*',
        map: {
            home: {
                GET: true,
                POST: true,
                alias: '/'
            },
            login: {
                POST: true
            },
            profile: {
                GET: true,
                auth: {
                    strategy: 'jwt',
                    fail: '/',
                }
            }
        }
    }
]

 

Our next task is to create plugin.js where we’ll put the Seneca action patterns for each of our routes:

$ touch plugin.js

 

Next copy this code into plugin.js, a quick look will see that the patterns correspond to the names of the routes, one for home, one for login and the last one for profile. Notice that the login pattern makes use of the repo to find a user by username:

'use strict'

var Jwt = require('jsonwebtoken')
var Repo = require('./repo.js')

module.exports = function plugin (options) {
    var seneca = this;

    seneca.add('role:admin,cmd:home', (msg, done) => {
        done(null, {ok: true, message: 'please log in...'})
    })

    seneca.add('role:admin,cmd:login', (msg, done) => {
        if (msg.args.body.username && msg.args.body.password) {
            var username = msg.args.body.username;
            var password = msg.args.body.password;
        }

        Repo.users.findByUsername(username, (err, user) => {
            if (err) {
                done(err)
            }
            else if (!user) {
                done({ ok: false, err: "no such user found" })
            }
            else if (user.password !== password) {
                done({ ok: false, err: "password did not match" })
            }
            else {
                var payload = {id: user.id};
                var token = Jwt.sign(payload, options.secretOrKey);
                
                done(null, {ok: true, token: token})
            }
        })
    })

    seneca.add('role:admin,cmd:profile', (msg, done) => {
        done(null, {ok: true, user: msg.args.user})
    })
}

 

Finally we create index.js which is where we’ll wire everything up:

$ touch index.js

 

Next copy this code into index.js:

  • Lines 1-16: Require all the packages we’re going to use.
  • Lines 18-20: Create our JWT options object that tells Passport where to look for the token and our chosen secret which you should definitely change when working with production code.
  • Lines 22-37: Create the JwtStrategy and action to check the payload from the token against our repo.
  • Lines 39-45: User serialization and deserialization methods for Passport.
  • Lines 47-52: Create the Express service to handle HTTP requests.
  • Lines 54-71: Create our Seneca configuration and service and tell the server which port it should be listening to.
'use strict'

var BodyParser = require('body-parser')
var Express = require('express')
var Passport = require('passport')
var PassportJwt = require('passport-jwt')
var Seneca = require('seneca')
var Web = require('seneca-web')

var Plugin = require('./plugin.js')
var Repo = require('./repo.js')
var Routes = require('./routes.js')

var ExtractJwt = PassportJwt.ExtractJwt
var JwtStrategy = PassportJwt.Strategy

var jwtOptions = {}
jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeader()
jwtOptions.secretOrKey = 'ArnoldRimmer'

var strategy = new JwtStrategy(jwtOptions, function(payload, next) {
    console.log('payload received', payload)
    Repo.users.findById(payload.id, (err, user) => {
        if (err) {
            next(err)
        }
        else if (!user) {
            next(null, false)
        }
        else {
            next(null, user)
        }
    })
})

Passport.use(strategy)

Passport.serializeUser((user, cb) => {
    cb(null, user)
})

Passport.deserializeUser((user, cb) => {
    cb(null, user)
})

var app = Express()
app.use(BodyParser.urlencoded({ extended: true }))
app.use(BodyParser.json())
app.use(Passport.initialize())

var config = {
    adapter: require('seneca-web-adapter-express'),
    auth: Passport,
    context: app,
    options: { parseBody: false },
    routes: Routes
}

var seneca = Seneca()
seneca.use(Plugin, { secretOrKey: jwtOptions.secretOrKey })
seneca.use(Web, config)
seneca.ready(() => {
    var server = seneca.export('web/context')()

    server.listen('4000', (err) => {
        console.log(err || 'server started on: 4000')
    })
})

 

Assuming that’s all done and it’s now time to start it up, use the following command:

$ node index.js

 

If all went ahead without a hitch you should see this output:

{"kind":"notice","notice":"hello seneca lckpjngjlfme/1487116033208/8390/3.3.0/-","level":"info","when":1487116033310}
server started on: 4000

 

Now it’s time to get testing, fire up Postman or an equivalent API querying tool and start by sending a GET request to http://localhost:4000 (See the screenshot below for request settings)

 

Then POST the username jack with a password of admin to http://localhost:4000/login (See the screenshot below for request settings)

 

You should then be rewarded with a JSON Web Token, which you can use in an Authorization header prepended with JWT:

Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNDg3MTE5MzQ0fQ.i5X2qOAdSGFahPZtFak-KpOXHA0tpLEaSfRpMn-CwM8

 

Copy the contents of token and add it to an Authorization and send a GET request to http://localhost:4000/profile (See the screenshot below for request settings)

 

If you get back a similar response to the one shown above, you’re done. If you found any problems with this tutorial please let me know in the comments, similarly if you found this tutorial helpful please feel free to let me know too!

(Visited 181 times, 1 visits today)

1 Comment

Leave A Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.