Skip to content

Commit dcd29ca

Browse files
[Engine] Replace vault access token with project secret key for authentication
1 parent 1665e4d commit dcd29ca

File tree

13 files changed

+227
-130
lines changed

13 files changed

+227
-130
lines changed

.changeset/icy-islands-dress.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+
Add encryption utilities

.changeset/loud-mails-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Make vault access token optional

apps/dashboard/src/@/components/project/create-project-modal/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { createProjectClient } from "@/hooks/useApi";
3838
import { useDashboardRouter } from "@/lib/DashboardRouter";
3939
import { projectDomainsSchema, projectNameSchema } from "@/schema/validations";
4040
import { toArrFromList } from "@/utils/string";
41+
import { createVaultAccountAndAccessToken } from "../../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
4142

4243
const ALL_PROJECT_SERVICES = SERVICES.filter(
4344
(srv) => srv.name !== "relayer" && srv.name !== "chainsaw",
@@ -63,6 +64,10 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
6364
<CreateProjectDialogUI
6465
createProject={async (params) => {
6566
const res = await createProjectClient(props.teamId, params);
67+
await createVaultAccountAndAccessToken({
68+
project: res.project,
69+
projectSecretKey: res.secret,
70+
});
6671
return {
6772
project: res.project,
6873
secret: res.secret,

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22
import Link from "next/link";
3-
import { useMemo, useState } from "react";
3+
import { useMemo } from "react";
44
import type { ThirdwebClient } from "thirdweb";
55
import type { Project } from "@/api/projects";
66
import { type Step, StepsCard } from "@/components/blocks/StepsCard";
@@ -22,24 +22,8 @@ interface Props {
2222
}
2323

2424
export const EngineChecklist: React.FC<Props> = (props) => {
25-
const [userAccessToken, setUserAccessToken] = useState<string | undefined>();
26-
2725
const finalSteps = useMemo(() => {
2826
const steps: Step[] = [];
29-
steps.push({
30-
children: (
31-
<CreateVaultAccountStep
32-
onUserAccessTokenCreated={(token) => setUserAccessToken(token)}
33-
project={props.project}
34-
teamSlug={props.teamSlug}
35-
/>
36-
),
37-
completed: !!props.managementAccessToken,
38-
description:
39-
"Vault is thirdweb's key management system. It allows you to create secure server wallets and manage access tokens.",
40-
showCompletedChildren: false,
41-
title: "Create a Vault Admin Account",
42-
});
4327
steps.push({
4428
children: (
4529
<CreateServerWalletStep
@@ -61,7 +45,6 @@ export const EngineChecklist: React.FC<Props> = (props) => {
6145
client={props.client}
6246
project={props.project}
6347
teamSlug={props.teamSlug}
64-
userAccessToken={userAccessToken}
6548
wallets={props.wallets}
6649
/>
6750
),
@@ -78,7 +61,6 @@ export const EngineChecklist: React.FC<Props> = (props) => {
7861
props.project,
7962
props.wallets,
8063
props.hasTransactions,
81-
userAccessToken,
8264
props.teamSlug,
8365
props.client,
8466
]);
@@ -94,7 +76,6 @@ export const EngineChecklist: React.FC<Props> = (props) => {
9476
client={props.client}
9577
project={props.project}
9678
teamSlug={props.teamSlug}
97-
userAccessToken={userAccessToken}
9879
walletId={props.testTxWithWallet}
9980
wallets={props.wallets}
10081
/>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/send-test-tx.client.tsx

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { engineCloudProxy } from "@/actions/proxies";
1111
import type { Project } from "@/api/projects";
1212
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
1313
import { Button } from "@/components/ui/button";
14-
import { CopyTextButton } from "@/components/ui/CopyTextButton";
1514
import { Input } from "@/components/ui/input";
1615
import {
1716
Select,
@@ -24,10 +23,10 @@ import { useAllChainsData } from "@/hooks/chains/allChains";
2423
import { useDashboardRouter } from "@/lib/DashboardRouter";
2524
import type { Wallet } from "../server-wallets/wallet-table/types";
2625
import { SmartAccountCell } from "../server-wallets/wallet-table/wallet-table-ui.client";
27-
import { deleteUserAccessToken, getUserAccessToken } from "./utils";
26+
import { deleteUserAccessToken } from "./utils";
2827

2928
const formSchema = z.object({
30-
accessToken: z.string().min(1, "Access token is required"),
29+
secretKey: z.string().min(1, "Secret key is required"),
3130
chainId: z.number(),
3231
walletIndex: z.string(),
3332
});
@@ -38,7 +37,6 @@ export function SendTestTransaction(props: {
3837
wallets?: Wallet[];
3938
project: Project;
4039
teamSlug: string;
41-
userAccessToken?: string;
4240
expanded?: boolean;
4341
walletId?: string;
4442
client: ThirdwebClient;
@@ -49,12 +47,9 @@ export function SendTestTransaction(props: {
4947

5048
const chainsQuery = useAllChainsData();
5149

52-
const userAccessToken =
53-
props.userAccessToken ?? getUserAccessToken(props.project.id) ?? "";
54-
5550
const form = useForm<FormValues>({
5651
defaultValues: {
57-
accessToken: userAccessToken,
52+
secretKey: "",
5853
chainId: 84532,
5954
walletIndex:
6055
props.wallets && props.walletId
@@ -73,7 +68,7 @@ export function SendTestTransaction(props: {
7368
const sendDummyTxMutation = useMutation({
7469
mutationFn: async (args: {
7570
walletAddress: string;
76-
accessToken: string;
71+
secretKey: string;
7772
chainId: number;
7873
}) => {
7974
const response = await engineCloudProxy({
@@ -93,7 +88,7 @@ export function SendTestTransaction(props: {
9388
"Content-Type": "application/json",
9489
"x-client-id": props.project.publishableKey,
9590
"x-team-id": props.project.teamId,
96-
"x-vault-access-token": args.accessToken,
91+
"x-secret-key": args.secretKey,
9792
},
9893
method: "POST",
9994
pathname: "/v1/write/transaction",
@@ -123,7 +118,7 @@ export function SendTestTransaction(props: {
123118

124119
const onSubmit = async (data: FormValues) => {
125120
await sendDummyTxMutation.mutateAsync({
126-
accessToken: data.accessToken,
121+
secretKey: data.secretKey,
127122
chainId: data.chainId,
128123
walletAddress: selectedWallet.address,
129124
});
@@ -141,44 +136,28 @@ export function SendTestTransaction(props: {
141136
)}
142137
<p className="flex items-center gap-2 text-sm text-warning-text">
143138
<LockIcon className="h-4 w-4" />
144-
{userAccessToken
145-
? "Copy your Vault access token, you'll need it for every HTTP call to Engine."
146-
: "Every wallet action requires your Vault access token."}
139+
Every server wallet action requires your project secret key.
147140
</p>
148141
<div className="h-4" />
149142
{/* Responsive container */}
150143
<div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-2">
151144
<div className="flex-grow">
152145
<div className="flex flex-col gap-2">
153-
<p className="text-sm">Vault Access Token</p>
154-
{userAccessToken ? (
155-
<div className="flex flex-col gap-2 ">
156-
<CopyTextButton
157-
className="!h-auto w-full justify-between bg-background px-3 py-3 font-mono text-xs"
158-
copyIconPosition="right"
159-
textToCopy={userAccessToken}
160-
textToShow={userAccessToken}
161-
tooltip="Copy Vault Access Token"
162-
/>
163-
<p className="text-muted-foreground text-xs">
164-
This is a project-wide access token to access your server
165-
wallets. You can create more access tokens using your admin
166-
key, with granular scopes and permissions.
167-
</p>
168-
</div>
169-
) : (
146+
<p className="text-sm">Project Secret Key</p>
170147
<Input
171-
placeholder="vt_act_1234....ABCD"
172-
type={userAccessToken ? "text" : "password"}
173-
{...form.register("accessToken")}
148+
placeholder="Enter your project secret key"
149+
type={"password"}
150+
{...form.register("secretKey")}
174151
className="text-xs"
175152
disabled={isLoading}
176153
/>
177-
)}
154+
<p className="text-muted-foreground text-xs">
155+
Your project secret key was generated when you created your project. If you lost it, you can regenerate one in the project settings.
156+
</p>
178157
</div>
179158
</div>
180159
</div>
181-
<div className="h-4" />
160+
<div className="h-6" />
182161
{/* Wallet Selector */}
183162
<div className="flex flex-col gap-2">
184163
<div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-2">

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client.ts

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
import {
44
createAccessToken,
5+
createServiceAccount,
56
createVaultClient,
67
type VaultClient,
78
} from "@thirdweb-dev/vault-sdk";
89
import type { Project } from "@/api/projects";
910
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
1011
import { updateProjectClient } from "@/hooks/useApi";
12+
import { encrypt } from "@thirdweb-dev/service-utils";
1113

1214
const SERVER_WALLET_ACCESS_TOKEN_PURPOSE =
1315
"Access Token for All Server Wallets";
@@ -27,6 +29,86 @@ export async function initVaultClient() {
2729
return vc;
2830
}
2931

32+
export async function createVaultAccountAndAccessToken(props: {
33+
project: Project;
34+
projectSecretKey: string;
35+
}) {
36+
try {
37+
const { project, projectSecretKey } = props;
38+
const vaultClient = await initVaultClient();
39+
const serviceAccount = await createServiceAccount({
40+
client: vaultClient,
41+
request: {
42+
options: {
43+
metadata: {
44+
projectId: props.project.id,
45+
purpose: "Thirdweb Project Server Wallet Service Account",
46+
teamId: props.project.teamId,
47+
},
48+
},
49+
},
50+
});
51+
if (serviceAccount.success === false) {
52+
throw new Error(
53+
`Failed to create service account: ${serviceAccount.error}`,
54+
);
55+
}
56+
const adminKey = serviceAccount.data.adminKey;
57+
const rotationCode = serviceAccount.data.rotationCode;
58+
59+
const [managementTokenResult, walletTokenResult] = await Promise.all([
60+
createManagementAccessToken({ project, adminKey, vaultClient }),
61+
createWalletAccessToken({ project, adminKey, vaultClient }),
62+
]);
63+
64+
if (!managementTokenResult.success) {
65+
throw new Error(
66+
`Failed to create management token: ${serviceAccount.error}`,
67+
);
68+
}
69+
70+
if (!walletTokenResult.success) {
71+
throw new Error(
72+
`Failed to create wallet token: ${walletTokenResult.error}`,
73+
);
74+
}
75+
76+
const managementToken = managementTokenResult.data;
77+
const walletToken = walletTokenResult.data;
78+
79+
// encrypt admin key and wallet token with project secret key
80+
const [encryptedAdminKey, encryptedWalletAccessToken] = await Promise.all([
81+
encrypt(adminKey, projectSecretKey),
82+
encrypt(walletToken.accessToken, projectSecretKey),
83+
]);
84+
85+
await updateProjectClient(
86+
{
87+
projectId: props.project.id,
88+
teamId: props.project.teamId,
89+
},
90+
{
91+
services: [
92+
...props.project.services,
93+
{
94+
name: "engineCloud",
95+
actions: [],
96+
managementAccessToken: managementToken.accessToken,
97+
maskedAdminKey: maskSecret(adminKey),
98+
encryptedAdminKey,
99+
encryptedWalletAccessToken,
100+
rotationCode: rotationCode,
101+
},
102+
],
103+
},
104+
);
105+
} catch (error) {
106+
throw new Error(
107+
`Failed to create vault account and access token: ${error}`,
108+
);
109+
}
110+
}
111+
30112
export async function createWalletAccessToken(props: {
31113
project: Project;
32114
adminKey: string;
@@ -246,10 +328,9 @@ export async function createWalletAccessToken(props: {
246328
export async function createManagementAccessToken(props: {
247329
project: Project;
248330
adminKey: string;
249-
rotationCode: string;
250331
vaultClient: VaultClient;
251332
}) {
252-
const res = await createAccessToken({
333+
return createAccessToken({
253334
client: props.vaultClient,
254335
request: {
255336
auth: {
@@ -333,29 +414,6 @@ export async function createManagementAccessToken(props: {
333414
},
334415
},
335416
});
336-
if (res.success) {
337-
const data = res.data;
338-
await updateProjectClient(
339-
{
340-
projectId: props.project.id,
341-
teamId: props.project.teamId,
342-
},
343-
{
344-
services: [
345-
...props.project.services,
346-
{
347-
actions: [],
348-
managementAccessToken: data.accessToken,
349-
maskedAdminKey: maskSecret(props.adminKey),
350-
name: "engineCloud",
351-
rotationCode: props.rotationCode,
352-
},
353-
],
354-
},
355-
);
356-
return res;
357-
}
358-
throw new Error(`Failed to create management access token: ${res.error}`);
359417
}
360418

361419
export function maskSecret(secret: string) {

0 commit comments

Comments
 (0)