Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ ENABLE_HTTPS="true"
REDIS_URL="redis://127.0.0.1:6379/0"
THIRDWEB_API_SECRET_KEY="my-thirdweb-secret-key"

TEST_AWS_KMS_KEY_ID=""
TEST_AWS_KMS_ACCESS_KEY_ID=""
TEST_AWS_KMS_SECRET_ACCESS_KEY=""
TEST_AWS_KMS_REGION=""
TEST_AWS_KMS_KEY_ID="UNIMPLEMENTED"
TEST_AWS_KMS_ACCESS_KEY_ID="UNIMPLEMENTED"
TEST_AWS_KMS_SECRET_ACCESS_KEY="UNIMPLEMENTED"
TEST_AWS_KMS_REGION="UNIMPLEMENTED"

TEST_GCP_KMS_RESOURCE_PATH=""
TEST_GCP_KMS_EMAIL=""
TEST_GCP_KMS_PK=""
TEST_GCP_KMS_RESOURCE_PATH="UNIMPLEMENTED"
TEST_GCP_KMS_EMAIL="UNIMPLEMENTED"
TEST_GCP_KMS_PK="UNIMPLEMENTED"
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"prom-client": "^15.1.3",
"superjson": "^2.2.1",
"thirdweb": "^5.83.0",
"undici": "^6.20.1",
"uuid": "^9.0.1",
"viem": "^2.21.54",
"winston": "^3.14.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "configuration" ADD COLUMN "mtlsCertificateEncrypted" TEXT,
ADD COLUMN "mtlsPrivateKeyEncrypted" TEXT;
3 changes: 3 additions & 0 deletions src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ model Configuration {
accessControlAllowOrigin String @default("https://thirdweb.com,https://embed.ipfscdn.io") @map("accessControlAllowOrigin")
ipAllowlist String[] @default([]) @map("ipAllowlist")
clearCacheCronSchedule String @default("*/30 * * * * *") @map("clearCacheCronSchedule")
// mTLS support
mtlsCertificateEncrypted String?
mtlsPrivateKeyEncrypted String?

@@map("configuration")
}
Expand Down
10 changes: 6 additions & 4 deletions src/server/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
type ThirdwebAuthUser,
} from "@thirdweb-dev/auth/fastify";
import { AsyncWallet } from "@thirdweb-dev/wallets/evm/wallets/async";
import { createHash } from "node:crypto";
import type { FastifyInstance } from "fastify";
import type { FastifyRequest } from "fastify/types/request";
import jsonwebtoken, { type JwtPayload } from "jsonwebtoken";
import { createHash } from "node:crypto";
import { validate as uuidValidate } from "uuid";
import { getPermissions } from "../../shared/db/permissions/get-permissions";
import { createToken } from "../../shared/db/tokens/create-token";
Expand Down Expand Up @@ -123,7 +123,8 @@ export async function withAuth(server: FastifyInstance) {
}
// Allow this request to proceed.
return;
}if (error) {
}
if (error) {
message = error;
}
} catch (err: unknown) {
Expand Down Expand Up @@ -172,10 +173,11 @@ export const onRequest = async ({
const authWallet = await getAuthWallet();
if (publicKey === (await authWallet.getAddress())) {
return await handleAccessToken(jwt, req, getUser);
}if (publicKey === THIRDWEB_DASHBOARD_ISSUER) {
}
if (publicKey === THIRDWEB_DASHBOARD_ISSUER) {
return await handleDashboardAuth(jwt);
}
return await handleKeypairAuth({ jwt, req, publicKey });
return await handleKeypairAuth({ jwt, req, publicKey });
}

// Get the public key hash from the `kid` header.
Expand Down
10 changes: 7 additions & 3 deletions src/server/routes/configuration/auth/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { standardResponseSchema } from "../../../schemas/shared-api-schemas";

export const responseBodySchema = Type.Object({
result: Type.Object({
domain: Type.String(),
authDomain: Type.String(),
mtlsCertificate: Type.Union([Type.String(), Type.Null()]),
// Do not return mtlsPrivateKey.
}),
});

Expand All @@ -27,10 +29,12 @@ export async function getAuthConfiguration(fastify: FastifyInstance) {
},
},
handler: async (_req, res) => {
const config = await getConfig();
const { authDomain, mtlsCertificate } = await getConfig();

res.status(StatusCodes.OK).send({
result: {
domain: config.authDomain,
authDomain,
mtlsCertificate,
},
});
},
Expand Down
55 changes: 50 additions & 5 deletions src/server/routes/configuration/auth/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@ import { updateConfiguration } from "../../../../shared/db/configuration/update-
import { getConfig } from "../../../../shared/utils/cache/get-config";
import { standardResponseSchema } from "../../../schemas/shared-api-schemas";
import { responseBodySchema } from "./get";
import { createCustomError } from "../../../middleware/error";
import { encrypt } from "../../../../shared/utils/crypto";

export const requestBodySchema = Type.Object({
domain: Type.String(),
});
export const requestBodySchema = Type.Partial(
Type.Object({
authDomain: Type.String(),
mtlsCertificate: Type.String({
description:
"Engine certificate used for outbound mTLS requests. Must provide the full certificate chain.",
}),
mtlsPrivateKey: Type.String({
description: "Engine private key used for outbound mTLS requests.",
}),
}),
);

export async function updateAuthConfiguration(fastify: FastifyInstance) {
fastify.route<{
Expand All @@ -29,15 +40,49 @@ export async function updateAuthConfiguration(fastify: FastifyInstance) {
},
},
handler: async (req, res) => {
const { authDomain, mtlsCertificate, mtlsPrivateKey } = req.body;

if (mtlsCertificate) {
if (
!mtlsCertificate.includes("-----BEGIN CERTIFICATE-----\n") ||
!mtlsCertificate.includes("\n-----END CERTIFICATE-----")
) {
throw createCustomError(
"Invalid mtlsCertificate.",
StatusCodes.BAD_REQUEST,
"INVALID_MTLS_CERTIFICATE",
);
}
}
if (mtlsPrivateKey) {
if (
!mtlsPrivateKey.startsWith("-----BEGIN PRIVATE KEY-----\n") ||
!mtlsPrivateKey.endsWith("\n-----END PRIVATE KEY-----")
) {
throw createCustomError(
"Invalid mtlsPrivateKey.",
StatusCodes.BAD_REQUEST,
"INVALID_MTLS_PRIVATE_KEY",
);
}
}

await updateConfiguration({
authDomain: req.body.domain,
authDomain,
mtlsCertificateEncrypted: mtlsCertificate
? encrypt(mtlsCertificate)
: undefined,
mtlsPrivateKeyEncrypted: mtlsPrivateKey
? encrypt(mtlsPrivateKey)
: undefined,
});

const config = await getConfig(false);

res.status(StatusCodes.OK).send({
result: {
domain: config.authDomain,
authDomain: config.authDomain,
mtlsCertificate: config.mtlsCertificate,
},
});
},
Expand Down
2 changes: 1 addition & 1 deletion src/server/routes/webhooks/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function createWebhookRoute(fastify: FastifyInstance) {
method: "POST",
url: "/webhooks/create",
schema: {
summary: "Create a webhook",
summary: "Create webhook",
description:
"Create a webhook to call when a specific Engine event occurs.",
tags: ["Webhooks"],
Expand Down
6 changes: 6 additions & 0 deletions src/shared/db/configuration/get-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
gcp: gcpWalletConfiguration,
legacyWalletType_removeInNextBreakingChange,
},
mtlsCertificate: config.mtlsCertificateEncrypted
? decrypt(config.mtlsCertificateEncrypted, env.ENCRYPTION_PASSWORD)
: null,
mtlsPrivateKey: config.mtlsPrivateKeyEncrypted
? decrypt(config.mtlsPrivateKeyEncrypted, env.ENCRYPTION_PASSWORD)
: null,
};
};

Expand Down
2 changes: 1 addition & 1 deletion src/shared/db/configuration/update-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { encrypt } from "../../utils/crypto";
import { prisma } from "../client";

export const updateConfiguration = async (
data: Prisma.ConfigurationUpdateArgs["data"],
data: Prisma.ConfigurationUpdateInput,
) => {
return prisma.configuration.update({
where: {
Expand Down
2 changes: 1 addition & 1 deletion src/shared/db/webhooks/create-webhook.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Webhooks } from "@prisma/client";
import { createHash, randomBytes } from "crypto";
import { createHash, randomBytes } from "node:crypto";
import type { WebhooksEventTypes } from "../../schemas/webhooks";
import { prisma } from "../client";

Expand Down
4 changes: 4 additions & 0 deletions src/shared/schemas/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface ParsedConfig
| "gcpApplicationCredentialEmail"
| "gcpApplicationCredentialPrivateKey"
| "contractSubscriptionsRetryDelaySeconds"
| "mtlsCertificateEncrypted"
| "mtlsPrivateKeyEncrypted"
> {
walletConfiguration: {
aws: AwsWalletConfiguration | null;
Expand All @@ -41,4 +43,6 @@ export interface ParsedConfig
};
contractSubscriptionsRequeryDelaySeconds: string;
chainOverridesParsed: Chain[];
mtlsCertificate: string | null;
mtlsPrivateKey: string | null;
}
14 changes: 7 additions & 7 deletions src/shared/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import crypto from "crypto";
import CryptoJS from "crypto-js";
import crypto from "node:crypto";
import { env } from "./env";

export const encrypt = (data: string): string => {
export function encrypt(data: string): string {
return CryptoJS.AES.encrypt(data, env.ENCRYPTION_PASSWORD).toString();
};
}

export const decrypt = (data: string, password: string) => {
export function decrypt(data: string, password: string) {
return CryptoJS.AES.decrypt(data, password).toString(CryptoJS.enc.Utf8);
};
}

export const isWellFormedPublicKey = (key: string) => {
export function isWellFormedPublicKey(key: string) {
try {
crypto.createPublicKey(key);
return true;
} catch (_e) {
return false;
}
};
}
63 changes: 63 additions & 0 deletions src/shared/utils/custom-auth-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createHmac } from "node:crypto";

/**
* Generates an HMAC-256 secret to set in the "Authorization" header.
*
* @param webhookUrl - The URL to call.
* @param body - The request body.
* @param timestamp - The request timestamp.
* @param nonce - A unique string for this request. Should not be re-used.
* @param clientId - Your application's client id.
* @param clientSecret - Your application's client secret.
* @returns
*/
export const generateSecretHmac256 = (args: {
webhookUrl: string;
body: Record<string, unknown>;
timestamp: Date;
nonce: string;
clientId: string;
clientSecret: string;
}): string => {
const { webhookUrl, body, timestamp, nonce, clientId, clientSecret } = args;

// Create the body hash by hashing the payload.
const bodyHash = createHmac("sha256", clientSecret)
.update(JSON.stringify(body), "utf8")
.digest("base64");

// Create the signature hash by hashing the signature.
const ts = timestamp.getTime(); // timestamp expected in milliseconds
const httpMethod = "POST";
const url = new URL(webhookUrl);
const resourcePath = url.pathname;
const host = url.hostname;
const port = url.port
? Number.parseInt(url.port)
: url.protocol === "https:"
? 443
: 80;

const signature = [
ts,
nonce,
httpMethod,
resourcePath,
host,
port,
bodyHash,
"", // to insert a newline at the end
].join("\n");

const signatureHash = createHmac("sha256", clientSecret)
.update(signature, "utf8")
.digest("base64");

return [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file generates the custom HMAC auth header used by certain clients.

`MAC id="${clientId}"`,
`ts="${ts}"`,
`nonce="${nonce}"`,
`bodyhash="${bodyHash}"`,
`mac="${signatureHash}"`,
].join(",");
};
23 changes: 9 additions & 14 deletions src/shared/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,6 @@ import { z } from "zod";
const path = process.env.NODE_ENV === "test" ? ".env.test" : ".env";
dotenv.config({ path });

export const JsonSchema = z.string().refine(
(value) => {
try {
JSON.parse(value);
return true;
} catch {
return false;
}
},
{ message: "Invalid JSON string" },
);

const boolEnvSchema = (defaultBool: boolean) =>
z
.string()
Expand Down Expand Up @@ -68,7 +56,6 @@ export const env = createEnv({
.default("https://c.thirdweb.com/event"),
SDK_BATCH_TIME_LIMIT: z.coerce.number().default(0),
SDK_BATCH_SIZE_LIMIT: z.coerce.number().default(100),
ENABLE_KEYPAIR_AUTH: boolEnvSchema(false),
REDIS_URL: z.string(),
SEND_TRANSACTION_QUEUE_CONCURRENCY: z.coerce.number().default(200),
CONFIRM_TRANSACTION_QUEUE_CONCURRENCY: z.coerce.number().default(200),
Expand Down Expand Up @@ -99,6 +86,11 @@ export const env = createEnv({
// Sets the number of recent nonces to map to queue IDs.
NONCE_MAP_COUNT: z.coerce.number().default(10_000),

ENABLE_KEYPAIR_AUTH: boolEnvSchema(false),
ENABLE_CUSTOM_HMAC_AUTH: boolEnvSchema(false),
CUSTOM_HMAC_AUTH_CLIENT_ID: z.string().optional(),
CUSTOM_HMAC_AUTH_CLIENT_SECRET: z.string().optional(),

/**
* Experimental env vars. These may be renamed or removed in future non-major releases.
*/
Expand Down Expand Up @@ -130,7 +122,6 @@ export const env = createEnv({
CLIENT_ANALYTICS_URL: process.env.CLIENT_ANALYTICS_URL,
SDK_BATCH_TIME_LIMIT: process.env.SDK_BATCH_TIME_LIMIT,
SDK_BATCH_SIZE_LIMIT: process.env.SDK_BATCH_SIZE_LIMIT,
ENABLE_KEYPAIR_AUTH: process.env.ENABLE_KEYPAIR_AUTH,
REDIS_URL: process.env.REDIS_URL,
SEND_TRANSACTION_QUEUE_CONCURRENCY:
process.env.SEND_TRANSACTION_QUEUE_CONCURRENCY,
Expand All @@ -150,6 +141,10 @@ export const env = createEnv({
process.env.EXPERIMENTAL__MAX_GAS_PRICE_WEI,
METRICS_PORT: process.env.METRICS_PORT,
METRICS_ENABLED: process.env.METRICS_ENABLED,
ENABLE_KEYPAIR_AUTH: process.env.ENABLE_KEYPAIR_AUTH,
ENABLE_CUSTOM_HMAC_AUTH: process.env.ENABLE_CUSTOM_HMAC_AUTH,
CUSTOM_HMAC_AUTH_CLIENT_ID: process.env.CUSTOM_HMAC_AUTH_CLIENT_ID,
CUSTOM_HMAC_AUTH_CLIENT_SECRET: process.env.CUSTOM_HMAC_AUTH_CLIENT_SECRET,
},
onValidationError: (error: ZodError) => {
console.error(
Expand Down
Loading
Loading