patrickpissurno / fastify-esso

The easiest authentication plugin for Fastify, with built-in support for single sign-on (SSO)
https://npm.im/fastify-esso
MIT License
52 stars 6 forks source link
auth authentication easy fast fastify jwe jwt login sign-on single

fastify-esso

npm-version build status coverage status known vulnerabilities downloads license

Hate boilerplate code? Want something fast and still impossible[1] to break?

Then, this plugin is for you.

This fastify plugin turns the usual authentication nightmare into something easily manageable.

And also has first-party support for TypeScript (completely optional). Thanks @dodiego for the PR!


How authentication works in 3 simple steps:

We'll start out with the following sample scenario:

// first let's import fastify and create a server instance
const fastify = require('fastify')();

// TODO: import the plugin
// TODO: implement our stuff

// let's start the server
fastify.listen(3000, '0.0.0.0', (err) => console.log(err ? err : 'Listening at 3000'));

1 - Credentials validation

Just like going to a party. In the entrance there is this guard. You can't just walk past her. You need to show your ID and then she'll check if you were invited (eg. have access).

This is not implemented by this plugin. But it is still quite simple. Let's write some sample code:

fastify.post('/auth', async (req, reply) => {

    // are the credentials valid? (PS: if you copy-pasted this code, take a look at the full example below)
    const valid_credentials = req.body.user === 'John' && req.body.password === '123';
    if(!valid_credentials)
        return { message: 'Invalid credentials. You shall not pass!' };

    return { message: 'Access granted. Enjoy the party!' };
});

2 - Token generation

It turns out you were invited, so the guard proceeds to give you a party wristband (eg. token). But this is a tech party, so it has a built-in NFC chip that can store some info. Cool!

This is implemented by this plugin! Let's take a look at the updated code:

fastify.post('/auth', async (req, reply) => {

    // are the credentials valid? (PS: if you copy-pasted this code, take a look at the full example below)
    const valid_credentials = req.body.user === 'John' && req.body.password === '123';
    if(!valid_credentials)
        return { message: 'Invalid credentials. You shall not pass!' };

    // here we're storing who you are inside the wristband
    const wristband = await fastify.generateAuthToken({ user: req.body.user });

    return { message: 'Access granted. Enjoy the party!', wristband: wristband  };
});

3 - Token validation

You join the party and dance a lot. Now you're thirst, so what about a drink? The barman scans your wristband (eg. validates) and instantly knows who you are, so he proceeds to give you the drink. Sweet!

This is also implemented by this plugin! So let's update the example:

async function privateRoutes(fastify){
    fastify.requireAuthentication(fastify); //this is where all the magic happens

    fastify.get('/order-drink', async (req, reply) => {
        return { message: 'Hello ' + req.auth.user + '. Here is your drink!' };
    });
}

// register our private routes
fastify.register(privateRoutes);

All you have to do is call fastify.requireAuthentication(fastify) and every route inside the current fastify scope will require authentication to be accessed.

You might want to take a deeper look into how Fastify's scopes work.

Full example:

// first let's import fastify and create a server instance
const fastify = require('fastify')();

/**
 * Registers the plugin. 
 * In the real world you should change this secret
 * to something complex, with at least 20 characters
 * for it to be safe
 */
fastify.register(require('fastify-esso')({ secret: '11111111111111111111' }));

fastify.post('/auth', async (req, reply) => {

    // tip: checking passwords with === is vulnerable, so in production use crypto.timingSafeEqual instead
    const valid_credentials = req.body.user === 'John' && req.body.password === '123';
    if(!valid_credentials)
        return { message: 'Invalid credentials. You shall not pass!' };

    // here we're storing who you are inside the wristband
    const wristband = await fastify.generateAuthToken({ user: req.body.user });

    return { message: 'Access granted. Enjoy the party!', wristband: wristband  };
});

async function privateRoutes(fastify){
    fastify.requireAuthentication(fastify); //this is where all the magic happens

    fastify.get('/order-drink', async (req, reply) => {
        return { message: 'Hello ' + req.auth.user + '. Here is your drink!' };
    });
}

// register our private routes
fastify.register(privateRoutes);

// let's start the server
fastify.listen(3000, '0.0.0.0', (err) => console.log(err ? err : 'Listening at 3000'));



FAQ (Frequently Asked Questions)

What does this plugin provide?

Actually, just two decorators to the Fastify server instance:

How does it work?

Symmetric encryption. This plugin uses the native Node.js crypto module to provide us with the military-grade[2][3] encryption AES (Advanced Encryption Standard), with 256-bits key size and CBC mode (TL;DR: aes-256-cbc).

It works in a quite similar way to JWTs, but reducing overhead and providing data encryption (instead of simply signing it).

When you call await fastify.generateAuthToken({ user: 'Josh' }), the plugin converts the data to JSON, and then encrypts it.

When the user uses this token (sends it in the request header, which defaults to authorization but that can be changed), this plugin will decrypt it and then decorate Fastify's request object with the original data.

By doing it this way we guarantee:

Is it safe?

