fastify-esso
v1.3.4
Published
Fastify plugin for easy single sign-on
Downloads
103
Maintainers
Readme
fastify-esso
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:
fastify.generateAuthToken(data)
Call this function to generate an authentication token that grants access to routes that require authentication.
Parameters:data
: anobject
(can contain any data) that will be decorated in the request object and can be accessed viareq.auth
only for routes inside authenticated scopes.
Returns: a
Promise
that once resolved, turns into astring
containing the token.fastify.requireAuthentication(fastify)
Call this function to require authentication for every route inside the current Fastify scope.
Parameters:fastify
: the current Fastify scope that will now require authentication.
Returns: nothing.
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:
- That the user authenticated successfully (otherwise they wouldn't have been able to generate valid encrypted content, as they don't know the secret).
JWT also gives you this. - That you don't need to access an external database, such as MySQL or Redis, just to verify whether the token is valid or not. This reduces latency and load in your databases.
JWT also gives you this - That nobody, including the user, can change the data (it's encrypted after all).
JWT also gives you this - That nobody, including the user, can view the data (without the encryption secret, it's just gibberish).
JWT doesn't provide this. - That the data is disguised as a regular bearer token, and that no one will ever know[1] that it actually means something. (We use random IVs, so you'll never get repeated tokens, even if the data itself is the same[4]).
JWT doesn't provide this. - Much more compact than JWTs, meaning less bandwidth usage. Still, we advise against storing a bunch of information in it. It's not because you can, that you should :)
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):
- User <-> AS (authenticates and receives token)
- User -> X (sends request plus token)
- X <-> AS (validates token)
- 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:
- User <-> AS (authenticates and receives token)
- User -> X (sends request plus token)
- X (validates the token locally, with blazing fast speeds)
- 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):
- User <-> W (authenticates and receives token)
- User -> X (sends request plus token)
- X <-> DB (validates token by making a network request to a database)
- 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):
- User <-> W (authenticates and receives token)
- User -> X (sends request plus token)
- X (validates the token locally, with blazing fast speeds)
- User <- X (sends the response back)
In this last example, the main advantages of using this plugin are:
- Much reduced database and network load
- Most of the boilerplate code and standardization is already handled by the plugin, making it much easier to maintain
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
- Is AES-256 a post-quantum secure cipher or not?
- NSA Encryption Systems - Advanced Encryption Standard (AES)
- NSA product types - Type 1 product
- Why should you use CBC with random IVs
- 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:
data
: anobject
(can contain any data) that will be decorated in the request object and can be accessed viareq.auth
only for routes inside authenticated scopes.
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:
fastify
: the current Fastify scope that will now require authentication.
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
- [x] Add unit tests
- [x] Add integration tests
- [x] Coverage 100%
- [x] Add first-party TypeScript support
- [ ] Add benchmarks
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.