Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// plugin.ts
// example without renaming decorators
import fp from 'fastify-plugin';
import token from 'fastify-esso';

export default fp(async function (fastify, opts) {
fastify.register(token({
secret: process.env.AUTH as string,
disable_headers: true,
disable_query: true,
header_name: "IMAT",
token_prefix: null
}));
});

// example with rename decorators
import fp from 'fastify-plugin';
import token from 'fastify-esso';

export default fp(async function (fastify, opts) {
fastify.register(token({
secret: process.env.COOKIE_AUTH as string,
disable_headers: true,
disable_query: true,
header_name: "IMAT",
token_prefix: null, // if you don't want token prefix
rename_decorators: {
auth: "test_auth",
requireAuthentication: "require_test_auth",
generateAuthToken: "generate_test_auth"
}
}));
});

declare module "fastify"{
export interface FastifyInstance{
require_test_auth: (arg0: FastifyInstance) => void;
generate_test_auth: (arg0: any) => Promise<string>;
}
export interface FastifyRequest {
test_auth: any;
}
}

// End of plugin.ts


// USAGE with rename decorators
// generate token
const token = await fastify.generate_test_auth({user: "anything"});

// require token
fastify.require_test_auth(fastify);

22 changes: 14 additions & 8 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,35 +59,41 @@ export interface EssoOptions {
}

export interface EssoRenameDecoratorsOptions {

/**
* Change the name of the `FastifyInstance.requireAuthentication` decorator
* @default 'requireAuthentication'
*/
requireAuthentication?: string;

/**
* Change the name of the `FastifyInstance.generateAuthToken` decorator
* @default 'generateAuthToken'
*/
generateAuthToken?: string;
generateAuthToken?: string;

/**
* Change the name of the `FastifyRequest.auth` decorator
* @default 'auth'
*/
auth?: string;
auth?: string;

}

type RequireAuthentication = {
(fastify: FastifyInstance): void;
(request: FastifyRequest, reply: FastifyReply): Promise<void>;
};


declare module 'fastify' {
interface FastifyRequest<HttpRequest> {
auth: any;
}

interface FastifyInstance {
generateAuthToken: function (any): Promise<string>;
requireAuthentication: function (FastifyInstance): void;
generateAuthToken: () => Promise<string>;
requireAuthentication: RequireAuthentication
}
}

Expand All @@ -98,4 +104,4 @@ declare const fastifyEsso: (options: EssoOptions) => fastify.Plugin<
{}
>;

