Skip to content

Commit b5ce807

Browse files
authored
Only send www-authenticate for 401. Make realm dynamic. (#40)
* Make realm dynamic * 100% code coverage * Only send www-authenticate for 401. Make realm dynamic. * 100% code coverage
1 parent 799d8f1 commit b5ce807

File tree

3 files changed

+216
-25
lines changed

3 files changed

+216
-25
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,19 @@ fastify.register(require('fastify-basic-auth'), {
166166
})
167167
```
168168

169+
The `realm` key could also be a function:
170+
171+
```js
172+
fastify.register(require('fastify-basic-auth'), {
173+
validate,
174+
authenticate: {
175+
realm(req) {
176+
return 'example' // WWW-Authenticate: Basic realm="example"
177+
}
178+
}
179+
})
180+
```
181+
169182

170183
## License
171184

index.js

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,15 @@ const fp = require('fastify-plugin')
44
const auth = require('basic-auth')
55
const { Unauthorized } = require('http-errors')
66

7-
function basicPlugin (fastify, opts, next) {
7+
async function basicPlugin (fastify, opts) {
88
if (typeof opts.validate !== 'function') {
9-
return next(new Error('Basic Auth: Missing validate function'))
9+
throw new Error('Basic Auth: Missing validate function')
1010
}
11-
const authenticateHeader = getAuthenticateHeader(opts.authenticate, next)
11+
const authenticateHeader = getAuthenticateHeader(opts.authenticate)
1212
const validate = opts.validate.bind(fastify)
1313
fastify.decorate('basicAuth', basicAuth)
1414

15-
next()
16-
1715
function basicAuth (req, reply, next) {
18-
if (authenticateHeader) {
19-
reply.header(authenticateHeader.key, authenticateHeader.value)
20-
}
2116
const credentials = auth(req)
2217
if (credentials == null) {
2318
done(new Unauthorized('Missing or bad formatted authorization header'))
@@ -34,6 +29,17 @@ function basicPlugin (fastify, opts, next) {
3429
if (!err.statusCode) {
3530
err.statusCode = 401
3631
}
32+
33+
if (err.statusCode === 401) {
34+
switch (typeof authenticateHeader) {
35+
case 'string':
36+
reply.header('WWW-Authenticate', authenticateHeader)
37+
break
38+
case 'function':
39+
reply.header('WWW-Authenticate', authenticateHeader(req))
40+
break
41+
}
42+
}
3743
next(err)
3844
} else {
3945
next()
@@ -42,25 +48,28 @@ function basicPlugin (fastify, opts, next) {
4248
}
4349
}
4450

45-
function getAuthenticateHeader (authenticate, next) {
51+
function getAuthenticateHeader (authenticate) {
4652
if (!authenticate) return false
4753
if (authenticate === true) {
48-
return {
49-
key: 'WWW-Authenticate',
50-
value: 'Basic'
51-
}
54+
return 'Basic'
5255
}
5356
if (typeof authenticate === 'object') {
54-
const realm = (authenticate.realm && typeof authenticate.realm === 'string')
55-
? authenticate.realm
56-
: ''
57-
return {
58-
key: 'WWW-Authenticate',
59-
value: 'Basic' + (realm ? ` realm="${realm}"` : '')
57+
const realm = authenticate.realm
58+
switch (typeof realm) {
59+
case 'undefined':
60+
return 'Basic'
61+
case 'boolean':
62+
return 'Basic'
63+
case 'string':
64+
return `Basic realm="${realm}"`
65+
case 'function':
66+
return function (req) {
67+
return `Basic realm="${realm(req)}"`
68+
}
6069
}
6170
}
6271

63-
next(new Error('Basic Auth: Invalid authenticate option'))
72+
throw new Error('Basic Auth: Invalid authenticate option')
6473
}
6574

6675
module.exports = fp(basicPlugin, {

test.js

Lines changed: 174 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ test('Basic with promises - 401', t => {
165165
})
166166

167167
test('WWW-Authenticate (authenticate: true)', t => {
168-
t.plan(3)
168+
t.plan(6)
169169

170170
const fastify = Fastify()
171171
const authenticate = true
@@ -190,21 +190,30 @@ test('WWW-Authenticate (authenticate: true)', t => {
190190
})
191191
})
192192

193+
fastify.inject({
194+
url: '/',
195+
method: 'GET'
196+
}, (err, res) => {
197+
t.error(err)
198+
t.equal(res.headers['www-authenticate'], 'Basic')
199+
t.equal(res.statusCode, 401)
200+
})
201+
193202
fastify.inject({
194203
url: '/',
195204
method: 'GET',
196205
headers: {
197206
authorization: basicAuthHeader('user', 'pwd')
198207
}
199208
}, (err, res) => {
200-
t.equal(res.headers['www-authenticate'], 'Basic')
201209
t.error(err)
210+
t.equal(res.headers['www-authenticate'], undefined)
202211
t.equal(res.statusCode, 200)
203212
})
204213
})
205214

206215
test('WWW-Authenticate Realm (authenticate: {realm: "example"})', t => {
207-
t.plan(3)
216+
t.plan(6)
208217

209218
const fastify = Fastify()
210219
const authenticate = { realm: 'example' }
@@ -229,15 +238,24 @@ test('WWW-Authenticate Realm (authenticate: {realm: "example"})', t => {
229238
})
230239
})
231240

241+
fastify.inject({
242+
url: '/',
243+
method: 'GET'
244+
}, (err, res) => {
245+
t.error(err)
246+
t.equal(res.headers['www-authenticate'], 'Basic realm="example"')
247+
t.equal(res.statusCode, 401)
248+
})
249+
232250
fastify.inject({
233251
url: '/',
234252
method: 'GET',
235253
headers: {
236254
authorization: basicAuthHeader('user', 'pwd')
237255
}
238256
}, (err, res) => {
239-
t.equal(res.headers['www-authenticate'], 'Basic realm="example"')
240257
t.error(err)
258+
t.equal(res.headers['www-authenticate'], undefined)
241259
t.equal(res.statusCode, 200)
242260
})
243261
})
@@ -572,7 +590,7 @@ test('Invalid options (authenticate)', t => {
572590
})
573591

574592
test('Invalid options (authenticate realm)', t => {
575-
t.plan(3)
593+
t.plan(6)
576594

577595
const fastify = Fastify()
578596
fastify
@@ -597,19 +615,170 @@ test('Invalid options (authenticate realm)', t => {
597615
})
598616
})
599617

618+
fastify.inject({
619+
url: '/',
620+
method: 'GET'
621+
}, (err, res) => {
622+
t.error(err)
623+
t.equal(res.headers['www-authenticate'], 'Basic')
624+
t.equal(res.statusCode, 401)
625+
})
626+
600627
fastify.inject({
601628
url: '/',
602629
method: 'GET',
603630
headers: {
604631
authorization: basicAuthHeader('user', 'pwd')
605632
}
606633
}, (err, res) => {
634+
t.error(err)
635+
t.equal(res.headers['www-authenticate'], undefined)
636+
t.equal(res.statusCode, 200)
637+
})
638+
})
639+
640+
test('Invalid options (authenticate realm = undefined)', t => {
641+
t.plan(6)
642+
643+
const fastify = Fastify()
644+
fastify
645+
.register(basicAuth, { validate, authenticate: { realm: undefined } })
646+
647+
function validate (username, password, req, res, done) {
648+
if (username === 'user' && password === 'pwd') {
649+
done()
650+
} else {
651+
done(new Error('Unauthorized'))
652+
}
653+
}
654+
655+
fastify.after(() => {
656+
fastify.route({
657+
method: 'GET',
658+
url: '/',
659+
preHandler: fastify.basicAuth,
660+
handler: (req, reply) => {
661+
reply.send({ hello: 'world' })
662+
}
663+
})
664+
})
665+
666+
fastify.inject({
667+
url: '/',
668+
method: 'GET'
669+
}, (err, res) => {
670+
t.error(err)
607671
t.equal(res.headers['www-authenticate'], 'Basic')
672+
t.equal(res.statusCode, 401)
673+
})
674+
675+
fastify.inject({
676+
url: '/',
677+
method: 'GET',
678+
headers: {
679+
authorization: basicAuthHeader('user', 'pwd')
680+
}
681+
}, (err, res) => {
682+
t.error(err)
683+
t.equal(res.headers['www-authenticate'], undefined)
684+
t.equal(res.statusCode, 200)
685+
})
686+
})
687+
688+
test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => {
689+
t.plan(7)
690+
691+
const fastify = Fastify()
692+
const authenticate = {
693+
realm (req) {
694+
t.equal(req.url, '/')
695+
return 'root'
696+
}
697+
}
698+
fastify.register(basicAuth, { validate, authenticate })
699+
700+
function validate (username, password, req, res, done) {
701+
if (username === 'user' && password === 'pwd') {
702+
done()
703+
} else {
704+
done(new Error('Unauthorized'))
705+
}
706+
}
707+
708+
fastify.after(() => {
709+
fastify.route({
710+
method: 'GET',
711+
url: '/',
712+
preHandler: fastify.basicAuth,
713+
handler: (req, reply) => {
714+
reply.send({ hello: 'world' })
715+
}
716+
})
717+
})
718+
719+
fastify.inject({
720+
url: '/',
721+
method: 'GET'
722+
}, (err, res) => {
723+
t.error(err)
724+
t.equal(res.headers['www-authenticate'], 'Basic realm="root"')
725+
t.equal(res.statusCode, 401)
726+
})
727+
728+
fastify.inject({
729+
url: '/',
730+
method: 'GET',
731+
headers: {
732+
authorization: basicAuthHeader('user', 'pwd')
733+
}
734+
}, (err, res) => {
608735
t.error(err)
736+
t.equal(res.headers['www-authenticate'], undefined)
609737
t.equal(res.statusCode, 200)
610738
})
611739
})
612740

741+
test('No 401 no realm', t => {
742+
t.plan(4)
743+
744+
const fastify = Fastify()
745+
fastify.register(basicAuth, { validate, authenticate: true })
746+
747+
function validate (username, password, req, res) {
748+
const err = new Error('Winter is coming')
749+
err.statusCode = 402
750+
return Promise.reject(err)
751+
}
752+
753+
fastify.after(() => {
754+
fastify.route({
755+
method: 'GET',
756+
url: '/',
757+
preHandler: fastify.basicAuth,
758+
handler: (req, reply) => {
759+
reply.send({ hello: 'world' })
760+
}
761+
})
762+
})
763+
764+
fastify.inject({
765+
url: '/',
766+
method: 'GET',
767+
headers: {
768+
authorization: basicAuthHeader('user', 'pwdd')
769+
}
770+
}, (err, res) => {
771+
t.error(err)
772+
t.equal(res.statusCode, 402)
773+
t.equal(res.headers['www-authenticate'], undefined)
774+
t.same(JSON.parse(res.payload), {
775+
error: 'Payment Required',
776+
message: 'Winter is coming',
777+
statusCode: 402
778+
})
779+
})
780+
})
781+
613782
function basicAuthHeader (username, password) {
614783
return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64')
615784
}

0 commit comments

Comments
 (0)