Skip to content

Commit 70b7b5c

Browse files
committed
[SDK]: add backend wallets for in app wallet (#5878)
--- title: "[SDK] Feature: add backend wallets for in app wallet" --- https://linear.app/thirdweb/issue/TOOL-2884/backend-server-wallets ## Notes for the reviewer This also fixes the cache mechanics for thirdweb clients to include a partial string of the secret key. Was running into an issue where the client was originall instantiated without the secret key and even when a new client with secret key was passed in, we keep falling back to the cached client without secret key ## How to test Add a button on playground with a client that has secret key and create a new backend wallet. <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces support for backend wallets in the `thirdweb` library, allowing applications to authenticate users with a backend strategy, enhancing security by managing wallet access without exposing private keys. ### Detailed summary - Added `backend` strategy for wallet authentication. - Updated `authenticate` function to support `backend` strategy. - Introduced `backendAuthenticate` function for backend authentication. - Updated `InAppWebConnector` and `InAppNativeConnector` to handle `backend` strategy. - Modified type definitions to include `backend` strategy. - Added tests for `backendAuthenticate` and its integration in connectors. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 5e2eec3 commit 70b7b5c

File tree

12 files changed

+360
-2
lines changed

12 files changed

+360
-2
lines changed

.changeset/popular-ravens-reflect.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Add support for backend wallets.
6+
7+
This is useful is you have a backend that is connected to an that you want to have programmatic access to a wallet without managing private keys.
8+
9+
Here's how you'd do it:
10+
11+
```typescript
12+
const wallet = inAppWallet();
13+
const account = await wallet.connect({
14+
strategy: "backend",
15+
client: createThirdwebClient({
16+
secretKey: "...",
17+
}),
18+
walletSecret: "...",
19+
});
20+
console.log("account.address", account.address);
21+
```
22+
23+
Note that `walletSecret` should be generated by you and securely stored to uniquely authenticate to the given wallet.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { TEST_CLIENT } from "~test/test-clients.js";
3+
import { getClientFetch } from "../../../../utils/fetch.js";
4+
import { stringify } from "../../../../utils/json.js";
5+
import { backendAuthenticate } from "./backend.js";
6+
import { getLoginUrl } from "./getLoginPath.js";
7+
8+
// Mock dependencies
9+
vi.mock("../../../../utils/fetch.js");
10+
vi.mock("./getLoginPath.js");
11+
12+
describe("backendAuthenticate", () => {
13+
it("should successfully authenticate and return token", async () => {
14+
// Mock response data
15+
const mockResponse = {
16+
token: "mock-token",
17+
cookieString: "mock-cookie",
18+
};
19+
20+
// Mock fetch implementation
21+
const mockFetch = vi.fn().mockResolvedValue({
22+
ok: true,
23+
json: () => Promise.resolve(mockResponse),
24+
});
25+
26+
// Mock dependencies
27+
vi.mocked(getClientFetch).mockReturnValue(mockFetch);
28+
vi.mocked(getLoginUrl).mockReturnValue("/auth/login");
29+
30+
const result = await backendAuthenticate({
31+
client: TEST_CLIENT,
32+
walletSecret: "test-secret",
33+
});
34+
35+
// Verify the fetch call
36+
expect(mockFetch).toHaveBeenCalledWith("/auth/login", {
37+
method: "POST",
38+
headers: {
39+
"Content-Type": "application/json",
40+
},
41+
body: stringify({ walletSecret: "test-secret" }),
42+
});
43+
44+
// Verify response
45+
expect(result).toEqual(mockResponse);
46+
});
47+
48+
it("should throw error when authentication fails", async () => {
49+
// Mock failed fetch response
50+
const mockFetch = vi.fn().mockResolvedValue({
51+
ok: false,
52+
});
53+
54+
// Mock dependencies
55+
vi.mocked(getClientFetch).mockReturnValue(mockFetch);
56+
vi.mocked(getLoginUrl).mockReturnValue("/auth/login");
57+
58+
// Test inputs
59+
const args = {
60+
client: TEST_CLIENT,
61+
walletSecret: "test-secret",
62+
};
63+
64+
// Verify error is thrown
65+
await expect(backendAuthenticate(args)).rejects.toThrow(
66+
"Failed to generate backend account",
67+
);
68+
});
69+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { ThirdwebClient } from "../../../../client/client.js";
2+
import { getClientFetch } from "../../../../utils/fetch.js";
3+
import { stringify } from "../../../../utils/json.js";
4+
import type { Ecosystem } from "../wallet/types.js";
5+
import { getLoginUrl } from "./getLoginPath.js";
6+
import type { AuthStoredTokenWithCookieReturnType } from "./types.js";
7+
8+
/**
9+
* Authenticates via the wallet secret
10+
* @internal
11+
*/
12+
export async function backendAuthenticate(args: {
13+
client: ThirdwebClient;
14+
walletSecret: string;
15+
ecosystem?: Ecosystem;
16+
}): Promise<AuthStoredTokenWithCookieReturnType> {
17+
const clientFetch = getClientFetch(args.client, args.ecosystem);
18+
const path = getLoginUrl({
19+
authOption: "backend",
20+
client: args.client,
21+
ecosystem: args.ecosystem,
22+
});
23+
const res = await clientFetch(`${path}`, {
24+
method: "POST",
25+
headers: {
26+
"Content-Type": "application/json",
27+
},
28+
body: stringify({
29+
walletSecret: args.walletSecret,
30+
}),
31+
});
32+
33+
if (!res.ok) throw new Error("Failed to generate backend account");
34+
35+
return (await res.json()) satisfies AuthStoredTokenWithCookieReturnType;
36+
}

packages/thirdweb/src/wallets/in-app/core/authentication/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ export type SingleStepAuthArgsType =
7474
}
7575
| {
7676
strategy: "guest";
77-
client: ThirdwebClient;
77+
}
78+
| {
79+
strategy: "backend";
80+
walletSecret: string;
7881
};
7982

8083
export type AuthArgsType = (MultiStepAuthArgsType | SingleStepAuthArgsType) & {
@@ -89,6 +92,7 @@ type RecoveryShareManagement = "USER_MANAGED" | "AWS_MANAGED" | "ENCLAVE";
8992
export type AuthProvider =
9093
| "Cognito"
9194
| "Guest"
95+
| "Backend"
9296
| "Google"
9397
| "EmailOtp"
9498
| "CustomJWT"

packages/thirdweb/src/wallets/in-app/core/wallet/in-app-core.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ export async function getOrCreateInAppWalletConnector(
2626
connectorFactory: (client: ThirdwebClient) => Promise<InAppConnector>,
2727
ecosystem?: Ecosystem,
2828
) {
29-
const key = stringify({ clientId: client.clientId, ecosystem });
29+
const key = stringify({
30+
clientId: client.clientId,
31+
ecosystem,
32+
partialSecretKey: client.secretKey?.slice(0, 5),
33+
});
3034
if (connectorCache.has(key)) {
3135
return connectorCache.get(key) as InAppConnector;
3236
}

packages/thirdweb/src/wallets/in-app/native/auth/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,17 @@ export async function preAuthenticate(args: PreAuthArgsType) {
133133
* verificationCode: "123456",
134134
* });
135135
* ```
136+
*
137+
* Authenticate to a backend account (only do this on your backend):
138+
* ```ts
139+
* import { authenticate } from "thirdweb/wallets/in-app";
140+
*
141+
* const result = await authenticate({
142+
* client,
143+
* strategy: "backend",
144+
* walletSecret: "...", // Provided by your app
145+
* });
146+
* ```
136147
* @wallet
137148
*/
138149
export async function authenticate(args: AuthArgsType) {

packages/thirdweb/src/wallets/in-app/native/native-connector.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { nativeLocalStorage } from "../../../utils/storage/nativeStorage.js";
44
import type { Account } from "../../interfaces/wallet.js";
55
import { getUserStatus } from "../core/actions/get-enclave-user-status.js";
66
import { authEndpoint } from "../core/authentication/authEndpoint.js";
7+
import { backendAuthenticate } from "../core/authentication/backend.js";
78
import { ClientScopedStorage } from "../core/authentication/client-scoped-storage.js";
89
import { guestAuthenticate } from "../core/authentication/guest.js";
910
import { customJwt } from "../core/authentication/jwt.js";
@@ -171,6 +172,13 @@ export class InAppNativeConnector implements InAppConnector {
171172
storage: nativeLocalStorage,
172173
});
173174
}
175+
case "backend": {
176+
return backendAuthenticate({
177+
client: this.client,
178+
walletSecret: params.walletSecret,
179+
ecosystem: params.ecosystem,
180+
});
181+
}
174182
case "wallet": {
175183
return siweAuthenticate({
176184
client: this.client,
@@ -179,6 +187,11 @@ export class InAppNativeConnector implements InAppConnector {
179187
ecosystem: params.ecosystem,
180188
});
181189
}
190+
case "github":
191+
case "twitch":
192+
case "steam":
193+
case "farcaster":
194+
case "telegram":
182195
case "google":
183196
case "facebook":
184197
case "discord":

packages/thirdweb/src/wallets/in-app/web/in-app.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,20 @@ import { createInAppWallet } from "../core/wallet/in-app-core.js";
139139
* });
140140
* ```
141141
*
142+
* ### Connect to a backend account
143+
*
144+
* ```ts
145+
* import { inAppWallet } from "thirdweb/wallets";
146+
*
147+
* const wallet = inAppWallet();
148+
*
149+
* const account = await wallet.connect({
150+
* client,
151+
* strategy: "backend",
152+
* walletSecret: "...", // Provided by your app
153+
* });
154+
* ```
155+
*
142156
* ### Connect with custom JWT (any OIDC provider)
143157
*
144158
* You can use any OIDC provider to authenticate your users. Make sure to configure it in your dashboard under in-app wallet settings.

packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ export async function preAuthenticate(args: PreAuthArgsType) {
140140
* verificationCode: "123456",
141141
* });
142142
* ```
143+
*
144+
* Authenticate to a backend account (only do this on your backend):
145+
* ```ts
146+
* import { authenticate } from "thirdweb/wallets/in-app";
147+
*
148+
* const result = await authenticate({
149+
* client,
150+
* strategy: "backend",
151+
* walletSecret: "...", // Provided by your app
152+
* });
153+
* ```
143154
* @wallet
144155
*/
145156
export async function authenticate(args: AuthArgsType) {

0 commit comments

Comments
 (0)