Skip to content

Commit 1ba386f

Browse files
authored
Add format method (#65)
1 parent 08b56ca commit 1ba386f

4 files changed

Lines changed: 217 additions & 7 deletions

File tree

README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ $ npm install basic-auth
2020

2121
## API
2222

23-
<!-- eslint-disable no-unused-vars -->
24-
2523
```js
2624
const { parse } = require('basic-auth');
2725
```
@@ -31,13 +29,16 @@ const { parse } = require('basic-auth');
3129
Parse a basic auth authorization header string. This will return an object
3230
with `name` and `pass` properties, or `undefined` if the string is invalid.
3331

32+
### format(credentials)
33+
34+
Format a credentials object with `name` and `pass` properties as a basic
35+
auth authorization header string.
36+
3437
## Example
3538

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

39-
<!-- eslint-disable no-unused-vars, no-undef -->
40-
4142
```js
4243
const { parse } = require('basic-auth');
4344
const user = parse(req.headers.authorization);
@@ -46,13 +47,21 @@ const user = parse(req.headers.authorization);
4647

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

49-
<!-- eslint-disable no-unused-vars, no-undef -->
50-
5150
```js
5251
const { parse } = require('basic-auth');
5352
const user = parse(req.getHeader('Proxy-Authorization'));
5453
```
5554

55+
A credentials object can be formatted with `auth.format` as
56+
basic auth header string.
57+
58+
```js
59+
const { format } = require('basic-auth');
60+
const credentials = { name: 'foo', pass: 'bar' };
61+
const authHeader = format(credentials);
62+
// => "Basic Zm9vOmJhcg=="
63+
```
64+
5665
### With vanilla node.js http server
5766

5867
```js

src/format.bench.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { bench, describe } from 'vitest';
2+
import { format } from './index';
3+
4+
describe('format', () => {
5+
bench('format with simple credentials', () => {
6+
const credentials = { name: 'user', pass: 'password' };
7+
format(credentials);
8+
});
9+
10+
bench('format with long credentials', () => {
11+
const credentials = {
12+
name: 'verylongusernameforbasicauth',
13+
pass: 'verylongpasswordwithmanycharactersforbenchmark',
14+
};
15+
format(credentials);
16+
});
17+
18+
bench('format with unicode credentials', () => {
19+
const credentials = { name: 'jürgen', pass: 'pässwörd' };
20+
format(credentials);
21+
});
22+
23+
bench('format with special characters', () => {
24+
const credentials = { name: 'user@domain', pass: 'p@ss!word#123' };
25+
format(credentials);
26+
});
27+
});

src/format.spec.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { describe, it, assert } from 'vitest';
2+
import { format } from './index';
3+
4+
describe('format(credentials)', function () {
5+
describe('arguments', function () {
6+
describe('credentials', function () {
7+
it('should be required', function () {
8+
assert.throws(
9+
() => (format as any)(),
10+
/argument credentials is required/,
11+
);
12+
});
13+
14+
it('should accept credentials', function () {
15+
const header = format({ name: 'foo', pass: 'bar' });
16+
assert.strictEqual(header, 'Basic Zm9vOmJhcg==');
17+
});
18+
19+
it('should reject null', function () {
20+
assert.throws(
21+
format.bind(null, null as any),
22+
/argument credentials is required/,
23+
);
24+
});
25+
26+
it('should reject a number', function () {
27+
assert.throws(
28+
format.bind(null, 42 as any),
29+
/argument credentials is required/,
30+
);
31+
});
32+
33+
it('should reject a string', function () {
34+
assert.throws(
35+
format.bind(null, '' as any),
36+
/argument credentials is required/,
37+
);
38+
});
39+
40+
it('should reject an object without name', function () {
41+
assert.throws(
42+
format.bind(null, { pass: 'bar' } as any),
43+
/argument credentials is required to have name and pass properties/,
44+
);
45+
});
46+
47+
it('should reject an object without pass', function () {
48+
assert.throws(
49+
format.bind(null, { name: 'foo' } as any),
50+
/argument credentials is required to have name and pass properties/,
51+
);
52+
});
53+
54+
it('should reject an object with non-string name', function () {
55+
assert.throws(
56+
format.bind(null, { name: 42, pass: 'bar' } as any),
57+
/argument credentials is required to have name and pass properties/,
58+
);
59+
});
60+
61+
it('should reject an object with non-string pass', function () {
62+
assert.throws(
63+
format.bind(null, { name: 'foo', pass: 42 } as any),
64+
/argument credentials is required to have name and pass properties/,
65+
);
66+
});
67+
68+
it('should reject userid containing colon', function () {
69+
assert.throws(
70+
format.bind(null, { name: 'foo:bar', pass: 'baz' }),
71+
/must not contain a colon or control characters/,
72+
);
73+
});
74+
75+
it('should reject control chars in userid', function () {
76+
assert.throws(
77+
format.bind(null, { name: 'foo\u0000bar', pass: 'baz' }),
78+
/must not contain a colon or control characters/,
79+
);
80+
});
81+
82+
it('should reject control chars in password', function () {
83+
assert.throws(
84+
format.bind(null, { name: 'foo', pass: 'bar\u007f' }),
85+
/must not contain control characters/,
86+
);
87+
});
88+
});
89+
});
90+
91+
describe('with valid credentials', function () {
92+
it('should return header', function () {
93+
const header = format({ name: 'foo', pass: 'bar' });
94+
assert.strictEqual(header, 'Basic Zm9vOmJhcg==');
95+
});
96+
});
97+
98+
describe('with empty password', function () {
99+
it('should throw', function () {
100+
const header = format({ name: 'foo', pass: '' });
101+
assert.strictEqual(header, 'Basic Zm9vOg==');
102+
});
103+
});
104+
105+
describe('with empty userid', function () {
106+
it('should throw', function () {
107+
const header = format({ name: '', pass: 'pass' });
108+
assert.strictEqual(header, 'Basic OnBhc3M=');
109+
});
110+
});
111+
112+
describe('with empty userid and pass', function () {
113+
it('should throw', function () {
114+
const header = format({ name: '', pass: '' });
115+
assert.strictEqual(header, 'Basic Og==');
116+
});
117+
});
118+
});

src/index.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,49 @@ export function parse(string: string): Credentials | undefined {
4444
};
4545
}
4646

