Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 29 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand All @@ -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()
Expand All @@ -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, {
Expand Down
179 changes: 174 additions & 5 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -190,21 +190,30 @@ 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',
headers: {
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' }
Expand All @@ -229,15 +238,24 @@ 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',
headers: {
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)
})
})
Expand Down Expand Up @@ -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
Expand All @@ -597,19 +615,170 @@ 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',
headers: {
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')
}