From 67563eaf87b1509bc19e5fed25bd3659990b558e Mon Sep 17 00:00:00 2001
From: Winston Yeo <44563205+ElasticBottle@users.noreply.github.com>
Date: Thu, 14 Dec 2023 10:46:26 -0800
Subject: [PATCH] docs: add custom auth docs (#1117)
* docs: add custom auth docs
* chore: update slugs
* chore: remove dup import
* chore: fix user formatting
* chore: update quickstart section
* fix: remove duplicate import on auto save
* chore: update wording
* chore: update custom auth config, fix typos, and update title
* chore: fix custom auth docs from feedback
* fix: remove dup import
* chore: update diagrams
* chore: add clarification on quickstart link
* chore: minor tweaks
---
.../4 Custom Auth/1 Bring your Auth.mdx | 87 +++++++-
.../4 Custom Auth/2 custom-auth-server.mdx | 195 +++++++++---------
.../3 custom-jwt-auth-server.mdx | 181 ++++++++++++++++
...Auth.mdx => 4 Integrate Firebase Auth.mdx} | 0
.../assets/ew-custom-auth-config.png | Bin 0 -> 128428 bytes
.../assets/ew-custom-auth-flow.png | Bin 0 -> 138587 bytes
6 files changed, 365 insertions(+), 98 deletions(-)
create mode 100644 docs/onboarding/21 Embedded Wallet/4 Custom Auth/3 custom-jwt-auth-server.mdx
rename docs/onboarding/21 Embedded Wallet/4 Custom Auth/{3 Integrate Firebase Auth.mdx => 4 Integrate Firebase Auth.mdx} (100%)
create mode 100644 docs/onboarding/21 Embedded Wallet/assets/ew-custom-auth-config.png
create mode 100644 docs/onboarding/21 Embedded Wallet/assets/ew-custom-auth-flow.png
diff --git a/docs/onboarding/21 Embedded Wallet/4 Custom Auth/1 Bring your Auth.mdx b/docs/onboarding/21 Embedded Wallet/4 Custom Auth/1 Bring your Auth.mdx
index 355e9694b..33a1e3344 100644
--- a/docs/onboarding/21 Embedded Wallet/4 Custom Auth/1 Bring your Auth.mdx
+++ b/docs/onboarding/21 Embedded Wallet/4 Custom Auth/1 Bring your Auth.mdx
@@ -3,25 +3,52 @@ slug: /embedded-wallet/custom-auth
title: Use your own auth
---
-import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
+import Tabs from "@theme/Tabs";
import QuickstartCard from "@components/QuickstartCard";
By default, the embedded wallet service handles two things: auth, and spinning up crypto wallets tied to the auth. We require
valid authentication to ensure a wallet is created for the right person.
-If you already have your own auth and only want to spin up wallets, we offer a simple way to hook up any **OpenID Connect ("OIDC") compatible** auth to create embedded wallets.
+If you already have your own auth and only want to spin up wallets, we offer a simple way to hook up any auth to create embedded wallets.
## How it works
+We offer two kinds of custom auth. One that is based on the OIDC standard, and one that is is based on you having you bring your own auth server.
+
+### Bring your own auth server
+
+- You have your own auth server that you use to authenticate users
+- When a user logs in, you are able to generate a public identifier that allows you to identify the user.
+- You can pass this identifier to the embedded wallet to generate a wallet for the user.
+- When verifying the user, we will hit an endopint that you provide to verify the user's identity.
+- We will then generate a wallet for the user if the provided payload is valid.
+
+### OIDC
+
- An OIDC auth system has a public-private keypair, where the private key is used to sign auth tokens
- The public key is uploaded to a public URL in JWKS format. The standard location is `https://{domain}.com/.well-known/jwks.json`
- When a user logs in, a JWT token called the idToken is generated and signed by the private key. The OIDC spec provides an interface for fields that are used in this token.
-- We use the public key to verify that the JWT was signed correctly, and proceed to generate a wallet based on the `sub` (user identifier) value of the idToken.
+- This JWT is then passed to the embedded wallet to generate a wallet for the user.
+- We will verify the JWT against the public key to verify that the JWT was signed correctly. Upon successful verification, we will proceed to generate a wallet based on the `sub` (user identifier) value of the idToken.
## Configuration Setup
In your API key settings, click edit, look for "Custom Auth" and provide the following values:
+### Bring your own auth server
+
+- An endpoint that we can hit to verify the user's identity
+ - This endpoint should accept a POST request with a JSON body containing the following fields:
+ - `payload`: This will correspont to the public identifier that was generated for your user.
+ - The endpoint should return a JSON body containing the following fields:
+ - `userId`: A uid for the user. Note that you can only create one wallet per `userId` at this point
+ - `email` (optional): If provided, the user will be able to access the same account outside of the platform for things like private key export // using with wallet connect etc.
+ - `exp` (optional): An expiration date for the user's wallet session. By default a session is 7 days long.
+- A list of custom headers (optional)
+ - These headers will be sent with every request to your verification endpoint. You can use these to authenticate the request.
+
+### OIDC
+
- The URL of the JWKS file (public key)
- This is used to verify the token was signed by you.
- The `aud` value of the idToken
@@ -29,7 +56,57 @@ In your API key settings, click edit, look for "Custom Auth" and provide the fol
## Authenticating a user
-Once you've logged in with your own auth, you can pass the user's JWT to the embedded wallet to authenticate and connect.
+Once you've logged in with your own auth, you can pass the user's detail to the embedded wallet to authenticate and connect.
+
+### Bring your own auth server
+
+
+
+
+
+In React and React Native, the `useEmbeddedWallet()` hook handles authentication and connection states.
+
+```typescript
+import { useEmbeddedWallet } from "@thirdweb-dev/react"; // or /react-native
+
+const embeddedWallet = useEmbeddedWallet();
+
+const handlePostLogin = async (jwt: string) => {
+ await embeddedWallet.connect({
+ strategy: "auth_endpoint",
+ payload,
+ });
+};
+```
+
+
+
+
+
+In other frameworks, use your own instance of the wallet to authenticate and connect.
+
+```typescript
+import { EmbeddedWallet } from "@thirdweb-dev/wallets";
+import { Goerli } from "@thirdweb-dev/chains";
+
+const embeddedWallet = new EmbeddedWallet({
+ chain: Goerli, // chain to connect to
+ clientId: "YOUR_CLIENT_ID", // Your thirdweb client ID
+});
+
+const authResult = await embeddedWallet.authenticate({
+ strategy: "auth_endpoint",
+ payload,
+});
+
+const walletAddress = await embeddedWallet.connect({ authResult });
+```
+
+
+
+
+
+### OIDC
@@ -51,7 +128,7 @@ const handlePostLogin = async (jwt: string) => {
-
+
In other frameworks, use your own instance of the wallet to authenticate and connect.
diff --git a/docs/onboarding/21 Embedded Wallet/4 Custom Auth/2 custom-auth-server.mdx b/docs/onboarding/21 Embedded Wallet/4 Custom Auth/2 custom-auth-server.mdx
index aa8764ef3..fed4fc4af 100644
--- a/docs/onboarding/21 Embedded Wallet/4 Custom Auth/2 custom-auth-server.mdx
+++ b/docs/onboarding/21 Embedded Wallet/4 Custom Auth/2 custom-auth-server.mdx
@@ -3,101 +3,138 @@ slug: /embedded-wallet/custom-auth-server
title: Custom Auth Server
---
+import TabItem from "@theme/TabItem";
+import Tabs from "@theme/Tabs";
+
# Create a custom auth server
-Learn how to integrate your auth backend with our embedded wallets solution so you can onboard your users into web3 seamlessly.
+Learn how to integrate your auth backend with our embedded wallets solution so you can onboard your users into web3 seamlessly.
-This guide will show you how to create your own Auth Server. By doing so, you can have full control over user authentication and data security. This allows you to ensure that your application meets specific compliance requirements while also providing a customized sign-in experience.
+This guide will show you how to create your own Auth Server that is compatible with the `auth_endpoint` strategy. By doing so, you can have full control over user authentication and data security. This allows you to ensure that your application meets specific compliance requirements while also providing a customized sign-in experience.
:::caution
This guide is simplified for demonstration purposes and is not ready for production use. When modifying it for production, secure your endpoints and avoid hard-coding secrets or sensitive information. We recommend using environment variables and secret managers.
:::
-### Setup
+## 5 minute quickstart
-1. Create a new directory for your project and navigate to it in your CLI
+1. Navigate to Wallets > [Embedded Wallets](https://thirdweb.com/dashboard/wallets/embedded) in the thirdweb dashboard.
+2. Create a thirdweb API key if you don't have one or select an existing key to use for this project. [Learn more about API keys.](https://portal.thirdweb.com/api-keys)
- ```bash
- mkdir jwt-auth-server
- cd jwt-auth-server
- ```
+ ![Embedded wallet dashboard with create key displayed](../assets/ew-create-key.png)
-2. Initialize a new Node.js application
+3. Allowlist domain or bundle ids in Access Restrictions.
+4. Navigate to the Configuration view and enable **Custom Auth Endpoint**
- ```bash
- npm init -y
+ ![Configuration view for embedded wallet](../assets/ew-custom-auth-config.png)
- yarn init -y
- ```
+5. Set the Auth Endpoint URL to `https://embedded-wallet.thirdweb.com/api/2023-11-30/embedded-wallet/auth/test-custom-auth-endpoint` for testing purposes. You will replace this later with your own auth server endpoint to verify the `payload`.
+6. Save the configuration.
+7. Copy the client ID.
+8. In your preferred thirdweb client SDK, pass the payload you retrieved from logging in to the server.
-3. Install the necessary packages
+You can now auth into the wallet and use it to sign transactions like so (see [use your own auth for more](/embedded-wallet/custom-auth)):
- ```bash
- npm install express jsonwebtoken
- ```
+
+
-### **Generate RSA Key Pair:**
+In React and React Native, the `useEmbeddedWallet()` hook handles authentication and connection states.
-1. To generate a private and a public key run
+```typescript
+import { useEmbeddedWallet } from "@thirdweb-dev/react"; // or /react-native
- ```bash
- ssh-keygen -t rsa -b 2048 -m PEM -f keys/rsa.key
- ```
+const embeddedWallet = useEmbeddedWallet();
-2. To create the output file run
+const handlePostLogin = async () => {
+ await embeddedWallet.connect({
+ strategy: "auth_endpoint",
+ // in production this would be your public identifier for the user
+ payload: JSON.stringify({ userId:"ANY_RANDOM_ID_HERE" }),
+ encryptionKey: "ANY_RANDOM_STRING_HERE"
+ });
+};
+```
- ```bash
- openssl rsa -in keys/rsa.key -pubout -outform PEM -out keys/rsa.key.pub
- ```
+
+
-### **Convert Public Key to JSON Web Key Set (JWKS):**
+In other frameworks, use your own instance of the wallet to authenticate and connect.
-1. Display the public key:
+```typescript
+import { EmbeddedWallet } from "@thirdweb-dev/wallets";
+import { Goerli } from "@thirdweb-dev/chains";
- ```bash
- cat keys/rsa.key.pub
- ```
+const embeddedWallet = new EmbeddedWallet({
+ chain: Goerli, // chain to connect to
+ clientId: "YOUR_CLIENT_ID", // Your thirdweb client ID
+});
+
+const authResult = await embeddedWallet.authenticate({
+ strategy: "auth_endpoint",
+ payload: JSON.stringify({ userId:"ANY_RANDOM_ID_HERE" }),
+ encryptionKey: "ANY_RANDOM_STRING_HERE"=
+});
+
+const walletAddress = await embeddedWallet.connect({ authResult });
+```
-2. Copy the displayed public key.
-3. Convert your public key to a JWK using an online JWK Creator tool. We recommend using [JWK Creator by Russel Davies](https://github.com/russelldavies/jwk-creator).
+
+
- 1. Paste the public key, set Key ID as `0` (arbitrary string, must match when signing JWT), and then note down the generated JWK.
+A persistent, cross-platform wallet is now created for your user!
- ![JWK Creator tool by Russel Davies showing key id of 0](../assets/jwk-creator-tool.png)
+Of course, you would use your own auth server instead of the one we provided. The rest of this guide will show you how to create your own auth server.
-4. Create a `jwks.json` in the project root and place the generated JWK in a `keys` array.
+### Setup
+
+The following steps will show you how to create a simple auth server that can be used with the embedded wallet.
+
+At a high level, the auth server will:
+
+1. Handle login for the user into your application.
+2. Have a way to get a public identifier for the user.
+3. Have an endpoint to verify the public identifier and return some basic information about the user
+
+Steps 1 and 2 are up to you to implement. You can use any auth strategy you want.
+
+The endpoint in step 3 is what your register as your auth endpoint on the thirdweb dashboard.
+
+Here's a high level diagram:
+![custom auth flow diagram](../assets/ew-custom-auth-flow.png)
+
+1. Create a new directory for your project and navigate to it in your CLI
+
+ ```bash
+ mkdir custom-auth-server
+ cd custom-auth-server
+ ```
+
+2. Initialize a new Node.js application
```bash
- {
- "keys": [
- {
- ... JWK ...
- }
- ]
- }
+ npm init -y
+
+ yarn init -y
```
### **Create the Server:**
-1. In the `jw-auth-server` directory, create a file at the root named `server.js` and paste the following:
+1. In the `custom-auth-server` directory, create a file at the root named `server.js` and paste the following:
```jsx
const express = require("express");
const fs = require("fs");
- const jwt = require("jsonwebtoken");
const app = express();
const PORT = process.env.PORT || 3000;
- const PRIVATE_KEY = fs.readFileSync("./keys/rsa.key", "utf8");
- const jwks = require("./jwks.json");
-
const users = [
{ id: 1, email: "user@example.com", password: "password123" },
];
app.use(express.json());
+ // This is what your app calls to login a user and get a public identifier for the user (otherwise known as the payload)
app.post("/login", (req, res) => {
const { email, password } = req.body;
const user = users.find(
@@ -105,24 +142,24 @@ This guide is simplified for demonstration purposes and is not ready for product
);
if (!user) return res.status(401).send({ message: "Invalid credentials" });
- const payload = {
- iss: "http://your-domain.com",
- sub: user.id.toString(),
- aud: "EpicGame",
- email: user.email,
- exp: Math.floor(Date.now() / 1000) + 3600,
- };
-
- const token = jwt.sign(payload, PRIVATE_KEY, {
- algorithm: "RS256",
- keyid: "0",
- });
-
- res.send({ token });
+ res.send({ payload: user.id });
});
+ // This is a sample endpoint that yuou would register on the thirdweb dashboard for us to verify the payload
+ app.get("/thirdweb-will-call-this", (req, res) => {
+ const { payload } = req.body;
+ if (!payload) return res.status(401).send({ message: "Invalid credentials" });
- app.get("/.well-known/jwks.json", (req, res) => {
- res.json(jwks);
+ // you would write your own logic here to verify the payload here
+ const user = users.find((u) => u.id === payload);
+ if (!user) return res.status(401).send({ message: "Invalid credentials" });
+
+ // once the user is successfully verified, you can return the following field
+ return res.send({
+ userId: user.id,
+ // the last two fields here are optional
+ email: user.email,
+ exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30,
+ });
});
app.listen(PORT, () => {
@@ -130,8 +167,6 @@ This guide is simplified for demonstration purposes and is not ready for product
});
```
-2. Replace `http://your-domain.com` with the actual domain for the application.
-
### **Test Locally**
1. Start the server:
@@ -146,36 +181,10 @@ This guide is simplified for demonstration purposes and is not ready for product
curl -X POST http://localhost:3000/login -H "Content-Type: application/json" -d '{"email": "user@example.com", "password": "password123"}'
```
-3. Test JWKS:
-
- ```bash
- curl http://localhost:3000/.well-known/jwks.json
- ```
-
### **Deploy**
To deploy the server, you can use use services such as [Zeet](https://zeet.co/) or [Docker](https://www.docker.com/).
-Once deployed, replace `http://localhost:3000` in the JWT payload with your actual domain
-
### **Integrate Embedded Wallets**
-1. Navigate to Wallets > [Embedded Wallets](https://thirdweb.com/dashboard/wallets/embedded) in the thirdweb dashboard.
-2. Create a thirdweb API key if you don’t have one or select an existing key to use for this project. [Learn more about API keys.](https://portal.thirdweb.com/api-keys)
-
- ![Embedded wallet dashboard with create key displayed](../assets/ew-create-key.png)
-
-3. Allowlist domain or bundle ids in Access Restrictions.
-4. Navigate to the Configuration view and enable **Custom JSON Web Token**
-
- ![Configuration view for embedded wallet](../assets/ew-configuration.png)
-
-5. Set the JWKS URI to `your-domain/.well-known/jwks.json`
-6. Set the AUD to `EpicGame` or the value you set as the aud in the `server.js` file.
-
- ![Options for EW Configuration](../assets/ew-configuration-opt.png)
-
-7. Copy the client ID.
-8. In your preferred thirdweb client SDK, pass the JWT you retrieved from logging in to the server.
-
-A persistent, cross-platform wallet is now created for your user.
+Refer top the [quickstart above](#5-minute-quickstart) to integrate the embedded wallet into your application.
\ No newline at end of file
diff --git a/docs/onboarding/21 Embedded Wallet/4 Custom Auth/3 custom-jwt-auth-server.mdx b/docs/onboarding/21 Embedded Wallet/4 Custom Auth/3 custom-jwt-auth-server.mdx
new file mode 100644
index 000000000..06753a6b1
--- /dev/null
+++ b/docs/onboarding/21 Embedded Wallet/4 Custom Auth/3 custom-jwt-auth-server.mdx
@@ -0,0 +1,181 @@
+---
+slug: /embedded-wallet/custom-jwt-auth-server
+title: Custom JWT Auth Server
+---
+
+# Create a custom JWT auth server
+
+Learn how to integrate your auth backend with our embedded wallets solution so you can onboard your users into web3 seamlessly.
+
+This guide will show you how to create your own Auth Server that is compatible with the JWT auth strategy. By doing so, you can have full control over user authentication and data security. This allows you to ensure that your application meets specific compliance requirements while also providing a customized sign-in experience.
+
+:::caution
+This guide is simplified for demonstration purposes and is not ready for production use. When modifying it for production, secure your endpoints and avoid hard-coding secrets or sensitive information. We recommend using environment variables and secret managers.
+:::
+
+### Setup
+
+1. Create a new directory for your project and navigate to it in your CLI
+
+ ```bash
+ mkdir jwt-auth-server
+ cd jwt-auth-server
+ ```
+
+2. Initialize a new Node.js application
+
+ ```bash
+ npm init -y
+
+ yarn init -y
+ ```
+
+3. Install the necessary packages
+
+ ```bash
+ npm install express jsonwebtoken
+ ```
+
+### **Generate RSA Key Pair:**
+
+1. To generate a private and a public key run
+
+ ```bash
+ ssh-keygen -t rsa -b 2048 -m PEM -f keys/rsa.key
+ ```
+
+2. To create the output file run
+
+ ```bash
+ openssl rsa -in keys/rsa.key -pubout -outform PEM -out keys/rsa.key.pub
+ ```
+
+### **Convert Public Key to JSON Web Key Set (JWKS):**
+
+1. Display the public key:
+
+ ```bash
+ cat keys/rsa.key.pub
+ ```
+
+2. Copy the displayed public key.
+3. Convert your public key to a JWK using an online JWK Creator tool. We recommend using [JWK Creator by Russel Davies](https://github.com/russelldavies/jwk-creator).
+
+ 1. Paste the public key, set Key ID as `0` (arbitrary string, must match when signing JWT), and then note down the generated JWK.
+
+ ![JWK Creator tool by Russel Davies showing key id of 0](../assets/jwk-creator-tool.png)
+
+4. Create a `jwks.json` in the project root and place the generated JWK in a `keys` array.
+
+ ```bash
+ {
+ "keys": [
+ {
+ ... JWK ...
+ }
+ ]
+ }
+ ```
+
+### **Create the Server:**
+
+1. In the `jw-auth-server` directory, create a file at the root named `server.js` and paste the following:
+
+ ```jsx
+ const express = require("express");
+ const fs = require("fs");
+ const jwt = require("jsonwebtoken");
+
+ const app = express();
+ const PORT = process.env.PORT || 3000;
+
+ const PRIVATE_KEY = fs.readFileSync("./keys/rsa.key", "utf8");
+ const jwks = require("./jwks.json");
+
+ const users = [
+ { id: 1, email: "user@example.com", password: "password123" },
+ ];
+
+ app.use(express.json());
+
+ app.post("/login", (req, res) => {
+ const { email, password } = req.body;
+ const user = users.find(
+ (u) => u.email === email && u.password === password,
+ );
+ if (!user) return res.status(401).send({ message: "Invalid credentials" });
+
+ const payload = {
+ iss: "http://your-domain.com",
+ sub: user.id.toString(),
+ aud: "EpicGame",
+ email: user.email,
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ };
+
+ const token = jwt.sign(payload, PRIVATE_KEY, {
+ algorithm: "RS256",
+ keyid: "0",
+ });
+
+ res.send({ token });
+ });
+
+ app.get("/.well-known/jwks.json", (req, res) => {
+ res.json(jwks);
+ });
+
+ app.listen(PORT, () => {
+ console.log(`Server started on port ${PORT}`);
+ });
+ ```
+
+2. Replace `http://your-domain.com` with the actual domain for the application.
+
+### **Test Locally**
+
+1. Start the server:
+
+ ```bash
+ node server.js
+ ```
+
+2. Test login:
+
+ ```bash
+ curl -X POST http://localhost:3000/login -H "Content-Type: application/json" -d '{"email": "user@example.com", "password": "password123"}'
+ ```
+
+3. Test JWKS:
+
+ ```bash
+ curl http://localhost:3000/.well-known/jwks.json
+ ```
+
+### **Deploy**
+
+To deploy the server, you can use use services such as [Zeet](https://zeet.co/) or [Docker](https://www.docker.com/).
+
+Once deployed, replace `http://localhost:3000` in the JWT payload with your actual domain
+
+### **Integrate Embedded Wallets**
+
+1. Navigate to Wallets > [Embedded Wallets](https://thirdweb.com/dashboard/wallets/embedded) in the thirdweb dashboard.
+2. Create a thirdweb API key if you don’t have one or select an existing key to use for this project. [Learn more about API keys.](https://portal.thirdweb.com/api-keys)
+
+ ![Embedded wallet dashboard with create key displayed](../assets/ew-create-key.png)
+
+3. Allowlist domain or bundle ids in Access Restrictions.
+4. Navigate to the Configuration view and enable **Custom JSON Web Token**
+
+ ![Configuration view for embedded wallet](../assets/ew-configuration.png)
+
+5. Set the JWKS URI to `your-domain/.well-known/jwks.json`
+6. Set the AUD to `EpicGame` or the value you set as the aud in the `server.js` file.
+
+ ![Options for EW Configuration](../assets/ew-configuration-opt.png)
+
+7. Copy the client ID.
+8. In your preferred thirdweb client SDK, pass the JWT you retrieved from logging in to the server.
+
+A persistent, cross-platform wallet is now created for your user.
diff --git a/docs/onboarding/21 Embedded Wallet/4 Custom Auth/3 Integrate Firebase Auth.mdx b/docs/onboarding/21 Embedded Wallet/4 Custom Auth/4 Integrate Firebase Auth.mdx
similarity index 100%
rename from docs/onboarding/21 Embedded Wallet/4 Custom Auth/3 Integrate Firebase Auth.mdx
rename to docs/onboarding/21 Embedded Wallet/4 Custom Auth/4 Integrate Firebase Auth.mdx
diff --git a/docs/onboarding/21 Embedded Wallet/assets/ew-custom-auth-config.png b/docs/onboarding/21 Embedded Wallet/assets/ew-custom-auth-config.png
new file mode 100644
index 0000000000000000000000000000000000000000..771d19c1f14b557848d08b97224c44eeb49cc6c6
GIT binary patch
literal 128428
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