Skip to content

Commit 4f2c50f

Browse files
authored
add support for custom auth server (#145)
* add support for login with custom auth endpoint * error if using custom auth in react package * add changesets * refactor wanderers user fetching
1 parent b82e78b commit 4f2c50f

File tree

9 files changed

+182
-2
lines changed

9 files changed

+182
-2
lines changed

.changeset/empty-planets-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@treasure-dev/tdk-react": patch
3+
---
4+
5+
Updated to handle attempts to log in with custom auth endpoint

.changeset/orange-turkeys-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@treasure-dev/tdk-core": patch
3+
---
4+
5+
Added support for login with custom auth endpoint

apps/api/src/routes/auth.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,36 @@ import "../middleware/chain";
99
import "../middleware/swagger";
1010
import type {
1111
LoginBody,
12+
LoginCustomBody,
13+
LoginCustomReply,
1214
LoginReply,
1315
ReadLoginPayloadQuerystring,
1416
ReadLoginPayloadReply,
1517
} from "../schema";
1618
import {
1719
type ErrorReply,
1820
loginBodySchema,
21+
loginCustomBodySchema,
22+
loginCustomReplySchema,
1923
loginReplySchema,
2024
readLoginPayloadQuerystringSchema,
2125
readLoginPayloadReplySchema,
2226
} from "../schema";
2327
import type { TdkApiContext } from "../types";
2428
import { USER_PROFILE_SELECT_FIELDS, USER_SELECT_FIELDS } from "../utils/db";
29+
import { throwUnauthorizedError } from "../utils/error";
2530
import { log } from "../utils/log";
2631
import {
2732
getThirdwebUser,
2833
parseThirdwebUserEmail,
2934
transformUserProfileResponseFields,
3035
} from "../utils/user";
36+
import { validateWanderersUser } from "../utils/wanderers";
37+
38+
type LoginCustomPayload = {
39+
wanderersCookie?: string;
40+
wanderersToken?: string;
41+
};
3142

3243
export const authRoutes =
3344
({
@@ -193,4 +204,37 @@ export const authRoutes =
193204
});
194205
},
195206
);
207+
208+
app.post<{ Body: LoginCustomBody; Reply: LoginCustomReply | ErrorReply }>(
209+
"/login/custom",
210+
{
211+
schema: {
212+
summary: "Log in with custom auth",
213+
description: "Log in with a custom auth payload",
214+
body: loginCustomBodySchema,
215+
response: {
216+
200: loginCustomReplySchema,
217+
},
218+
},
219+
},
220+
async (req, reply) => {
221+
let payload: LoginCustomPayload | undefined;
222+
223+
try {
224+
payload = JSON.parse(req.body.payload);
225+
} catch (err) {
226+
log.error("Error parsing custom login payload:", err);
227+
}
228+
229+
if (payload?.wanderersCookie || payload?.wanderersToken) {
230+
const user = await validateWanderersUser(
231+
payload.wanderersCookie,
232+
payload.wanderersToken,
233+
);
234+
return reply.send(user);
235+
}
236+
237+
throwUnauthorizedError("Invalid request");
238+
},
239+
);
196240
};

apps/api/src/schema/auth.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,21 @@ export const loginReplySchema = Type.Object({
4949
]),
5050
});
5151

52+
export const loginCustomBodySchema = Type.Object({
53+
payload: Type.String(),
54+
});
55+
56+
export const loginCustomReplySchema = Type.Object({
57+
userId: Type.String(),
58+
email: Type.String(),
59+
exp: Type.Number(),
60+
});
61+
5262
export type ReadLoginPayloadQuerystring = Static<
5363
typeof readLoginPayloadQuerystringSchema
5464
>;
5565
export type ReadLoginPayloadReply = Static<typeof readLoginPayloadReplySchema>;
5666
export type LoginBody = Static<typeof loginBodySchema>;
5767
export type LoginReply = Static<typeof loginReplySchema>;
68+
export type LoginCustomBody = Static<typeof loginCustomBodySchema>;
69+
export type LoginCustomReply = Static<typeof loginCustomReplySchema>;

apps/api/src/utils/wanderers.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { LoginCustomReply } from "../schema";
2+
3+
const WANDERERS_API_URL = "https://id.wanderers.ai/sessions/whoami";
4+
5+
type WanderersSession = {
6+
active: boolean;
7+
expires_at: string;
8+
identity: {
9+
id: string;
10+
traits: {
11+
email: string;
12+
};
13+
};
14+
};
15+
16+
export const validateWanderersUser = async (
17+
cookie?: string,
18+
token?: string,
19+
): Promise<LoginCustomReply | undefined> => {
20+
const response = await fetch(WANDERERS_API_URL, {
21+
headers: cookie ? { Cookie: cookie } : { "X-Session-Token": token ?? "" },
22+
});
23+
24+
const session: WanderersSession = await response.json();
25+
const expiresAt = new Date(session.expires_at).getTime();
26+
if (!session.active || expiresAt < Date.now()) {
27+
return undefined;
28+
}
29+
30+
return {
31+
userId: session.identity.id,
32+
email: session.identity.traits.email,
33+
exp: Math.floor(expiresAt / 1000),
34+
};
35+
};

