Skip to content

Commit 076395a

Browse files
committed
feat(stringify): add serializer for authrization
1 parent 0a51170 commit 076395a

File tree

5 files changed

+244
-1
lines changed

5 files changed

+244
-1
lines changed

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,72 @@ assertThrows(() =>
7878
);
7979
```
8080

81+
## Serialization
82+
83+
Serialize [Authorization](#authorization) into string.
84+
85+
```ts
86+
import { stringifyAuthorization } from "https://deno.land/x/authorization_parser@$VERSION/stringify.ts";
87+
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
88+
89+
assertEquals(
90+
stringifyAuthorization({ authScheme: "Basic", params: "token68==" }),
91+
"Basic token68",
92+
);
93+
assertEquals(
94+
stringifyAuthorization({
95+
authScheme: "Bearer",
96+
params: { realm: `"Secure area"`, error: `"invalid_token"` },
97+
}),
98+
`Bearer realm="Secure area", error="invalid_token"`,
99+
);
100+
```
101+
102+
### Error
103+
104+
Throws an error in the following cases:
105+
106+
- `authScheme` is invalid
107+
[auth-scheme](https://www.rfc-editor.org/rfc/rfc9110.html#section-11.1-2)
108+
- `params` is invalid
109+
[token68](https://www.rfc-editor.org/rfc/rfc9110.html#section-11.2-2)
110+
- `params` key is invalid
111+
[token](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.2-2)
112+
- `params` value is invalid
113+
[token](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.2-2) or
114+
[quoted-string](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.4-2)
115+
- There is a duplication in `params` keys(case-insensitive)
116+
117+
```ts
118+
import { stringifyAuthorization } from "https://deno.land/x/authorization_parser@$VERSION/stringify.ts";
119+
import { assertThrows } from "https://deno.land/std/testing/asserts.ts";
120+
121+
assertThrows(() =>
122+
stringifyAuthorization({ authScheme: "<invalid:auth-scheme>" })
123+
);
124+
assertThrows(() =>
125+
stringifyAuthorization({ authScheme: "<valid>", params: "<invalid:token68>" })
126+
);
127+
assertThrows(() =>
128+
stringifyAuthorization({
129+
authScheme: "<valid>",
130+
params: { "<invalid:token>": "<valid>" },
131+
})
132+
);
133+
assertThrows(() =>
134+
stringifyAuthorization({
135+
authScheme: "<valid>",
136+
params: { "<valid>": "<invalid:token|quoted-string>" },
137+
})
138+
);
139+
assertThrows(() =>
140+
stringifyAuthorization({
141+
authScheme: "<valid>",
142+
params: { "<duplicate>": "<valid>", "<DUPLICATE>": "<valid>" },
143+
})
144+
);
145+
```
146+
81147
## Authorization
82148

83149
`Authorization` is following structure:

deps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
export { toLowerCase } from "https://deno.land/x/[email protected]/to_lower_case.ts";
55
export { trim } from "https://deno.land/x/[email protected]/trim.ts";
66
export { head } from "https://deno.land/x/[email protected]/head.ts";
7+
export { isString } from "https://deno.land/x/[email protected]/is_string.ts";
8+
export { isNullable } from "https://deno.land/x/[email protected]/is_nullable.ts";

mod.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
// This module is browser compatible.
33

44
export { parseAuthorization } from "./parse.ts";
5-
export type { Authorization, AuthParam, Token68 } from "./types.ts";
5+
export { type AuthorizationLike, stringifyAuthorization } from "./stringify.ts";
6+
export type { Authorization, AuthParams, Token68 } from "./types.ts";

stringify.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
2+
// This module is browser compatible.
3+
4+
import { isNullable, isString, toLowerCase } from "./deps.ts";
5+
import { duplicate } from "./utils.ts";
6+
import { Msg } from "./constants.ts";
7+
import type { Authorization, AuthParams } from "./types.ts";
8+
9+
export interface AuthorizationLike
10+
extends
11+
Pick<Authorization, "authScheme">,
12+
Partial<Pick<Authorization, "params">> {
13+
}
14+
15+
/** Serialize {@link AuthorizationLike} into string.
16+
*
17+
* @example
18+
* ```ts
19+
* import { stringifyAuthorization } from "https://deno.land/x/authorization_parser@$VERSION/stringify.ts";
20+
* import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
21+
*
22+
* assertEquals(
23+
* stringifyAuthorization({ authScheme: "Basic", params: "token68==" }),
24+
* "Basic token68",
25+
* );
26+
* assertEquals(
27+
* stringifyAuthorization({
28+
* authScheme: "Bearer",
29+
* params: { realm: `"Secure area"`, error: `"invalid_token"` },
30+
* }),
31+
* `Bearer realm="Secure area", error="invalid_token"`,
32+
* );
33+
* ```
34+
*
35+
* @throws {Error} If the input is invalid {@link AuthorizationLike}.
36+
*/
37+
export function stringifyAuthorization(input: AuthorizationLike): string {
38+
assertToken(input.authScheme, `authScheme ${Msg.InvalidToken}`);
39+
40+
if (isNullable(input.params)) return input.authScheme;
41+
42+
const data = isString(input.params)
43+
? (assertToken68(input.params, `token ${Msg.InvalidToken68}`), input.params)
44+
: stringifyAuthParams(input.params);
45+
46+
return [input.authScheme, data].filter(Boolean).join(" ");
47+
}
48+
49+
const reToken = /^[\w!#$%&'*+.^`|~-]+$/;
50+
51+
export function assertToken(
52+
input: string,
53+
msg?: string,
54+
constructor: ErrorConstructor = Error,
55+
): asserts input {
56+
if (!isToken(input)) throw new constructor(msg);
57+
}
58+
59+
export function isToken(input: string): boolean {
60+
return reToken.test(input);
61+
}
62+
63+
const reToken68 = /^[A-Za-z\d+./_~-]+=*$/;
64+
65+
export function isToken68(input: string): boolean {
66+
return reToken68.test(input);
67+
}
68+
69+
export function assertToken68(
70+
input: string,
71+
msg?: string,
72+
constructor: ErrorConstructor = Error,
73+
): asserts input {
74+
if (!isToken68(input)) throw new constructor(msg);
75+
}
76+
77+
const reQuotedString =
78+
/^"(?:\t| |!|[ \x23-\x5B\x5D-\x7E]|[\x80-\xFF]|\\(?:\t| |[\x21-\x7E])[\x80-\xFF])*"$/;
79+
80+
export function isQuotedString(input: string): boolean {
81+
return reQuotedString.test(input);
82+
}
83+
84+
function assertAuthParam(input: AuthParams): asserts input {
85+
for (const key in input) {
86+
assertToken(key, `token key ${Msg.InvalidToken}`);
87+
88+
const value = input[key]!;
89+
90+
if (isToken(value) || isQuotedString(value)) continue;
91+
92+
throw Error(`token value should be <token> or <quoted-string> format`);
93+
}
94+
95+
const duplicates = duplicate(Object.keys(input).map(toLowerCase));
96+
97+
if (duplicates.length) throw Error(Msg.DuplicatedKeys);
98+
}
99+
100+
export function stringifyAuthParams(input: AuthParams): string {
101+
assertAuthParam(input);
102+
103+
return Object
104+
.entries(input)
105+
.map(joinEntry)
106+
.join(", ");
107+
}
108+
109+
function joinEntry(
110+
entry: readonly [string, string],
111+
): string {
112+
return entry[0] + "=" + entry[1];
113+
}

