Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

jaunty

labsvisual9MIT1.2.2

A simple, all-in-one, lightweight JWT authentication and authorization middleware for express.

jwt, auth, authen, authorization, authz, express, framework, passport, oauth, json, token

readme

Jaunty

A simple, easy-to-use JWT authentication and authorization middleware for express.js optimised for performance.

forthebadge

Build Status GitHub issues PRs Welcome GitHub license XO code style

Introduction and Rationale

I come from a Hapi.js background where most of what we want comes right out of the box or requires a little, light-weight implementation. With Express.js, however, I had the challenge of using Passport.js which is an over-kill is you want to add server-side authentication for your RESTful APIs. Honestly, you don't need so much if all you're doing is validating just a JSON web token and adding scoped authentication and/or role based authorization. Sadly I did not have any other alternatives and hence had to use whatever I got without making a fuss about it. So I did.

The code base grew in size pretty quickly and we had a lot of developers joining us; this meant that we had to explain to them how the auth framework worked, what are the good practices, etc. This was just extraneous for something as simple as outlined above.

Finally, I decided that I had no other option but to implement a middleware myself which takes care of all of this; and hence, jaunty was born. As the complexity, and knobs and switches of an application grow, so does the probability of someone messing them up. In my mind, the middleware I was creating has to be as simple as possible and at the same time as extensible as possible. A very core Hapi.js philosophy.

Jaunty has only one required parameter and the rest of them are just augmentations on validation functions and deserializations.

Intallation

npm i -S jaunty jsonwebtoken

'Bear' in mind that you need to install jsonwebtoken and express for this to work properly. They are listed, in the package.json file, as peerDependencies.

Usage - Jaunty JWT Verification

The jaunty middleware helps you automatically parse, validate and deserialize JSON Web Tokens. If you don't know what they are and how they work, I'd suggest you give the above link a read and come back.

In its simplest form, you can use jaunty like this:

const Jaunty = require( 'jaunty' );

// ...
app.use( Jaunty.createInstance( {
    signingSecret: 'abc'
} ) );
// ...

app.get( '/', function homeHandler( request, response, next ) {

    return response.status( 200 ).json( request.user );

} );

One thing you need to pay attention to is the fact that you MUST attach the jaunty middleware BEFORE you attach your routes.

In the most basic sense, you're pretty much done. That's all you need.

Options

The .createInstance( opts: Object ) method takes an options argument with the following shape:

  • signingSecret (String) - The secret used to sign the JWT.
  • validate (Function) - A function which is invoked just after the signature verification of the token is complete. You can use this to verify if the user's session is valid, etc. The function signature is: function validate( decodedToken: Object, [fn(error, data)] ). The function can return a Promise (or, in extension, can be async) or have the second parameter as a standard error-first callback. In either case, the data which has to be returned by the function should have the following shape:
    • isValid (Boolean) - Specifies if the provided token has passed external, probably non-cryptographic validation like session ID checks, etc.
    • payload (Object) [null] - The custom deserialized version of the JWT payload provided to the function. This can be useful in case you are fetching some additional data from your database (say, for example, the authentication/role scope). If this property is present, Jaunty will use it as the deserialized form of the user object and assign it to your specified attachments (documented below).
  • ignoreAuthentication (Set) - A set of routes which the middleware should ignore and allow to pass without auth.
  • attachments (Object) - An object which contains:
    • request (String) [user] - The name of the property on the request object which will contain the decoded and deserialized payload.
    • response (String) [null] - Similarly, the name of the propery on the response object.

Handling Jaunty Errors

Jaunty exposes a common base error type called AuthorizationError which acts as the base class for all the errors emitted by Jaunty. Following are the errors emitted by Jaunty at various points in time:

  • BadSchemeError - this error is thrown by Jaunty when, for a required route, no Authorization header is provided or when the header is not in the form of Authorization: Bearer <Token>.
  • BadTokenError - thrown when the JWT token is malformed and/or can not be parsed.
  • UnauthorizedError - thrown when the user isn't authorized/authenticated to access the route.

All of these errors are exported in the Jaunty module as Jaunty.Errors. A simple example handler for errors can have the following form:

const Jaunty = require( 'jaunty' );

// ... basic config ...

app.use( Jaunty.createInstance( {
    signingSecret: 'My_SECRET!'
} ) );

// ... other middlewares ...

app.get( '/', handler );

// ... other routes ...

app.use( function baseErrorHandler( err, req, res, next ) {

    //
    if ( err instanceof Jaunty.Errors.UnauthorizedError ) {

        return res.status( 403 ).json( {
            errors: [
                {
                    message: 'You are not allowed to access this route.'
                }
            ]
        } );

    } else if ( err instanceof Jaunty.Errors.AuthorizationError ) {

        return res.status( 401 ).json( {
            errors: [
                {
                    message: 'You are not authenticated.'
                }
            ]
        } );

    }

    return next();

} );