examples/connect-core/src/main.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,27 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
5757
</button>
5858
</div>
5959
</div>
60+
<p>
61+
or
62+
</p>
63+
<div id="custom-auth-container">
64+
<h2>Connect with Custom Auth</h2>
65+
<div>
66+
<input
67+
id="custom-auth-key"
68+
type="text"
69+
placeholder="Auth key name"
70+
/>
71+
<input
72+
id="custom-auth-value"
73+
type="text"
74+
placeholder="Auth value"
75+
/>
76+
<button type="button">
77+
Connect
78+
</button>
79+
</div>
80+
</div>
6081
</div>
6182
<div id="user-container">
6283
<h2>Logged in as <span id="user-email" /></h2>
@@ -86,6 +107,13 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
86107
const emailButton = emailContainer.querySelector<HTMLButtonElement>("button");
87108
const codeInput = codeContainer.querySelector<HTMLInputElement>("input")!;
88109
const codeButton = codeContainer.querySelector<HTMLButtonElement>("button");
110+
const customAuthKeyInput =
111+
document.querySelector<HTMLInputElement>("#custom-auth-key")!;
112+
const customAuthValueInput =
113+
document.querySelector<HTMLInputElement>("#custom-auth-value")!;
114+
const customAuthButton = document.querySelector<HTMLButtonElement>(
115+
"#custom-auth-container button",
116+
);
89117
const mintButton = userContainer.querySelector<HTMLButtonElement>("#mint");
90118
const logOutButton =
91119
userContainer.querySelector<HTMLButtonElement>("#log-out");
@@ -169,6 +197,34 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
169197
codeButton.disabled = false;
170198
});
171199

200+
// Set up Connect with Custom Auth flow
201+
customAuthButton?.addEventListener("click", async () => {
202+
customAuthButton.disabled = true;
203+
try {
204+
const result = await logIn({
205+
client,
206+
ecosystemId: import.meta.env.VITE_TDK_ECOSYSTEM_ID,
207+
ecosystemPartnerId: import.meta.env.VITE_TDK_ECOSYSTEM_PARTNER_ID,
208+
method: "auth_endpoint",
209+
payload: JSON.stringify({
210+
[customAuthKeyInput.value]: customAuthValueInput.value,
211+
}),
212+
apiUri,
213+
chainId,
214+
sessionOptions,
215+
});
216+
tdk = result.tdk;
217+
user = result.user;
218+
userEmail.innerHTML = user.email || user.id;
219+
connectContainer.hidden = true;
220+
userContainer.hidden = false;
221+
} catch (err) {
222+
console.error("Error logging in with email:", err);
223+
}
224+
225+
customAuthButton.disabled = false;
226+
});
227+
172228
// Set up Mint button
173229
mintButton?.addEventListener("click", async () => {
174230
mintButton.disabled = true;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dev:launcher": "pnpm --filter ./packages/launcher dev",
1616
"dev:api": "pnpm --filter ./apps/api dev",
1717
"dev:react": "pnpm --filter ./packages/react dev",
18+
"dev:connect-core": "pnpm --filter ./examples/connect-core dev",
1819
"dev:connect-electron": "pnpm --filter ./examples/connect-electron dev",
1920
"dev:connect-react": "pnpm --filter ./examples/connect-react dev",
2021
"start:api": "pnpm --filter ./apps/api start",

packages/core/src/connect/login.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ export type ConnectMethod =
4747
| (typeof SUPPORTED_SOCIAL_OPTIONS)[number]
4848
| "email"
4949
| "passkey"
50-
| "wallet";
50+
| "wallet"
51+
| "auth_endpoint";
5152

5253
type ConnectWalletConfig = {
5354
client: TreasureConnectClient;
@@ -70,6 +71,10 @@ type ConnectWalletConfig = {
7071
passkeyName?: string;
7172
hasStoredPasskey?: boolean;
7273
}
74+
| {
75+
method: "auth_endpoint";
76+
payload: string;
77+
}
7378
);
7479

7580
export const isSocialConnectMethod = (method: ConnectMethod) =>
@@ -115,6 +120,15 @@ export const connectEcosystemWallet = async (params: ConnectWalletConfig) => {
115120
type: hasPasskey ? "sign-in" : "sign-up",
116121
passkeyName: params.passkeyName,
117122
});
123+
} else if (params.method === "auth_endpoint") {
124+
// Connect with auth endpoint
125+
await wallet.connect({
126+
client,
127+
chain,
128+
strategy: "auth_endpoint",
129+
payload: params.payload,
130+
encryptionKey: "any", // Unused with enclave ecosystem wallets
131+
});
118132
} else {
119133
// Connect with social
120134
await wallet.connect({
@@ -214,7 +228,11 @@ export const logIn = async (params: ConnectWalletConfig & ConnectConfig) => {
214228
...connectWalletParams
215229
} = params;
216230

217-
const wallet = await connectWallet({ client, ...connectWalletParams });
231+
const wallet = await connectWallet({
232+
client,
233+
chainId,
234+
...connectWalletParams,
235+
});
218236

219237
const tdk = new TDKAPI({
220238
baseUri: apiUri,

packages/react/src/components/connect/ConnectModal.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ export const ConnectModal = ({
214214
setError(err);
215215
onConnectError?.(method, err);
216216
}
217+
} else if (method === "auth_endpoint") {
218+
throw new Error(
219+
"Auth endpoint not supported in Treasure Connect modal. Use TDK Core package to connect.",
220+
);
217221
} else {
218222
// Handle connecting with social / passkey
219223
try {

0 commit comments

Comments
 (0)