stringify_test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { AuthorizationLike, stringifyAuthorization } from "./stringify.ts";
2+
import {
3+
assertEquals,
4+
assertThrows,
5+
Authorization,
6+
describe,
7+
it,
8+
} from "./_dev_deps.ts";
9+
10+
import authorization from "./authorization.json" assert { type: "json" };
11+
12+
describe("stringifyAuthorization", () => {
13+
it("should return string if the input is valid", () => {
14+
const table: [AuthorizationLike, string][] = [
15+
[{ authScheme: "a" }, "a"],
16+
[{ authScheme: "basic" }, "basic"],
17+
[{ authScheme: "basic", params: "abcde" }, "basic abcde"],
18+
[{ authScheme: "basic", params: "abcde==" }, "basic abcde=="],
19+
[{ authScheme: "basic", params: {} }, "basic"],
20+
[{ authScheme: "basic", params: { a: "a" } }, "basic a=a"],
21+
[
22+
{ authScheme: "basic", params: { a: "a", b: `"test"` } },
23+
`basic a=a, b="test"`,
24+
],
25+
];
26+
27+
table.forEach(([input, expected]) => {
28+
assertEquals(stringifyAuthorization(input), expected);
29+
});
30+
});
31+
32+
it("should throw error if the input is invalid", () => {
33+
const table: AuthorizationLike[] = [
34+
{ authScheme: "" },
35+
{ authScheme: "<invalid>" },
36+
{ authScheme: "a", params: "" },
37+
{ authScheme: "a", params: "<invalid>" },
38+
{ authScheme: "a", params: { "": "" } },
39+
{ authScheme: "a", params: { "<invalid>": "" } },
40+
{ authScheme: "a", params: { a: "" } },
41+
{ authScheme: "a", params: { a: "<invalid>" } },
42+
{ authScheme: "a", params: { a: `"\x00"` } },
43+
{ authScheme: "a", params: { a: "test", A: "test" } },
44+
];
45+
46+
table.forEach((input) => {
47+
assertThrows(() => stringifyAuthorization(input));
48+
});
49+
});
50+
51+
authorization.forEach((suite) => {
52+
it(suite.name, () => {
53+
if (!suite.must_fail) {
54+
assertEquals(
55+
stringifyAuthorization(suite.expected as Authorization),
56+
suite.header,
57+
);
58+
}
59+
});
60+
});
61+
});

0 commit comments

Comments
 (0)