// ... bootstrapping code ...

Take note of the two things we are doing here and their order; the first construct checks specifically for UnauthorizedError whilst the second one catches everything else. Make sure that the block to check for specific errors is always at the last to avoid confusion.

Usage - Jaunty ACL

With release 1.1.0, Jaunty comes with its own ACL (Access Control List) module which is, much like Jaunty, super-simple to use. To get started with the ACL, you can do something like the following:

const Jaunty = require( 'jaunty' );
const aclProvider = Jaunty.createACL();

// Use the Jaunty to verify JWTs at a router/application level.
app.use( Jaunty.createInstance( {
    signingSecret: 'WHAT_EVER_STRING',
    ignoreAuthentication: new Set( [ '/login' ] )
} ) );

// Now you can use the ACL like so:
// Per route level
app.get( '/', aclProvider.hasPermissions( 'user:write' ), function handleGet() { ... } );

// Per router level
const adminRouter = express.Router();

adminRouter.use( aclProvider.hasPermissions( 'admin' ) );
adminRouter.get( ... );

app.use( '/admin', adminRouter );

Options

The .createACL() function takes an object as its options. There are just two properties on the options object:

  • attachmentPath (String) [user] - This is the path to the User's object on express's request.
  • permissionsPath (String) [permissions] - This is the path to the permissions property on the User object.

With the defaults, permissions is at request.user.permissions. I hope this makes sense.

After you execute createACL(), an object is returned which contains just one function (yet again) called hasPermissions([permissions]). This hasPermissions() functions is responsible for ultimately compiling and spitting out the middleware which validates the routes for permissions.

You have a couple of ways in which you can specify permissions to the hasPermissions() function.

  • hasPermissions( 'permission1', 'permission2' ) - this translates to: make sure the user has permission1 and permission2;
  • hasPermissions( [ 'permission1' ], [ 'permission2', 'permission3' ] ) - this translates to: make sure the user has either permission1 or permission2 and permission3.

Similarly, your permissions object on the user can be either an array of string or a space separated OAuth style scope. Which is uber-jargon to say:


// Type one
const user = {
    permissions: [ 'read', 'write' ]
};

// Type two
const user = {
    permissions: 'admin:read admin:write'
};

Examples

Ignore Routes

In an API, there needs to be a way to get the authentication token; by its very design, this route needs to be open for use without any form of user-delegated authentication. This is supported in Jaunty by using the ignoreAuthentication property while creating and initializing the instance.

// ... other express-related stuff ...
app.use( Jaunty.createInstance( {
    signingSecret: 'WHAT_EVER_STRING',
    ignoreAuthentication: new Set( [ '/login' ] )
} ) );
// ... other express-related stuff ...

Now, every request sent to the /login route will be open and not require any form of validation.

Complicated Ignore Patterns

The ignoreAuthentication property is good for simple rules like the ones defined above. What about parameterized paths or what about when you want to ignore a specific path if it matches a specific HTTP method? For that, we are using the excellent express-unless.

Look up its documentation to read more. As a quick example, if you want to open the /test/:id path but keep /test closed, you can do something like the following:

