Skip to content
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'), {
Expand Down
27 changes: 14 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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])
Expand All @@ -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
Expand Down
62 changes: 62 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ declare namespace fastifyBasicAuth {
done: (err?: Error) => void
): void | Promise<void | Error>;
authenticate?: boolean | { realm?: string | ((req: FastifyRequest) => string); header?: string };
proxyMode?: boolean;
header?: string;
strictCredentials?: boolean | undefined;
utf8?: boolean | undefined;
Expand Down
7 changes: 7 additions & 0 deletions types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down