Skip to content

Commit 4efb670

Browse files
committed
Improve tokens a slight bit
1 parent 2b95dfb commit 4efb670

File tree

3 files changed

+36
-83
lines changed

3 files changed

+36
-83
lines changed

src/token.ts

Lines changed: 34 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
import { TokenSearchRules, TokenOptions } from "./types";
2-
import { MeiliSearchError } from "./errors";
3-
import { validateUuid4 } from "./utils";
1+
import type { TokenSearchRules, TokenOptions } from "./types";
42

5-
function encode64(data: unknown): string {
6-
return btoa(JSON.stringify(data));
3+
const UUID_V4_REGEXP = /^[0-9a-f]{8}\b(?:-[0-9a-f]{4}\b){3}-[0-9a-f]{12}$/i;
4+
function isValidUUIDv4(uuid: string): boolean {
5+
return UUID_V4_REGEXP.test(uuid);
76
}
87

8+
function encodeToBase64(data: unknown): string {
9+
// TODO: instead of btoa use Uint8Array.prototype.toBase64() when it becomes available in supported runtime versions
10+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64
11+
return btoa(typeof data === "string" ? data : JSON.stringify(data));
12+
}
13+
14+
// missing crypto global for Node.js 18 https://nodejs.org/api/globals.html#crypto_1
15+
// TODO: Improve error handling?
16+
const compatCrypto =
17+
typeof crypto === "undefined"
18+
? (await import("node:crypto")).webcrypto
19+
: crypto;
20+
21+
const textEncoder = new TextEncoder();
22+
923
/**
1024
* Create the header of the token.
1125
*
@@ -19,29 +33,21 @@ async function sign(
1933
encodedHeader: string,
2034
encodedPayload: string,
2135
): Promise<string> {
22-
// missing crypto global for Node.js 18
23-
const localCrypto =
24-
typeof crypto === "undefined"
25-
? // @ts-expect-error: Need to add @types/node for this and remove dom lib
26-
(await import("node:crypto")).webcrypto
27-
: crypto;
28-
29-
const textEncoder = new TextEncoder();
30-
31-
const cryptoKey = await localCrypto.subtle.importKey(
36+
const cryptoKey = await compatCrypto.subtle.importKey(
3237
"raw",
3338
textEncoder.encode(apiKey),
3439
{ name: "HMAC", hash: "SHA-256" },
3540
false,
3641
["sign"],
3742
);
3843

39-
const signature = await localCrypto.subtle.sign(
44+
const signature = await compatCrypto.subtle.sign(
4045
"HMAC",
4146
cryptoKey,
4247
textEncoder.encode(`${encodedHeader}.${encodedPayload}`),
4348
);
4449

50+
// TODO: Same problem as in `encodeToBase64` above
4551
const digest = btoa(String.fromCharCode(...new Uint8Array(signature)))
4652
.replace(/\+/g, "-")
4753
.replace(/\//g, "_")
@@ -55,63 +61,13 @@ async function sign(
5561
*
5662
* @returns The header encoded in base64.
5763
*/
58-
function createHeader() {
64+
function createHeader(): string {
5965
const header = {
6066
alg: "HS256",
6167
typ: "JWT",
6268
};
6369

64-
return encode64(header).replace(/=/g, "");
65-
}
66-
67-
/**
68-
* Validate the parameter used for the payload of the token.
69-
*
70-
* @param searchRules - Search rules that are applied to every search.
71-
* @param apiKey - Api key used as issuer of the token.
72-
* @param uid - The uid of the api key used as issuer of the token.
73-
* @param expiresAt - Date at which the token expires.
74-
*/
75-
function validateTokenParameters({
76-
searchRules,
77-
apiKeyUid,
78-
expiresAt,
79-
}: {
80-
searchRules: TokenSearchRules;
81-
apiKeyUid: string;
82-
expiresAt?: Date;
83-
}) {
84-
if (expiresAt) {
85-
if (!(expiresAt instanceof Date)) {
86-
throw new MeiliSearchError(
87-
`Meilisearch: The expiredAt field must be an instance of Date.`,
88-
);
89-
} else if (expiresAt.getTime() < Date.now()) {
90-
throw new MeiliSearchError(
91-
`Meilisearch: The expiresAt field must be a date in the future.`,
92-
);
93-
}
94-
}
95-
96-
if (searchRules) {
97-
if (!(typeof searchRules === "object" || Array.isArray(searchRules))) {
98-
throw new MeiliSearchError(
99-
`Meilisearch: The search rules added in the token generation must be of type array or object.`,
100-
);
101-
}
102-
}
103-
104-
if (!apiKeyUid || typeof apiKeyUid !== "string") {
105-
throw new MeiliSearchError(
106-
`Meilisearch: The uid of the api key used for the token generation must exist, be of type string and comply to the uuid4 format.`,
107-
);
108-
}
109-
110-
if (!validateUuid4(apiKeyUid)) {
111-
throw new MeiliSearchError(
112-
`Meilisearch: The uid of your key is not a valid uuid4. To find out the uid of your key use getKey().`,
113-
);
114-
}
70+
return encodeToBase64(header).replace(/=/g, "");
11571
}
11672

11773
/**
@@ -137,7 +93,7 @@ function createPayload({
13793
exp: expiresAt ? Math.floor(expiresAt.getTime() / 1000) : undefined,
13894
};
13995

140-
return encode64(payload).replace(/=/g, "");
96+
return encodeToBase64(payload).replace(/=/g, "");
14197
}
14298

14399
/**
@@ -153,7 +109,15 @@ export async function generateTenantToken(
153109
searchRules: TokenSearchRules,
154110
{ apiKey, expiresAt }: TokenOptions,
155111
): Promise<string> {
156-
validateTokenParameters({ apiKeyUid, expiresAt, searchRules });
112+
if (expiresAt !== undefined && expiresAt.getTime() < Date.now()) {
113+
throw new Error("the `expiresAt` field must be a date in the future");
114+
}
115+
116+
if (!isValidUUIDv4(apiKeyUid)) {
117+
throw new Error(
118+
"the uid of your key is not a valid UUIDv4; to find out the uid of your key use `getKey()`",
119+
);
120+
}
157121

158122
const encodedHeader = createHeader();
159123
const encodedPayload = createPayload({

src/utils.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,9 @@ function addTrailingSlash(url: string): string {
2828
return url;
2929
}
3030

31-
function validateUuid4(uuid: string): boolean {
32-
const regexExp =
33-
/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi;
34-
return regexExp.test(uuid);
35-
}
36-
3731
export {
3832
sleep,
3933
removeUndefinedFromObject,
4034
addProtocolIfNotPresent,
4135
addTrailingSlash,
42-
validateUuid4,
4336
};

tests/token.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,7 @@ describe.each([{ permission: "Admin" }])(
178178

179179
await expect(
180180
generateTenantToken(uid, ["*"], { apiKey, expiresAt: date }),
181-
).rejects.toThrow(
182-
`Meilisearch: The expiresAt field must be a date in the future.`,
183-
);
181+
).rejects.toThrow("the `expiresAt` field must be a date in the future");
184182
});
185183

186184
test(`${permission} key: Search in tenant token with specific index set to null`, async () => {
@@ -257,9 +255,7 @@ describe.each([{ permission: "Admin" }])(
257255

258256
await expect(
259257
generateTenantToken(uid, {}, { apiKey, expiresAt: date }),
260-
).rejects.toThrow(
261-
`Meilisearch: The expiresAt field must be a date in the future.`,
262-
);
258+
).rejects.toThrow("the `expiresAt` field must be a date in the future");
263259
});
264260
},
265261
);

0 commit comments

Comments
 (0)