Skip to content

Commit c6af295

Browse files
jnsdlsjoaquim-verges
authored andcommitted
Prioritize JWT over service API keys in authentication (#7020)
1 parent 72bc53b commit c6af295

File tree

7 files changed

+206
-108
lines changed

7 files changed

+206
-108
lines changed

.changeset/shaky-eels-shine.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@thirdweb-dev/service-utils": patch
3+
---
4+
5+
Prioritize JWT over service API keys in authentication

packages/service-utils/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist/
2+
coverage/
3+
.watchmanconfig
4+
*storybook.log

packages/service-utils/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"devDependencies": {
5353
"@cloudflare/workers-types": "4.20250421.0",
5454
"@types/node": "22.14.1",
55+
"@vitest/coverage-v8": "3.1.2",
5556
"typescript": "5.8.3",
5657
"vitest": "3.1.2"
5758
},
@@ -64,6 +65,7 @@
6465
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
6566
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
6667
"build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
67-
"test": "vitest run"
68+
"test": "vitest run",
69+
"coverage": "vitest run --coverage"
6870
}
6971
}

packages/service-utils/src/core/api.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AuthorizationInput } from "./authorize/index.js";
2+
import { getAuthHeaders } from "./get-auth-headers.js";
23
import type { ServiceName } from "./services.js";
34

