From c5f3ec9bb09879396d6d2ab3034ec1642fc69728 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 7 May 2021 01:39:01 +0200 Subject: [PATCH 1/4] 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/4] 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 551903fd142c528b3536ea52111e465eddbe6988 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 7 May 2021 14:55:36 +0200 Subject: [PATCH 3/4] Only send www-authenticate for 401. Make realm dynamic. --- index.js | 18 ++++++++++------ test.js | 65 +++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 84da90b..0b637a7 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')) @@ -36,6 +29,17 @@ async function basicPlugin (fastify, opts) { if (!err.statusCode) { err.statusCode = 401 } + + if (err.statusCode === 401) { + switch (typeof authenticateHeader) { + case 'string': + reply.header('WWW-Authenticate', authenticateHeader) + break + case 'function': + reply.header('WWW-Authenticate', authenticateHeader(req)) + break + } + } next(err) } else { next() diff --git a/test.js b/test.js index 2a79089..89b6fb8 100644 --- a/test.js +++ b/test.js @@ -165,7 +165,7 @@ test('Basic with promises - 401', t => { }) test('WWW-Authenticate (authenticate: true)', t => { - t.plan(3) + t.plan(6) const fastify = Fastify() const authenticate = true @@ -190,6 +190,15 @@ test('WWW-Authenticate (authenticate: true)', t => { }) }) + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic') + t.equal(res.statusCode, 401) + }) + fastify.inject({ url: '/', method: 'GET', @@ -197,14 +206,14 @@ test('WWW-Authenticate (authenticate: true)', t => { authorization: basicAuthHeader('user', 'pwd') } }, (err, res) => { - t.equal(res.headers['www-authenticate'], 'Basic') t.error(err) + t.equal(res.headers['www-authenticate'], undefined) t.equal(res.statusCode, 200) }) }) test('WWW-Authenticate Realm (authenticate: {realm: "example"})', t => { - t.plan(3) + t.plan(6) const fastify = Fastify() const authenticate = { realm: 'example' } @@ -229,6 +238,15 @@ test('WWW-Authenticate Realm (authenticate: {realm: "example"})', t => { }) }) + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic realm="example"') + t.equal(res.statusCode, 401) + }) + fastify.inject({ url: '/', method: 'GET', @@ -237,7 +255,7 @@ test('WWW-Authenticate Realm (authenticate: {realm: "example"})', t => { } }, (err, res) => { t.error(err) - t.equal(res.headers['www-authenticate'], 'Basic realm="example"') + t.equal(res.headers['www-authenticate'], undefined) t.equal(res.statusCode, 200) }) }) @@ -572,7 +590,7 @@ test('Invalid options (authenticate)', t => { }) test('Invalid options (authenticate realm)', t => { - t.plan(3) + t.plan(6) const fastify = Fastify() fastify @@ -597,6 +615,15 @@ test('Invalid options (authenticate realm)', t => { }) }) + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic') + t.equal(res.statusCode, 401) + }) + fastify.inject({ url: '/', method: 'GET', @@ -605,13 +632,13 @@ test('Invalid options (authenticate realm)', t => { } }, (err, res) => { t.error(err) - t.equal(res.headers['www-authenticate'], 'Basic') + t.equal(res.headers['www-authenticate'], undefined) t.equal(res.statusCode, 200) }) }) test('Invalid options (authenticate realm = undefined)', t => { - t.plan(3) + t.plan(6) const fastify = Fastify() fastify @@ -636,6 +663,15 @@ test('Invalid options (authenticate realm = undefined)', t => { }) }) + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic') + t.equal(res.statusCode, 401) + }) + fastify.inject({ url: '/', method: 'GET', @@ -644,13 +680,13 @@ test('Invalid options (authenticate realm = undefined)', t => { } }, (err, res) => { t.error(err) - t.equal(res.headers['www-authenticate'], 'Basic') + t.equal(res.headers['www-authenticate'], undefined) t.equal(res.statusCode, 200) }) }) test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { - t.plan(4) + t.plan(7) const fastify = Fastify() const authenticate = { @@ -680,6 +716,15 @@ test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { }) }) + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic realm="root"') + t.equal(res.statusCode, 401) + }) + fastify.inject({ url: '/', method: 'GET', @@ -687,8 +732,8 @@ test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { authorization: basicAuthHeader('user', 'pwd') } }, (err, res) => { - t.equal(res.headers['www-authenticate'], 'Basic realm="root"') t.error(err) + t.equal(res.headers['www-authenticate'], undefined) t.equal(res.statusCode, 200) }) }) From 4ed858da34be982c85da25ae8f835d4e5dcab423 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 7 May 2021 15:59:29 +0200 Subject: [PATCH 4/4] 100% code coverage --- test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test.js b/test.js index 89b6fb8..33ce6c3 100644 --- a/test.js +++ b/test.js @@ -738,6 +738,47 @@ test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { }) }) +test('No 401 no realm', t => { + t.plan(4) + + const fastify = Fastify() + fastify.register(basicAuth, { validate, authenticate: true }) + + function validate (username, password, req, res) { + const err = new Error('Winter is coming') + err.statusCode = 402 + return Promise.reject(err) + } + + 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', 'pwdd') + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 402) + t.equal(res.headers['www-authenticate'], undefined) + t.same(JSON.parse(res.payload), { + error: 'Payment Required', + message: 'Winter is coming', + statusCode: 402 + }) + }) +}) + function basicAuthHeader (username, password) { return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64') }