diff --git a/example.ts b/example.ts new file mode 100644 index 0000000..cb610d2 --- /dev/null +++ b/example.ts @@ -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; + } + 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); + diff --git a/index.d.ts b/index.d.ts index ecc766b..c5a565c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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; +}; + + declare module 'fastify' { interface FastifyRequest { auth: any; } interface FastifyInstance { - generateAuthToken: function (any): Promise; - requireAuthentication: function (FastifyInstance): void; + generateAuthToken: () => Promise; + requireAuthentication: RequireAuthentication } } @@ -98,4 +104,4 @@ declare const fastifyEsso: (options: EssoOptions) => fastify.Plugin< {} >; -export = fastifyEsso; \ No newline at end of file +export = fastifyEsso; diff --git a/lib/index.js b/lib/index.js index 2ca04b8..cb41a04 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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', @@ -49,45 +49,45 @@ 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'); /** @@ -95,7 +95,7 @@ module.exports = function builder(opts = default_opts){ * @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); /** @@ -103,24 +103,24 @@ module.exports = function builder(opts = default_opts){ * @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); @@ -131,26 +131,49 @@ 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'); + } } /** @@ -158,15 +181,15 @@ module.exports = function builder(opts = default_opts){ * @param { object } data This data will be made available in request.auth for routes inside an authenticated scope * @returns { Promise } */ - 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); @@ -174,4 +197,4 @@ module.exports = function builder(opts = default_opts){ } return fp((f, o, d) => plugin(f, o, d)); -} \ No newline at end of file +} diff --git a/lib/index.test.js b/lib/index.test.js index b61f90f..f0f1029 100644 --- a/lib/index.test.js +++ b/lib/index.test.js @@ -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 = {}`); -} \ No newline at end of file +}