47+
/**
48+
* Format Basic Authorization Header
49+
*
50+
* @param {Credentials} credentials
51+
* @return {string}
52+
* @public
53+
*/
54+
export function format(credentials: Credentials): string {
55+
if (!credentials) {
56+
throw new TypeError('argument credentials is required');
57+
}
58+
59+
if (typeof credentials !== 'object') {
60+
throw new TypeError('argument credentials is required to be an object');
61+
}
62+
63+
if (
64+
typeof credentials.name !== 'string' ||
65+
typeof credentials.pass !== 'string'
66+
) {
67+
throw new TypeError(
68+
'argument credentials is required to have name and pass properties',
69+
);
70+
}
71+
72+
if (
73+
credentials.name.includes(':') || // RFC 7617 disallows colon in username
74+
CONTROL_CHARS_REGEXP.test(credentials.name)
75+
) {
76+
throw new TypeError(
77+
'argument credentials.name must not contain a colon or control characters',
78+
);
79+
}
80+
81+
if (CONTROL_CHARS_REGEXP.test(credentials.pass)) {
82+
throw new TypeError(
83+
'argument credentials.pass must not contain control characters',
84+
);
85+
}
86+
87+
return 'Basic ' + encodeBase64(credentials.name + ':' + credentials.pass);
88+
}
89+
4790
/**
4891
* RegExp for basic auth credentials
4992
*
@@ -57,10 +100,23 @@ const CREDENTIALS_REGEXP =
57100
/^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/;
58101

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

108+
/**
109+
* Decode base64 string.
110+
* @private
111+
*/
64112
function decodeBase64(str: string): string {
65113
return Buffer.from(str, 'base64').toString();
66114
}
115+
116+
/**
117+
* Encode string to base64.
118+
* @private
119+
*/
120+
function encodeBase64(str: string): string {
121+
return Buffer.from(str, 'utf-8').toString('base64');
122+
}

0 commit comments

Comments
 (0)