diff --git a/README.md b/README.md index 12f5907..9bff716 100644 --- a/README.md +++ b/README.md @@ -170,12 +170,20 @@ This can trigger client-side authentication interfaces, such as the browser auth Setting `authenticate` to `true` adds the header `WWW-Authenticate: Basic`. When `false`, no header is added (default). +When `proxyMode` is `true` it will use the [`Proxy-Authenticate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authenticate) header instead. + ```js fastify.register(require('@fastify/basic-auth'), { validate, authenticate: true // WWW-Authenticate: Basic }) +fastify.register(require('@fastify/basic-auth'), { + validate, + proxyMode: true, + authenticate: true // Proxy-Authenticate: Basic +}) + fastify.register(require('@fastify/basic-auth'), { validate, authenticate: false // no authenticate header, same as omitting authenticate option @@ -216,10 +224,17 @@ fastify.register(require('@fastify/basic-auth'), { }) ``` +### `proxyMode` Boolean (optional, default: false) + +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: + +- read credentials from the `Proxy-Authorization` header rather than `Authorization` +- use `407` response status code instead of `401` to signal missing or invalid credentials +- use the `Proxy-Authenticate` header rather than `WWW-Authenticate` if the `authenticate` option is set ### `header` String (optional) -The `header` option specifies the header name to get credentials from for validation. +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) ```js fastify.register(require('@fastify/basic-auth'), { diff --git a/index.js b/index.js index e6ff7c2..da3d390 100644 --- a/index.js +++ b/index.js @@ -3,12 +3,6 @@ const fp = require('fastify-plugin') const createError = require('@fastify/error') -const MissingOrBadAuthorizationHeader = createError( - 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', - 'Missing or bad formatted authorization header', - 401 -) - /** * HTTP provides a simple challenge-response authentication framework * that can be used by a server to challenge a client request and by a @@ -73,8 +67,15 @@ async function fastifyBasicAuth (fastify, opts) { const strictCredentials = opts.strictCredentials ?? true const useUtf8 = opts.utf8 ?? true const charset = useUtf8 ? 'utf-8' : 'ascii' - const authenticateHeader = getAuthenticateHeaders(opts.authenticate, useUtf8) - const header = opts.header?.toLowerCase() || 'authorization' + const authenticateHeader = getAuthenticateHeaders(opts.authenticate, useUtf8, opts.proxyMode) + const header = opts.header?.toLowerCase() || (opts.proxyMode ? 'proxy-authorization' : 'authorization') + const errorResponseCode = opts.proxyMode ? 407 : 401 + + const MissingOrBadAuthorizationHeader = createError( + 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', + 'Missing or bad formatted authorization header', + errorResponseCode + ) const credentialsRE = strictCredentials ? credentialsStrictRE @@ -124,12 +125,12 @@ async function fastifyBasicAuth (fastify, opts) { function done (err) { if (err !== undefined) { - // We set the status code to be 401 if it is not set + // We set the status code to be `errorResponseCode` (normally 401) if it is not set if (!err.statusCode) { - err.statusCode = 401 + err.statusCode = errorResponseCode } - if (err.statusCode === 401) { + if (err.statusCode === errorResponseCode) { const header = authenticateHeader(req) if (header) { reply.header(header[0], header[1]) @@ -143,8 +144,8 @@ async function fastifyBasicAuth (fastify, opts) { } } -function getAuthenticateHeaders (authenticate, useUtf8) { - const defaultHeaderName = 'WWW-Authenticate' +function getAuthenticateHeaders (authenticate, useUtf8, proxyMode) { + const defaultHeaderName = proxyMode ? 'Proxy-Authenticate' : 'WWW-Authenticate' if (!authenticate) return () => false if (authenticate === true) { return useUtf8 diff --git a/test/index.test.js b/test/index.test.js index 3f35aad..e8987af 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -685,6 +685,68 @@ test('WWW-Authenticate Custom Header (authenticate: {realm: "example", header: " t.assert.strictEqual(res2.statusCode, 200) }) +test("Proxy authentication (proxyMode: true, authenticate: { realm: 'example' }, utf8: true)", async t => { + t.plan(12) + + const fastify = Fastify() + const authenticate = { realm: 'example' } + fastify.register(basicAuth, { validate, authenticate, utf8: true, proxyMode: true }) + + 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' }) + } + }) + }) + + const res1 = await fastify.inject({ + url: '/', + method: 'GET' + }) + t.assert.ok(res1.body) + t.assert.strictEqual(res1.headers['proxy-authenticate'], 'Basic realm="example", charset="UTF-8"') + t.assert.strictEqual(res1.headers['www-authenticate'], undefined) + t.assert.strictEqual(res1.statusCode, 407) + + const res2 = await fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: basicAuthHeader('user', 'pwd') + } + }) + + t.assert.ok(res2.body) + t.assert.strictEqual(res2.headers['proxy-authenticate'], 'Basic realm="example", charset="UTF-8"') + t.assert.strictEqual(res2.headers['www-authenticate'], undefined) + t.assert.strictEqual(res2.statusCode, 407) + + const res3 = await fastify.inject({ + url: '/', + method: 'GET', + headers: { + 'proxy-authorization': basicAuthHeader('user', 'pwd') + } + }) + + t.assert.ok(res3.body) + t.assert.strictEqual(res3.headers['proxy-authenticate'], undefined) + t.assert.strictEqual(res3.headers['www-authenticate'], undefined) + t.assert.strictEqual(res3.statusCode, 200) +}) + test('Header option specified', async t => { t.plan(2) diff --git a/types/index.d.ts b/types/index.d.ts index f1911df..135c0f8 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -29,6 +29,7 @@ declare namespace fastifyBasicAuth { done: (err?: Error) => void ): void | Promise; authenticate?: boolean | { realm?: string | ((req: FastifyRequest) => string); header?: string }; + proxyMode?: boolean; header?: string; strictCredentials?: boolean | undefined; utf8?: boolean | undefined; diff --git a/types/index.test-d.ts b/types/index.test-d.ts index aed580a..2eb4c79 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -75,6 +75,13 @@ app.register(fastifyBasicAuth, { authenticate: { header: 'x-custom-authenticate' } }) +// authenticate in proxy mode +app.register(fastifyBasicAuth, { + validate: () => {}, + proxyMode: true, + authenticate: true, +}) + app.register(fastifyBasicAuth, { validate: () => {}, strictCredentials: true