Skip to content

Commit 05bc284

Browse files
authored
feat: add proxyMode option (#169)
1 parent bbcf506 commit 05bc284

5 files changed

Lines changed: 100 additions & 14 deletions

File tree

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,20 @@ This can trigger client-side authentication interfaces, such as the browser auth
170170

171171
Setting `authenticate` to `true` adds the header `WWW-Authenticate: Basic`. When `false`, no header is added (default).
172172

173+
When `proxyMode` is `true` it will use the [`Proxy-Authenticate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authenticate) header instead.
174+
173175
```js
174176
fastify.register(require('@fastify/basic-auth'), {
175177
validate,
176178
authenticate: true // WWW-Authenticate: Basic
177179
})
178180

181+
fastify.register(require('@fastify/basic-auth'), {
182+
validate,
183+
proxyMode: true,
184+
authenticate: true // Proxy-Authenticate: Basic
185+
})
186+
179187
fastify.register(require('@fastify/basic-auth'), {
180188
validate,
181189
authenticate: false // no authenticate header, same as omitting authenticate option
@@ -216,10 +224,17 @@ fastify.register(require('@fastify/basic-auth'), {
216224
})
217225
```
218226

227+
### `proxyMode` Boolean (optional, default: false)
228+
229+
Setting the `proxyMode` to `true` will make the plugin implement [HTTP proxy authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication#proxy_authentication), rather than resource authentication. In other words, the plugin will:
230+
231+
- read credentials from the `Proxy-Authorization` header rather than `Authorization`
232+
- use `407` response status code instead of `401` to signal missing or invalid credentials
233+
- use the `Proxy-Authenticate` header rather than `WWW-Authenticate` if the `authenticate` option is set
219234

220235
### `header` String (optional)
221236

222-
The `header` option specifies the header name to get credentials from for validation.
237+
The `header` option specifies the header name to get credentials from for validation. If not specified it defaults to `Authorization` or `Proxy-Authorization` (according to the value of `proxyMode` option)
223238

224239
```js
225240
fastify.register(require('@fastify/basic-auth'), {

index.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@
33
const fp = require('fastify-plugin')
44
const createError = require('@fastify/error')
55

6-
const MissingOrBadAuthorizationHeader = createError(
7-
'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER',
8-
'Missing or bad formatted authorization header',
9-
401
10-
)
11-
126
/**
137
* HTTP provides a simple challenge-response authentication framework
148
* that can be used by a server to challenge a client request and by a
@@ -73,8 +67,15 @@ async function fastifyBasicAuth (fastify, opts) {
7367
const strictCredentials = opts.strictCredentials ?? true
7468
const useUtf8 = opts.utf8 ?? true
7569
const charset = useUtf8 ? 'utf-8' : 'ascii'
76-
const authenticateHeader = getAuthenticateHeaders(opts.authenticate, useUtf8)
77-
const header = opts.header?.toLowerCase() || 'authorization'
70+
const authenticateHeader = getAuthenticateHeaders(opts.authenticate, useUtf8, opts.proxyMode)
71+
const header = opts.header?.toLowerCase() || (opts.proxyMode ? 'proxy-authorization' : 'authorization')
72+
const errorResponseCode = opts.proxyMode ? 407 : 401
73+
74+
const MissingOrBadAuthorizationHeader = createError(
75+
'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER',
76+
'Missing or bad formatted authorization header',
77+
errorResponseCode
78+
)
7879

7980
const credentialsRE = strictCredentials
8081
? credentialsStrictRE
@@ -124,12 +125,12 @@ async function fastifyBasicAuth (fastify, opts) {
124125

125126
function done (err) {
126127
if (err !== undefined) {
127-
// We set the status code to be 401 if it is not set
128+
// We set the status code to be `errorResponseCode` (normally 401) if it is not set
128129
if (!err.statusCode) {
129-
err.statusCode = 401
130+
err.statusCode = errorResponseCode
130131
}
131132

132-
if (err.statusCode === 401) {
133+
if (err.statusCode === errorResponseCode) {
133134
const header = authenticateHeader(req)
134135
if (header) {
135136
reply.header(header[0], header[1])
@@ -143,8 +144,8 @@ async function fastifyBasicAuth (fastify, opts) {
143144
}
144145
}
145146

146-
function getAuthenticateHeaders (authenticate, useUtf8) {
147-
const defaultHeaderName = 'WWW-Authenticate'
147+
function getAuthenticateHeaders (authenticate, useUtf8, proxyMode) {
148+
const defaultHeaderName = proxyMode ? 'Proxy-Authenticate' : 'WWW-Authenticate'
148149
if (!authenticate) return () => false
149150
if (authenticate === true) {
150151
return useUtf8

test/index.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,68 @@ test('WWW-Authenticate Custom Header (authenticate: {realm: "example", header: "
685685
t.assert.strictEqual(res2.statusCode, 200)
686686
})
687687

688+
test("Proxy authentication (proxyMode: true, authenticate: { realm: 'example' }, utf8: true)", async t => {
689+
t.plan(12)
690+
691+
const fastify = Fastify()
692+
const authenticate = { realm: 'example' }
693+
fastify.register(basicAuth, { validate, authenticate, utf8: true, proxyMode: true })
694+
695+
function validate (username, password, _req, _res, done) {
696+
if (username === 'user' && password === 'pwd') {
697+
done()
698+
} else {
699+
done(new Error('Unauthorized'))
700+
}
701+
}
702+
703+
fastify.after(() => {
704+
fastify.route({
705+
method: 'GET',
706+
url: '/',
707+
preHandler: fastify.basicAuth,
708+
handler: (_req, reply) => {
709+
reply.send({ hello: 'world' })
710+
}
711+
})
712+
})
713+
714+
const res1 = await fastify.inject({
715+
url: '/',
716+
method: 'GET'
717+
})
718+
t.assert.ok(res1.body)
719+
t.assert.strictEqual(res1.headers['proxy-authenticate'], 'Basic realm="example", charset="UTF-8"')
720+
t.assert.strictEqual(res1.headers['www-authenticate'], undefined)
721+
t.assert.strictEqual(res1.statusCode, 407)
722+
723+
const res2 = await fastify.inject({
724+
url: '/',
725+
method: 'GET',
726+
headers: {
727+
authorization: basicAuthHeader('user', 'pwd')
728+
}
729+
})
730+
731+
t.assert.ok(res2.body)
732+
t.assert.strictEqual(res2.headers['proxy-authenticate'], 'Basic realm="example", charset="UTF-8"')
733+
t.assert.strictEqual(res2.headers['www-authenticate'], undefined)
734+
t.assert.strictEqual(res2.statusCode, 407)
735+
736+
const res3 = await fastify.inject({
737+
url: '/',
738+
method: 'GET',
739+
headers: {
740+
'proxy-authorization': basicAuthHeader('user', 'pwd')
741+
}
742+
})
743+
744+
t.assert.ok(res3.body)
745+
t.assert.strictEqual(res3.headers['proxy-authenticate'], undefined)
746+
t.assert.strictEqual(res3.headers['www-authenticate'], undefined)
747+
t.assert.strictEqual(res3.statusCode, 200)
748+
})
749+
688750
test('Header option specified', async t => {
689751
t.plan(2)
690752

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ declare namespace fastifyBasicAuth {
2929
done: (err?: Error) => void
3030
): void | Promise<void | Error>;
3131
authenticate?: boolean | { realm?: string | ((req: FastifyRequest) => string); header?: string };
32+
proxyMode?: boolean;
3233
header?: string;
3334
strictCredentials?: boolean | undefined;
3435
utf8?: boolean | undefined;

types/index.test-d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ app.register(fastifyBasicAuth, {
7575
authenticate: { header: 'x-custom-authenticate' }
7676
})
7777

78+
// authenticate in proxy mode
79+
app.register(fastifyBasicAuth, {
80+
validate: () => {},
81+
proxyMode: true,
82+
authenticate: true,
83+
})
84+
7885
app.register(fastifyBasicAuth, {
7986
validate: () => {},
8087
strictCredentials: true

0 commit comments

Comments
 (0)