Skip to content

Commit

Permalink
wip: auth for lite mode
Browse files Browse the repository at this point in the history
  • Loading branch information
arcoraven committed Dec 11, 2024
1 parent e4e5dda commit 00e126b
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 112 deletions.
219 changes: 150 additions & 69 deletions src/server/middleware/auth.ts

Large diffs are not rendered by default.

55 changes: 45 additions & 10 deletions src/server/middleware/engine-mode.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,52 @@
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":
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
8 changes: 5 additions & 3 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,8 +42,9 @@ 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();

Expand Down Expand Up @@ -75,13 +77,13 @@ export async function createAccessToken(fastify: FastifyInstance) {
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
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) => {
});
},
});
};
}
2 changes: 2 additions & 0 deletions 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 { getDecryptionPassword } from "../../utils/auth";

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 decryptionPassword = getDecryptionPassword(request);

if (isBytes && !isHex(message)) {
throw createCustomError(
Expand Down
27 changes: 27 additions & 0 deletions src/server/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { FastifyRequest } from "fastify";
import type { AuthenticationType } from "../middleware/auth";
import { createCustomError } from "../middleware/error";
import { StatusCodes } from "http-status-codes";
import { env } from "node:process";

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",
);
}
}

export function getDecryptionPassword(req: FastifyRequest) {
if (env.ENGINE_MODE === "lite" && req.authentication.type === "lite") {
return req.authentication.litePassword;
}
return env.ENCRYPTION_PASSWORD;
}
4 changes: 3 additions & 1 deletion src/shared/utils/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export const getAccount = async (args: {
const { chainId, from, accountAddress } = args;
const chain = await getChain(chainId);

if (accountAddress) return getSmartWalletV5({ chain, accountAddress, from });
if (accountAddress) {
return getSmartWalletV5({ chain, accountAddress, from });
}

// Get from cache.
const cacheKey = getAccountCacheKey({ chainId, from, accountAddress });
Expand Down
2 changes: 1 addition & 1 deletion src/shared/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const env = createEnv({
SEND_TRANSACTION_QUEUE_CONCURRENCY: z.coerce.number().default(200),
CONFIRM_TRANSACTION_QUEUE_CONCURRENCY: z.coerce.number().default(200),
ENGINE_MODE: z
.enum(["default", "sandbox", "server_only", "worker_only"])
.enum(["default", "sandbox", "server_only", "worker_only", "lite"])
.default("default"),
GLOBAL_RATE_LIMIT_PER_MIN: z.coerce.number().default(400 * 60),
DD_TRACER_ACTIVATED: boolEnvSchema(false),
Expand Down

0 comments on commit 00e126b

Please sign in to comment.