Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 26 additions & 56 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
57 changes: 49 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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){
Expand All @@ -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.
75 changes: 43 additions & 32 deletions index.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)`.
*
Expand All @@ -45,48 +44,62 @@ 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;
Comment on lines +56 to 72
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lirantal any chance you can review this? 🙏

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using timingSafeEqual is indeed the best practice to follow for a timing safe comparison to avoid TOCTOU and side channel attacks that leak information about the username and password being used.

However, secure timing comparison is useful when you are comparing hashes which always produce the same length, regardless of the input string length. Here, you are padding it, which could also end up with false positives. Buffer.alloc() will pad with zero length strings (`\x00') and if the input provided to you includes that byte sequence you'd have a false positive. Here's an example based on the above:

const res = callbackComparison("user", "pass", "user\x00", "pass");
console.log("Zero Padding Result:", res);

You'd expect res to be false but it is actually true.
You could fix it by filling with another byte like ... Buffer.alloc(maxLength - buf.length, 0x01)])); but my recommendation would be to completely drop the padding altogether and keep it simple and rely on the timingSafeEqual to either throw if the length is different (which is ok and not a timing leak) or perform secure comparison if the length is the same.

}
};
}

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) {
unauthorized(res, 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;
Expand All @@ -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`
Expand All @@ -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;
};
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -23,10 +23,10 @@
"should": "*",
"supertest": "*"
},
"engines": {
"node": ">= 18.0.0"
},
"scripts": {
"test": "make test"
},
"dependencies": {
"tsscmp": "^1.0.6"
}
}
Loading