Skip to content

Commit 585eac2

Browse files
[Dashboard] Replace vault access token with project secret key for authentication (#7570)
1 parent c54db52 commit 585eac2

File tree

23 files changed

+577
-304
lines changed

23 files changed

+577
-304
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/@/hooks/useApi.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
22
import { useActiveAccount } from "thirdweb/react";
33
import { apiServerProxy } from "@/actions/proxies";
44
import type { Project } from "@/api/projects";
5+
import { createVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
56
import { accountKeys, authorizedWallets } from "../query-keys/cache-keys";
67

78
// FIXME: We keep repeating types, API server should provide them
@@ -311,26 +312,35 @@ export type RotateSecretKeyAPIReturnType = {
311312
data: {
312313
secret: string;
313314
secretMasked: string;
315+
secretHash: string;
314316
};
315317
};
316318

317-
export async function rotateSecretKeyClient(params: {
318-
teamId: string;
319-
projectId: string;
320-
}) {
319+
export async function rotateSecretKeyClient(params: { project: Project }) {
321320
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
322321
body: JSON.stringify({}),
323322
headers: {
324323
"Content-Type": "application/json",
325324
},
326325
method: "POST",
327-
pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}/rotate-secret-key`,
326+
pathname: `/v1/teams/${params.project.teamId}/projects/${params.project.id}/rotate-secret-key`,
328327
});
329328

330329
if (!res.ok) {
331330
throw new Error(res.error);
332331
}
333332

333+
try {
334+
// if the project has a vault admin key, rotate it as well
335+
await createVaultAccountAndAccessToken({
336+
project: params.project,
337+
projectSecretKey: res.data.data.secret,
338+
projectSecretHash: res.data.data.secretHash,
339+
});
340+
} catch (error) {
341+
console.error("Failed to rotate vault admin key", error);
342+
}
343+
334344
return res.data;
335345
}
336346

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,8 @@ function IntegrateAPIKeySection({
6262
<ClientIDSection clientId={clientId} />
6363
{secretKeyMasked && (
6464
<SecretKeySection
65-
projectId={project.id}
65+
project={project}
6666
secretKeyMasked={secretKeyMasked}
67-
teamId={project.teamId}
6867
/>
6968
)}
7069
</div>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"use client";
22

33
import { useState } from "react";
4+
import type { Project } from "@/api/projects";
45
import { rotateSecretKeyClient } from "@/hooks/useApi";
56
import { RotateSecretKeyButton } from "../../settings/ProjectGeneralSettingsPage";
67

78
export function SecretKeySection(props: {
89
secretKeyMasked: string;
9-
teamId: string;
10-
projectId: string;
10+
project: Project;
1111
}) {
1212
const [secretKeyMasked, setSecretKeyMasked] = useState(props.secretKeyMasked);
1313

@@ -31,8 +31,7 @@ export function SecretKeySection(props: {
3131
}}
3232
rotateSecretKey={async () => {
3333
return rotateSecretKeyClient({
34-
projectId: props.projectId,
35-
teamId: props.teamId,
34+
project: props.project,
3635
});
3736
}}
3837
/>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function Story(props: { isOwnerAccount: boolean }) {
4747
data: {
4848
secret: `sk_${new Array(86).fill("x").join("")}`,
4949
secretMasked: "sk_123...4567",
50+
secretHash: "sk_123...4567",
5051
},
5152
};
5253
}}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@ export function ProjectGeneralSettingsPage(props: {
135135
project={props.project}
136136
rotateSecretKey={async () => {
137137
return rotateSecretKeyClient({
138-
projectId: props.project.id,
139-
teamId: props.project.teamId,
138+
project: props.project,
140139
});
141140
}}
142141
showNebulaSettings={props.showNebulaSettings}

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

Lines changed: 7 additions & 28 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";
@@ -9,7 +9,6 @@ import { CreateVaultAccountButton } from "../../vault/components/create-vault-ac
99
import CreateServerWallet from "../server-wallets/components/create-server-wallet.client";
1010
import type { Wallet } from "../server-wallets/wallet-table/types";
1111
import { SendTestTransaction } from "./send-test-tx.client";
12-
import { deleteUserAccessToken } from "./utils";
1312

1413
interface Props {
1514
managementAccessToken: string | undefined;
@@ -19,27 +18,12 @@ interface Props {
1918
teamSlug: string;
2019
testTxWithWallet?: string | undefined;
2120
client: ThirdwebClient;
21+
isManagedVault: boolean;
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
@@ -48,7 +32,7 @@ export const EngineChecklist: React.FC<Props> = (props) => {
4832
teamSlug={props.teamSlug}
4933
/>
5034
),
51-
completed: props.wallets.length > 0,
35+
completed: props.wallets.length > 0 || props.hasTransactions,
5236
description:
5337
"Server wallets are smart wallets, they don't require any gas funds to send transactions.",
5438
showCompletedChildren: false,
@@ -58,10 +42,10 @@ export const EngineChecklist: React.FC<Props> = (props) => {
5842
steps.push({
5943
children: (
6044
<SendTestTransaction
45+
isManagedVault={props.isManagedVault}
6146
client={props.client}
6247
project={props.project}
6348
teamSlug={props.teamSlug}
64-
userAccessToken={userAccessToken}
6549
wallets={props.wallets}
6650
/>
6751
),
@@ -78,9 +62,9 @@ export const EngineChecklist: React.FC<Props> = (props) => {
7862
props.project,
7963
props.wallets,
8064
props.hasTransactions,
81-
userAccessToken,
8265
props.teamSlug,
8366
props.client,
67+
props.isManagedVault,
8468
]);
8569

8670
const isComplete = useMemo(
@@ -91,19 +75,17 @@ export const EngineChecklist: React.FC<Props> = (props) => {
9175
if (props.testTxWithWallet) {
9276
return (
9377
<SendTestTransaction
78+
isManagedVault={props.isManagedVault}
9479
client={props.client}
9580
project={props.project}
9681
teamSlug={props.teamSlug}
97-
userAccessToken={userAccessToken}
9882
walletId={props.testTxWithWallet}
9983
wallets={props.wallets}
10084
/>
10185
);
10286
}
10387

10488
if (finalSteps.length === 0 || isComplete) {
105-
// clear token from local storage after FTUX is complete
106-
deleteUserAccessToken(props.project.id);
10789
return null;
10890
}
10991
return (
@@ -122,10 +104,7 @@ function CreateVaultAccountStep(props: {
122104
}) {
123105
return (
124106
<div className="mt-4 flex flex-row gap-4">
125-
<CreateVaultAccountButton
126-
onUserAccessTokenCreated={props.onUserAccessTokenCreated}
127-
project={props.project}
128-
/>
107+
<CreateVaultAccountButton project={props.project} />
129108
<Button asChild variant="outline">
130109
<Link
131110
href="https://portal.thirdweb.com/vault"

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

Lines changed: 32 additions & 42 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,9 @@ 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";
2826

2927
const formSchema = z.object({
30-
accessToken: z.string().min(1, "Access token is required"),
28+
secretKey: z.string().min(1, "Secret key is required"),
3129
chainId: z.number(),
3230
walletIndex: z.string(),
3331
});
@@ -38,9 +36,9 @@ export function SendTestTransaction(props: {
3836
wallets?: Wallet[];
3937
project: Project;
4038
teamSlug: string;
41-
userAccessToken?: string;
4239
expanded?: boolean;
4340
walletId?: string;
41+
isManagedVault: boolean;
4442
client: ThirdwebClient;
4543
}) {
4644
const queryClient = useQueryClient();
@@ -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,9 @@ 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+
...(props.isManagedVault
92+
? { "x-secret-key": args.secretKey }
93+
: { "x-vault-access-token": args.secretKey }),
9794
},
9895
method: "POST",
9996
pathname: "/v1/write/transaction",
@@ -123,7 +120,7 @@ export function SendTestTransaction(props: {
123120

124121
const onSubmit = async (data: FormValues) => {
125122
await sendDummyTxMutation.mutateAsync({
126-
accessToken: data.accessToken,
123+
secretKey: data.secretKey,
127124
chainId: data.chainId,
128125
walletAddress: selectedWallet.address,
129126
});
@@ -141,44 +138,39 @@ export function SendTestTransaction(props: {
141138
)}
142139
<p className="flex items-center gap-2 text-sm text-warning-text">
143140
<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."}
141+
Every server wallet action requires your{" "}
142+
{props.isManagedVault ? "project secret key" : "vault access token"}.
147143
</p>
148144
<div className="h-4" />
149145
{/* Responsive container */}
150146
<div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-2">
151147
<div className="flex-grow">
152148
<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-
) : (
170-
<Input
171-
placeholder="vt_act_1234....ABCD"
172-
type={userAccessToken ? "text" : "password"}
173-
{...form.register("accessToken")}
174-
className="text-xs"
175-
disabled={isLoading}
176-
/>
177-
)}
149+
<p className="text-sm">
150+
{props.isManagedVault
151+
? "Project Secret Key"
152+
: "Vault Access Token"}
153+
</p>
154+
<Input
155+
placeholder={
156+
props.isManagedVault
157+
? "Enter your project secret key"
158+
: "Enter your vault access token"
159+
}
160+
type={"password"}
161+
{...form.register("secretKey")}
162+
className="text-xs"
163+
disabled={isLoading}
164+
/>
165+
<p className="text-muted-foreground text-xs">
166+
{props.isManagedVault
167+
? "Your project secret key was generated when you created your project. If you lost it, you can regenerate one in the project settings."
168+
: "Your vault access token was generated when you created your vault. If you lost it, you can regenerate one in the vault settings."}
169+
</p>
178170
</div>
179171
</div>
180172
</div>
181-
<div className="h-4" />
173+
<div className="h-6" />
182174
{/* Wallet Selector */}
183175
<div className="flex flex-col gap-2">
184176
<div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-2">
@@ -268,8 +260,6 @@ export function SendTestTransaction(props: {
268260
} else {
269261
router.refresh();
270262
}
271-
// clear token from local storage after FTUX is complete
272-
deleteUserAccessToken(props.project.id);
273263
}}
274264
variant="primary"
275265
>

0 commit comments

Comments
 (0)