Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
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) => {
});
},
});
};
}
1 change: 0 additions & 1 deletion src/server/routes/backend-wallet/sign-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export async function signMessageRoute(fastify: FastifyInstance) {
}

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

const { account } = await walletDetailsToAccount({
walletDetails,
chain,
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
walletWithAAHeaderSchema,
} from "../../../../../schemas/wallet";
import { getChainIdFromChain } from "../../../../../utils/chain";
import { getTransactionCredentials } from "../../../../../../shared/lib/transaction/transaction-credentials";

// INPUTS
const requestSchema = contractParamSchema;
Expand Down Expand Up @@ -79,6 +80,7 @@ export async function erc721claimTo(fastify: FastifyInstance) {
"x-account-factory-address": accountFactoryAddress,
"x-account-salt": accountSalt,
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
const credentials = await getTransactionCredentials(request);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's the start of the /claim-to call @d4mr


const chainId = await getChainIdFromChain(chain);
const contract = await getContractV5({
Expand Down Expand Up @@ -106,6 +108,7 @@ export async function erc721claimTo(fastify: FastifyInstance) {
txOverrides,
idempotencyKey,
shouldSimulate: simulateTx,
credentials,
});

reply.status(StatusCodes.OK).send({
Expand Down
3 changes: 3 additions & 0 deletions src/server/routes/contract/extensions/erc721/write/mint-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
walletWithAAHeaderSchema,
} from "../../../../../schemas/wallet";
import { getChainIdFromChain } from "../../../../../utils/chain";
import { getTransactionCredentials } from "../../../../../../shared/lib/transaction/transaction-credentials";

// INPUTS
const requestSchema = contractParamSchema;
Expand Down Expand Up @@ -79,6 +80,7 @@ export async function erc721mintTo(fastify: FastifyInstance) {
"x-account-factory-address": accountFactoryAddress,
"x-account-salt": accountSalt,
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
const credentials = await getTransactionCredentials(request);

const chainId = await getChainIdFromChain(_chain);
const chain = await getChain(chainId);
Expand Down Expand Up @@ -123,6 +125,7 @@ export async function erc721mintTo(fastify: FastifyInstance) {
extension: "erc721",
functionName: "mintTo",
shouldSimulate: simulateTx,
credentials,
});

reply.status(StatusCodes.OK).send({
Expand Down
2 changes: 2 additions & 0 deletions src/server/routes/system/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const ReplySchemaOk = Type.Object({
]),
),
clientId: Type.String(),
mode: Type.String(),
});

const ReplySchemaError = Type.Object({
Expand Down Expand Up @@ -76,6 +77,7 @@ export async function healthCheck(fastify: FastifyInstance) {
engineTier: env.ENGINE_TIER ?? "SELF_HOSTED",
features: getFeatures(),
clientId: thirdwebClientId,
mode: env.ENGINE_MODE,
});
},
});
Expand Down
19 changes: 19 additions & 0 deletions src/server/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { FastifyRequest } from "fastify";
import type { AuthenticationType } from "../middleware/auth";
import { createCustomError } from "../middleware/error";
import { StatusCodes } from "http-status-codes";

export function assertAuthenticationType<T extends AuthenticationType>(
req: FastifyRequest,
types: T[],
): asserts req is FastifyRequest & {
authentication: { type: T };
} {
if (!types.includes(req.authentication.type as T)) {
throw createCustomError(
`This endpoint requires authentication type: ${types.join(", ")}`,
StatusCodes.FORBIDDEN,
"FORBIDDEN_AUTHENTICATION_TYPE",
);
}
}
Loading