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..0b637a7 100644 --- a/index.js +++ b/index.js @@ -4,20 +4,15 @@ 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) - } const credentials = auth(req) if (credentials == null) { done(new Unauthorized('Missing or bad formatted authorization header')) @@ -34,6 +29,17 @@ function basicPlugin (fastify, opts, next) { 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() @@ -42,25 +48,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..33ce6c3 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', @@ -236,8 +254,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'], 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', @@ -604,12 +631,154 @@ test('Invalid options (authenticate realm)', t => { authorization: basicAuthHeader('user', 'pwd') } }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], undefined) + t.equal(res.statusCode, 200) + }) +}) + +test('Invalid options (authenticate realm = undefined)', t => { + t.plan(6) + + 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' + }, (err, res) => { + t.error(err) t.equal(res.headers['www-authenticate'], 'Basic') + t.equal(res.statusCode, 401) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: basicAuthHeader('user', 'pwd') + } + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], undefined) + t.equal(res.statusCode, 200) + }) +}) + +test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { + t.plan(7) + + 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' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic realm="root"') + t.equal(res.statusCode, 401) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: basicAuthHeader('user', 'pwd') + } + }, (err, res) => { t.error(err) + t.equal(res.headers['www-authenticate'], undefined) t.equal(res.statusCode, 200) }) }) +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') }