Skip to content

Commit 1bb6eb2

Browse files
committed
Improve, extend, fix types, test and code
1 parent cebab9a commit 1bb6eb2

File tree

3 files changed

+121
-80
lines changed

3 files changed

+121
-80
lines changed

src/token.ts

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
import type { TenantTokenGeneratorOptions } from "./types";
1+
import type { TenantTokenGeneratorOptions, TokenSearchRules } from "./types";
2+
3+
function getOptionsWithDefaults(options: TenantTokenGeneratorOptions) {
4+
const {
5+
searchRules = [],
6+
algorithm = "HS256",
7+
force = false,
8+
...restOfOptions
9+
} = options;
10+
return { searchRules, algorithm, force, ...restOfOptions };
11+
}
12+
13+
type TenantTokenGeneratorOptionsWithDefaults = ReturnType<
14+
typeof getOptionsWithDefaults
15+
>;
216

317
const UUID_V4_REGEXP = /^[0-9a-f]{8}\b(?:-[0-9a-f]{4}\b){3}-[0-9a-f]{12}$/i;
418
function isValidUUIDv4(uuid: string): boolean {
@@ -21,18 +35,21 @@ const textEncoder = new TextEncoder();
2135

2236
/** Create the signature of the token. */
2337
async function sign(
24-
apiKey: string,
38+
{ apiKey, algorithm }: TenantTokenGeneratorOptionsWithDefaults,
2539
encodedPayload: string,
2640
encodedHeader: string,
2741
): Promise<string> {
2842
const crypto = await cryptoPonyfill;
2943

3044
const cryptoKey = await crypto.subtle.importKey(
45+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#raw
3146
"raw",
3247
textEncoder.encode(apiKey),
33-
// TODO: Does alg depend on this too?
34-
{ name: "HMAC", hash: "SHA-256" },
48+
// https://developer.mozilla.org/en-US/docs/Web/API/HmacImportParams#instance_properties
49+
{ name: "HMAC", hash: `SHA-${algorithm.slice(2)}` },
50+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#extractable
3551
false,
52+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#keyusages
3653
["sign"],
3754
);
3855

@@ -53,32 +70,39 @@ async function sign(
5370

5471
/** Create the header of the token. */
5572
function getHeader({
56-
algorithm: alg = "HS256",
57-
}: TenantTokenGeneratorOptions): string {
73+
algorithm: alg,
74+
}: TenantTokenGeneratorOptionsWithDefaults): string {
5875
const header = { alg, typ: "JWT" };
5976
return encodeToBase64(header).replace(/=/g, "");
6077
}
6178

79+
/**
80+
* @see {@link https://www.meilisearch.com/docs/learn/security/tenant_token_reference | Tenant token payload reference}
81+
* @see {@link https://github.com/meilisearch/meilisearch/blob/b21d7aedf9096539041362d438e973a18170f3fc/crates/meilisearch/src/extractors/authentication/mod.rs#L334-L340 | GitHub source code}
82+
*/
83+
type TokenClaims = {
84+
searchRules: TokenSearchRules;
85+
exp?: number;
86+
apiKeyUid: string;
87+
};
88+
6289
/** Create the payload of the token. */
6390
function getPayload({
64-
searchRules = [],
91+
searchRules,
6592
apiKeyUid,
6693
expiresAt,
67-
}: TenantTokenGeneratorOptions): string {
94+
}: TenantTokenGeneratorOptionsWithDefaults): string {
6895
if (!isValidUUIDv4(apiKeyUid)) {
6996
throw new Error("the uid of your key is not a valid UUIDv4");
7097
}
7198

72-
const payload = {
73-
searchRules,
74-
apiKeyUid,
75-
exp: expiresAt
76-
? Math.floor(
77-
(typeof expiresAt === "number" ? expiresAt : expiresAt.getTime()) /
78-
1000,
79-
)
80-
: undefined,
81-
};
99+
const payload: TokenClaims = { searchRules, apiKeyUid };
100+
if (expiresAt !== undefined) {
101+
payload.exp =
102+
typeof expiresAt === "number"
103+
? expiresAt
104+
: Math.floor(expiresAt.getTime() / 1000);
105+
}
82106

83107
return encodeToBase64(payload).replace(/=/g, "");
84108
}
@@ -87,13 +111,14 @@ function getPayload({
87111
* Try to detect if the script is running in a server-side runtime.
88112
*
89113
* @remarks
90-
* This is not a silver bullet method for determining the environment.
114+
* There is no silver bullet way for determining the environment. Even so, this
115+
* is the recommended way according to
116+
* {@link https://min-common-api.proposal.wintercg.org/#navigator-useragent-requirements | WinterCG specs}.
91117
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent | User agent }
92-
* can be spoofed, `process` can be patched. Never the less theoretically it
93-
* should prevent misuse for the overwhelming majority of cases.
118+
* can be spoofed, `process` can be patched. It should prevent misuse for the
119+
* overwhelming majority of cases.
94120
*/
95121
function tryDetectEnvironment(): void {
96-
// https://min-common-api.proposal.wintercg.org/#navigator-useragent-requirements
97122
if (
98123
typeof navigator !== "undefined" &&
99124
Object.hasOwn(navigator, "userAgent")
@@ -120,31 +145,40 @@ function tryDetectEnvironment(): void {
120145
return;
121146
}
122147

123-
throw new Error("TODO");
148+
throw new Error(
149+
"failed to detect a server-side environment; do not generate tokens on the frontend in production!\n" +
150+
"use the `force` option to disable environment detection, consult the documentation (Use at your own risk!)",
151+
);
124152
}
125153

126154
/**
127155
* Generate a tenant token.
128156
*
129157
* @remarks
130-
* TODO: Describe how this should be used safely.
158+
* Warning: while this can be used in browsers with
159+
* {@link TenantTokenGeneratorOptions.force}, it is only intended for server
160+
* side. Don't use this in production on the frontend, unless you really know
161+
* what you're doing!
131162
* @param options - Options object for tenant token generation
132163
* @returns The token in JWT (JSON Web Token) format
133-
* @see {@link https://www.meilisearch.com/docs/learn/security/generate_tenant_token_sdk | Using tenant tokens with an official SDK}
134-
* @see {@link https://www.meilisearch.com/docs/learn/security/tenant_token_reference | Tenant token payload reference}
164+
* @see {@link https://www.meilisearch.com/docs/learn/security/basic_security | Securing your project}
135165
*/
136166
export async function generateTenantToken(
137167
options: TenantTokenGeneratorOptions,
138168
): Promise<string> {
139-
const { apiKey, force = false } = options;
169+
const optionsWithDefaults = getOptionsWithDefaults(options);
140170

141-
if (!force) {
171+
if (!optionsWithDefaults.force) {
142172
tryDetectEnvironment();
143173
}
144174

145-
const encodedPayload = getPayload(options);
146-
const encodedHeader = getHeader(options);
147-
const signature = await sign(apiKey, encodedPayload, encodedHeader);
175+
const encodedPayload = getPayload(optionsWithDefaults);
176+
const encodedHeader = getHeader(optionsWithDefaults);
177+
const signature = await sign(
178+
optionsWithDefaults,
179+
encodedPayload,
180+
encodedHeader,
181+
);
148182

149183
return `${encodedHeader}.${encodedPayload}.${signature}`;
150184
}

src/types/types.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,16 +1124,20 @@ export type TokenSearchRules =
11241124

11251125
/** Options object for tenant token generation. */
11261126
export type TenantTokenGeneratorOptions = {
1127+
/** API key used to sign the token. */
1128+
apiKey: string;
11271129
/**
11281130
* The uid of the api key used as issuer of the token.
11291131
*
11301132
* @see {@link https://www.meilisearch.com/docs/learn/security/tenant_token_reference#api-key-uid}
11311133
*/
11321134
apiKeyUid: string;
1133-
/** Search rules that are applied to every search. */
1135+
/**
1136+
* Search rules that are applied to every search.
1137+
*
1138+
* @defaultValue `[]`
1139+
*/
11341140
searchRules?: TokenSearchRules;
1135-
/** API key used to sign the token. */
1136-
apiKey: string;
11371141
/**
11381142
* {@link https://en.wikipedia.org/wiki/Unix_time | UNIX timestamp} or
11391143
* {@link Date} object at which the token expires.
@@ -1142,13 +1146,20 @@ export type TenantTokenGeneratorOptions = {
11421146
*/
11431147
expiresAt?: number | Date;
11441148
/**
1145-
* Encryption algorithm used by TODO what. Supported values are HS256, HS384,
1146-
* HS512.
1149+
* Encryption algorithm used to sign the JWT. Supported values by Meilisearch
1150+
* are HS256, HS384, HS512. (HS[number] means HMAC using SHA-[number])
1151+
*
1152+
* @defaultValue `"HS256"`
1153+
* @see {@link https://www.meilisearch.com/docs/learn/security/generate_tenant_token_scratch#prepare-token-header}
11471154
*/
11481155
algorithm?: `HS${256 | 384 | 512}`;
11491156
/**
11501157
* By default if a non-safe environment is detected, an error is thrown.
1151-
* Setting this to `true` skips environment detection.
1158+
* Setting this to `true` skips environment detection. This is intended for
1159+
* server-side environments where detection fails or usage in a browser is
1160+
* intentional (Use at your own risk).
1161+
*
1162+
* @defaultValue `false`
11521163
*/
11531164
force?: boolean;
11541165
};

0 commit comments

Comments
 (0)