From 9734986e3258a2ab0927701227c1597bd5bbf9b9 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 29 Nov 2022 14:10:52 +0100 Subject: [PATCH 1/9] integrate basic-auth --- index.js | 94 ++++++++++++++++++++++++++++++--- package.json | 1 - test/index.test.js | 129 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 7b3c5bc..ecd4009 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ 'use strict' const fp = require('fastify-plugin') -const auth = require('basic-auth') const createError = require('@fastify/error') const MissingOrBadAuthorizationHeader = createError( @@ -10,6 +9,57 @@ const MissingOrBadAuthorizationHeader = createError( 401 ) +/** + * HTTP provides a simple challenge-response authentication framework + * that can be used by a server to challenge a client request and by a + * client to provide authentication information. It uses a case- + * insensitive token as a means to identify the authentication scheme, + * followed by additional information necessary for achieving + * authentication via that scheme. + * + * @see https://datatracker.ietf.org/doc/html/rfc7235#section-2.1 + * + * The scheme name is "Basic". + * @see https://datatracker.ietf.org/doc/html/rfc7617#section-2 + */ +const authScheme = '(?:[Bb][Aa][Ss][Ii][Cc])' +/** + * The BWS rule is used where the grammar allows optional whitespace + * only for historical reasons. A sender MUST NOT generate BWS in + * messages. A recipient MUST parse for such bad whitespace and remove + * it before interpreting the protocol element. + * + * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3 + */ +const BWS = '[ \t]*' +/** + * The token68 syntax allows the 66 unreserved URI characters + * ([RFC3986]), plus a few others, so that it can hold a base64, + * base64url (URL and filename safe alphabet), base32, or base16 (hex) + * encoding, with or without padding, but excluding whitespace + * ([RFC4648]). + * @see https://datatracker.ietf.org/doc/html/rfc7235#section-2.1 + */ +const token68 = '+([A-Za-z0-9._~+/-]+=*)' + +const credentialsRE = new RegExp(`^${BWS}${authScheme} ${token68}${BWS}$`) + +/** + * @see https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 + */ +const CTL = '[\x00-\x1F\x7F]' +const controlRE = new RegExp(CTL) + +/** + * RegExp for basic auth user/pass + * + * user-pass = userid ":" password + * userid = * + * password = *TEXT + */ + +const userPassRE = /^([^:]*):(.*)$/ + async function fastifyBasicAuth (fastify, opts) { if (typeof opts.validate !== 'function') { throw new Error('Basic Auth: Missing validate function') @@ -21,14 +71,42 @@ async function fastifyBasicAuth (fastify, opts) { fastify.decorate('basicAuth', basicAuth) function basicAuth (req, reply, next) { - const credentials = auth.parse(req.headers[header]) - if (credentials == null) { + const credentials = req.headers[header] + + if (typeof credentials !== 'string') { done(new MissingOrBadAuthorizationHeader()) - } else { - const result = validate(credentials.name, credentials.pass, req, reply, done) - if (result && typeof result.then === 'function') { - result.then(done, done) - } + return + } + + // parse header + const match = credentialsRE.exec(credentials) + if (match === null) { + done(new MissingOrBadAuthorizationHeader()) + return + } + + // decode user pass + const credentialsDecoded = Buffer.from(match[1], 'base64').toString() + + /** + * The user-id and password MUST NOT contain any control characters (see + * "CTL" in Appendix B.1 of [RFC5234]). + * @see https://datatracker.ietf.org/doc/html/rfc7617#section-2 + */ + if (controlRE.test(credentialsDecoded)) { + done(new MissingOrBadAuthorizationHeader()) + return + } + + const userPass = userPassRE.exec(credentialsDecoded) + if (userPass === null) { + done(new MissingOrBadAuthorizationHeader()) + return + } + + const result = validate(userPass[1], userPass[2], req, reply, done) + if (result && typeof result.then === 'function') { + result.then(done, done) } function done (err) { diff --git a/package.json b/package.json index 43a2cbe..19cf781 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ }, "dependencies": { "@fastify/error": "^3.0.0", - "basic-auth": "^2.0.1", "fastify-plugin": "^4.0.0" }, "publishConfig": { diff --git a/test/index.test.js b/test/index.test.js index 4adb633..2efdc58 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -84,6 +84,135 @@ test('Basic - 401', t => { }) }) +test('Basic - Invalid Header value /1', t => { + t.plan(3) + + const fastify = Fastify() + fastify.register(basicAuth, { validate }) + + function validate (username, password, req, res, done) { + if (username === 'user' && password === 'pwd') { + done() + } else { + done(new Error('Winter is coming')) + } + } + + fastify.after(() => { + fastify.route({ + method: 'GET', + url: '/', + preHandler: fastify.basicAuth, + handler: (req, reply) => { + reply.send({ hello: 'world' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: 'Bearer ' + Buffer.from('user:pass').toString('base64') + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 401) + t.same(JSON.parse(res.payload), { + code: 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', + error: 'Unauthorized', + message: 'Missing or bad formatted authorization header', + statusCode: 401 + }) + }) +}) + +test('Basic - Invalid Header value /2', t => { + t.plan(3) + + const fastify = Fastify() + fastify.register(basicAuth, { validate }) + + function validate (username, password, req, res, done) { + if (username === 'user' && password === 'pwd') { + done() + } else { + done(new Error('Winter is coming')) + } + } + + fastify.after(() => { + fastify.route({ + method: 'GET', + url: '/', + preHandler: fastify.basicAuth, + handler: (req, reply) => { + reply.send({ hello: 'world' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: 'Basic ' + Buffer.from('user').toString('base64') + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 401) + t.same(JSON.parse(res.payload), { + code: 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', + error: 'Unauthorized', + message: 'Missing or bad formatted authorization header', + statusCode: 401 + }) + }) +}) + +test('Basic - Invalid Header value /3', t => { + t.plan(3) + + const fastify = Fastify() + fastify.register(basicAuth, { validate }) + + function validate (username, password, req, res, done) { + if (username === 'user' && password === 'pwd') { + done() + } else { + done(new Error('Winter is coming')) + } + } + + fastify.after(() => { + fastify.route({ + method: 'GET', + url: '/', + preHandler: fastify.basicAuth, + handler: (req, reply) => { + reply.send({ hello: 'world' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: 'Basic ' + Buffer.from('user\x00:password').toString('base64') + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 401) + t.same(JSON.parse(res.payload), { + code: 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', + error: 'Unauthorized', + message: 'Missing or bad formatted authorization header', + statusCode: 401 + }) + }) +}) + test('Basic with promises', t => { t.plan(2) From fe2fd4055bd4c7bcb87d433f33177edbd4ea9841 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 29 Nov 2022 14:13:28 +0100 Subject: [PATCH 2/9] make credentials more strict --- index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index ecd4009..cbd3ab9 100644 --- a/index.js +++ b/index.js @@ -31,7 +31,7 @@ const authScheme = '(?:[Bb][Aa][Ss][Ii][Cc])' * * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3 */ -const BWS = '[ \t]*' +// const BWS = '[ \t]*' /** * The token68 syntax allows the 66 unreserved URI characters * ([RFC3986]), plus a few others, so that it can hold a base64, @@ -42,7 +42,10 @@ const BWS = '[ \t]*' */ const token68 = '+([A-Za-z0-9._~+/-]+=*)' -const credentialsRE = new RegExp(`^${BWS}${authScheme} ${token68}${BWS}$`) +/** + * @see https://datatracker.ietf.org/doc/html/rfc7235#appendix-C + */ +const credentialsRE = new RegExp(`^${authScheme} ${token68}$`) /** * @see https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 From 84a561f2d6b023c55591b2579bf2a2546e559185 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 29 Nov 2022 14:30:30 +0100 Subject: [PATCH 3/9] add strict option for fallback opportunity --- index.js | 12 ++++++++++-- test/index.test.js | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index cbd3ab9..6bc0b38 100644 --- a/index.js +++ b/index.js @@ -31,7 +31,7 @@ const authScheme = '(?:[Bb][Aa][Ss][Ii][Cc])' * * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3 */ -// const BWS = '[ \t]*' +const BWS = '[ \t]*' /** * The token68 syntax allows the 66 unreserved URI characters * ([RFC3986]), plus a few others, so that it can hold a base64, @@ -45,7 +45,9 @@ const token68 = '+([A-Za-z0-9._~+/-]+=*)' /** * @see https://datatracker.ietf.org/doc/html/rfc7235#appendix-C */ -const credentialsRE = new RegExp(`^${authScheme} ${token68}$`) +const credentialsStrictRE = new RegExp(`^${authScheme} ${token68}$`) + +const credentialsLaxRE = new RegExp(`^${BWS}${authScheme} ${token68}${BWS}$`) /** * @see https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 @@ -67,9 +69,15 @@ async function fastifyBasicAuth (fastify, opts) { if (typeof opts.validate !== 'function') { throw new Error('Basic Auth: Missing validate function') } + + const strict = opts.strict ?? true const authenticateHeader = getAuthenticateHeader(opts.authenticate) const header = (opts.header && opts.header.toLowerCase()) || 'authorization' + const credentialsRE = strict + ? credentialsStrictRE + : credentialsLaxRE + const validate = opts.validate.bind(fastify) fastify.decorate('basicAuth', basicAuth) diff --git a/test/index.test.js b/test/index.test.js index 2efdc58..66d6e64 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -199,7 +199,7 @@ test('Basic - Invalid Header value /3', t => { url: '/', method: 'GET', headers: { - authorization: 'Basic ' + Buffer.from('user\x00:password').toString('base64') + authorization: 'Basic ' + Buffer.from('user\x00:pwd').toString('base64') } }, (err, res) => { t.error(err) @@ -213,6 +213,43 @@ test('Basic - Invalid Header value /3', t => { }) }) +test('Basic - strict: false', t => { + t.plan(2) + + const fastify = Fastify() + fastify.register(basicAuth, { validate, strict: false }) + + function validate (username, password, req, res, done) { + if (username === 'user' && password === 'pwd') { + done() + } else { + done(new Error('Winter is coming')) + } + } + + fastify.after(() => { + fastify.route({ + method: 'GET', + url: '/', + preHandler: fastify.basicAuth, + handler: (req, reply) => { + reply.send({ hello: 'world' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET', + headers: { + authorization: ' Basic ' + Buffer.from('user:pwd').toString('base64') + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + }) +}) + test('Basic with promises', t => { t.plan(2) From 4006ac4d3f3f4273c371dc3c2270bad3a0125836 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 29 Nov 2022 14:39:53 +0100 Subject: [PATCH 4/9] modify strictness --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 6bc0b38..d93e00c 100644 --- a/index.js +++ b/index.js @@ -40,14 +40,14 @@ const BWS = '[ \t]*' * ([RFC4648]). * @see https://datatracker.ietf.org/doc/html/rfc7235#section-2.1 */ -const token68 = '+([A-Za-z0-9._~+/-]+=*)' +const token68 = '([A-Za-z0-9._~+/-]+=*)' /** * @see https://datatracker.ietf.org/doc/html/rfc7235#appendix-C */ const credentialsStrictRE = new RegExp(`^${authScheme} ${token68}$`) -const credentialsLaxRE = new RegExp(`^${BWS}${authScheme} ${token68}${BWS}$`) +const credentialsLaxRE = new RegExp(`^${BWS}${authScheme} +${token68}${BWS}$`) /** * @see https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 From 7055ae060d21fc497230f2625138ac7148b7cb4b Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 29 Nov 2022 15:19:21 +0100 Subject: [PATCH 5/9] handle charset auth-token in challenge properly --- index.js | 39 ++--- test/index.test.js | 348 ++++++++++++++++++++++++++++++++++++++++-- types/index.d.ts | 2 + types/index.test-d.ts | 12 +- 4 files changed, 371 insertions(+), 30 deletions(-) diff --git a/index.js b/index.js index d93e00c..3369719 100644 --- a/index.js +++ b/index.js @@ -71,7 +71,9 @@ async function fastifyBasicAuth (fastify, opts) { } const strict = opts.strict ?? true - const authenticateHeader = getAuthenticateHeader(opts.authenticate) + const useUtf8 = opts.utf8 ?? true + const charset = useUtf8 ? 'utf-8' : 'ascii' + const authenticateHeader = getAuthenticateHeader(opts.authenticate, useUtf8) const header = (opts.header && opts.header.toLowerCase()) || 'authorization' const credentialsRE = strict @@ -97,7 +99,7 @@ async function fastifyBasicAuth (fastify, opts) { } // decode user pass - const credentialsDecoded = Buffer.from(match[1], 'base64').toString() + const credentialsDecoded = Buffer.from(match[1], 'base64').toString(charset) /** * The user-id and password MUST NOT contain any control characters (see @@ -128,14 +130,7 @@ async function fastifyBasicAuth (fastify, opts) { } if (err.statusCode === 401) { - switch (typeof authenticateHeader) { - case 'string': - reply.header('WWW-Authenticate', authenticateHeader) - break - case 'function': - reply.header('WWW-Authenticate', authenticateHeader(req)) - break - } + reply.header('WWW-Authenticate', authenticateHeader(req)) } next(err) } else { @@ -145,24 +140,30 @@ async function fastifyBasicAuth (fastify, opts) { } } -function getAuthenticateHeader (authenticate) { - if (!authenticate) return false +function getAuthenticateHeader (authenticate, useUtf8) { + if (!authenticate) return () => false if (authenticate === true) { - return 'Basic' + return useUtf8 + ? () => 'Basic charset="UTF-8"' + : () => 'Basic' } if (typeof authenticate === 'object') { const realm = authenticate.realm switch (typeof realm) { case 'undefined': - return 'Basic' case 'boolean': - return 'Basic' + return useUtf8 + ? () => 'Basic charset="UTF-8"' + : () => 'Basic' case 'string': - return `Basic realm="${realm}"` + return useUtf8 + ? () => `Basic realm="${realm}", charset="UTF-8"` + : () => `Basic realm="${realm}"` case 'function': - return function (req) { - return `Basic realm="${realm(req)}"` - } + + return useUtf8 + ? (req) => `Basic realm="${realm(req)}", charset="UTF-8"` + : (req) => `Basic realm="${realm(req)}"` } } diff --git a/test/index.test.js b/test/index.test.js index 66d6e64..215b0f3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -42,6 +42,91 @@ test('Basic', t => { }) }) +test('Basic utf8: true', t => { + t.plan(2) + + const fastify = Fastify() + fastify.register(basicAuth, { validate, utf8: true }) + + function validate (username, password, req, res, done) { + if (username === 'test' && password === '123\u00A3') { + 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', + headers: { + /** + * @see https://datatracker.ietf.org/doc/html/rfc7617#section-2.1 + */ + authorization: 'Basic dGVzdDoxMjPCow==' + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + }) +}) + +test('Basic - 401, sending utf8 credentials base64 but utf8: false', t => { + t.plan(3) + + const fastify = Fastify() + fastify.register(basicAuth, { validate, utf8: false }) + + function validate (username, password, req, res, done) { + if (username === 'test' && password === '123\u00A3') { + 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', + headers: { + /** + * @see https://datatracker.ietf.org/doc/html/rfc7617#section-2.1 + */ + authorization: 'Basic dGVzdDoxMjPCow==' + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 401) + t.same(JSON.parse(res.payload), { + error: 'Unauthorized', + message: 'Unauthorized', + statusCode: 401 + }) + }) +}) + test('Basic - 401', t => { t.plan(3) @@ -334,7 +419,7 @@ test('WWW-Authenticate (authenticate: true)', t => { const fastify = Fastify() const authenticate = true - fastify.register(basicAuth, { validate, authenticate }) + fastify.register(basicAuth, { validate, authenticate, utf8: false }) function validate (username, password, req, res, done) { if (username === 'user' && password === 'pwd') { @@ -377,12 +462,12 @@ test('WWW-Authenticate (authenticate: true)', t => { }) }) -test('WWW-Authenticate Realm (authenticate: {realm: "example"})', t => { +test('WWW-Authenticate Realm (authenticate: {realm: "example"}, utf8: false)', t => { t.plan(6) const fastify = Fastify() const authenticate = { realm: 'example' } - fastify.register(basicAuth, { validate, authenticate }) + fastify.register(basicAuth, { validate, authenticate, utf8: false }) function validate (username, password, req, res, done) { if (username === 'user' && password === 'pwd') { @@ -425,6 +510,54 @@ test('WWW-Authenticate Realm (authenticate: {realm: "example"})', t => { }) }) +test('WWW-Authenticate Realm (authenticate: {realm: "example"}, utf8: true)', t => { + t.plan(6) + + const fastify = Fastify() + const authenticate = { realm: 'example' } + fastify.register(basicAuth, { validate, authenticate, utf8: 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' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic realm="example", charset="UTF-8"') + 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('Header option specified', t => { t.plan(2) @@ -799,12 +932,108 @@ test('Invalid options (authenticate)', t => { }) }) -test('Invalid options (authenticate realm)', t => { +test('authenticate: true, utf8: true', t => { + t.plan(6) + + const fastify = Fastify() + fastify + .register(basicAuth, { validate, authenticate: true, utf8: 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' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic charset="UTF-8"') + 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('authenticate realm: false, utf8: true', t => { t.plan(6) const fastify = Fastify() fastify - .register(basicAuth, { validate, authenticate: { realm: true } }) + .register(basicAuth, { validate, authenticate: { realm: false }, utf8: 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' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic charset="UTF-8"') + 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, utf8: false)', t => { + t.plan(6) + + const fastify = Fastify() + fastify + .register(basicAuth, { validate, authenticate: { realm: true }, utf8: false }) function validate (username, password, req, res, done) { if (username === 'user' && password === 'pwd') { @@ -847,12 +1076,60 @@ test('Invalid options (authenticate realm)', t => { }) }) -test('Invalid options (authenticate realm = undefined)', t => { +test('Invalid options (authenticate realm), utf8: true', t => { + t.plan(6) + + const fastify = Fastify() + fastify + .register(basicAuth, { validate, utf8: true, authenticate: { realm: 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' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic charset="UTF-8"') + 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, utf8: false)', t => { t.plan(6) const fastify = Fastify() fastify - .register(basicAuth, { validate, authenticate: { realm: undefined } }) + .register(basicAuth, { validate, authenticate: { realm: undefined }, utf8: false }) function validate (username, password, req, res, done) { if (username === 'user' && password === 'pwd') { @@ -895,7 +1172,7 @@ test('Invalid options (authenticate realm = undefined)', t => { }) }) -test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { +test('WWW-Authenticate Realm (authenticate: {realm (req) { }}, utf8: false)', t => { t.plan(7) const fastify = Fastify() @@ -905,7 +1182,7 @@ test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { return 'root' } } - fastify.register(basicAuth, { validate, authenticate }) + fastify.register(basicAuth, { validate, authenticate, utf8: false }) function validate (username, password, req, res, done) { if (username === 'user' && password === 'pwd') { @@ -948,6 +1225,59 @@ test('WWW-Authenticate Realm (authenticate: {realm (req) { }})', t => { }) }) +test('WWW-Authenticate Realm (authenticate: {realm (req) { }}), utf8', t => { + t.plan(7) + + const fastify = Fastify() + const authenticate = { + realm (req) { + t.equal(req.url, '/') + return 'root' + } + } + fastify.register(basicAuth, { validate, authenticate, utf8: 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' }) + } + }) + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.headers['www-authenticate'], 'Basic realm="root", charset="UTF-8"') + 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) diff --git a/types/index.d.ts b/types/index.d.ts index 4724b24..387f3c8 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -30,6 +30,8 @@ declare namespace fastifyBasicAuth { ): void | Promise; authenticate?: boolean | { realm: string | ((req: FastifyRequest) => string) }; header?: string; + strict?: boolean; + utf8?: boolean; } export const fastifyBasicAuth: FastifyBasicAuth diff --git a/types/index.test-d.ts b/types/index.test-d.ts index 9b895f2..a6c4a74 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -36,8 +36,6 @@ app.register(fastifyBasicAuth, { header: 'x-forwarded-authorization' }) - - app.register(fastifyBasicAuth, { validate: function validateCallback (username, password, req, reply, done) { expectType(username) @@ -69,6 +67,16 @@ app.register(fastifyBasicAuth, { }} }) +app.register(fastifyBasicAuth, { + validate: () => {}, + strict: true +}) + +app.register(fastifyBasicAuth, { + validate: () => {}, + utf8: true +}) + expectAssignable(app.basicAuth) expectAssignable(app.basicAuth) expectAssignable(app.basicAuth) From 3f5e98517bf766c72b82c62931739d85427cfdf1 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 29 Nov 2022 15:27:46 +0100 Subject: [PATCH 6/9] add options to readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 40f85d8..a0788f5 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,22 @@ fastify.setErrorHandler(function (err, req, reply) { ## Options +### `utf8` (optional, default: true) + +User-ids or passwords containing characters outside the US-ASCII +character repertoire will cause interoperability issues, unless both +communication partners agree on what character encoding scheme is to +be used. If utf8 is set to true the server will send the 'charset' parameter +to indicate a preference of "UTF-8", increasing the probability that +clients will switch to that encoding. + +### `strict` (optional, default: true) + +If strict is set to false the authorization header can contain additional +whitespaces at the beginning and the end of the authorization header. This is a +fallback option to ensure the same behaviour as @fastify/basic-auth version +<=5.x. + ### `validate` (required) The `validate` function is called on each request made, From 889cc95787755ae4077b2c4c10a5aa30c4ddb102 Mon Sep 17 00:00:00 2001 From: Uzlopak Date: Tue, 29 Nov 2022 15:35:01 +0100 Subject: [PATCH 7/9] Update README.md Co-authored-by: James Sumners --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0788f5..bb3c639 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ fastify.setErrorHandler(function (err, req, reply) { ### `utf8` (optional, default: true) User-ids or passwords containing characters outside the US-ASCII -character repertoire will cause interoperability issues, unless both +character set will cause interoperability issues, unless both communication partners agree on what character encoding scheme is to be used. If utf8 is set to true the server will send the 'charset' parameter to indicate a preference of "UTF-8", increasing the probability that From d1987d4052f90f29eced9bfbe8aeae8f29d2a05e Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 29 Nov 2022 16:14:47 +0100 Subject: [PATCH 8/9] rename strict to strictCredentials --- README.md | 11 ++++++----- index.js | 9 ++++----- test/index.test.js | 4 ++-- types/index.d.ts | 2 +- types/index.test-d.ts | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bb3c639..9278f1b 100644 --- a/README.md +++ b/README.md @@ -129,12 +129,13 @@ be used. If utf8 is set to true the server will send the 'charset' parameter to indicate a preference of "UTF-8", increasing the probability that clients will switch to that encoding. -### `strict` (optional, default: true) +### `strictCredentials` (optional, default: true) -If strict is set to false the authorization header can contain additional -whitespaces at the beginning and the end of the authorization header. This is a -fallback option to ensure the same behaviour as @fastify/basic-auth version -<=5.x. +If strictCredentials is set to false the authorization header can contain +additional whitespaces at the beginning, in the midde and at the end of the +authorization header. +This is a fallback option to ensure the same behaviour as @fastify/basic-auth +version <=5.x. ### `validate` (required) diff --git a/index.js b/index.js index 3369719..b7b8bab 100644 --- a/index.js +++ b/index.js @@ -31,7 +31,7 @@ const authScheme = '(?:[Bb][Aa][Ss][Ii][Cc])' * * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3 */ -const BWS = '[ \t]*' +const BWS = '[ \t]' /** * The token68 syntax allows the 66 unreserved URI characters * ([RFC3986]), plus a few others, so that it can hold a base64, @@ -47,7 +47,7 @@ const token68 = '([A-Za-z0-9._~+/-]+=*)' */ const credentialsStrictRE = new RegExp(`^${authScheme} ${token68}$`) -const credentialsLaxRE = new RegExp(`^${BWS}${authScheme} +${token68}${BWS}$`) +const credentialsLaxRE = new RegExp(`^${BWS}*${authScheme}${BWS}+${token68}${BWS}*$`) /** * @see https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 @@ -70,13 +70,13 @@ async function fastifyBasicAuth (fastify, opts) { throw new Error('Basic Auth: Missing validate function') } - const strict = opts.strict ?? true + const strictCredentials = opts.strictCredentials ?? true const useUtf8 = opts.utf8 ?? true const charset = useUtf8 ? 'utf-8' : 'ascii' const authenticateHeader = getAuthenticateHeader(opts.authenticate, useUtf8) const header = (opts.header && opts.header.toLowerCase()) || 'authorization' - const credentialsRE = strict + const credentialsRE = strictCredentials ? credentialsStrictRE : credentialsLaxRE @@ -160,7 +160,6 @@ function getAuthenticateHeader (authenticate, useUtf8) { ? () => `Basic realm="${realm}", charset="UTF-8"` : () => `Basic realm="${realm}"` case 'function': - return useUtf8 ? (req) => `Basic realm="${realm(req)}", charset="UTF-8"` : (req) => `Basic realm="${realm(req)}"` diff --git a/test/index.test.js b/test/index.test.js index 215b0f3..be4966e 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -298,11 +298,11 @@ test('Basic - Invalid Header value /3', t => { }) }) -test('Basic - strict: false', t => { +test('Basic - strictCredentials: false', t => { t.plan(2) const fastify = Fastify() - fastify.register(basicAuth, { validate, strict: false }) + fastify.register(basicAuth, { validate, strictCredentials: false }) function validate (username, password, req, res, done) { if (username === 'user' && password === 'pwd') { diff --git a/types/index.d.ts b/types/index.d.ts index 387f3c8..8cc9b35 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -30,7 +30,7 @@ declare namespace fastifyBasicAuth { ): void | Promise; authenticate?: boolean | { realm: string | ((req: FastifyRequest) => string) }; header?: string; - strict?: boolean; + strictCredentials?: boolean; utf8?: boolean; } diff --git a/types/index.test-d.ts b/types/index.test-d.ts index a6c4a74..abae2e5 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -69,7 +69,7 @@ app.register(fastifyBasicAuth, { app.register(fastifyBasicAuth, { validate: () => {}, - strict: true + strictCredentials: true }) app.register(fastifyBasicAuth, { From c91af1aa2ad425118aa68c46f75b27952f7cda7b Mon Sep 17 00:00:00 2001 From: Uzlopak Date: Tue, 29 Nov 2022 19:39:37 +0100 Subject: [PATCH 9/9] Update README.md Co-authored-by: Manuel Spigolon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9278f1b..77a3c1b 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ clients will switch to that encoding. If strictCredentials is set to false the authorization header can contain additional whitespaces at the beginning, in the midde and at the end of the authorization header. -This is a fallback option to ensure the same behaviour as @fastify/basic-auth +This is a fallback option to ensure the same behaviour as `@fastify/basic-auth` version <=5.x. ### `validate` (required)