Skip to content

Commit

Permalink
add support for custom auth server (#145)
Browse files Browse the repository at this point in the history
* add support for login with custom auth endpoint

* error if using custom auth in react package

* add changesets

* refactor wanderers user fetching
  • Loading branch information
alecananian authored Oct 18, 2024
1 parent b82e78b commit 4f2c50f
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-planets-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@treasure-dev/tdk-react": patch
---

Updated to handle attempts to log in with custom auth endpoint
5 changes: 5 additions & 0 deletions .changeset/orange-turkeys-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@treasure-dev/tdk-core": patch
---

Added support for login with custom auth endpoint
44 changes: 44 additions & 0 deletions apps/api/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,36 @@ import "../middleware/chain";
import "../middleware/swagger";
import type {
LoginBody,
LoginCustomBody,
LoginCustomReply,
LoginReply,
ReadLoginPayloadQuerystring,
ReadLoginPayloadReply,
} from "../schema";
import {
type ErrorReply,
loginBodySchema,
loginCustomBodySchema,
loginCustomReplySchema,
loginReplySchema,
readLoginPayloadQuerystringSchema,
readLoginPayloadReplySchema,
} from "../schema";
import type { TdkApiContext } from "../types";
import { USER_PROFILE_SELECT_FIELDS, USER_SELECT_FIELDS } from "../utils/db";
import { throwUnauthorizedError } from "../utils/error";
import { log } from "../utils/log";
import {
getThirdwebUser,
parseThirdwebUserEmail,
transformUserProfileResponseFields,
} from "../utils/user";
import { validateWanderersUser } from "../utils/wanderers";

type LoginCustomPayload = {
wanderersCookie?: string;
wanderersToken?: string;
};

export const authRoutes =
({
Expand Down Expand Up @@ -193,4 +204,37 @@ export const authRoutes =
});
},
);

app.post<{ Body: LoginCustomBody; Reply: LoginCustomReply | ErrorReply }>(
"/login/custom",
{
schema: {
summary: "Log in with custom auth",
description: "Log in with a custom auth payload",
body: loginCustomBodySchema,
response: {
200: loginCustomReplySchema,
},
},
},
async (req, reply) => {
let payload: LoginCustomPayload | undefined;

try {
payload = JSON.parse(req.body.payload);
} catch (err) {
log.error("Error parsing custom login payload:", err);
}

if (payload?.wanderersCookie || payload?.wanderersToken) {
const user = await validateWanderersUser(
payload.wanderersCookie,
payload.wanderersToken,
);
return reply.send(user);
}

throwUnauthorizedError("Invalid request");
},
);
};
12 changes: 12 additions & 0 deletions apps/api/src/schema/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,21 @@ export const loginReplySchema = Type.Object({
]),
});

export const loginCustomBodySchema = Type.Object({
payload: Type.String(),
});

export const loginCustomReplySchema = Type.Object({
userId: Type.String(),
email: Type.String(),
exp: Type.Number(),
});

export type ReadLoginPayloadQuerystring = Static<
typeof readLoginPayloadQuerystringSchema
>;
export type ReadLoginPayloadReply = Static<typeof readLoginPayloadReplySchema>;
export type LoginBody = Static<typeof loginBodySchema>;
export type LoginReply = Static<typeof loginReplySchema>;
export type LoginCustomBody = Static<typeof loginCustomBodySchema>;
export type LoginCustomReply = Static<typeof loginCustomReplySchema>;
35 changes: 35 additions & 0 deletions apps/api/src/utils/wanderers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { LoginCustomReply } from "../schema";

const WANDERERS_API_URL = "https://id.wanderers.ai/sessions/whoami";

type WanderersSession = {
active: boolean;
expires_at: string;
identity: {
id: string;
traits: {
email: string;
};
};
};

