diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa5fdb2..b619b78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,70 +1,40 @@ name: ci on: -- pull_request -- push + - pull_request + - push jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - name: - - Node.js 0.10 - - Node.js 0.12 - - io.js 1.x - - io.js 2.x - - io.js 3.x - - Node.js 4.x - - Node.js 6.x - - Node.js 8.x - - Node.js 10.x - - Node.js 11.x - - Node.js 12.x - - Node.js 13.x - - Node.js 14.x - - Node.js 15.x - - Node.js 16.x - - Node.js 17.x - - Node.js 18.x - - Node.js 19.x - - Node.js 20.x - - Node.js 21.x - - Node.js 22.x + node-version: + - 18.x + - 19.x + - 20.x + - 21.x + - 22.x steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - - name: Install Node.js ${{ matrix.node-version }} - shell: bash -eo pipefail -l {0} - run: | - nvm install --default ${{ matrix.node-version }} - if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then - nvm install --alias=npm 0.10 - nvm use ${{ matrix.node-version }} - if [[ "$(npm -v)" == 1.1.* ]]; then - nvm exec npm npm install -g npm@1.1 - ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" - else - sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" - fi - npm config set strict-ssl false - fi - dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} - - name: Install Node.js dependencies - run: npm install + - name: Install Node.js dependencies + run: npm install - - name: List environment - id: list_env - shell: bash - run: | - echo "node@$(node -v)" - echo "npm@$(npm -v)" - npm -s ls ||: - (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" + - name: List environment + id: list_env + run: | + echo "node@$(node -v)" + echo "npm@$(npm -v)" + npm -s ls ||: + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" - - name: Run tests - shell: bash - run: | - npm test + - name: Run tests + run: npm test diff --git a/README.md b/README.md index 54f5886..271e084 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,60 @@ Connect's Basic Auth middleware in its own module. You should consider to create your own middleware with [basic-auth](https://github.com/visionmedia/node-basic-auth). +It requires Node.js 18.x or higher. + +## Installation + +```bash +npm install basic-auth-connect +``` + ## API +Import the module + ```js -var basicAuth = require('basic-auth-connect'); +const basicAuth = require('basic-auth-connect'); ``` Simple username and password ```js -connect() -.use(basicAuth('username', 'password')); +const connect = require('connect'); + +const app = connect() + .use(basicAuth('username', 'password')) + .use((req, res) => { + res.end('Authenticated!'); + }); + +app.listen(3000, () => { + console.log('Server running on http://localhost:3000'); +}); ``` Callback verification ```js -connect() -.use(basicAuth(function(user, pass){ - return 'tj' == user && 'wahoo' == pass; -})) +const connect = require('connect'); + +const app = connect() + .use(basicAuth((user, pass) => { + return user === 'tj' && pass === 'wahoo'; + })) + .use((req, res) => { + res.end('Authenticated!'); + }); + +app.listen(3000, () => { + console.log('Server running on http://localhost:3000'); +}); ``` Async callback verification, accepting `fn(err, user)`. +Note: It is recommended to use `crypto.timingSafeEqual(a, b)` [(Doc)](https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b) to compare the user and password strings. + ```js connect() .use(basicAuth(function(user, pass, fn){ @@ -37,6 +67,17 @@ connect() Important: When using the callback method, it is recommended to use a time-safe comparison function like [crypto.timingSafeEqual](https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b) to prevent timing attacks. +## Running Tests + +To run the tests, use the following command: + +```bash +npm test +``` + +This will execute the tests defined in the [Makefile](./Makefile) using Mocha. + + ## License -[MIT](./LICENSE) +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/index.js b/index.js index cb86d5f..e8f292b 100644 --- a/index.js +++ b/index.js @@ -1,20 +1,17 @@ -var timingSafeCompare = require('tsscmp'); -var http = require('http'); +const http = require('http'); +const crypto = require('crypto'); /*! * Connect - basicAuth * Copyright(c) 2010 Sencha Inc. * Copyright(c) 2011 TJ Holowaychuk + * Copyright(c) 2024 Ulises Gascón * MIT Licensed */ /** * Basic Auth: - * - * Status: Deprecated. No bug reports or pull requests are welcomed - * for this middleware. However, this middleware will not be removed. - * Instead, you should use [basic-auth](https://github.com/visionmedia/node-basic-auth). - * + * * Enfore basic authentication by providing a `callback(user, pass)`, * which must return `true` in order to gain access. Alternatively an async * method is provided as well, invoking `callback(user, pass, callback)`. Populates @@ -29,8 +26,10 @@ var http = require('http'); * * connect() * .use(connect.basicAuth(function(user, pass){ - * return 'tj' == user && 'wahoo' == pass; + * return 'tj' === user && 'wahoo' === pass; * })) + * + * Note: it is recommended to use `crypto.timingSafeEqual(a, b)` https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b * * Async callback verification, accepting `fn(err, user)`. * @@ -45,25 +44,39 @@ var http = require('http'); */ module.exports = function basicAuth(callback, realm) { - var username, password; + let username, password; // user / pass strings - if ('string' == typeof callback) { + if (typeof callback === 'string') { username = callback; password = realm; - if ('string' != typeof password) throw new Error('password argument required'); + if (typeof password !== 'string') throw new Error('password argument required'); realm = arguments[2]; - callback = function(user, pass){ - const usernameValid = timingSafeCompare(user, username); - const passwordValid = timingSafeCompare(pass, password); + + callback = (user, pass) => { + const buffers = [ + Buffer.from(user), + Buffer.from(pass), + Buffer.from(username), + Buffer.from(password) + ]; + + // Determine the maximum length among all buffers + const maxLength = Math.max(...buffers.map(buf => buf.length)); + + // Pad each buffer to the maximum length + const paddedBuffers = buffers.map(buf => Buffer.concat([buf, Buffer.alloc(maxLength - buf.length)])); + + const usernameValid = crypto.timingSafeEqual(paddedBuffers[0], paddedBuffers[2]) + const passwordValid = crypto.timingSafeEqual(paddedBuffers[1], paddedBuffers[3]) return usernameValid && passwordValid; - } + }; } realm = realm || 'Authorization Required'; - return function(req, res, next) { - var authorization = req.headers.authorization; + return (req, res, next) => { + const authorization = req.headers.authorization; if (req.user) return next(); if (!authorization) { @@ -71,22 +84,22 @@ module.exports = function basicAuth(callback, realm) { return; } - var parts = authorization.split(' '); + const parts = authorization.split(' '); if (parts.length !== 2) return next(error(400)); - var scheme = parts[0] - , credentials = new Buffer(parts[1], 'base64').toString() - , index = credentials.indexOf(':'); + const scheme = parts[0]; + const credentials = Buffer.from(parts[1], 'base64').toString(); + const index = credentials.indexOf(':'); - if ('Basic' != scheme || index < 0) return next(error(400)); + if (scheme !== 'Basic' || index < 0) return next(error(400)); - var user = credentials.slice(0, index) - , pass = credentials.slice(index + 1); + const user = credentials.slice(0, index); + const pass = credentials.slice(index + 1); // async if (callback.length >= 3) { - callback(user, pass, function(err, user){ + callback(user, pass, (err, user) => { if (err || !user) { unauthorized(res, realm); return; @@ -113,12 +126,11 @@ module.exports = function basicAuth(callback, realm) { * @param {String} realm * @api private */ - function unauthorized(res, realm) { res.statusCode = 401; - res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"'); + res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`); res.end('Unauthorized'); -}; +} /** * Generate an `Error` from the given status `code` @@ -129,9 +141,8 @@ function unauthorized(res, realm) { * @return {Error} * @api private */ - -function error(code, msg){ - var err = new Error(msg || http.STATUS_CODES[code]); +function error(code, msg) { + const err = new Error(msg || http.STATUS_CODES[code]); err.status = code; return err; -}; \ No newline at end of file +} \ No newline at end of file diff --git a/package.json b/package.json index 406afa1..5771139 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "basic-auth-connect", "description": "Basic auth middleware for node and connect", - "version": "1.1.0", + "version": "2.0.0", "author": { "name": "Jonathan Ong", "email": "me@jongleberry.com", @@ -23,10 +23,10 @@ "should": "*", "supertest": "*" }, + "engines": { + "node": ">= 18.0.0" + }, "scripts": { "test": "make test" - }, - "dependencies": { - "tsscmp": "^1.0.6" } } diff --git a/test.js b/test.js index bc7ac8c..ab61cab 100644 --- a/test.js +++ b/test.js @@ -1,115 +1,106 @@ - -var request = require('supertest'); -var connect = require('connect'); -var basicAuth = require('./'); - -function test(app, signature) { - describe(signature, function(){ - describe('when missing Authorization', function(){ - it('should respond with 401 and WWW-Authenticate', function(done){ +const request = require('supertest'); +const connect = require('connect'); +const basicAuth = require('./'); + +const test = (app, signature) => { + describe(signature, () => { + describe('when missing Authorization', () => { + it('should respond with 401 and WWW-Authenticate', (done) => { request(app) - .get('/') - .end(function(err, res){ - res.statusCode.should.equal(401); - res.headers['www-authenticate'].should.equal('Basic realm="Authorization Required"'); - done(); - }); - }) - }) - - describe('when valid', function(){ - it('should next()', function(done){ + .get('/') + .end((err, res) => { + res.statusCode.should.equal(401); + res.headers['www-authenticate'].should.equal('Basic realm="Authorization Required"'); + done(); + }); + }); + }); + + describe('when valid', () => { + it('should next()', (done) => { request(app) - .get('/') - .set('Authorization', 'Basic dGo6dG9iaTpsZWFybmJvb3N0') - .end(function(err, res){ - res.statusCode.should.equal(200); - res.text.should.equal('secret!'); - done(); - }); - }) - }) - - describe('when invalid credentials', function(){ - it('should respond with 401', function(done){ + .get('/') + .set('Authorization', 'Basic dGo6dG9iaTpsZWFybmJvb3N0') + .end((err, res) => { + res.statusCode.should.equal(200); + res.text.should.equal('secret!'); + done(); + }); + }); + }); + + describe('when invalid credentials', () => { + it('should respond with 401', (done) => { request(app) - .get('/') - .set('Authorization', 'Basic dGo69iaQ==') - .end(function(err, res){ - res.statusCode.should.equal(401); - res.headers['www-authenticate'].should.equal('Basic realm="Authorization Required"'); - res.text.should.equal('Unauthorized'); - done(); - }); - }) - }) - - describe('when authorization header is not Basic', function(){ - it('should respond with 400', function(done){ + .get('/') + .set('Authorization', 'Basic dGo69iaQ==') + .end((err, res) => { + res.statusCode.should.equal(401); + res.headers['www-authenticate'].should.equal('Basic realm="Authorization Required"'); + res.text.should.equal('Unauthorized'); + done(); + }); + }); + }); + + describe('when authorization header is not Basic', () => { + it('should respond with 400', (done) => { request(app) - .get('/') - .set('Authorization', 'Digest dGo69iaQ==') - .end(function(err, res){ - res.statusCode.should.equal(400); - res.text.should.match(/Bad Request/); - done(); - }); - }) - }) - - describe('when authorization header is malformed - contains only one part', function(){ - it('should respond with 400', function(done){ + .get('/') + .set('Authorization', 'Digest dGo69iaQ==') + .end((err, res) => { + res.statusCode.should.equal(400); + res.text.should.match(/Bad Request/); + done(); + }); + }); + }); + + describe('when authorization header is malformed - contains only one part', () => { + it('should respond with 400', (done) => { request(app) - .get('/') - .set('Authorization', 'invalid') - .end(function(err, res){ - res.statusCode.should.equal(400); - res.text.should.match(/Bad Request/); - done(); - }); - }) - }) - }) -} - -var app = connect(); + .get('/') + .set('Authorization', 'invalid') + .end((err, res) => { + res.statusCode.should.equal(400); + res.text.should.match(/Bad Request/); + done(); + }); + }); + }); + }); +}; + +let app = connect(); app.use(basicAuth('tj', 'tobi:learnboost')); -app.use(function(req, res, next){ +app.use((req, res, next) => { req.user.should.equal('tj'); res.end('secret!'); }); test(app, 'basicAuth(user, pass)'); - - app = connect(); -app.use(basicAuth(function(user, pass){ - return 'tj' == user && 'tobi:learnboost' == pass; -})); +app.use(basicAuth((user, pass) => 'tj' === user && 'tobi:learnboost' === pass)); -app.use(function(req, res, next){ +app.use((req, res, next) => { req.user.should.equal('tj'); res.end('secret!'); }); test(app, 'basicAuth(callback)'); - - app = connect(); -app.use(basicAuth(function(user, pass, fn){ - var ok = 'tj' == user && 'tobi:learnboost' == pass; - fn(null, ok - ? { name: 'tj' } - : null); +app.use(basicAuth((user, pass, fn) => { + const ok = 'tj' === user && 'tobi:learnboost' === pass; + fn(null, ok ? { name: 'tj' } : null); })); -app.use(function(req, res, next){ +app.use((req, res, next) => { req.user.name.should.equal('tj'); res.end('secret!'); });