Skip to content

Commit 82207de

Browse files
committed
feat: add ecosystem heads to extractAuthorizationData
1 parent 0a1d59d commit 82207de

File tree

4 files changed

+200
-172
lines changed

4 files changed

+200
-172
lines changed

.changeset/tame-tips-type.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+
Adds ecosystem headers to extractAuthorizationData result
Lines changed: 180 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type {
2-
ExecutionContext,
3-
KVNamespace,
4-
Response,
2+
ExecutionContext,
3+
KVNamespace,
4+
Response,
55
} from "@cloudflare/workers-types";
66
import type { Request } from "@cloudflare/workers-types";
77
import type {
8-
AccountMetadata,
9-
ApiKeyMetadata,
10-
CoreServiceConfig,
8+
AccountMetadata,
9+
ApiKeyMetadata,
10+
CoreServiceConfig,
1111
} from "../core/api.js";
1212
import { authorize } from "../core/authorize/index.js";
1313
import type { AuthorizationInput } from "../core/authorize/index.js";
@@ -20,197 +20,205 @@ export * from "../core/rateLimit/index.js";
2020
export * from "../core/usageLimit/index.js";
2121

2222
export type WorkerServiceConfig = CoreServiceConfig & {
23-
kvStore: KVNamespace;
24-
ctx: ExecutionContext;
25-
cacheTtlSeconds?: number;
23+
kvStore: KVNamespace;
24+
ctx: ExecutionContext;
25+
cacheTtlSeconds?: number;
2626
};
2727

2828
const DEFAULT_CACHE_TTL_SECONDS = 60;
2929

3030
type AuthInput = CoreAuthInput & {
31-
req: Request;
31+
req: Request;
3232
};
3333

3434
export async function authorizeWorker(
35-
authInput: AuthInput,
36-
serviceConfig: WorkerServiceConfig,
35+
authInput: AuthInput,
36+
serviceConfig: WorkerServiceConfig,
3737
): Promise<AuthorizationResult> {
38-
let authData: AuthorizationInput;
39-
try {
40-
authData = await extractAuthorizationData(authInput);
41-
} catch (e) {
42-
if (e instanceof Error && e.message === "KEY_CONFLICT") {
43-
return {
44-
authorized: false,
45-
status: 400,
46-
errorMessage: "Please pass either a client id or a secret key.",
47-
errorCode: "KEY_CONFLICT",
48-
};
49-
}
50-
return {
51-
authorized: false,
52-
status: 500,
53-
errorMessage: "Internal Server Error",
54-
errorCode: "INTERNAL_SERVER_ERROR",
55-
};
56-
}
57-
58-
return await authorize(authData, serviceConfig, {
59-
get: async (clientId: string) => serviceConfig.kvStore.get(clientId),
60-
put: (clientId: string, apiKeyMeta: ApiKeyMetadata | AccountMetadata) =>
61-
serviceConfig.ctx.waitUntil(
62-
serviceConfig.kvStore.put(
63-
clientId,
64-
JSON.stringify({
65-
updatedAt: Date.now(),
66-
apiKeyMeta,
67-
}),
68-
{
69-
expirationTtl:
70-
serviceConfig.cacheTtlSeconds &&
71-
serviceConfig.cacheTtlSeconds >= DEFAULT_CACHE_TTL_SECONDS
72-
? serviceConfig.cacheTtlSeconds
73-
: DEFAULT_CACHE_TTL_SECONDS,
74-
},
75-
),
76-
),
77-
cacheTtlSeconds: serviceConfig.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS,
78-
});
38+
let authData: AuthorizationInput;
39+
try {
40+
authData = await extractAuthorizationData(authInput);
41+
} catch (e) {
42+
if (e instanceof Error && e.message === "KEY_CONFLICT") {
43+
return {
44+
authorized: false,
45+
status: 400,
46+
errorMessage: "Please pass either a client id or a secret key.",
47+
errorCode: "KEY_CONFLICT",
48+
};
49+
}
50+
return {
51+
authorized: false,
52+
status: 500,
53+
errorMessage: "Internal Server Error",
54+
errorCode: "INTERNAL_SERVER_ERROR",
55+
};
56+
}
57+
58+
return await authorize(authData, serviceConfig, {
59+
get: async (clientId: string) => serviceConfig.kvStore.get(clientId),
60+
put: (clientId: string, apiKeyMeta: ApiKeyMetadata | AccountMetadata) =>
61+
serviceConfig.ctx.waitUntil(
62+
serviceConfig.kvStore.put(
63+
clientId,
64+
JSON.stringify({
65+
updatedAt: Date.now(),
66+
apiKeyMeta,
67+
}),
68+
{
69+
expirationTtl:
70+
serviceConfig.cacheTtlSeconds &&
71+
serviceConfig.cacheTtlSeconds >= DEFAULT_CACHE_TTL_SECONDS
72+
? serviceConfig.cacheTtlSeconds
73+
: DEFAULT_CACHE_TTL_SECONDS,
74+
},
75+
),
76+
),
77+
cacheTtlSeconds: serviceConfig.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS,
78+
});
7979
}
8080