export const validateWanderersUser = async (
cookie?: string,
token?: string,
): Promise<LoginCustomReply | undefined> => {
const response = await fetch(WANDERERS_API_URL, {
headers: cookie ? { Cookie: cookie } : { "X-Session-Token": token ?? "" },
});

const session: WanderersSession = await response.json();
const expiresAt = new Date(session.expires_at).getTime();
if (!session.active || expiresAt < Date.now()) {
return undefined;
}

return {
userId: session.identity.id,
email: session.identity.traits.email,
exp: Math.floor(expiresAt / 1000),
};
};
56 changes: 56 additions & 0 deletions examples/connect-core/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
</button>
</div>
</div>
<p>
or
</p>
<div id="custom-auth-container">
<h2>Connect with Custom Auth</h2>
<div>
<input
id="custom-auth-key"
type="text"
placeholder="Auth key name"
/>
<input
id="custom-auth-value"
type="text"
placeholder="Auth value"
/>
<button type="button">
Connect
</button>
</div>
</div>
</div>
<div id="user-container">
<h2>Logged in as <span id="user-email" /></h2>
Expand Down Expand Up @@ -86,6 +107,13 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
const emailButton = emailContainer.querySelector<HTMLButtonElement>("button");
const codeInput = codeContainer.querySelector<HTMLInputElement>("input")!;
const codeButton = codeContainer.querySelector<HTMLButtonElement>("button");
const customAuthKeyInput =
document.querySelector<HTMLInputElement>("#custom-auth-key")!;
const customAuthValueInput =
document.querySelector<HTMLInputElement>("#custom-auth-value")!;
const customAuthButton = document.querySelector<HTMLButtonElement>(
"#custom-auth-container button",
);
const mintButton = userContainer.querySelector<HTMLButtonElement>("#mint");
const logOutButton =
userContainer.querySelector<HTMLButtonElement>("#log-out");
Expand Down Expand Up @@ -169,6 +197,34 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
codeButton.disabled = false;
});

// Set up Connect with Custom Auth flow
customAuthButton?.addEventListener("click", async () => {
customAuthButton.disabled = true;
try {
const result = await logIn({
client,
ecosystemId: import.meta.env.VITE_TDK_ECOSYSTEM_ID,
ecosystemPartnerId: import.meta.env.VITE_TDK_ECOSYSTEM_PARTNER_ID,
method: "auth_endpoint",
payload: JSON.stringify({
[customAuthKeyInput.value]: customAuthValueInput.value,
}),
apiUri,
chainId,
sessionOptions,
});
tdk = result.tdk;
user = result.user;
userEmail.innerHTML = user.email || user.id;
connectContainer.hidden = true;
userContainer.hidden = false;
} catch (err) {
console.error("Error logging in with email:", err);
}

customAuthButton.disabled = false;
});

// Set up Mint button
mintButton?.addEventListener("click", async () => {
mintButton.disabled = true;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dev:launcher": "pnpm --filter ./packages/launcher dev",
"dev:api": "pnpm --filter ./apps/api dev",
"dev:react": "pnpm --filter ./packages/react dev",
"dev:connect-core": "pnpm --filter ./examples/connect-core dev",
"dev:connect-electron": "pnpm --filter ./examples/connect-electron dev",
"dev:connect-react": "pnpm --filter ./examples/connect-react dev",
"start:api": "pnpm --filter ./apps/api start",
Expand Down
22 changes: 20 additions & 2 deletions packages/core/src/connect/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export type ConnectMethod =
| (typeof SUPPORTED_SOCIAL_OPTIONS)[number]
| "email"
| "passkey"
| "wallet";
| "wallet"
| "auth_endpoint";

type ConnectWalletConfig = {
client: TreasureConnectClient;
Expand All @@ -70,6 +71,10 @@ type ConnectWalletConfig = {
passkeyName?: string;
hasStoredPasskey?: boolean;
}
| {
method: "auth_endpoint";
payload: string;
}
);

export const isSocialConnectMethod = (method: ConnectMethod) =>
Expand Down Expand Up @@ -115,6 +120,15 @@ export const connectEcosystemWallet = async (params: ConnectWalletConfig) => {
type: hasPasskey ? "sign-in" : "sign-up",
passkeyName: params.passkeyName,
});
} else if (params.method === "auth_endpoint") {
// Connect with auth endpoint
await wallet.connect({
client,
chain,
strategy: "auth_endpoint",
payload: params.payload,
encryptionKey: "any", // Unused with enclave ecosystem wallets
});
} else {
// Connect with social
await wallet.connect({
Expand Down Expand Up @@ -214,7 +228,11 @@ export const logIn = async (params: ConnectWalletConfig & ConnectConfig) => {
...connectWalletParams
} = params;

const wallet = await connectWallet({ client, ...connectWalletParams });
const wallet = await connectWallet({
client,
chainId,
...connectWalletParams,
});

const tdk = new TDKAPI({
baseUri: apiUri,
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/components/connect/ConnectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ export const ConnectModal = ({
setError(err);
onConnectError?.(method, err);
}
} else if (method === "auth_endpoint") {
throw new Error(
"Auth endpoint not supported in Treasure Connect modal. Use TDK Core package to connect.",
);
} else {
// Handle connecting with social / passkey
try {
Expand Down

0 comments on commit 4f2c50f

Please sign in to comment.