45
export type UserOpData = {
@@ -240,7 +241,7 @@ export async function fetchTeamAndProject(
240241
config: CoreServiceConfig,
241242
): Promise<ApiResponse> {
242243
const { apiUrl, serviceApiKey } = config;
243-
const { teamId, clientId, incomingServiceApiKey } = authData;
244+
const { teamId, clientId } = authData;
244245

245246
const url = new URL("/v2/keys/use", apiUrl);
246247
if (clientId) {
@@ -250,20 +251,17 @@ export async function fetchTeamAndProject(
250251
url.searchParams.set("teamId", teamId);
251252
}
252253

254+
// compute the appropriate auth headers based on the auth data
255+
const authHeaders = getAuthHeaders(authData, serviceApiKey);
256+
253257
const retryCount = config.retryCount ?? 3;
254258
let error: unknown | undefined;
255259
for (let i = 0; i < retryCount; i++) {
256260
try {
257261
const response = await fetch(url, {
258262
method: "GET",
259263
headers: {
260-
...(authData.secretKey ? { "x-secret-key": authData.secretKey } : {}),
261-
...(authData.jwt ? { Authorization: `Bearer ${authData.jwt}` } : {}),
262-
// use the incoming service api key if it exists, otherwise use the service api key
263-
// this is done to ensure that the incoming service API key is VALID in the first place
264-
"x-service-api-key": incomingServiceApiKey
265-
? incomingServiceApiKey
266-
: serviceApiKey,
264+
...authHeaders,
267265
"content-type": "application/json",
268266
},
269267
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import type { AuthorizationInput } from "./authorize/index.js";
4+
import { getAuthHeaders } from "./get-auth-headers.js";
5+
6+
describe("getAuthHeaders", () => {
7+
const mockServiceApiKey = "test-service-api-key";
8+
const defaultAuthData: AuthorizationInput = {
9+
incomingServiceApiKey: null,
10+
incomingServiceApiKeyHash: null,
11+
secretKey: null,
12+
clientId: null,
13+
ecosystemId: null,
14+
ecosystemPartnerId: null,
15+
origin: null,
16+
bundleId: null,
17+
secretKeyHash: null,
18+
jwt: null,
19+
hashedJWT: null,
20+
};
21+
22+
it("should use secret key when provided", () => {
23+
const authData: AuthorizationInput = {
24+
...defaultAuthData,
25+
secretKey: "test-secret-key",
26+
};
27+
28+
const headers = getAuthHeaders(authData, mockServiceApiKey);
29+
30+
expect(headers).toEqual({
31+
"x-secret-key": "test-secret-key",
32+
});
33+
});
34+
35+
it("should use JWT when both JWT and teamId are provided", () => {
36+
const authData: AuthorizationInput = {
37+
...defaultAuthData,
38+
jwt: "test-jwt",
39+
teamId: "test-team-id",
40+
};
41+
42+
const headers = getAuthHeaders(authData, mockServiceApiKey);
43+
44+
expect(headers).toEqual({
45+
Authorization: "Bearer test-jwt",
46+
});
47+
});
48+
49+
it("should use JWT when both JWT and clientId are provided", () => {
50+
const authData: AuthorizationInput = {
51+
...defaultAuthData,
52+
jwt: "test-jwt",
53+
clientId: "test-client-id",
54+
};
55+
56+
const headers = getAuthHeaders(authData, mockServiceApiKey);
57+
58+
expect(headers).toEqual({
59+
Authorization: "Bearer test-jwt",
60+
});
61+
});
62+
63+
it("should use incoming service api key when provided", () => {
64+
const authData: AuthorizationInput = {
65+
...defaultAuthData,
66+
incomingServiceApiKey: "test-incoming-service-api-key",
67+
};
68+
69+
const headers = getAuthHeaders(authData, mockServiceApiKey);
70+
71+
expect(headers).toEqual({
72+
"x-service-api-key": "test-incoming-service-api-key",
73+
});
74+
});
75+
76+
it("should fall back to service api key when no other auth method is provided", () => {
77+
const headers = getAuthHeaders(defaultAuthData, mockServiceApiKey);
78+
79+
expect(headers).toEqual({
80+
"x-service-api-key": mockServiceApiKey,
81+
});
82+
});
83+
84+
it("should prioritize secret key over other auth methods", () => {
85+
const authData: AuthorizationInput = {
86+
...defaultAuthData,
87+
secretKey: "test-secret-key",
88+
jwt: "test-jwt",
89+
teamId: "test-team-id",
90+
incomingServiceApiKey: "test-incoming-service-api-key",
91+
};
92+
93+
const headers = getAuthHeaders(authData, mockServiceApiKey);
94+
95+
expect(headers).toEqual({
96+
"x-secret-key": "test-secret-key",
97+
});
98+
});
99+
100+
it("should prioritize JWT over incoming service api key when teamId is present", () => {
101+
const authData: AuthorizationInput = {
102+
...defaultAuthData,
103+
jwt: "test-jwt",
104+
teamId: "test-team-id",
105+
incomingServiceApiKey: "test-incoming-service-api-key",
106+
};
107+
108+
const headers = getAuthHeaders(authData, mockServiceApiKey);
109+
110+
expect(headers).toEqual({
111+
Authorization: "Bearer test-jwt",
112+
});
113+
});
114+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { AuthorizationInput } from "./authorize/index.js";
2+
3+
/**
4+
* Computes the appropriate auth headers based on the auth data.
5+
*
6+
* @param authData - The auth data to use.
7+
* @param serviceApiKey - The service api key to use.
8+
* @returns The auth headers.
9+
*/
10+
export function getAuthHeaders(
11+
authData: AuthorizationInput,
12+
serviceApiKey: string,
13+
): Record<string, string> {
14+
const { teamId, clientId, jwt, secretKey, incomingServiceApiKey } = authData;
15+
16+
switch (true) {
17+
// 1. if we have a secret key, we'll use it
18+
case !!secretKey:
19+
return {
20+
"x-secret-key": secretKey,
21+
} as Record<string, string>;
22+
23+
// 2. if we have a JWT AND either a teamId or clientId, we'll use the JWT for auth
24+
case !!(jwt && (teamId || clientId)):
25+
return {
26+
Authorization: `Bearer ${jwt}`,
27+
} as Record<string, string>;
28+
29+
// 3. if we have an incoming service api key, we'll use it
30+
case !!incomingServiceApiKey: {
31+
return {
32+
"x-service-api-key": incomingServiceApiKey,
33+
} as Record<string, string>;
34+
}
35+
36+
// 4. if nothing else is present, we'll use the service api key
37+
default: {
38+
return {
39+
"x-service-api-key": serviceApiKey,
40+
};
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)