From c5f3ec9bb09879396d6d2ab3034ec1642fc69728 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 7 May 2021 01:39:01 +0200 Subject: [PATCH 1/3] Make realm dynamic --- README.md | 13 +++++++++++++ index.js | 43 ++++++++++++++++++++++++------------------- test.js | 46 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2ebbf8d..bed4154 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,19 @@ fastify.register(require('fastify-basic-auth'), { }) ``` +The `realm` key could also be a function: + +```js +fastify.register(require('fastify-basic-auth'), { + validate, + authenticate: { + realm(req) { + return 'example' // WWW-Authenticate: Basic realm="example" + } + } +}) +``` + ## License diff --git a/index.js b/index.js index 561f33a..6f61745 100644 --- a/index.js +++ b/index.js @@ -4,19 +4,21 @@ const fp = require('fastify-plugin') const auth = require('basic-auth') const { Unauthorized } = require('http-errors') -function basicPlugin (fastify, opts, next) { +async function basicPlugin (fastify, opts) { if (typeof opts.validate !== 'function') { - return next(new Error('Basic Auth: Missing validate function')) + throw new Error('Basic Auth: Missing validate function') } - const authenticateHeader = getAuthenticateHeader(opts.authenticate, next) + const authenticateHeader = getAuthenticateHeader(opts.authenticate) const validate = opts.validate.bind(fastify) fastify.decorate('basicAuth', basicAuth) - next() - function basicAuth (req, reply, next) { - if (authenticateHeader) { - reply.header(authenticateHeader.key, authenticateHeader.value) + switch (typeof authenticateHeader) { + case 'string': + reply.header('WWW-Authenticate', authenticateHeader) + break + case 'function': + reply.header('WWW-Authenticate', authenticateHeader(req)) } const credentials = auth(req) if (credentials == null) { @@ -42,25 +44,28 @@ function basicPlugin (fastify, opts, next) { } } -function getAuthenticateHeader (authenticate, next) { +function getAuthenticateHeader (authenticate) { if (!authenticate) return false if (authenticate === true) { - return { - key: 'WWW-Authenticate', - value: 'Basic' - } + return 'Basic' } if (typeof authenticate === 'object') { - const realm = (authenticate.realm && typeof authenticate.realm === 'string') - ? authenticate.realm - : '' - return { - key: 'WWW-Authenticate', - value: 'Basic' + (realm ? ` realm="${realm}"` : '') + const realm = authenticate.realm + switch (typeof realm) { + case 'undefined': + return 'Basic' + case 'boolean': + return 'Basic' + case 'string': + return `Basic realm="${realm}"` + case 'function': + return function (req) { + return `Basic realm="${realm(req)}"` + } } } - next(new Error('Basic Auth: Invalid authenticate option')) + throw new Error('Basic Auth: Invalid authenticate option') } module.exports = fp(basicPlugin, { diff --git a/test.js b/test.js index c2b9d0f..54dc1e2 100644 --- a/test.js +++ b/test.js @@ -236,8 +236,8 @@ test('WWW-Authenticate Realm (authenticate: {realm: "example"})', t => { authorization: basicAuthHeader('user', 'pwd') } }, (err, res) => { - t.equal(res.headers['www-authenticate'], 'Basic realm="example"') t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic realm="example"') t.equal(res.statusCode, 200) }) }) @@ -604,7 +604,51 @@ test('Invalid options (authenticate realm)', t => { authorization: basicAuthHeader('user', 'pwd') } }, (err, res) => { + t.error(err) t.equal(res.headers['www-authenticate'], 'Basic') + t.equal(res.statusCode, 200) + }) +}) + +test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { + t.plan(4) + + const fastify = Fastify() + const authenticate = { + realm(req) { + t.equal(req.url, '/') + return 'root' + } + } + fastify.register(basicAuth, { validate, authenticate }) + + function validate (username, password, req, res, done) { + if (username === 'user' && password === 'pwd') { + done() + } else { + done(new Error('Unauthorized')) + } + } + + fastify.after(() => { + fastify.route({ + method: 'GET', + url: '/', + preHandler: fastify.basicAuth, + handler: (req, reply) => { + reply.send({ hello: 'world' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: basicAuthHeader('user', 'pwd') + } + }, (err, res) => { + t.equal(res.headers['www-authenticate'], 'Basic realm="root"') t.error(err) t.equal(res.statusCode, 200) }) From 9d636437defcf230be57a74180a5080bcb0d1053 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 7 May 2021 09:19:09 +0200 Subject: [PATCH 2/3] 100% code coverage --- index.js | 4 ++-- test.js | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 6f61745..84da90b 100644 --- a/index.js +++ b/index.js @@ -57,10 +57,10 @@ function getAuthenticateHeader (authenticate) { case 'boolean': return 'Basic' case 'string': - return `Basic realm="${realm}"` + return `Basic realm="${realm}"` case 'function': return function (req) { - return `Basic realm="${realm(req)}"` + return `Basic realm="${realm(req)}"` } } } diff --git a/test.js b/test.js index 54dc1e2..2a79089 100644 --- a/test.js +++ b/test.js @@ -610,12 +610,51 @@ test('Invalid options (authenticate realm)', t => { }) }) +test('Invalid options (authenticate realm = undefined)', t => { + t.plan(3) + + const fastify = Fastify() + fastify + .register(basicAuth, { validate, authenticate: { realm: undefined } }) + + function validate (username, password, req, res, done) { + if (username === 'user' && password === 'pwd') { + done() + } else { + done(new Error('Unauthorized')) + } + } + + fastify.after(() => { + fastify.route({ + method: 'GET', + url: '/', + preHandler: fastify.basicAuth, + handler: (req, reply) => { + reply.send({ hello: 'world' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: basicAuthHeader('user', 'pwd') + } + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic') + t.equal(res.statusCode, 200) + }) +}) + test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { t.plan(4) const fastify = Fastify() const authenticate = { - realm(req) { + realm (req) { t.equal(req.url, '/') return 'root' } From 3110dde8730d74c3b724777b9edaf8e81d2c7cfa Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 7 May 2021 09:38:15 +0200 Subject: [PATCH 3/3] validate can return a custom realm --- README.md | 46 ++++++++++++++++++++++------------- index.js | 44 ++++++++++++++++++---------------- test.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 117 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index bed4154..ea04e2d 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ const authenticate = {realm: 'Westeros'} fastify.register(require('fastify-basic-auth'), { validate, authenticate }) async function validate (username, password, req, reply) { if (username !== 'Tyrion' || password !== 'wine') { - return new Error('Winter is coming') + throw new Error('Winter is coming') } } @@ -79,7 +79,7 @@ fastify.register(require('fastify-auth')) fastify.register(require('fastify-basic-auth'), { validate, authenticate }) async function validate (username, password, req, reply) { if (username !== 'Tyrion' || password !== 'wine') { - return new Error('Winter is coming') + throw new Error('Winter is coming') } } @@ -132,7 +132,33 @@ the `validate` function may return a promise, resolving for valid requests and rejecting for invalid. This can also be achieved using an `async/await` function, and throwing for invalid requests. -See code above for examples. +It is also possible to override set the `realm` dynamically by returning it +as the first argument. + +```js +const fastify = require('fastify')() +const authenticate = {realm: 'Westeros'} +fastify.register(require('fastify-basic-auth'), { validate, authenticate }) +async function validate (username, password, req, reply) { + if (username !== 'Tyrion' || password !== 'Wine') { + throw new Error('Winter is coming') + } + + // custom realm + return 'Lannister' +} + +fastify.after(() => { + fastify.route({ + method: 'GET', + url: '/', + onRequest: fastify.basicAuth, + handler: async (req, reply) => { + return { hello: 'world' } + } + }) +}) +``` ### `authenticate` (optional, default: false) @@ -166,20 +192,6 @@ fastify.register(require('fastify-basic-auth'), { }) ``` -The `realm` key could also be a function: - -```js -fastify.register(require('fastify-basic-auth'), { - validate, - authenticate: { - realm(req) { - return 'example' // WWW-Authenticate: Basic realm="example" - } - } -}) -``` - - ## License Licensed under [MIT](./LICENSE). diff --git a/index.js b/index.js index 84da90b..af111dc 100644 --- a/index.js +++ b/index.js @@ -13,13 +13,6 @@ async function basicPlugin (fastify, opts) { fastify.decorate('basicAuth', basicAuth) function basicAuth (req, reply, next) { - switch (typeof authenticateHeader) { - case 'string': - reply.header('WWW-Authenticate', authenticateHeader) - break - case 'function': - reply.header('WWW-Authenticate', authenticateHeader(req)) - } const credentials = auth(req) if (credentials == null) { done(new Unauthorized('Missing or bad formatted authorization header')) @@ -30,14 +23,21 @@ async function basicPlugin (fastify, opts) { } } - function done (err) { - if (err !== undefined) { + function done (err, realm) { + // TODO remove in the next major + if (typeof err === 'string') { + realm = err + err = undefined + } + if (err) { // We set the status code to be 401 if it is not set if (!err.statusCode) { err.statusCode = 401 } next(err) } else { + const header = realm ? formatRealm(realm) : authenticateHeader + reply.header('WWW-Authenticate', header) next() } } @@ -50,24 +50,26 @@ function getAuthenticateHeader (authenticate) { return 'Basic' } if (typeof authenticate === 'object') { - const realm = authenticate.realm - switch (typeof realm) { - case 'undefined': - return 'Basic' - case 'boolean': - return 'Basic' - case 'string': - return `Basic realm="${realm}"` - case 'function': - return function (req) { - return `Basic realm="${realm(req)}"` - } + const realm = formatRealm(authenticate.realm) + if (realm) { + return realm } } throw new Error('Basic Auth: Invalid authenticate option') } +function formatRealm (realm) { + switch (typeof realm) { + case 'undefined': + return 'Basic' + case 'boolean': + return 'Basic' + case 'string': + return `Basic realm="${realm}"` + } +} + module.exports = fp(basicPlugin, { fastify: '3.x', name: 'fastify-basic-auth' diff --git a/test.js b/test.js index 2a79089..0d8b3ae 100644 --- a/test.js +++ b/test.js @@ -571,6 +571,26 @@ test('Invalid options (authenticate)', t => { }) }) +test('Invalid options (realm is a number)', t => { + t.plan(1) + + const fastify = Fastify() + fastify + .register(basicAuth, { validate, authenticate: { realm: 42 } }) + + function validate (username, password, req, res, done) { + if (username === 'user' && password === 'pwd') { + done() + } else { + done(new Error('Unauthorized')) + } + } + + fastify.ready(function (err) { + t.equal(err.message, 'Basic Auth: Invalid authenticate option') + }) +}) + test('Invalid options (authenticate realm)', t => { t.plan(3) @@ -649,21 +669,18 @@ test('Invalid options (authenticate realm = undefined)', t => { }) }) -test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { - t.plan(4) +test('WWW-Authenticate Realm dynamic realm', t => { + t.plan(3) const fastify = Fastify() const authenticate = { - realm (req) { - t.equal(req.url, '/') - return 'root' - } + realm: true } fastify.register(basicAuth, { validate, authenticate }) function validate (username, password, req, res, done) { if (username === 'user' && password === 'pwd') { - done() + done(null, 'root') } else { done(new Error('Unauthorized')) } @@ -687,8 +704,49 @@ test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { authorization: basicAuthHeader('user', 'pwd') } }, (err, res) => { + t.error(err) t.equal(res.headers['www-authenticate'], 'Basic realm="root"') + t.equal(res.statusCode, 200) + }) +}) + +test('WWW-Authenticate Realm dynamic realm promise', t => { + t.plan(3) + + const fastify = Fastify() + const authenticate = { + realm: true + } + fastify.register(basicAuth, { validate, authenticate }) + + function validate (username, password, req, res) { + if (username === 'user' && password === 'pwd') { + return Promise.resolve('root') + } else { + return Promise.reject(new Error('Unauthorized')) + } + } + + fastify.after(() => { + fastify.route({ + method: 'GET', + url: '/', + preHandler: fastify.basicAuth, + handler: (req, reply) => { + reply.send({ hello: 'world' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: basicAuthHeader('user', 'pwd') + } + }, (err, res) => { t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic realm="root"') t.equal(res.statusCode, 200) }) })