Skip to content
Closed
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
219 changes: 150 additions & 69 deletions src/server/middleware/auth.ts

Large diffs are not rendered by default.

56 changes: 46 additions & 10 deletions src/server/middleware/engine-mode.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
import type { FastifyInstance } from "fastify";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { env } from "../../shared/utils/env";
import { StatusCodes } from "http-status-codes";

export function withEnforceEngineMode(server: FastifyInstance) {
if (env.ENGINE_MODE === "sandbox") {
server.addHook("onRequest", async (request, reply) => {
if (request.method !== "GET") {
return reply.status(405).send({
statusCode: 405,
error: "Engine is in read-only mode. Only GET requests are allowed.",
message:
"Engine is in read-only mode. Only GET requests are allowed.",
});
switch (env.ENGINE_MODE) {
case "lite":
// DEBUG: USED TO DEBUG DASHBOARD. REMOVE WHEN SHIPPING.
// server.addHook("onRequest", enforceLiteMode);
break;
case "sandbox":
server.addHook("onRequest", enforceSandboxMode);
break;
}
}

const ALLOWED_LITE_MODE_PATHS_GET = new Set(["/backend-wallet/lite/:teamId"]);
const ALLOWED_LITE_MODE_PATHS_POST = new Set([
"/backend-wallet/lite/:teamId",
"/backend-wallet/sign-message",
]);
async function enforceLiteMode(request: FastifyRequest, reply: FastifyReply) {
if (request.routeOptions.url) {
if (request.method === "GET") {
if (ALLOWED_LITE_MODE_PATHS_GET.has(request.routeOptions.url)) {
return;
}
} else if (request.method === "POST") {
if (ALLOWED_LITE_MODE_PATHS_POST.has(request.routeOptions.url)) {
return;
}
}
}

return reply.status(StatusCodes.FORBIDDEN).send({
statusCode: StatusCodes.FORBIDDEN,
message: "Engine is in lite mode. Only limited endpoints are allowed.",
error: "ENGINE_MODE_FORBIDDEN",
});
}

async function enforceSandboxMode(
request: FastifyRequest,
reply: FastifyReply,
) {
if (request.method !== "GET") {
return reply.status(StatusCodes.FORBIDDEN).send({
statusCode: StatusCodes.FORBIDDEN,
message: "Engine is in sandbox mode. Only GET requests are allowed.",
error: "ENGINE_MODE_FORBIDDEN",
});
}
}
2 changes: 1 addition & 1 deletion src/server/middleware/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const IGNORE_LOG_PATHS = new Set([
const SENSITIVE_LOG_PATHS = new Set([
"/backend-wallet/import",
"/configuration/wallets",
"/backend-wallet/lite",
"/backend-wallet/lite/:teamId",
]);

function shouldLog(request: FastifyRequest) {
Expand Down
27 changes: 23 additions & 4 deletions src/server/middleware/rate-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { env } from "../../shared/utils/env";
import { redis } from "../../shared/utils/redis/redis";
import { createCustomError } from "./error";
import { OPENAPI_ROUTES } from "./open-api";
import { getTransactionCredentials } from "../../shared/lib/transaction/transaction-credentials";
import { toClientId } from "../../shared/utils/sdk";

const SKIP_RATELIMIT_PATHS = ["/", ...OPENAPI_ROUTES];

Expand All @@ -13,10 +15,11 @@ export function withRateLimit(server: FastifyInstance) {
return;
}

const epochTimeInMinutes = Math.floor(new Date().getTime() / (1000 * 60));
const key = `rate-limit:global:${epochTimeInMinutes}`;
const count = await redis.incr(key);
redis.expire(key, 2 * 60);
const epochMinutes = Math.floor(new Date().getTime() / (1000 * 60));

const globalRateLimitKey = `rate-limit:global:${epochMinutes}`;
const count = await redis.incr(globalRateLimitKey);
redis.expire(globalRateLimitKey, 2 * 60);

if (count > env.GLOBAL_RATE_LIMIT_PER_MIN) {
throw createCustomError(
Expand All @@ -25,5 +28,21 @@ export function withRateLimit(server: FastifyInstance) {
"TOO_MANY_REQUESTS",
);
}

// Lite mode enforces a rate limit per team.
if (env.ENGINE_MODE === "lite") {
const { clientId } = getTransactionCredentials(request);
const clientRateLimitKey = `rate-limit:client-id:${clientId}`;
const count = await redis.incr(clientRateLimitKey);
redis.expire(globalRateLimitKey, 2 * 60);

if (count > env.LITE_CLIENT_RATE_LIMIT_PER_MIN) {
throw createCustomError(
`${env.LITE_CLIENT_RATE_LIMIT_PER_MIN} requests/minute rate limit exceeded. Upgrade to Engine Standard to get a dedicated Engine without rate limits.`,
StatusCodes.TOO_MANY_REQUESTS,
"TOO_MANY_REQUESTS",
);
}
}
});
}
38 changes: 10 additions & 28 deletions src/server/routes/auth/access-tokens/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getConfig } from "../../../../shared/utils/cache/get-config";
import { env } from "../../../../shared/utils/env";
import { standardResponseSchema } from "../../../schemas/shared-api-schemas";
import { AccessTokenSchema } from "./get-all";
import { assertAuthenticationType } from "../../../utils/auth";

const requestBodySchema = Type.Object({
label: Type.Optional(Type.String()),
Expand Down Expand Up @@ -41,47 +42,28 @@ export async function createAccessToken(fastify: FastifyInstance) {
},
},
handler: async (req, res) => {
const { label } = req.body;
assertAuthenticationType(req, ["dashboard", "secret-key"]);

const { label } = req.body;
const config = await getConfig();
const wallet = new LocalWallet();

// TODO: Remove this with next breaking change
try {
// First try to load the wallet using the encryption password
await wallet.import({
encryptedJson: config.authWalletEncryptedJson,
password: env.ENCRYPTION_PASSWORD,
});
} catch {
// If that fails, try the thirdweb api secret key for backwards compatibility
await wallet.import({
encryptedJson: config.authWalletEncryptedJson,
password: env.THIRDWEB_API_SECRET_KEY,
});

// If that works, save the wallet using the encryption password for the future
const authWalletEncryptedJson = await wallet.export({
strategy: "encryptedJson",
password: env.ENCRYPTION_PASSWORD,
});

await updateConfiguration({
authWalletEncryptedJson,
});
}
const wallet = new LocalWallet();
await wallet.import({
encryptedJson: config.authWalletEncryptedJson,
password: env.ENCRYPTION_PASSWORD,
});

const jwt = await buildJWT({
wallet,
payload: {
iss: await wallet.getAddress(),
sub: req.user.address,
sub: req.authentication.user.address,
aud: config.authDomain,
nbf: new Date(),
// Set to expire in 100 years
exp: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100),
iat: new Date(),
ctx: req.user.session,
ctx: req.authentication.user.session,
},
});

Expand Down
5 changes: 3 additions & 2 deletions src/server/routes/backend-wallet/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createSmartGcpWalletDetails,
createSmartLocalWalletDetails,
} from "../../utils/wallets/create-smart-wallet";
import { getTransactionCredentials } from "../../../shared/lib/transaction/transaction-credentials";

const requestBodySchema = Type.Object({
label: Type.Optional(Type.String()),
Expand Down Expand Up @@ -72,14 +73,14 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
},
handler: async (req, reply) => {
const { label } = req.body;
const credentials = await getTransactionCredentials(req);

let walletAddress: string;
const config = await getConfig();

const walletType =
req.body.type ??
config.walletConfiguration.legacyWalletType_removeInNextBreakingChange;

let walletAddress: string;
switch (walletType) {
case WalletType.local:
walletAddress = await createLocalWalletDetails({ label });
Expand Down
30 changes: 16 additions & 14 deletions src/server/routes/backend-wallet/lite/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "thirdweb/wallets/smart";
import { createSmartLocalWalletDetails } from "../../../utils/wallets/create-smart-wallet";
import { updateBackendWalletLiteAccess } from "../../../../shared/db/wallets/update-backend-wallet-lite-access";
import { assertAuthenticationType } from "../../../utils/auth";

const requestSchema = Type.Object({
teamId: Type.String({
Expand Down Expand Up @@ -42,9 +43,7 @@ responseSchema.example = {
},
};

export const createBackendWalletLiteRoute = async (
fastify: FastifyInstance,
) => {
export async function createBackendWalletLiteRoute(fastify: FastifyInstance) {
fastify.withTypeProvider().route<{
Params: Static<typeof requestSchema>;
Body: Static<typeof requestBodySchema>;
Expand All @@ -66,17 +65,13 @@ export const createBackendWalletLiteRoute = async (
hide: true,
},
handler: async (req, reply) => {
const dashboardUserAddress = checksumAddress(req.user.address);
if (!dashboardUserAddress) {
throw createCustomError(
"This endpoint must be called from the thirdweb dashboard.",
StatusCodes.FORBIDDEN,
"DASHBOARD_AUTH_REQUIRED",
);
}
assertAuthenticationType(req, ["dashboard"]);

const { teamId } = req.params;
const { salt, litePassword } = req.body;
const dashboardUserAddress = checksumAddress(
req.authentication.user.address,
);

const liteAccess = await getBackendWalletLiteAccess({ teamId });
if (
Expand All @@ -86,15 +81,22 @@ export const createBackendWalletLiteRoute = async (
liteAccess.salt !== salt
) {
throw createCustomError(
"The salt does not match the authenticated user. Try requesting a backend wallet again.",
"The salt does not exist for this user. Try requesting a backend wallet again.",
StatusCodes.BAD_REQUEST,
"INVALID_LITE_WALLET_SALT",
);
}
if (liteAccess.accountAddress) {
throw createCustomError(
"A backend wallet already exists for this team.",
StatusCodes.BAD_REQUEST,
"LITE_WALLET_ALREADY_EXISTS",
);
}

// Generate a signer wallet and store the smart:local wallet, encrypted with `litePassword`.
const walletDetails = await createSmartLocalWalletDetails({
label: `${teamId} (${new Date()})`,
label: `${teamId} (${new Date().toISOString()})`,
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7,
entrypointAddress: ENTRYPOINT_ADDRESS_v0_7,
encryptionPassword: litePassword,
Expand All @@ -120,4 +122,4 @@ export const createBackendWalletLiteRoute = async (
});
},
});
};
}
22 changes: 9 additions & 13 deletions src/server/routes/backend-wallet/lite/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createBackendWalletLiteAccess } from "../../../../shared/db/wallets/cre
import { getBackendWalletLiteAccess } from "../../../../shared/db/wallets/get-backend-wallet-lite-access";
import { AddressSchema } from "../../../schemas/address";
import { standardResponseSchema } from "../../../schemas/shared-api-schemas";
import { createCustomError } from "../../../middleware/error";
import { assertAuthenticationType } from "../../../utils/auth";

const requestSchema = Type.Object({
teamId: Type.String({
Expand All @@ -33,7 +33,7 @@ responseSchema.example = {
},
};

export const listBackendWalletsLiteRoute = async (fastify: FastifyInstance) => {
export async function listBackendWalletsLiteRoute(fastify: FastifyInstance) {
fastify.withTypeProvider().route<{
Params: Static<typeof requestSchema>;
Reply: Static<typeof responseSchema>;
Expand All @@ -53,20 +53,16 @@ export const listBackendWalletsLiteRoute = async (fastify: FastifyInstance) => {
hide: true,
},
handler: async (req, reply) => {
const dashboardUserAddress = checksumAddress(req.user.address);
if (!dashboardUserAddress) {
throw createCustomError(
"This endpoint must be called from the thirdweb dashboard.",
StatusCodes.FORBIDDEN,
"DASHBOARD_AUTH_REQUIRED",
);
}
assertAuthenticationType(req, ["dashboard"]);

const { teamId } = req.params;
const liteAccess = await getBackendWalletLiteAccess({ teamId });
const dashboardUserAddress = checksumAddress(
req.authentication.user.address,
);

// If a wallet exists, return it.
if (liteAccess?.accountAddress) {
// If a salt exists (even if the wallet isn't created yet), return it.
if (liteAccess) {
return reply.status(StatusCodes.OK).send({
result: {
walletAddress: liteAccess.accountAddress,
Expand All @@ -91,4 +87,4 @@ export const listBackendWalletsLiteRoute = async (fastify: FastifyInstance) => {
});
},
});
};
}
4 changes: 3 additions & 1 deletion src/server/routes/backend-wallet/sign-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getChain } from "../../../shared/utils/chain";
import { createCustomError } from "../../middleware/error";
import { standardResponseSchema } from "../../schemas/shared-api-schemas";
import { walletHeaderSchema } from "../../schemas/wallet";
import { getTransactionCredentials } from "../../../shared/lib/transaction/transaction-credentials";

const requestBodySchema = Type.Object({
message: Type.String(),
Expand Down Expand Up @@ -46,6 +47,7 @@ export async function signMessageRoute(fastify: FastifyInstance) {
const { message, isBytes, chainId } = request.body;
const { "x-backend-wallet-address": walletAddress } =
request.headers as Static<typeof walletHeaderSchema>;
const credentials = getTransactionCredentials(request);

if (isBytes && !isHex(message)) {
throw createCustomError(
Expand All @@ -68,10 +70,10 @@ export async function signMessageRoute(fastify: FastifyInstance) {
}

const chain = chainId ? await getChain(chainId) : arbitrumSepolia;

const { account } = await walletDetailsToAccount({
walletDetails,
chain,
credentials,
});

const messageToSign = isBytes ? { raw: message as Hex } : message;
Expand Down
4 changes: 3 additions & 1 deletion src/server/routes/contract/extensions/erc20/write/mint-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
walletWithAAHeaderSchema,
} from "../../../../../schemas/wallet";
import { getChainIdFromChain } from "../../../../../utils/chain";
import { getTransactionCredentials } from "../../../../../../shared/lib/transaction/transaction-credentials";

// INPUTS
const requestSchema = erc20ContractParamSchema;
Expand Down Expand Up @@ -103,9 +104,10 @@ export async function erc20mintTo(fastify: FastifyInstance) {
accountSalt,
txOverrides,
idempotencyKey,
shouldSimulate: simulateTx,
credentials: getTransactionCredentials(request),
extension: "erc20",
functionName: "mintTo",
shouldSimulate: simulateTx,
});

reply.status(StatusCodes.OK).send({
Expand Down
Loading
Loading