export = fastifyEsso;
export = fastifyEsso;
95 changes: 59 additions & 36 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ const default_opts = {
* @param { import('fastify').FastifyRequest } request fastify request object
* @param { import('fastify').FastifyReply } reply fastify reply object
*/
extra_validation: /* istanbul ignore next */ async function validation (request, reply){
extra_validation: /* istanbul ignore next */ async function validation(request, reply) {
//by default does nothing
},

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

/** Request header name / query parameter name / cookie name */
header_name: 'authorization',

Expand Down Expand Up @@ -49,78 +49,78 @@ const default_opts = {

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

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

module.exports = function builder(opts = default_opts){
if(opts.rename_decorators)
module.exports = function builder(opts = default_opts) {
if (opts.rename_decorators)
opts.rename_decorators = Object.assign({}, default_opts.rename_decorators, opts.rename_decorators);
opts = Object.assign({}, default_opts, opts);

if(!opts.header_name)
if (!opts.header_name)
throw new Error('header_name cannot be null');

if(typeof(opts.header_name) !== 'string')
if (typeof (opts.header_name) !== 'string')
throw new Error('header_name should be a string');

if(!opts.secret || opts.secret.length < 20)
if (!opts.secret || opts.secret.length < 20)
throw new Error('the secret cannot be null and should have at least 20 characters to be considered secure');

if(opts.extra_validation != null && typeof(opts.extra_validation) !== 'function')
if (opts.extra_validation != null && typeof (opts.extra_validation) !== 'function')
throw new Error('extra validation should be either null or a function');

if(opts.disable_headers && opts.disable_query && opts.disable_cookies)
if (opts.disable_headers && opts.disable_query && opts.disable_cookies)
throw new Error('at least one of the following flags should be false: disable_headers, disable_query, disable_cookies');

if(opts.token_prefix != null && typeof(opts.token_prefix) !== 'string')
if (opts.token_prefix != null && typeof (opts.token_prefix) !== 'string')
throw new Error('token_prefix should be either null or a string');

if(!opts.rename_decorators.auth || typeof(opts.rename_decorators.auth) !== 'string')
if (!opts.rename_decorators.auth || typeof (opts.rename_decorators.auth) !== 'string')
throw new Error('rename_decorators.auth should be a non empty string');

if(!opts.rename_decorators.generateAuthToken || typeof(opts.rename_decorators.generateAuthToken) !== 'string')
if (!opts.rename_decorators.generateAuthToken || typeof (opts.rename_decorators.generateAuthToken) !== 'string')
throw new Error('rename_decorators.generateAuthToken should be a non empty string');

if(!opts.rename_decorators.requireAuthentication || typeof(opts.rename_decorators.requireAuthentication) !== 'string')
if (!opts.rename_decorators.requireAuthentication || typeof (opts.rename_decorators.requireAuthentication) !== 'string')
throw new Error('rename_decorators.requireAuthentication should be a non empty string');

if(opts.rename_decorators.generateAuthToken === opts.rename_decorators.requireAuthentication)
if (opts.rename_decorators.generateAuthToken === opts.rename_decorators.requireAuthentication)
throw new Error('rename_decorators.generateAuthToken and rename_decorators.requireAuthentication should have distinct values');

/**
* @param { import('fastify').FastifyInstance } fastify
* @param { any } options
* @param { function } done
*/
async function plugin (fastify, options, done) {
async function plugin(fastify, options, done) {
const key = await scryptAsync(opts.secret, opts.header_name, 32);

/**
* Custom validation function that is called after basic validation is ensured
* @param { import('fastify').FastifyRequest } req fastify request
* @param { import('fastify').FastifyReply } reply fastify reply
*/
async function validation(req, reply){
async function validation(req, reply) {
/** @type { string } */
let field = null;
if(!opts.disable_headers && req.headers[opts.header_name])
if (!opts.disable_headers && req.headers[opts.header_name])
field = req.headers[opts.header_name]
else if(!opts.disable_query && req.query[opts.header_name])
else if (!opts.disable_query && req.query[opts.header_name])
field = req.query[opts.header_name];
else if(!opts.disable_cookies && req.cookies && req.cookies[opts.header_name])
else if (!opts.disable_cookies && req.cookies && req.cookies[opts.header_name])
field = req.cookies[opts.header_name];

if(!field)
if (!field)
throw new err.Unauthorized();

/** @type { string } */
let token;

if(opts.token_prefix != null){
if(field.substring(0, opts.token_prefix.length) !== opts.token_prefix)
if (opts.token_prefix != null) {
if (field.substring(0, opts.token_prefix.length) !== opts.token_prefix)
throw new err.Forbidden();

token = field.substring(opts.token_prefix.length, field.length);
Expand All @@ -131,47 +131,70 @@ module.exports = function builder(opts = default_opts){
try {
const json = await decrypt(key, token);

if(json === '`')
req[opts.rename_decorators.auth] = { };
if (json === '`')
req[opts.rename_decorators.auth] = {};
else
req[opts.rename_decorators.auth] = JSON.parse(json);
}
catch(ex){
catch (ex) {
throw new err.Forbidden();
}

if(opts.extra_validation != null)
if (opts.extra_validation != null)
await opts.extra_validation(req, reply);
}

/**
* Call this function to require authentication for every route inside a Fastify Scope
/**
* Function to manage authentication within Fastify routes or hooks
* https://www.fastify.io/docs/latest/Plugins/
* @param { import('fastify').FastifyInstance } fastify
@param {...any} args Accepts multiple arguments:
* - If a single argument is provided it is considered as Fastify instance, it sets up a hook for authentication.
* - If two arguments are provided, they are considered as Fastify request and reply for direct validation.
*/
function requireAuthentication(fastify){
fastify.addHook('preHandler', validation);
async function requireAuthentication(...args) {
if (args.length === 1) {
const [fastify] = args;
if (fastify && typeof fastify.addHook === 'function') {
fastify.addHook('preHandler', validation);
} else {
throw new Error('Invalid Fastify instance provided');
}

}

else if (args.length === 2) {
const [request, reply] = args;
if (request && reply) {
await validation(request, reply);
} else {
throw new Error('Both request and reply instances are required');
}
}

else {
throw new Error('Invalid number of arguments provided');
}
}

/**
* Call this function to generate an authentication token that grants access to routes that require authentication
* @param { object } data This data will be made available in request.auth for routes inside an authenticated scope
* @returns { Promise<string> }
*/
async function generateAuthToken(data){
async function generateAuthToken(data) {
const prefix = opts.token_prefix != null ? opts.token_prefix : '';

if(!data || Object.keys(data).length < 1)
if (!data || Object.keys(data).length < 1)
return prefix + await encrypt(key, '`'); // we need to encrypt something, so let's just save bandwidth

return prefix + await encrypt(key, JSON.stringify(data));
}

fastify.decorate(opts.rename_decorators.requireAuthentication, requireAuthentication);
fastify.decorate(opts.rename_decorators.generateAuthToken, generateAuthToken);

done();
}

return fp((f, o, d) => plugin(f, o, d));
}
}
2 changes: 1 addition & 1 deletion lib/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,4 +313,4 @@ async function rename_decorators(tap){

response = await fastify.inject({ method: 'GET', url: '/test', headers: { authorization: await fastify.rick() } });
tap.same(JSON.parse(response.body), {}, `requests with valid tokens generated by fastify.rick() should have req.auth = {}`);
}
}