8181
export async function extractAuthorizationData(
82-
authInput: AuthInput,
82+
authInput: AuthInput,
8383
): Promise<AuthorizationInput> {
84-
const requestUrl = new URL(authInput.req.url);
85-
const headers = authInput.req.headers;
86-
const secretKey = headers.get("x-secret-key");
87-
88-
// prefer clientId that is explicitly passed in
89-
let clientId = authInput.clientId ?? null;
90-
91-
if (!clientId) {
92-
// next preference is clientId from header
93-
clientId = headers.get("x-client-id");
94-
}
95-
96-
// next preference is search param
97-
if (!clientId) {
98-
clientId = requestUrl.searchParams.get("clientId");
99-
}
100-
// bundle id from header is first preference
101-
let bundleId = headers.get("x-bundle-id");
102-
103-
// next preference is search param
104-
if (!bundleId) {
105-
bundleId = requestUrl.searchParams.get("bundleId");
106-
}
107-
108-
let origin = headers.get("origin");
109-
// if origin header is not available we'll fall back to referrer;
110-
if (!origin) {
111-
origin = headers.get("referer");
112-
}
113-
// if we have an origin at this point, normalize it
114-
if (origin) {
115-
try {
116-
origin = new URL(origin).host;
117-
} catch (e) {
118-
console.warn("failed to parse origin", origin, e);
119-
}
120-
}
121-
122-
// handle if we a secret key is passed in the headers
123-
let secretKeyHash: string | null = null;
124-
if (secretKey) {
125-
// hash the secret key
126-
secretKeyHash = await hashSecretKey(secretKey);
127-
// derive the client id from the secret key hash
128-
const derivedClientId = deriveClientIdFromSecretKeyHash(secretKeyHash);
129-
// if we already have a client id passed in we need to make sure they match
130-
if (clientId && clientId !== derivedClientId) {
131-
throw new Error("KEY_CONFLICT");
132-
}
133-
// otherwise set the client id to the derived client id (client id based off of secret key)
134-
clientId = derivedClientId;
135-
}
136-
137-
let jwt: string | null = null;
138-
if (headers.has("authorization")) {
139-
const authHeader = headers.get("authorization");
140-
if (authHeader) {
141-
const [type, token] = authHeader.split(" ");
142-
if (type?.toLowerCase() === "bearer" && !!token) {
143-
jwt = token;
144-
}
145-
}
146-
}
147-
148-
return {
149-
jwt,
150-
hashedJWT: jwt ? await hashSecretKey(jwt) : null,
151-
secretKey,
152-
clientId,
153-
origin,
154-
bundleId,
155-
secretKeyHash,
156-
targetAddress: authInput.targetAddress,
157-
};
84+
const requestUrl = new URL(authInput.req.url);
85+
const headers = authInput.req.headers;
86+
const secretKey = headers.get("x-secret-key");
87+
88+
// prefer clientId that is explicitly passed in
89+
let clientId = authInput.clientId ?? null;
90+
91+
if (!clientId) {
92+
// next preference is clientId from header
93+
clientId = headers.get("x-client-id");
94+
}
95+
96+
// next preference is search param
97+
if (!clientId) {
98+
clientId = requestUrl.searchParams.get("clientId");
99+
}
100+
// bundle id from header is first preference
101+
let bundleId = headers.get("x-bundle-id");
102+
103+
// next preference is search param
104+
if (!bundleId) {
105+
bundleId = requestUrl.searchParams.get("bundleId");
106+
}
107+
108+
let ecosystemId =
109+
headers.get("x-ecosystem-id") ?? requestUrl.searchParams.get("ecosystemId");
110+
let ecosystemPartnerId =
111+
headers.get("x-ecosystem-partner-id") ??
112+
requestUrl.searchParams.get("ecosystemPartnerId");
113+
114+
let origin = headers.get("origin");
115+
// if origin header is not available we'll fall back to referrer;
116+
if (!origin) {
117+
origin = headers.get("referer");
118+
}
119+
// if we have an origin at this point, normalize it
120+
if (origin) {
121+
try {
122+
origin = new URL(origin).host;
123+
} catch (e) {
124+
console.warn("failed to parse origin", origin, e);
125+
}
126+
}
127+
128+
// handle if we a secret key is passed in the headers
129+
let secretKeyHash: string | null = null;
130+
if (secretKey) {
131+
// hash the secret key
132+
secretKeyHash = await hashSecretKey(secretKey);
133+
// derive the client id from the secret key hash
134+
const derivedClientId = deriveClientIdFromSecretKeyHash(secretKeyHash);
135+
// if we already have a client id passed in we need to make sure they match
136+
if (clientId && clientId !== derivedClientId) {
137+
throw new Error("KEY_CONFLICT");
138+
}
139+
// otherwise set the client id to the derived client id (client id based off of secret key)
140+
clientId = derivedClientId;
141+
}
142+
143+
let jwt: string | null = null;
144+
if (headers.has("authorization")) {
145+
const authHeader = headers.get("authorization");
146+
if (authHeader) {
147+
const [type, token] = authHeader.split(" ");
148+
if (type?.toLowerCase() === "bearer" && !!token) {
149+
jwt = token;
150+
}
151+
}
152+
}
153+
154+
return {
155+
jwt,
156+
hashedJWT: jwt ? await hashSecretKey(jwt) : null,
157+
secretKey,
158+
clientId,
159+
ecosystemId,
160+
ecosystemPartnerId,
161+
origin,
162+
bundleId,
163+
secretKeyHash,
164+
targetAddress: authInput.targetAddress,
165+
};
158166
}
159167

