Skip to content

Commit cf4f076

Browse files
committed
feat: add auth.format for formatting credentials
1 parent c0d7950 commit cf4f076

5 files changed

Lines changed: 232 additions & 1 deletion

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ otherwise an object with `name` and `pass` properties.
3737
Parse a basic auth authorization header string. This will return an object
3838
with `name` and `pass` properties, or `undefined` if the string is invalid.
3939

40+
### auth.format(credentials)
41+
42+
Format a credentials object with `name` and `pass` properties as a basic
43+
auth authorization header string.
44+
4045
## Example
4146

4247
Pass a Node.js request object to the module export. If parsing fails
@@ -60,6 +65,18 @@ var auth = require('basic-auth');
6065
var user = auth.parse(req.getHeader('Proxy-Authorization'));
6166
```
6267

68+
A credentials object can be formatted with `auth.format` as
69+
basic auth header string.
70+
71+
<!-- eslint-disable no-unused-vars, no-undef -->
72+
73+
```js
74+
var auth = require('basic-auth');
75+
var credentials = { name: 'foo', pass: 'bar' };
76+
var authHeader = auth.format(credentials);
77+
// => "Basic Zm9vOmJhcg=="
78+
```
79+
6380
### With vanilla node.js http server
6481

6582
```js

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"dist/"
1919
],
2020
"scripts": {
21+
"bench": "vitest bench",
2122
"build": "ts-scripts build",
2223
"format": "ts-scripts format",
2324
"lint": "ts-scripts lint",

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: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
assert.throws(
101+
format.bind(null, { name: 'foo', pass: '' }),
102+
/argument credentials.name and credentials.pass must not be empty/,
103+
);
104+
});
105+
});
106+
107+
describe('with empty userid', function () {
108+
it('should throw', function () {
109+
assert.throws(
110+
format.bind(null, { name: '', pass: 'pass' }),
111+
/argument credentials.name and credentials.pass must not be empty/,
112+
);
113+
});
114+
});
115+
116+
describe('with empty userid and pass', function () {
117+
it('should throw', function () {
118+
assert.throws(
119+
format.bind(null, { name: '', pass: '' }),
120+
/argument credentials.name and credentials.pass must not be empty/,
121+
);
122+
});
123+
});
124+
});

src/index.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,55 @@ export function parse(string: string): Credentials | undefined {
4747
return new CredentialsImpl(userPass[1], userPass[2]);
4848
}
4949

50+
/**
51+
* Format Basic Authorization Header
52+
*
53+
* @param {Credentials} credentials
54+
* @return {string}
55+
* @public
56+
*/
57+
export function format(credentials: Credentials): string {
58+
if (!credentials) {
59+
throw new TypeError('argument credentials is required');
60+
}
61+
62+
if (typeof credentials !== 'object') {
63+
throw new TypeError('argument credentials is required to be an object');
64+
}
65+
66+
if (
67+
typeof credentials.name !== 'string' ||
68+
typeof credentials.pass !== 'string'
69+
) {
70+
throw new TypeError(
71+
'argument credentials is required to have name and pass properties',
72+
);
73+
}
74+
75+
if (credentials.name.length === 0 || credentials.pass.length === 0) {
76+
throw new TypeError(
77+
'argument credentials.name and credentials.pass must not be empty',
78+
);
79+
}
80+
81+
if (
82+
credentials.name.includes(':') || // RFC 7617 disallows colon in username
83+
CONTROL_CHARS_REGEXP.test(credentials.name)
84+
) {
85+
throw new TypeError(
86+
'argument credentials.name must not contain a colon or control characters',
87+
);
88+
}
89+
90+
if (CONTROL_CHARS_REGEXP.test(credentials.pass)) {
91+
throw new TypeError(
92+
'argument credentials.pass must not contain control characters',
93+
);
94+
}
95+
96+
return 'Basic ' + encodeBase64(credentials.name + ':' + credentials.pass);
97+
}
98+
5099
/**
51100
* RegExp for basic auth credentials
52101
*
@@ -71,14 +120,27 @@ const CREDENTIALS_REGEXP =
71120
const USER_PASS_REGEXP = /^([^:]*):(.*)$/;
72121

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

128+
/**
129+
* Decode base64 string.
130+
* @private
131+
*/
78132
function decodeBase64(str: string): string {
79133
return Buffer.from(str, 'base64').toString();
80134
}
81135

136+
/**
137+
* Encode string to base64.
138+
* @private
139+
*/
140+
function encodeBase64(str: string): string {
141+
return Buffer.from(str, 'utf-8').toString('base64');
142+
}
143+
82144
class CredentialsImpl implements Credentials {
83145
name: string;
84146
pass: string;

0 commit comments

Comments
 (0)