diff --git a/README.md b/README.md index 2ebbf8d..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,7 +192,6 @@ fastify.register(require('fastify-basic-auth'), { }) ``` - ## License Licensed under [MIT](./LICENSE). diff --git a/index.js b/index.js index 561f33a..af111dc 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')) @@ -28,39 +23,51 @@ function basicPlugin (fastify, opts, next) { } } - 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() } } } } -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 = formatRealm(authenticate.realm) + if (realm) { + return realm } } - next(new Error('Basic Auth: Invalid authenticate option')) + 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, { diff --git a/test.js b/test.js index c2b9d0f..0d8b3ae 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) }) }) @@ -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) @@ -604,8 +624,129 @@ 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('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 dynamic realm', t => { + t.plan(3) + + const fastify = Fastify() + const authenticate = { + realm: true + } + fastify.register(basicAuth, { validate, authenticate }) + + function validate (username, password, req, res, done) { + if (username === 'user' && password === 'pwd') { + done(null, 'root') + } 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 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) }) })