Skip to content
Merged
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
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ $ npm install basic-auth

## API

<!-- eslint-disable no-unused-vars -->

```js
const { parse } = require('basic-auth');
```
Expand All @@ -31,13 +29,16 @@ const { parse } = require('basic-auth');
Parse a basic auth authorization header string. This will return an object
with `name` and `pass` properties, or `undefined` if the string is invalid.

### format(credentials)

Format a credentials object with `name` and `pass` properties as a basic
auth authorization header string.

## Example

Pass a Basic auth header to the `parse()` method. If parsing fails
`undefined` is returned, otherwise an object with `.name` and `.pass`.

<!-- eslint-disable no-unused-vars, no-undef -->

```js
const { parse } = require('basic-auth');
const user = parse(req.headers.authorization);
Expand All @@ -46,13 +47,21 @@ const user = parse(req.headers.authorization);

A header string from any other location can also be parsed for example a `Proxy-Authorization` header:

<!-- eslint-disable no-unused-vars, no-undef -->

```js
const { parse } = require('basic-auth');
const user = parse(req.getHeader('Proxy-Authorization'));
```

A credentials object can be formatted with `auth.format` as
basic auth header string.

```js
const { format } = require('basic-auth');
const credentials = { name: 'foo', pass: 'bar' };
const authHeader = format(credentials);
// => "Basic Zm9vOmJhcg=="
```

### With vanilla node.js http server

```js
Expand Down
27 changes: 27 additions & 0 deletions src/format.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { bench, describe } from 'vitest';
import { format } from './index';

describe('format', () => {
bench('format with simple credentials', () => {
const credentials = { name: 'user', pass: 'password' };
format(credentials);
});

bench('format with long credentials', () => {
const credentials = {
name: 'verylongusernameforbasicauth',
pass: 'verylongpasswordwithmanycharactersforbenchmark',
};
format(credentials);
});

bench('format with unicode credentials', () => {
const credentials = { name: 'jürgen', pass: 'pässwörd' };
format(credentials);
});

bench('format with special characters', () => {
const credentials = { name: 'user@domain', pass: 'p@ss!word#123' };
format(credentials);
});
});
118 changes: 118 additions & 0 deletions src/format.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, assert } from 'vitest';
import { format } from './index';

describe('format(credentials)', function () {
describe('arguments', function () {
describe('credentials', function () {
it('should be required', function () {
assert.throws(
() => (format as any)(),
/argument credentials is required/,
);
});

it('should accept credentials', function () {
const header = format({ name: 'foo', pass: 'bar' });
assert.strictEqual(header, 'Basic Zm9vOmJhcg==');
});

it('should reject null', function () {
assert.throws(
format.bind(null, null as any),
/argument credentials is required/,
);
});

it('should reject a number', function () {
assert.throws(
format.bind(null, 42 as any),
/argument credentials is required/,
);
});

it('should reject a string', function () {
assert.throws(
format.bind(null, '' as any),
/argument credentials is required/,
);
});

it('should reject an object without name', function () {
assert.throws(
format.bind(null, { pass: 'bar' } as any),
/argument credentials is required to have name and pass properties/,
);
});

it('should reject an object without pass', function () {
assert.throws(
format.bind(null, { name: 'foo' } as any),
/argument credentials is required to have name and pass properties/,
);
});

it('should reject an object with non-string name', function () {
assert.throws(
format.bind(null, { name: 42, pass: 'bar' } as any),
/argument credentials is required to have name and pass properties/,
);
});

it('should reject an object with non-string pass', function () {
assert.throws(
format.bind(null, { name: 'foo', pass: 42 } as any),
/argument credentials is required to have name and pass properties/,
);
});

it('should reject userid containing colon', function () {
assert.throws(
format.bind(null, { name: 'foo:bar', pass: 'baz' }),
/must not contain a colon or control characters/,
);
});

it('should reject control chars in userid', function () {
assert.throws(
format.bind(null, { name: 'foo\u0000bar', pass: 'baz' }),
/must not contain a colon or control characters/,
);
});

it('should reject control chars in password', function () {
assert.throws(
format.bind(null, { name: 'foo', pass: 'bar\u007f' }),
/must not contain control characters/,
);
});
});
});

describe('with valid credentials', function () {
it('should return header', function () {
const header = format({ name: 'foo', pass: 'bar' });
assert.strictEqual(header, 'Basic Zm9vOmJhcg==');
});
});

describe('with empty password', function () {
it('should throw', function () {
const header = format({ name: 'foo', pass: '' });
assert.strictEqual(header, 'Basic Zm9vOg==');
});
});

describe('with empty userid', function () {
it('should throw', function () {
const header = format({ name: '', pass: 'pass' });
assert.strictEqual(header, 'Basic OnBhc3M=');
});
});

describe('with empty userid and pass', function () {
it('should throw', function () {
const header = format({ name: '', pass: '' });
assert.strictEqual(header, 'Basic Og==');
});
});
});
58 changes: 57 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,49 @@ export function parse(string: string): Credentials | undefined {
};
}

/**
* Format Basic Authorization Header
*
* @param {Credentials} credentials
* @return {string}
* @public
*/
export function format(credentials: Credentials): string {
if (!credentials) {
throw new TypeError('argument credentials is required');
}

if (typeof credentials !== 'object') {
throw new TypeError('argument credentials is required to be an object');
}

if (
typeof credentials.name !== 'string' ||
typeof credentials.pass !== 'string'
Comment thread
Phillip9587 marked this conversation as resolved.
) {
throw new TypeError(
'argument credentials is required to have name and pass properties',
);
}

if (
credentials.name.includes(':') || // RFC 7617 disallows colon in username
CONTROL_CHARS_REGEXP.test(credentials.name)
) {
throw new TypeError(
'argument credentials.name must not contain a colon or control characters',
);
}

if (CONTROL_CHARS_REGEXP.test(credentials.pass)) {
throw new TypeError(
'argument credentials.pass must not contain control characters',
);
}

return 'Basic ' + encodeBase64(credentials.name + ':' + credentials.pass);
}

/**
* RegExp for basic auth credentials
*
Expand All @@ -57,10 +100,23 @@ const CREDENTIALS_REGEXP =
/^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/;

/**
* Decode base64 string.
* RegExp for RFC 5234 CTL characters (US-ASCII 0-31 and 127).
* @private
*/
const CONTROL_CHARS_REGEXP = /[\x00-\x1F\x7F]/;

/**
* Decode base64 string.
* @private
*/
function decodeBase64(str: string): string {
return Buffer.from(str, 'base64').toString();
}

/**
* Encode string to base64.
* @private
*/
function encodeBase64(str: string): string {
return Buffer.from(str, 'utf-8').toString('base64');
}
Loading