// ... other express-related stuff ...
app.use( jwtLock.unless( {
    path: [ /\/test\/*./ ]
} ) );
// ... dragons ...

Validate Sessions

Once the cryptographic verification of the token is done, an optional callback function can be supplied which checks if the session for that token is still valid. The validate property provided to Jaunty takes the form function( token: Object, [function (error, data)] ). Let's see a quick example of that in actions:

In the app.js file, you can have the middleware defined as follows:

// app.js

// ... express stuff ...
app.use( Jaunty.createInstance( {
    signingSecret: 'MY_SUPER_SECRET',
    validate: require( './validate' )
} ) );

// ... other middlewares ...
// ... error handlers ...

module.exports = app;

And in your validate.js file you can have something like the following:

// validate.js

const { Session } = require( '../models' );

module.exports = async function validateJWT( token ) {

    try {

        /*
         * The token is a COMPLETE decode of the data which
         * means that it includes the header. In general,
         * following is the shape of a JWT:
         *      {
         *          header: Object,
         *          payload: Object,
         *          signature: String | Buffer
         *      }
         */

        // You can also detructure it in the argument definition.
        const { payload } = token;
        const sessionData = await Session.findById( payload.sessionId );

        if ( !sessionData ) {

            return { isValid: false };

        }

        if ( sessionData.userId === payload.userId ) {

            return {
                isValid: true,

                /*
                 * You can also, optionally, specify a payload.
                 * If provided, Jaunty will use that when it
                 * attaches the deserialized user to the
                 * attachment you have provided.
                 */
                payload: {}
            }

        }

    } catch ( error ) {

        // Jaunty will catch it and respond.
        throw error;

    }

};

What Jaunty will effectively do is that once the provided JWT gets cryptographically validated, it'll call the validate() method with the entire token. You can now run your own checks and add whatever logic you deem fit. Finally, return an object which contains { isValid: Boolean } and optionally the modified version of the token payload you want.

Which means that whatever you return in the payload property will be what you can access from request.user.

Advice on Security

Just to make your application extra secure, please do not store the signing secret in any unsafe/insecure place and make sure that you rotate it regularly.

In case you want better security, think about using asymmetric key pairs (support coming soon) for signing and verifying JW tokens.

One of the best methods of managing sensitive cryptographic keys is to use AWS Secrets Manager along with AWS Key Management Service.

For systems not so heavy on compliance, you can get away with dynamic environment variables written in your .env file at the time of build on the CI. This comes with its own risk since the hardware on which the CI builder runs is multi-tenacy.

Contributing

In case you have a feature in mind or a bug fix, feel free to send a PR! And don't worry; your PRs won't be ignored: at all.

Style Guide

The project uses the xo coding style with a few modifications.

To aid with changelog generation and release management, we urge everyone to use the conventional-changelog format. In case you don't want to pollute the system with another global binary, you can just follow the commit style while writing your message. (Personally, I find that to be faster.)

License

Copyright 2019 Shreyansh Pandey

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

changelog

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

Generated by auto-changelog.

v1.2.2 - 2019-07-18

Fixed

  • fix(npm): fix #7 and #8 #7

Commits

  • chore(package): bump version 5a8c958

v1.2.1 - 2019-06-19

Commits

  • chore(package-lock): update package-lock c7bf743
  • build(deps): bump handlebars from 4.1.0 to 4.1.2 4d2b5a1
  • chore(changelog): update the changelog 008bf23
  • build(deps): bump js-yaml from 3.12.2 to 3.13.1 7e59e4c
  • chore(package): bump version d9ab01c

v1.2.0 - 2019-04-30

Fixed

  • fix(path-matching): fix #4 #4

Commits

  • chore: add express-unless dd10716
  • chore(changelog): update the changelog 6e7d5c4
  • chore(changelog): update the changelog f6a55d1
  • docs(README): update the README to include information about express-unless 811d45c
  • chore(test): update the test script to install deps on pretest c49ac24
  • chore: version bump f270f68
  • chore(package): resolve merge conflict 99ca7ce

v1.1.0 - 2019-04-03

Commits

  • chore(package-lock): update package-lock 672c920
  • test(acl): add integration and unit tests for the ACL library 6c4d14e
  • feat(acl): add the ACL library 62e5eb4
  • docs(README): update the README to include documentation for the ACL module 863ed73
  • test(acl): update tests to include the next() call 87e3f12
  • chore(package): bump version 2005a0a
  • feat(utils/get-types): add a function to get the exact type of a variable ccd29f2
  • chore(npmrc): add npmrc to prevent npm from saving packages by default cd17832
  • feat(acl): add the ACL object to exports b165e05

v1.0.0 - 2019-03-12

Commits

  • chore(merge-conflict): resolve merge-conflict 8d2aa31
  • build(npm): add the package.json and package-lock.json files 10ba955
  • chore(changelog): add the changelog config and script d9682b0
  • build(npm/travis): add a pre-install script to install peerdeps 6e3afc8
  • test(unit): add the initial unit tests acb1712
  • docs(README): update the readme to include the required documentation d18f867
  • test(integration): add the initial integration tests 172c3af
  • feat(lib/core): add the core library module f8bb786
  • feat(lib/utils): add the utility functions module 9273a08
  • chore(gitignore): add gitignore file e371484
  • test(unit): update the test definitions 112df17
  • feat(lib/errors): add the custom error definitions 6e4bbcc
  • chore(changelog): add the changelog config and script 235f5bf
  • build(package/changelog): create a postchangelog hook to automatically commit changes 9ed0222
  • chore(changelog): update the changelog 9352e6a
  • docs(LICENSE): add the license file 2b18c78
  • feat(core): finalize the library definitions 406373f
  • build(package/changelog): create a postchangelog hook to automatically commit changes 7d3f531
  • chore(editorconfig): add editorconfig for better cross-platform dev experience d6c9607
  • build(travis): add the travis file aafab97
  • chore(npm): format and update the package.json file f62d43a
  • feat(utils): refractor the utility functions a935c55
  • chore(changelog): update the changelog 62f7241