We use the industry-standard[2][3] symmetric encryption algorithm (AES-256-CBC) and a strong key derivation function, scrypt, to make it impossible to break[5, p. 14] as long as you keep the secret safe, and use one that is a random enough (eg. don't use 12345678 or anything like that). The secret also has to have a length of at least 20 characters, otherwise this plugin will throw an error. We also use cryptographically-secure random IVs (Initialization Vectors). This way we end up with a very strong encryption.

So yeah, this is really safe.

Is it tested?

We adhere to a strict 100% coverage standard. There are continuous integration tools in place. Which means that all tests are run with every commit. If any of them fail, or the code coverage isn't 100%, then it won't go to NPM. Simple as that.

So yeah, it's tested.

Great plugin, but what about SSO and federated logins?

Great question! In a microservices architecture, services should be decoupled. How can you decouple stuff if you still need authentication in every one of them? There are two options:

Centralized Authentication Server (AS) example:

Let's say you have 3 microservices (X, Y and Z). You can create a single AS and make users authenticate directly with it.

It would work like this (without this plugin):

  1. User <-> AS (authenticates and receives token)
  2. User -> X (sends request plus token)
  3. X <-> AS (validates token)
  4. User <- X (sends the response back)

This would have poor performance (for every request to any microservice, there would be another request to the AS, causing it to suffer a big load) and a single point-of-failure (if the AS goes down, everything goes down too). It simply doesn't scale nor work great for distributed stuff.

Now let's make this right by using this plugin. It would work like this:

  1. User <-> AS (authenticates and receives token)
  2. User -> X (sends request plus token)
  3. X (validates the token locally, with blazing fast speeds)
  4. User <- X (sends the response back)

WOW! Now there is no need to make any additional requests to the AS, which means that the microservices don't even need to be connected to it by network. They can be completely isolated from each other. Also, you can scale up as much as you can the number of microservices or instances, without requiring you to scale the AS. Sounds great to me!

In case of having different permissions for each microservice, or sensitive authentication info that needs to be passed from the AS to the microservices, you would just have to put it inside the token, and rest assured: it's safe. Amazing, right?

Distributed Authentication example:

Let's say you have 3 microservices (X, Y and Z). Now you won't create a dedicated AS. Instead, users will authenticate directly with each microservice.

It would work like this (without this plugin, W means any microservice):

  1. User <-> W (authenticates and receives token)
  2. User -> X (sends request plus token)
  3. X <-> DB (validates token by making a network request to a database)
  4. User <- X (sends the response back)

It would be complicated to implement, as a lot of code would be repeated, and a huge standardization would have to take place to make sure that each microservice would implement authentication and validation the same way. Also, it would incur in a big overhead to the authentication database. Not that great.

In general, the first approach (Centralized Authentication Server) is better. But we can still improve on this one to make it more viable, if for some reason it suits you better.

Improved approach (using this plugin, W means any microservice):

  1. User <-> W (authenticates and receives token)
  2. User -> X (sends request plus token)
  3. X (validates the token locally, with blazing fast speeds)
  4. User <- X (sends the response back)

In this last example, the main advantages of using this plugin are:

Conclusion

In almost every situation, this plugin helps improve stuff. So yeah, you should be using it. And I'm aware that there are other Fastify plugins for authentication (even official ones). But after reading through all of this, you're probably aware of why this one is the one authentication plugin you should be using.

References

  1. Is AES-256 a post-quantum secure cipher or not?
  2. NSA Encryption Systems - Advanced Encryption Standard (AES)
  3. NSA product types - Type 1 product
  4. Why should you use CBC with random IVs
  5. Stronger Key Derivation Via Sequential Memory-hard Functions (page 14, "Estimated cost of hardware to crack a password in 1 year")



API Reference

Register

const opts = {
    /** Request header name / query parameter name / cookie name */
    header_name: 'authorization', // defaults to 'authorization'

    /** Secure key that will be used to do the encryption stuff */
    secret: '11111111111111111111', // cannot be ommited

    /** request and reply are Fastify's request and reply objects **/
    extra_validation: async function validation (request, reply){ // can be ommited
        /**
         * Custom validation function that is called after basic validation is executed
         * This function should throw in case validation fails
         * request.auth is already available here
         */
    },

    /** Set this to true if you don't want to allow the token to be passed as a header */
    disable_headers: false,  // defaults to false

    /** Set this to true if you don't want to allow the token to be passed as a query parameter */
    disable_query: false,  // defaults to false

    /** Set this to true if you don't want to allow the token to be passed as a cookie */
    disable_cookies: false,  // defaults to false

    /** Sets the token prefix. A null value means no prefix */
    token_prefix: 'Bearer ', // defaults to 'Bearer '

    /**
     * Allows for renaming the decorators this plugin adds to Fastify.
     * Useful if you want to register this plugin multiple times in the same scope
     * (not usually needed, but can be useful sometimes).
     * 
     * Note: if using TypeScript and intending to use this feature, you'll probably
     * want to add type definitions for the renamed decorators, otherwise it might complain
     * that they don't exist.
     * */
    rename_decorators: {
        /** Change the name of the FastifyInstance.requireAuthentication decorator */
        requireAuthentication: 'requireAuthentication',

        /** Change the name of the FastifyInstance.generateAuthToken decorator */
        generateAuthToken: 'generateAuthToken',

        /** Change the name of the FastifyRequest.auth decorator */
        auth: 'auth',
    }
};

fastify.register(require('fastify-esso')(opts));


fastify.generateAuthToken(data)

Call this function to generate an authentication token that grants access to routes that require authentication.

Parameters:

Returns: a Promise that once resolved, turns into a string containing the token.

Note: you can rename this decorator if you want to (look at the rename_decorators option above).


fastify.requireAuthentication(fastify)

Call this function to require authentication for every route inside the current Fastify scope.

Parameters:

Returns: nothing.

Note: you can rename this decorator if you want to (look at the rename_decorators option above).



Benchmarks

None yet. But you're welcome to open a PR.


TODO


License

MIT License

Copyright (c) 2020-2023 Patrick Pissurno

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.