160168
export async function hashSecretKey(secretKey: string) {
161-
return bufferToHex(
162-
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secretKey)),
163-
);
169+
return bufferToHex(
170+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secretKey)),
171+
);
164172
}
165173

166174
export function deriveClientIdFromSecretKeyHash(secretKeyHash: string) {
167-
return secretKeyHash.slice(0, 32);
175+
return secretKeyHash.slice(0, 32);
168176
}
169177

170178
function bufferToHex(buffer: ArrayBuffer) {
171-
return [...new Uint8Array(buffer)]
172-
.map((x) => x.toString(16).padStart(2, "0"))
173-
.join("");
179+
return [...new Uint8Array(buffer)]
180+
.map((x) => x.toString(16).padStart(2, "0"))
181+
.join("");
174182
}
175183

176184
export async function logHttpRequest({
177-
clientId,
178-
req,
179-
res,
180-
isAuthed,
181-
statusMessage,
182-
latencyMs,
185+
clientId,
186+
req,
187+
res,
188+
isAuthed,
189+
statusMessage,
190+
latencyMs,
183191
}: AuthInput & {
184-
// @deprecated
185-
source: string;
186-
res: Response;
187-
isAuthed?: boolean;
188-
statusMessage?: Error | string;
189-
latencyMs?: number;
192+
// @deprecated
193+
source: string;
194+
res: Response;
195+
isAuthed?: boolean;
196+
statusMessage?: Error | string;
197+
latencyMs?: number;
190198
}) {
191-
try {
192-
const authorizationData = await extractAuthorizationData({ req, clientId });
193-
const headers = req.headers;
194-
195-
console.log(
196-
JSON.stringify({
197-
method: req.method,
198-
pathname: req.url,
199-
hasSecretKey: !!authorizationData.secretKey,
200-
hasClientId: !!authorizationData.clientId,
201-
hasJwt: !!authorizationData.jwt,
202-
clientId: authorizationData.clientId,
203-
isAuthed,
204-
status: res.status,
205-
sdkName: headers.get("x-sdk-name") ?? undefined,
206-
sdkVersion: headers.get("x-sdk-version") ?? undefined,
207-
platform: headers.get("x-sdk-platform") ?? undefined,
208-
os: headers.get("x-sdk-os") ?? undefined,
209-
latencyMs,
210-
}),
211-
);
212-
if (statusMessage) {
213-
console.log(`statusMessage=${statusMessage}`);
214-
}
215-
} catch {}
199+
try {
200+
const authorizationData = await extractAuthorizationData({ req, clientId });
201+
const headers = req.headers;
202+
203+
console.log(
204+
JSON.stringify({
205+
method: req.method,
206+
pathname: req.url,
207+
hasSecretKey: !!authorizationData.secretKey,
208+
hasClientId: !!authorizationData.clientId,
209+
hasJwt: !!authorizationData.jwt,
210+
clientId: authorizationData.clientId,
211+
isAuthed,
212+
status: res.status,
213+
sdkName: headers.get("x-sdk-name") ?? undefined,
214+
sdkVersion: headers.get("x-sdk-version") ?? undefined,
215+
platform: headers.get("x-sdk-platform") ?? undefined,
216+
os: headers.get("x-sdk-os") ?? undefined,
217+
latencyMs,
218+
}),
219+
);
220+
if (statusMessage) {
221+
console.log(`statusMessage=${statusMessage}`);
222+
}
223+
} catch {}
216224
}

0 commit comments

Comments
 (0)