main.js
server.js
database.js
jwt.js

_145
import React, { useState, useEffect } from "react";
_145
import ReactDOM from "react-dom/client";
_145
import axios from "axios";
_145
import DataTable from "./DataTable";
_145
import { ThemeProvider, Button, Container, Typography } from "@mui/material";
_145
import theme from "./theme";
_145
_145
const App = () => {
_145
const [idToken, setIdToken] = useState("");
_145
const [sessionToken, setSessionToken] = useState("");
_145
const [user, setUser] = useState({});
_145
const [siteData, setSiteData] = useState([]);
_145
_145
const PORT = 3000;
_145
const API_URL = `http://localhost:${PORT}/`;
_145
_145
useEffect(() => {
_145
// Set Extension Size
_145
webflow.setExtensionSize("default");
_145
_145
// Function to exchange and verify ID token
_145
const exchangeAndVerifyIdToken = async () => {
_145
try {
_145
const idToken = await webflow.getIdToken();
_145
const siteInfo = await webflow.getSiteInfo();
_145
setIdToken(idToken);
_145
_145
// Resolve token by sending it to the backend server
_145
const response = await axios.post(API_URL + "token", {
_145
idToken: idToken,
_145
siteId: siteInfo.siteId,
_145
});
_145
_145
try {
_145
// Parse information from resolved token
_145
const sessionToken = response.data.sessionToken;
_145
const expAt = response.data.exp;
_145
const decodedToken = JSON.parse(atob(sessionToken.split(".")[1]));
_145
const firstName = decodedToken.user.firstName;
_145
const email = decodedToken.user.email;
_145
_145
// Store information in Local Storage
_145
localStorage.setItem(
_145
"wf_hybrid_user",
_145
JSON.stringify({ sessionToken, firstName, email, exp: expAt })
_145
);
_145
setUser({ firstName, email });
_145
setSessionToken(sessionToken);
_145
console.log(`Session Token: ${sessionToken}`);
_145
} catch (error) {
_145
console.error("No Token", error);
_145
}
_145
} catch (error) {
_145
console.error("Error fetching ID Token:", error);
_145
}
_145
};
_145
_145
// Check local storage for session token
_145
const localStorageUser = localStorage.getItem("wf_hybrid_user");
_145
if (localStorageUser) {
_145
const userParse = JSON.parse(localStorageUser);
_145
const userStoredSessionToken = userParse.sessionToken;
_145
const userStoredTokenExp = userParse.exp;
_145
if (userStoredSessionToken && Date.now() < userStoredTokenExp) {
_145
if (!sessionToken) {
_145
setSessionToken(userStoredSessionToken);
_145
setUser({ firstName: userParse.firstName, email: userParse.email });
_145
}
_145
} else {
_145
localStorage.removeItem("wf_hybrid_user");
_145
exchangeAndVerifyIdToken();
_145
}
_145
} else {
_145
exchangeAndVerifyIdToken();
_145
}
_145
_145
// Listen for message from the OAuth callback window
_145
const handleAuthComplete = (event) => {
_145
if (
_145
// event.origin === "http://localhost:3000" &&
_145
event.data === "authComplete"
_145
) {
_145
exchangeAndVerifyIdToken(); // Retry the token exchange
_145
}
_145
};
_145
_145
window.addEventListener("message", handleAuthComplete);
_145
_145
return () => {
_145
window.removeEventListener("message", handleAuthComplete);
_145
};
_145
}, [sessionToken]);
_145
_145
// Handle request for site data
_145
const getSiteData = async () => {
_145
const sites = await axios.get(API_URL + "sites", {
_145
headers: { authorization: `Bearer ${sessionToken}` },
_145
});
_145
setSiteData(sites.data.data.sites);
_145
};
_145
_145
// Open OAuth screen
_145
const openAuthScreen = () => {
_145
window.open("http://localhost:3000", "_blank", "width=600,height=400");
_145
};
_145
_145
return (
_145
<ThemeProvider theme={theme}>
_145
<div>
_145
{!user.firstName ? (
_145
// If no user is found, Send a Hello Stranger Message and Button to Authorize
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello Stranger</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={openAuthScreen}
_145
>
_145
Authorize App
_145
</Button>
_145
</Container>
_145
) : (
_145
// If a user is found send welcome message with their name
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello {user.firstName}</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={getSiteData}
_145
>
_145
Get Sites
_145
</Button>
_145
{siteData.length > 0 && <DataTable data={siteData} />}
_145
</Container>
_145
)}
_145
</div>
_145
</ThemeProvider>
_145
);
_145
};
_145
_145
// Render your App component inside the root
_145
const rootElement = document.getElementById("root");
_145
const root = ReactDOM.createRoot(rootElement);
_145
_145
root.render(<App />);

Securely connect your browser-based Designer Extension to your server-side Data Client.

In this tutorial we’ll build a Hybrid App that:

  • Authorizes sites upon App installation
  • Transmits a Site ID and ID Token
    Send a Site ID and ID Token from the Designer Extension to the Data Client for verification.
  • Resolves the ID Token on the Server Side
    The Data Client resolves the ID Token to get user details from Webflow and verify access.
  • Maps user authorization
    Establish a link between the user and their authorized sites, allowing for secure access to site data.
  • Creates a Session Token for the user
    Generate and send a session token to the Designer Extension to securely maintain the user’s session and enable authenticated requests.
  • Makes authenticated requests to Webflow’s Data APIs
    Use the session token and stored tokens to make authenticated API calls to Webflow’s Data APIs.

By the end, you’ll have a secure, fully integrated setup to handle user sessions and seamlessly make requests to external APIs.

Prerequisites

  • A Hybrid App with the following scopes: sites:read, authorized_user:read
  • The CLIENT_ID and CLIENT_SECRET for your App
  • An ngrok authentication token
  • An understanding of how to authenticate a Webflow User
  • Basic knowledge of Node.js and Express
  • Familiarity with building React Single-Page Applications

Set up your development environment

Clone the starter code

If you have the GitHub CLI installed, type the following command into your terminal.

$ gh repo clone Webflow-Examples/Hybrid-App-Authentication

Otherwise, you can clone the repository from GitHub.com

Install dependencies

This example contains both a Designer Extension and Data Client project. Input the following commands in your terminal to install all necessary dependencies for the example.

$ cd hybrid-app-authentication
npm install
npm run install-frontend
npm run install-backend

Add environment variables

main.js
server.js
database.js
jwt.js
.env

_4
WEBFLOW_CLIENT_ID=XXX
_4
WEBFLOW_CLIENT_SECRET=XXX
_4
PORT=3000
_4
NGROK_AUTH_TOKEN=XXX

Replace the example values in the .env.example file with your credentials. Rename the file to .env

Review Designer Extension

main.js
server.js
database.js
jwt.js
.env

_145
import React, { useState, useEffect } from "react";
_145
import ReactDOM from "react-dom/client";
_145
import axios from "axios";
_145
import DataTable from "./DataTable";
_145
import { ThemeProvider, Button, Container, Typography } from "@mui/material";
_145
import theme from "./theme";
_145
_145
const App = () => {
_145
const [idToken, setIdToken] = useState("");
_145
const [sessionToken, setSessionToken] = useState("");
_145
const [user, setUser] = useState({});
_145
const [siteData, setSiteData] = useState([]);
_145
_145
const PORT = 3000;
_145
const API_URL = `http://localhost:${PORT}/`;
_145
_145
useEffect(() => {
_145
// Set Extension Size
_145
webflow.setExtensionSize("default");
_145
_145
// Function to exchange and verify ID token
_145
const exchangeAndVerifyIdToken = async () => {
_145
try {
_145
const idToken = await webflow.getIdToken();
_145
const siteInfo = await webflow.getSiteInfo();
_145
setIdToken(idToken);
_145
_145
// Resolve token by sending it to the backend server
_145
const response = await axios.post(API_URL + "token", {
_145
idToken: idToken,
_145
siteId: siteInfo.siteId,
_145
});
_145
_145
try {
_145
// Parse information from resolved token
_145
const sessionToken = response.data.sessionToken;
_145
const expAt = response.data.exp;
_145
const decodedToken = JSON.parse(atob(sessionToken.split(".")[1]));
_145
const firstName = decodedToken.user.firstName;
_145
const email = decodedToken.user.email;
_145
_145
// Store information in Local Storage
_145
localStorage.setItem(
_145
"wf_hybrid_user",
_145
JSON.stringify({ sessionToken, firstName, email, exp: expAt })
_145
);
_145
setUser({ firstName, email });
_145
setSessionToken(sessionToken);
_145
console.log(`Session Token: ${sessionToken}`);
_145
} catch (error) {
_145
console.error("No Token", error);
_145
}
_145
} catch (error) {
_145
console.error("Error fetching ID Token:", error);
_145
}
_145
};
_145
_145
// Check local storage for session token
_145
const localStorageUser = localStorage.getItem("wf_hybrid_user");
_145
if (localStorageUser) {
_145
const userParse = JSON.parse(localStorageUser);
_145
const userStoredSessionToken = userParse.sessionToken;
_145
const userStoredTokenExp = userParse.exp;
_145
if (userStoredSessionToken && Date.now() < userStoredTokenExp) {
_145
if (!sessionToken) {
_145
setSessionToken(userStoredSessionToken);
_145
setUser({ firstName: userParse.firstName, email: userParse.email });
_145
}
_145
} else {
_145
localStorage.removeItem("wf_hybrid_user");
_145
exchangeAndVerifyIdToken();
_145
}
_145
} else {
_145
exchangeAndVerifyIdToken();
_145
}
_145
_145
// Listen for message from the OAuth callback window
_145
const handleAuthComplete = (event) => {
_145
if (
_145
// event.origin === "http://localhost:3000" &&
_145
event.data === "authComplete"
_145
) {
_145
exchangeAndVerifyIdToken(); // Retry the token exchange
_145
}
_145
};
_145
_145
window.addEventListener("message", handleAuthComplete);
_145
_145
return () => {
_145
window.removeEventListener("message", handleAuthComplete);
_145
};
_145
}, [sessionToken]);
_145
_145
// Handle request for site data
_145
const getSiteData = async () => {
_145
const sites = await axios.get(API_URL + "sites", {
_145
headers: { authorization: `Bearer ${sessionToken}` },
_145
});
_145
setSiteData(sites.data.data.sites);
_145
};
_145
_145
// Open OAuth screen
_145
const openAuthScreen = () => {
_145
window.open("http://localhost:3000", "_blank", "width=600,height=400");
_145
};
_145
_145
return (
_145
<ThemeProvider theme={theme}>
_145
<div>
_145
{!user.firstName ? (
_145
// If no user is found, Send a Hello Stranger Message and Button to Authorize
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello Stranger</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={openAuthScreen}
_145
>
_145
Authorize App
_145
</Button>
_145
</Container>
_145
) : (
_145
// If a user is found send welcome message with their name
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello {user.firstName}</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={getSiteData}
_145
>
_145
Get Sites
_145
</Button>
_145
{siteData.length > 0 && <DataTable data={siteData} />}
_145
</Container>
_145
)}
_145
</div>
_145
</ThemeProvider>
_145
);
_145
};
_145
_145
// Render your App component inside the root
_145
const rootElement = document.getElementById("root");
_145
const root = ReactDOM.createRoot(rootElement);
_145
_145
root.render(<App />);

In this tutorial, most of our focus will be on setting up and configuring the Data Client. However, to kick off the authentication process, we need to first send an ID token and Site ID from the Designer Extension to the Data Client. This step will allow us to exchange these tokens for a session token, which is required for making authenticated requests to Webflow’s API from the browser.

To get started, review the exchangeAndVerifyIdToken function in the Designer Extension.

Retrieving the idToken from the Designer Extension.

main.js
server.js
database.js
jwt.js
.env

_145
import React, { useState, useEffect } from "react";
_145
import ReactDOM from "react-dom/client";
_145
import axios from "axios";
_145
import DataTable from "./DataTable";
_145
import { ThemeProvider, Button, Container, Typography } from "@mui/material";
_145
import theme from "./theme";
_145
_145
const App = () => {
_145
const [idToken, setIdToken] = useState("");
_145
const [sessionToken, setSessionToken] = useState("");
_145
const [user, setUser] = useState({});
_145
const [siteData, setSiteData] = useState([]);
_145
_145
const PORT = 3000;
_145
const API_URL = `http://localhost:${PORT}/`;
_145
_145
useEffect(() => {
_145
// Set Extension Size
_145
webflow.setExtensionSize("default");
_145
_145
// Function to exchange and verify ID token
_145
const exchangeAndVerifyIdToken = async () => {
_145
try {
_145
const idToken = await webflow.getIdToken();
_145
const siteInfo = await webflow.getSiteInfo();
_145
setIdToken(idToken);
_145
_145
// Resolve token by sending it to the backend server
_145
const response = await axios.post(API_URL + "token", {
_145
idToken: idToken,
_145
siteId: siteInfo.siteId,
_145
});
_145
_145
try {
_145
// Parse information from resolved token
_145
const sessionToken = response.data.sessionToken;
_145
const expAt = response.data.exp;
_145
const decodedToken = JSON.parse(atob(sessionToken.split(".")[1]));
_145
const firstName = decodedToken.user.firstName;
_145
const email = decodedToken.user.email;
_145
_145
// Store information in Local Storage
_145
localStorage.setItem(
_145
"wf_hybrid_user",
_145
JSON.stringify({ sessionToken, firstName, email, exp: expAt })
_145
);
_145
setUser({ firstName, email });
_145
setSessionToken(sessionToken);
_145
console.log(`Session Token: ${sessionToken}`);
_145
} catch (error) {
_145
console.error("No Token", error);
_145
}
_145
} catch (error) {
_145
console.error("Error fetching ID Token:", error);
_145
}
_145
};
_145
_145
// Check local storage for session token
_145
const localStorageUser = localStorage.getItem("wf_hybrid_user");
_145
if (localStorageUser) {
_145
const userParse = JSON.parse(localStorageUser);
_145
const userStoredSessionToken = userParse.sessionToken;
_145
const userStoredTokenExp = userParse.exp;
_145
if (userStoredSessionToken && Date.now() < userStoredTokenExp) {
_145
if (!sessionToken) {
_145
setSessionToken(userStoredSessionToken);
_145
setUser({ firstName: userParse.firstName, email: userParse.email });
_145
}
_145
} else {
_145
localStorage.removeItem("wf_hybrid_user");
_145
exchangeAndVerifyIdToken();
_145
}
_145
} else {
_145
exchangeAndVerifyIdToken();
_145
}
_145
_145
// Listen for message from the OAuth callback window
_145
const handleAuthComplete = (event) => {
_145
if (
_145
// event.origin === "http://localhost:3000" &&
_145
event.data === "authComplete"
_145
) {
_145
exchangeAndVerifyIdToken(); // Retry the token exchange
_145
}
_145
};
_145
_145
window.addEventListener("message", handleAuthComplete);
_145
_145
return () => {
_145
window.removeEventListener("message", handleAuthComplete);
_145
};
_145
}, [sessionToken]);
_145
_145
// Handle request for site data
_145
const getSiteData = async () => {
_145
const sites = await axios.get(API_URL + "sites", {
_145
headers: { authorization: `Bearer ${sessionToken}` },
_145
});
_145
setSiteData(sites.data.data.sites);
_145
};
_145
_145
// Open OAuth screen
_145
const openAuthScreen = () => {
_145
window.open("http://localhost:3000", "_blank", "width=600,height=400");
_145
};
_145
_145
return (
_145
<ThemeProvider theme={theme}>
_145
<div>
_145
{!user.firstName ? (
_145
// If no user is found, Send a Hello Stranger Message and Button to Authorize
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello Stranger</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={openAuthScreen}
_145
>
_145
Authorize App
_145
</Button>
_145
</Container>
_145
) : (
_145
// If a user is found send welcome message with their name
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello {user.firstName}</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={getSiteData}
_145
>
_145
Get Sites
_145
</Button>
_145
{siteData.length > 0 && <DataTable data={siteData} />}
_145
</Container>
_145
)}
_145
</div>
_145
</ThemeProvider>
_145
);
_145
};
_145
_145
// Render your App component inside the root
_145
const rootElement = document.getElementById("root");
_145
const root = ReactDOM.createRoot(rootElement);
_145
_145
root.render(<App />);

The exchangeAndVerifyIdToken function initiates the authentication process by using the Webflow Designer API’s getIdToken and getSiteInfo methods to retrieve the idToken and siteId from the Designer Extension.

Exchanging the idToken and siteId for a Session Token

main.js
server.js
database.js
jwt.js
.env

_145
import React, { useState, useEffect } from "react";
_145
import ReactDOM from "react-dom/client";
_145
import axios from "axios";
_145
import DataTable from "./DataTable";
_145
import { ThemeProvider, Button, Container, Typography } from "@mui/material";
_145
import theme from "./theme";
_145
_145
const App = () => {
_145
const [idToken, setIdToken] = useState("");
_145
const [sessionToken, setSessionToken] = useState("");
_145
const [user, setUser] = useState({});
_145
const [siteData, setSiteData] = useState([]);
_145
_145
const PORT = 3000;
_145
const API_URL = `http://localhost:${PORT}/`;
_145
_145
useEffect(() => {
_145
// Set Extension Size
_145
webflow.setExtensionSize("default");
_145
_145
// Function to exchange and verify ID token
_145
const exchangeAndVerifyIdToken = async () => {
_145
try {
_145
const idToken = await webflow.getIdToken();
_145
const siteInfo = await webflow.getSiteInfo();
_145
setIdToken(idToken);
_145
_145
// Resolve token by sending it to the backend server
_145
const response = await axios.post(API_URL + "token", {
_145
idToken: idToken,
_145
siteId: siteInfo.siteId,
_145
});
_145
_145
try {
_145
// Parse information from resolved token
_145
const sessionToken = response.data.sessionToken;
_145
const expAt = response.data.exp;
_145
const decodedToken = JSON.parse(atob(sessionToken.split(".")[1]));
_145
const firstName = decodedToken.user.firstName;
_145
const email = decodedToken.user.email;
_145
_145
// Store information in Local Storage
_145
localStorage.setItem(
_145
"wf_hybrid_user",
_145
JSON.stringify({ sessionToken, firstName, email, exp: expAt })
_145
);
_145
setUser({ firstName, email });
_145
setSessionToken(sessionToken);
_145
console.log(`Session Token: ${sessionToken}`);
_145
} catch (error) {
_145
console.error("No Token", error);
_145
}
_145
} catch (error) {
_145
console.error("Error fetching ID Token:", error);
_145
}
_145
};
_145
_145
// Check local storage for session token
_145
const localStorageUser = localStorage.getItem("wf_hybrid_user");
_145
if (localStorageUser) {
_145
const userParse = JSON.parse(localStorageUser);
_145
const userStoredSessionToken = userParse.sessionToken;
_145
const userStoredTokenExp = userParse.exp;
_145
if (userStoredSessionToken && Date.now() < userStoredTokenExp) {
_145
if (!sessionToken) {
_145
setSessionToken(userStoredSessionToken);
_145
setUser({ firstName: userParse.firstName, email: userParse.email });
_145
}
_145
} else {
_145
localStorage.removeItem("wf_hybrid_user");
_145
exchangeAndVerifyIdToken();
_145
}
_145
} else {
_145
exchangeAndVerifyIdToken();
_145
}
_145
_145
// Listen for message from the OAuth callback window
_145
const handleAuthComplete = (event) => {
_145
if (
_145
// event.origin === "http://localhost:3000" &&
_145
event.data === "authComplete"
_145
) {
_145
exchangeAndVerifyIdToken(); // Retry the token exchange
_145
}
_145
};
_145
_145
window.addEventListener("message", handleAuthComplete);
_145
_145
return () => {
_145
window.removeEventListener("message", handleAuthComplete);
_145
};
_145
}, [sessionToken]);
_145
_145
// Handle request for site data
_145
const getSiteData = async () => {
_145
const sites = await axios.get(API_URL + "sites", {
_145
headers: { authorization: `Bearer ${sessionToken}` },
_145
});
_145
setSiteData(sites.data.data.sites);
_145
};
_145
_145
// Open OAuth screen
_145
const openAuthScreen = () => {
_145
window.open("http://localhost:3000", "_blank", "width=600,height=400");
_145
};
_145
_145
return (
_145
<ThemeProvider theme={theme}>
_145
<div>
_145
{!user.firstName ? (
_145
// If no user is found, Send a Hello Stranger Message and Button to Authorize
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello Stranger</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={openAuthScreen}
_145
>
_145
Authorize App
_145
</Button>
_145
</Container>
_145
) : (
_145
// If a user is found send welcome message with their name
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello {user.firstName}</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={getSiteData}
_145
>
_145
Get Sites
_145
</Button>
_145
{siteData.length > 0 && <DataTable data={siteData} />}
_145
</Container>
_145
)}
_145
</div>
_145
</ThemeProvider>
_145
);
_145
};
_145
_145
// Render your App component inside the root
_145
const rootElement = document.getElementById("root");
_145
const root = ReactDOM.createRoot(rootElement);
_145
_145
root.render(<App />);

With the ID token and Site ID in hand, the Designer Extension then sends this information to an endpoint on the Data Client.

The Data Client verifies the ID token and returns a session token, which the Designer Extension will use to make authenticated requests to Webflow. This token ensures secure, temporary access and allows the Designer Extension to manage user sessions as we continue through the setup.

Set up your server

Switching to the Data Client, let’s quickly set up an Express server to handle incoming requests and prepare for adding authentication in Data Client/server.js.

Initialize Express

Create your server with Express, configure CORS to accept incoming requests from your Designer, and set up middleware for JSON and URL-encoded requests

main.js
server.js
database.js
jwt.js
.env

_20
const express = require("express");
_20
const cors = require("cors");
_20
const { WebflowClient } = require("webflow-api");
_20
const axios = require("axios");
_20
require("dotenv").config();
_20
_20
const app = express(); // Create an Express application
_20
_20
var corsOptions = { origin: ["http://localhost:1337"] };
_20
_20
// Middleware
_20
app.use(cors(corsOptions)); // Enable CORS with the specified options
_20
app.use(express.json()); // Parse JSON-formatted incoming requests
_20
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_20
_20
// Start the server
_20
const PORT = process.env.PORT || 3000;
_20
app.listen(PORT, () => {
_20
console.log(`Server is running on http://localhost:${PORT}`);
_20
});

Configure authorization flow

Add an endpoint on your server to handle the OAuth callback and retrieve an authorization_code from the URI’s query parameters. Then, create a /callback endpoint to exchange the code for an accessToken for your user.

For an in-depth explanation on setting up auth, check out the guide

main.js
server.js
database.js
jwt.js
.env

_44
const express = require("express");
_44
const cors = require("cors");
_44
const { WebflowClient } = require("webflow-api");
_44
const axios = require("axios");
_44
require("dotenv").config();
_44
_44
const app = express(); // Create an Express application
_44
_44
var corsOptions = { origin: ["http://localhost:1337"] };
_44
_44
// Middleware
_44
app.use(cors(corsOptions)); // Enable CORS with the specified options
_44
app.use(express.json()); // Parse JSON-formatted incoming requests
_44
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_44
_44
// Redirect user to Webflow Authorization screen
_44
app.get("/authorize", (req, res) => {
_44
const authorizeUrl = WebflowClient.authorizeURL({
_44
scope: ["sites:read", "authorized_user:read"],
_44
clientId: process.env.WEBFLOW_CLIENT_ID,
_44
});
_44
res.redirect(authorizeUrl);
_44
});
_44
_44
// Exchange the authorization code for an access token and save to DB
_44
app.get("/callback", async (req, res) => {
_44
const { code } = req.query;
_44
_44
// Get Access Token
_44
const accessToken = await WebflowClient.getAccessToken({
_44
clientId: process.env.WEBFLOW_CLIENT_ID,
_44
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_44
code: code,
_44
});
_44
_44
// TODO: Save accessToken to the database, associated with user/site info
_44
// db.insertAuthorization({ userId: "userID", accessToken });
_44
})
_44
_44
// Start the server
_44
const PORT = process.env.PORT || 3000;
_44
app.listen(PORT, () => {
_44
console.log(`Server is running on http://localhost:${PORT}`);
_44
});

Handling the ID Token and Site ID from the Designer Extension

main.js
server.js
database.js
jwt.js
.env

_61
const express = require("express");
_61
const cors = require("cors");
_61
const { WebflowClient } = require("webflow-api");
_61
const axios = require("axios");
_61
require("dotenv").config();
_61
_61
const app = express(); // Create an Express application
_61
_61
var corsOptions = { origin: ["http://localhost:1337"] };
_61
_61
// Middleware
_61
app.use(cors(corsOptions)); // Enable CORS with the specified options
_61
app.use(express.json()); // Parse JSON-formatted incoming requests
_61
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_61
_61
// Redirect user to Webflow Authorization screen
_61
app.get("/authorize", (req, res) => {
_61
const authorizeUrl = WebflowClient.authorizeURL({
_61
scope: ["sites:read", "authorized_user:read"],
_61
clientId: process.env.WEBFLOW_CLIENT_ID,
_61
});
_61
res.redirect(authorizeUrl);
_61
});
_61
_61
// Exchange the authorization code for an access token and save to DB
_61
app.get("/callback", async (req, res) => {
_61
const { code } = req.query;
_61
_61
// Get Access Token
_61
const accessToken = await WebflowClient.getAccessToken({
_61
clientId: process.env.WEBFLOW_CLIENT_ID,
_61
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_61
code: code,
_61
});
_61
_61
// TODO: Save accessToken to the database, associated with user/site info
_61
// db.insertAuthorization({ userId: "userID", accessToken });
_61
})
_61
_61
// Endpoint to authenticate user with ID Token and Site ID
_61
app.post("/token", (req, res) => {
_61
const idToken = req.body.idToken; // Get token from request
_61
_61
// Placeholder for token validation
_61
// jwt.retrieveAccessToken();
_61
_61
// TODO: Resolve ID Token with Webflow API and create a session token
_61
// - Make a request to the Webflow API to resolve the ID Token
_61
// - Create a session token for the user
_61
// - Store user authorization info in the database
_61
// - Respond with the session token
_61
_61
res.json({ message: "Token endpoint setup pending." });
_61
});
_61
_61
_61
// Start the server
_61
const PORT = process.env.PORT || 3000;
_61
app.listen(PORT, () => {
_61
console.log(`Server is running on http://localhost:${PORT}`);
_61
});

In addition to retrieving an accessToken through OAuth, your Designer Extension will also receive an idToken and a siteId during the authentication process for the Designer Extension. These tokens enable you to verify and authorize user access securely.

To manage this, you’ll need to set up a new endpoint that:

  1. Validates the ID Token received from the Designer Extension using the Resolve ID Token endpoint.
  2. Retrieves an accessToken associated with the Site ID from a database
  3. Generates a Session Token for secure, temporary access.
  4. Stores the accessToken in your database, associating it with the user or site.
  5. Sends the Session Token to the Designer Extension

For now, we’ll set up the endpoint to receive the ID Token. We’ll add the database storage, access token retrieval, and session token generation in the next steps.

Set up your database to save and retrieve credentials

main.js
server.js
database.js
jwt.js
.env

_186
import sqlite3 from "sqlite3";
_186
import jwt from "jsonwebtoken";
_186
import dotenv from "dotenv";
_186
_186
// Enable dotenv to load environment variables
_186
dotenv.config();
_186
_186
// Enable verbose mode for SQLite3
_186
const sqlite3Verbose = sqlite3.verbose();
_186
_186
// Open SQLite database connection
_186
const db = new sqlite3Verbose.Database("./db/database.db");
_186
_186
// Create tables
_186
db.serialize(() => {
_186
_186
// Table to associate site ID with access token from OAuth
_186
db.run(`
_186
CREATE TABLE IF NOT EXISTS siteAuthorizations (
_186
siteId TEXT PRIMARY KEY,
_186
accessToken TEXT
_186
)
_186
`);
_186
_186
// Table to associate user ID with the access token
_186
db.run(`
_186
CREATE TABLE IF NOT EXISTS userAuthorizations (
_186
id INTEGER PRIMARY KEY AUTOINCREMENT,
_186
userId TEXT,
_186
accessToken TEXT
_186
)
_186
`);
_186
});
_186
_186
// Insert a record after exchanging the OAuth code for an access token
_186
function insertSiteAuthorization(siteId, accessToken) {
_186
db.get(
_186
"SELECT * FROM siteAuthorizations WHERE siteId = ?",
_186
[siteId],
_186
(err, existingAuth) => {
_186
if (err) {
_186
console.error("Error checking for existing site authorization:", err);
_186
return;
_186
}
_186
_186
// If the user already exists, return the existing user
_186
if (existingAuth) {
_186
console.log("Site auth already exists:", existingAuth);
_186
return;
_186
}
_186
_186
db.run(
_186
"INSERT INTO siteAuthorizations (siteId, accessToken) VALUES (?, ?)",
_186
[siteId, accessToken],
_186
(err) => {
_186
if (err) {
_186
console.error("Error inserting site authorization pairing:", err);
_186
} else {
_186
console.log("Site authorization pairing inserted successfully.");
_186
}
_186
}
_186
);
_186
}
_186
);
_186
}
_186
_186
// Insert a record when the /resolve endpoint succeeds and can trust the user to be associated
_186
// with the access token
_186
function insertUserAuthorization(userId, accessToken) {
_186
db.get(
_186
"SELECT * FROM userAuthorizations WHERE userId = ?",
_186
[userId],
_186
(err, existingTokenAuth) => {
_186
if (err) {
_186
console.error("Error checking for existing user access token:", err);
_186
return;
_186
}
_186
_186
// If the user already exists, return the existing user
_186
if (existingTokenAuth) {
_186
console.log("Access token pairing already exists:", existingTokenAuth);
_186
return;
_186
}
_186
_186
db.run(
_186
"INSERT INTO userAuthorizations (userId, accessToken) VALUES (?, ?)",
_186
[userId, accessToken],
_186
(err) => {
_186
if (err) {
_186
console.error("Error inserting user access token pairing:", err);
_186
} else {
_186
console.log("User access token pairing inserted successfully.");
_186
}
_186
}
_186
);
_186
}
_186
);
_186
}
_186
_186
function getAccessTokenFromSiteId(siteId, callback) {
_186
// Retrieve the access token from the database
_186
db.get(
_186
"SELECT accessToken FROM siteAuthorizations WHERE siteId = ?",
_186
[siteId],
_186
(err, row) => {
_186
if (err) {
_186
console.error("Error retrieving access token:", err);
_186
return callback(err, null);
_186
}
_186
// Check if site exists and has an accessToken
_186
if (row && row.accessToken) {
_186
return callback(null, row.accessToken);
_186
} else {
_186
// No user or no access token available
_186
return callback(
_186
new Error("No access token found or site does not exist"),
_186
null
_186
);
_186
}
_186
}
_186
);
_186
}
_186
_186
function getAccessTokenFromUserId(userId, callback) {
_186
// Retrieve the access token from the database
_186
db.get(
_186
"SELECT accessToken FROM userAuthorizations WHERE userId = ?",
_186
[userId],
_186
(err, row) => {
_186
if (err) {
_186
console.error("Error retrieving access token:", err);
_186
return callback(err, null);
_186
}
_186
// Check if user exists and has an accessToken
_186
if (row && row.accessToken) {
_186
return callback(null, row.accessToken);
_186
} else {
_186
// No user or no access token available
_186
return callback(
_186
new Error("No access token found or user does not exist"),
_186
null
_186
);
_186
}
_186
}
_186
);
_186
}
_186
_186
function clearDatabase() {
_186
db.serialize(() => {
_186
// Clear data from authorizations table
_186
db.run("DELETE FROM authorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing authorizations table:", err);
_186
} else {
_186
console.log("Authorizations table cleared.");
_186
}
_186
});
_186
_186
// Clear data from siteAuthorizations table
_186
db.run("DELETE FROM siteAuthorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing siteAuthorizations table:", err);
_186
} else {
_186
console.log("Site Authorizations table cleared.");
_186
}
_186
});
_186
_186
// Clear data from userAuthorizations table
_186
db.run("DELETE FROM userAuthorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing userAuthorizations table:", err);
_186
} else {
_186
console.log("User Authorizations table cleared.");
_186
}
_186
});
_186
});
_186
}
_186
_186
export default {
_186
db,
_186
insertSiteAuthorization,
_186
insertUserAuthorization,
_186
getAccessTokenFromSiteId,
_186
getAccessTokenFromUserId,
_186
clearDatabase,
_186
};

Now you've set up your server to retreive an access token, let’s configure a database to store user credentials and authorization details in database.js.

Set up the database schema and tables

First, you’ll create the necessary database schema to store site and user authorizations. This includes two main tables to associate site IDs and user IDs with their respective access tokens.

Store authorization data

main.js
server.js
database.js
jwt.js
.env

_186
import sqlite3 from "sqlite3";
_186
import jwt from "jsonwebtoken";
_186
import dotenv from "dotenv";
_186
_186
// Enable dotenv to load environment variables
_186
dotenv.config();
_186
_186
// Enable verbose mode for SQLite3
_186
const sqlite3Verbose = sqlite3.verbose();
_186
_186
// Open SQLite database connection
_186
const db = new sqlite3Verbose.Database("./db/database.db");
_186
_186
// Create tables
_186
db.serialize(() => {
_186
_186
// Table to associate site ID with access token from OAuth
_186
db.run(`
_186
CREATE TABLE IF NOT EXISTS siteAuthorizations (
_186
siteId TEXT PRIMARY KEY,
_186
accessToken TEXT
_186
)
_186
`);
_186
_186
// Table to associate user ID with the access token
_186
db.run(`
_186
CREATE TABLE IF NOT EXISTS userAuthorizations (
_186
id INTEGER PRIMARY KEY AUTOINCREMENT,
_186
userId TEXT,
_186
accessToken TEXT
_186
)
_186
`);
_186
});
_186
_186
// Insert a record after exchanging the OAuth code for an access token
_186
function insertSiteAuthorization(siteId, accessToken) {
_186
db.get(
_186
"SELECT * FROM siteAuthorizations WHERE siteId = ?",
_186
[siteId],
_186
(err, existingAuth) => {
_186
if (err) {
_186
console.error("Error checking for existing site authorization:", err);
_186
return;
_186
}
_186
_186
// If the user already exists, return the existing user
_186
if (existingAuth) {
_186
console.log("Site auth already exists:", existingAuth);
_186
return;
_186
}
_186
_186
db.run(
_186
"INSERT INTO siteAuthorizations (siteId, accessToken) VALUES (?, ?)",
_186
[siteId, accessToken],
_186
(err) => {
_186
if (err) {
_186
console.error("Error inserting site authorization pairing:", err);
_186
} else {
_186
console.log("Site authorization pairing inserted successfully.");
_186
}
_186
}
_186
);
_186
}
_186
);
_186
}
_186
_186
// Insert a record when the /resolve endpoint succeeds and can trust the user to be associated
_186
// with the access token
_186
function insertUserAuthorization(userId, accessToken) {
_186
db.get(
_186
"SELECT * FROM userAuthorizations WHERE userId = ?",
_186
[userId],
_186
(err, existingTokenAuth) => {
_186
if (err) {
_186
console.error("Error checking for existing user access token:", err);
_186
return;
_186
}
_186
_186
// If the user already exists, return the existing user
_186
if (existingTokenAuth) {
_186
console.log("Access token pairing already exists:", existingTokenAuth);
_186
return;
_186
}
_186
_186
db.run(
_186
"INSERT INTO userAuthorizations (userId, accessToken) VALUES (?, ?)",
_186
[userId, accessToken],
_186
(err) => {
_186
if (err) {
_186
console.error("Error inserting user access token pairing:", err);
_186
} else {
_186
console.log("User access token pairing inserted successfully.");
_186
}
_186
}
_186
);
_186
}
_186
);
_186
}
_186
_186
function getAccessTokenFromSiteId(siteId, callback) {
_186
// Retrieve the access token from the database
_186
db.get(
_186
"SELECT accessToken FROM siteAuthorizations WHERE siteId = ?",
_186
[siteId],
_186
(err, row) => {
_186
if (err) {
_186
console.error("Error retrieving access token:", err);
_186
return callback(err, null);
_186
}
_186
// Check if site exists and has an accessToken
_186
if (row && row.accessToken) {
_186
return callback(null, row.accessToken);
_186
} else {
_186
// No user or no access token available
_186
return callback(
_186
new Error("No access token found or site does not exist"),
_186
null
_186
);
_186
}
_186
}
_186
);
_186
}
_186
_186
function getAccessTokenFromUserId(userId, callback) {
_186
// Retrieve the access token from the database
_186
db.get(
_186
"SELECT accessToken FROM userAuthorizations WHERE userId = ?",
_186
[userId],
_186
(err, row) => {
_186
if (err) {
_186
console.error("Error retrieving access token:", err);
_186
return callback(err, null);
_186
}
_186
// Check if user exists and has an accessToken
_186
if (row && row.accessToken) {
_186
return callback(null, row.accessToken);
_186
} else {
_186
// No user or no access token available
_186
return callback(
_186
new Error("No access token found or user does not exist"),
_186
null
_186
);
_186
}
_186
}
_186
);
_186
}
_186
_186
function clearDatabase() {
_186
db.serialize(() => {
_186
// Clear data from authorizations table
_186
db.run("DELETE FROM authorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing authorizations table:", err);
_186
} else {
_186
console.log("Authorizations table cleared.");
_186
}
_186
});
_186
_186
// Clear data from siteAuthorizations table
_186
db.run("DELETE FROM siteAuthorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing siteAuthorizations table:", err);
_186
} else {
_186
console.log("Site Authorizations table cleared.");
_186
}
_186
});
_186
_186
// Clear data from userAuthorizations table
_186
db.run("DELETE FROM userAuthorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing userAuthorizations table:", err);
_186
} else {
_186
console.log("User Authorizations table cleared.");
_186
}
_186
});
_186
});
_186
}
_186
_186
export default {
_186
db,
_186
insertSiteAuthorization,
_186
insertUserAuthorization,
_186
getAccessTokenFromSiteId,
_186
getAccessTokenFromUserId,
_186
clearDatabase,
_186
};

Next, you’ll store the Site IDs and user access tokens in the database using functions that insert new records if they don’t already exist.

  1. Store Site Authorization Data with insertSiteAuthorization
    Use this function to pair a siteId with its access token when a site is initially authorized.
  2. Store User Authorization Data with insertUserAuthorization
    Use this function to pair a userId and access token.

Retrieve authorization data

main.js
server.js
database.js
jwt.js
.env

_186
import sqlite3 from "sqlite3";
_186
import jwt from "jsonwebtoken";
_186
import dotenv from "dotenv";
_186
_186
// Enable dotenv to load environment variables
_186
dotenv.config();
_186
_186
// Enable verbose mode for SQLite3
_186
const sqlite3Verbose = sqlite3.verbose();
_186
_186
// Open SQLite database connection
_186
const db = new sqlite3Verbose.Database("./db/database.db");
_186
_186
// Create tables
_186
db.serialize(() => {
_186
_186
// Table to associate site ID with access token from OAuth
_186
db.run(`
_186
CREATE TABLE IF NOT EXISTS siteAuthorizations (
_186
siteId TEXT PRIMARY KEY,
_186
accessToken TEXT
_186
)
_186
`);
_186
_186
// Table to associate user ID with the access token
_186
db.run(`
_186
CREATE TABLE IF NOT EXISTS userAuthorizations (
_186
id INTEGER PRIMARY KEY AUTOINCREMENT,
_186
userId TEXT,
_186
accessToken TEXT
_186
)
_186
`);
_186
});
_186
_186
// Insert a record after exchanging the OAuth code for an access token
_186
function insertSiteAuthorization(siteId, accessToken) {
_186
db.get(
_186
"SELECT * FROM siteAuthorizations WHERE siteId = ?",
_186
[siteId],
_186
(err, existingAuth) => {
_186
if (err) {
_186
console.error("Error checking for existing site authorization:", err);
_186
return;
_186
}
_186
_186
// If the user already exists, return the existing user
_186
if (existingAuth) {
_186
console.log("Site auth already exists:", existingAuth);
_186
return;
_186
}
_186
_186
db.run(
_186
"INSERT INTO siteAuthorizations (siteId, accessToken) VALUES (?, ?)",
_186
[siteId, accessToken],
_186
(err) => {
_186
if (err) {
_186
console.error("Error inserting site authorization pairing:", err);
_186
} else {
_186
console.log("Site authorization pairing inserted successfully.");
_186
}
_186
}
_186
);
_186
}
_186
);
_186
}
_186
_186
// Insert a record when the /resolve endpoint succeeds and can trust the user to be associated
_186
// with the access token
_186
function insertUserAuthorization(userId, accessToken) {
_186
db.get(
_186
"SELECT * FROM userAuthorizations WHERE userId = ?",
_186
[userId],
_186
(err, existingTokenAuth) => {
_186
if (err) {
_186
console.error("Error checking for existing user access token:", err);
_186
return;
_186
}
_186
_186
// If the user already exists, return the existing user
_186
if (existingTokenAuth) {
_186
console.log("Access token pairing already exists:", existingTokenAuth);
_186
return;
_186
}
_186
_186
db.run(
_186
"INSERT INTO userAuthorizations (userId, accessToken) VALUES (?, ?)",
_186
[userId, accessToken],
_186
(err) => {
_186
if (err) {
_186
console.error("Error inserting user access token pairing:", err);
_186
} else {
_186
console.log("User access token pairing inserted successfully.");
_186
}
_186
}
_186
);
_186
}
_186
);
_186
}
_186
_186
function getAccessTokenFromSiteId(siteId, callback) {
_186
// Retrieve the access token from the database
_186
db.get(
_186
"SELECT accessToken FROM siteAuthorizations WHERE siteId = ?",
_186
[siteId],
_186
(err, row) => {
_186
if (err) {
_186
console.error("Error retrieving access token:", err);
_186
return callback(err, null);
_186
}
_186
// Check if site exists and has an accessToken
_186
if (row && row.accessToken) {
_186
return callback(null, row.accessToken);
_186
} else {
_186
// No user or no access token available
_186
return callback(
_186
new Error("No access token found or site does not exist"),
_186
null
_186
);
_186
}
_186
}
_186
);
_186
}
_186
_186
function getAccessTokenFromUserId(userId, callback) {
_186
// Retrieve the access token from the database
_186
db.get(
_186
"SELECT accessToken FROM userAuthorizations WHERE userId = ?",
_186
[userId],
_186
(err, row) => {
_186
if (err) {
_186
console.error("Error retrieving access token:", err);
_186
return callback(err, null);
_186
}
_186
// Check if user exists and has an accessToken
_186
if (row && row.accessToken) {
_186
return callback(null, row.accessToken);
_186
} else {
_186
// No user or no access token available
_186
return callback(
_186
new Error("No access token found or user does not exist"),
_186
null
_186
);
_186
}
_186
}
_186
);
_186
}
_186
_186
function clearDatabase() {
_186
db.serialize(() => {
_186
// Clear data from authorizations table
_186
db.run("DELETE FROM authorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing authorizations table:", err);
_186
} else {
_186
console.log("Authorizations table cleared.");
_186
}
_186
});
_186
_186
// Clear data from siteAuthorizations table
_186
db.run("DELETE FROM siteAuthorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing siteAuthorizations table:", err);
_186
} else {
_186
console.log("Site Authorizations table cleared.");
_186
}
_186
});
_186
_186
// Clear data from userAuthorizations table
_186
db.run("DELETE FROM userAuthorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing userAuthorizations table:", err);
_186
} else {
_186
console.log("User Authorizations table cleared.");
_186
}
_186
});
_186
});
_186
}
_186
_186
export default {
_186
db,
_186
insertSiteAuthorization,
_186
insertUserAuthorization,
_186
getAccessTokenFromSiteId,
_186
getAccessTokenFromUserId,
_186
clearDatabase,
_186
};

Finally, you’ll retrieve the stored access tokens using functions designed to fetch access tokens based on siteId or userId.

  1. Retrieve Site Access Tokens with getAccessTokenFromSiteId
    This function retrieves the access token for a specific siteId. We'll use this function to obtain user details from Webflow's Resolve ID Token endpoint when a Designer Extension sends an idToken and siteId to our /token endpoint.
  2. Retrieve User Access Tokens with getAccessTokenFromUserId
    Use this function to get a user-specific access token, enabling authenticated access to the Webflow API based on the userId.

Export functions

main.js
server.js
database.js
jwt.js
.env

_186
import sqlite3 from "sqlite3";
_186
import jwt from "jsonwebtoken";
_186
import dotenv from "dotenv";
_186
_186
// Enable dotenv to load environment variables
_186
dotenv.config();
_186
_186
// Enable verbose mode for SQLite3
_186
const sqlite3Verbose = sqlite3.verbose();
_186
_186
// Open SQLite database connection
_186
const db = new sqlite3Verbose.Database("./db/database.db");
_186
_186
// Create tables
_186
db.serialize(() => {
_186
_186
// Table to associate site ID with access token from OAuth
_186
db.run(`
_186
CREATE TABLE IF NOT EXISTS siteAuthorizations (
_186
siteId TEXT PRIMARY KEY,
_186
accessToken TEXT
_186
)
_186
`);
_186
_186
// Table to associate user ID with the access token
_186
db.run(`
_186
CREATE TABLE IF NOT EXISTS userAuthorizations (
_186
id INTEGER PRIMARY KEY AUTOINCREMENT,
_186
userId TEXT,
_186
accessToken TEXT
_186
)
_186
`);
_186
});
_186
_186
// Insert a record after exchanging the OAuth code for an access token
_186
function insertSiteAuthorization(siteId, accessToken) {
_186
db.get(
_186
"SELECT * FROM siteAuthorizations WHERE siteId = ?",
_186
[siteId],
_186
(err, existingAuth) => {
_186
if (err) {
_186
console.error("Error checking for existing site authorization:", err);
_186
return;
_186
}
_186
_186
// If the user already exists, return the existing user
_186
if (existingAuth) {
_186
console.log("Site auth already exists:", existingAuth);
_186
return;
_186
}
_186
_186
db.run(
_186
"INSERT INTO siteAuthorizations (siteId, accessToken) VALUES (?, ?)",
_186
[siteId, accessToken],
_186
(err) => {
_186
if (err) {
_186
console.error("Error inserting site authorization pairing:", err);
_186
} else {
_186
console.log("Site authorization pairing inserted successfully.");
_186
}
_186
}
_186
);
_186
}
_186
);
_186
}
_186
_186
// Insert a record when the /resolve endpoint succeeds and can trust the user to be associated
_186
// with the access token
_186
function insertUserAuthorization(userId, accessToken) {
_186
db.get(
_186
"SELECT * FROM userAuthorizations WHERE userId = ?",
_186
[userId],
_186
(err, existingTokenAuth) => {
_186
if (err) {
_186
console.error("Error checking for existing user access token:", err);
_186
return;
_186
}
_186
_186
// If the user already exists, return the existing user
_186
if (existingTokenAuth) {
_186
console.log("Access token pairing already exists:", existingTokenAuth);
_186
return;
_186
}
_186
_186
db.run(
_186
"INSERT INTO userAuthorizations (userId, accessToken) VALUES (?, ?)",
_186
[userId, accessToken],
_186
(err) => {
_186
if (err) {
_186
console.error("Error inserting user access token pairing:", err);
_186
} else {
_186
console.log("User access token pairing inserted successfully.");
_186
}
_186
}
_186
);
_186
}
_186
);
_186
}
_186
_186
function getAccessTokenFromSiteId(siteId, callback) {
_186
// Retrieve the access token from the database
_186
db.get(
_186
"SELECT accessToken FROM siteAuthorizations WHERE siteId = ?",
_186
[siteId],
_186
(err, row) => {
_186
if (err) {
_186
console.error("Error retrieving access token:", err);
_186
return callback(err, null);
_186
}
_186
// Check if site exists and has an accessToken
_186
if (row && row.accessToken) {
_186
return callback(null, row.accessToken);
_186
} else {
_186
// No user or no access token available
_186
return callback(
_186
new Error("No access token found or site does not exist"),
_186
null
_186
);
_186
}
_186
}
_186
);
_186
}
_186
_186
function getAccessTokenFromUserId(userId, callback) {
_186
// Retrieve the access token from the database
_186
db.get(
_186
"SELECT accessToken FROM userAuthorizations WHERE userId = ?",
_186
[userId],
_186
(err, row) => {
_186
if (err) {
_186
console.error("Error retrieving access token:", err);
_186
return callback(err, null);
_186
}
_186
// Check if user exists and has an accessToken
_186
if (row && row.accessToken) {
_186
return callback(null, row.accessToken);
_186
} else {
_186
// No user or no access token available
_186
return callback(
_186
new Error("No access token found or user does not exist"),
_186
null
_186
);
_186
}
_186
}
_186
);
_186
}
_186
_186
function clearDatabase() {
_186
db.serialize(() => {
_186
// Clear data from authorizations table
_186
db.run("DELETE FROM authorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing authorizations table:", err);
_186
} else {
_186
console.log("Authorizations table cleared.");
_186
}
_186
});
_186
_186
// Clear data from siteAuthorizations table
_186
db.run("DELETE FROM siteAuthorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing siteAuthorizations table:", err);
_186
} else {
_186
console.log("Site Authorizations table cleared.");
_186
}
_186
});
_186
_186
// Clear data from userAuthorizations table
_186
db.run("DELETE FROM userAuthorizations", (err) => {
_186
if (err) {
_186
console.error("Error clearing userAuthorizations table:", err);
_186
} else {
_186
console.log("User Authorizations table cleared.");
_186
}
_186
});
_186
});
_186
}
_186
_186
export default {
_186
db,
_186
insertSiteAuthorization,
_186
insertUserAuthorization,
_186
getAccessTokenFromSiteId,
_186
getAccessTokenFromUserId,
_186
clearDatabase,
_186
};

Once these functions are ready, export them along with the database connection.

Configure authorization flow with token storage

main.js
server.js
database.js
jwt.js
.env

_115
const express = require("express");
_115
const cors = require("cors");
_115
const { WebflowClient } = require("webflow-api");
_115
const axios = require("axios");
_115
require("dotenv").config();
_115
_115
const app = express(); // Create an Express application
_115
const db = require("./database.js"); // Load DB Logic
_115
const jwt = require("./jwt.js")
_115
_115
var corsOptions = { origin: ["http://localhost:1337"] };
_115
_115
// Middleware
_115
app.use(cors(corsOptions)); // Enable CORS with the specified options
_115
app.use(express.json()); // Parse JSON-formatted incoming requests
_115
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_115
_115
// Redirect user to Webflow Authorization screen
_115
app.get("/authorize", (req, res) => {
_115
_115
const authorizeUrl = WebflowClient.authorizeURL({
_115
scope: ["sites:read","authorized_user:read"],
_115
clientId: process.env.WEBFLOW_CLIENT_ID,
_115
})
_115
res.redirect(authorizeUrl)
_115
})
_115
_115
// Optional: Redirect root to Webflow Authorization screen
_115
app.get("/", (req,res) =>{
_115
res.redirect("/authorize")
_115
})
_115
_115
// Exchange the authorization code for an access token and save to DB
_115
app.get("/callback", async (req, res) => {
_115
const { code } = req.query;
_115
_115
// Get Access Token
_115
const accessToken = await WebflowClient.getAccessToken({
_115
clientId: process.env.WEBFLOW_CLIENT_ID,
_115
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_115
code: code,
_115
});
_115
_115
// Instantiate the Webflow Client
_115
const webflow = new WebflowClient({ accessToken });
_115
_115
// Get site ID to pair with the authorization access token
_115
const sites = await webflow.sites.list();
_115
sites.sites.forEach((site) => {
_115
db.insertSiteAuthorization(site.id, accessToken);
_115
});
_115
_115
// Redirect URI with first site, can improve UX later for choosing a site
_115
// to redirect to
_115
const firstSite = sites.sites?.[0];
_115
if (firstSite) {
_115
const shortName = firstSite.shortName;
_115
res.redirect(
_115
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_115
);
_115
return;
_115
}
_115
_115
});
_115
_115
// Authenticate Designer Extension User via ID Token
_115
app.post("/token", async (req, res) => {
_115
const token = req.body.idToken; // Get token from request
_115
_115
// Resolve Session token by makeing a Request to Webflow API
_115
const APP_TOKEN = process.env.APP_TOKEN;
_115
const options = {
_115
method: "POST",
_115
url: "https://api.webflow.com/beta/token/resolve",
_115
headers: {
_115
accept: "application/json",
_115
"content-type": "application/json",
_115
authorization: `Bearer ${process.env.APP_TOKEN}`,
_115
},
_115
data: {
_115
idToken: token,
_115
},
_115
};
_115
const request = await axios.request(options);
_115
const user = request.data;
_115
_115
// Generate a Session Token
_115
const sessionToken = jwt.createSessionToken(user)
_115
_115
// Respond to user with sesion token
_115
res.json({ sessionToken });
_115
});
_115
_115
// Make authenticated request with user's session token
_115
app.get("/sites", jwt.authenticateToken ,async (req, res) => {
_115
_115
try {
_115
// Initialize Webflow Client and make request to sites endpoint
_115
const accessToken = req.accessToken
_115
const webflow = new WebflowClient({ accessToken });
_115
const data = await webflow.sites.list();
_115
console.log(accessToken)
_115
// Send the retrieved data back to the client
_115
res.json({ data });
_115
} catch (error) {
_115
console.error("Error handling authenticated request:", error);
_115
res.status(500).json({ error: "Internal server error" });
_115
}
_115
});
_115
_115
// Start the server
_115
const PORT = process.env.PORT || 3000;
_115
app.listen(PORT, () => {
_115
console.log(`Server is running on http://localhost:${PORT}`);
_115
});

To handle authorization effectively, you’ll need to store access tokens securely.

Import the database module into server.js

Exchange Aauthorization Code for Access Token and store it in the database

main.js
server.js
database.js
jwt.js
.env

_115
const express = require("express");
_115
const cors = require("cors");
_115
const { WebflowClient } = require("webflow-api");
_115
const axios = require("axios");
_115
require("dotenv").config();
_115
_115
const app = express(); // Create an Express application
_115
const db = require("./database.js"); // Load DB Logic
_115
const jwt = require("./jwt.js")
_115
_115
var corsOptions = { origin: ["http://localhost:1337"] };
_115
_115
// Middleware
_115
app.use(cors(corsOptions)); // Enable CORS with the specified options
_115
app.use(express.json()); // Parse JSON-formatted incoming requests
_115
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_115
_115
// Redirect user to Webflow Authorization screen
_115
app.get("/authorize", (req, res) => {
_115
_115
const authorizeUrl = WebflowClient.authorizeURL({
_115
scope: ["sites:read","authorized_user:read"],
_115
clientId: process.env.WEBFLOW_CLIENT_ID,
_115
})
_115
res.redirect(authorizeUrl)
_115
})
_115
_115
// Optional: Redirect root to Webflow Authorization screen
_115
app.get("/", (req,res) =>{
_115
res.redirect("/authorize")
_115
})
_115
_115
// Exchange the authorization code for an access token and save to DB
_115
app.get("/callback", async (req, res) => {
_115
const { code } = req.query;
_115
_115
// Get Access Token
_115
const accessToken = await WebflowClient.getAccessToken({
_115
clientId: process.env.WEBFLOW_CLIENT_ID,
_115
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_115
code: code,
_115
});
_115
_115
// Instantiate the Webflow Client
_115
const webflow = new WebflowClient({ accessToken });
_115
_115
// Get site ID to pair with the authorization access token
_115
const sites = await webflow.sites.list();
_115
sites.sites.forEach((site) => {
_115
db.insertSiteAuthorization(site.id, accessToken);
_115
});
_115
_115
// Redirect URI with first site, can improve UX later for choosing a site
_115
// to redirect to
_115
const firstSite = sites.sites?.[0];
_115
if (firstSite) {
_115
const shortName = firstSite.shortName;
_115
res.redirect(
_115
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_115
);
_115
return;
_115
}
_115
_115
});
_115
_115
// Authenticate Designer Extension User via ID Token
_115
app.post("/token", async (req, res) => {
_115
const token = req.body.idToken; // Get token from request
_115
_115
// Resolve Session token by makeing a Request to Webflow API
_115
const APP_TOKEN = process.env.APP_TOKEN;
_115
const options = {
_115
method: "POST",
_115
url: "https://api.webflow.com/beta/token/resolve",
_115
headers: {
_115
accept: "application/json",
_115
"content-type": "application/json",
_115
authorization: `Bearer ${process.env.APP_TOKEN}`,
_115
},
_115
data: {
_115
idToken: token,
_115
},
_115
};
_115
const request = await axios.request(options);
_115
const user = request.data;
_115
_115
// Generate a Session Token
_115
const sessionToken = jwt.createSessionToken(user)
_115
_115
// Respond to user with sesion token
_115
res.json({ sessionToken });
_115
});
_115
_115
// Make authenticated request with user's session token
_115
app.get("/sites", jwt.authenticateToken ,async (req, res) => {
_115
_115
try {
_115
// Initialize Webflow Client and make request to sites endpoint
_115
const accessToken = req.accessToken
_115
const webflow = new WebflowClient({ accessToken });
_115
const data = await webflow.sites.list();
_115
console.log(accessToken)
_115
// Send the retrieved data back to the client
_115
res.json({ data });
_115
} catch (error) {
_115
console.error("Error handling authenticated request:", error);
_115
res.status(500).json({ error: "Internal server error" });
_115
}
_115
});
_115
_115
// Start the server
_115
const PORT = process.env.PORT || 3000;
_115
app.listen(PORT, () => {
_115
console.log(`Server is running on http://localhost:${PORT}`);
_115
});

Update the /callback endpoint in server.js to exchange the code for an accessToken.

To get a list of Sites that the App is authorized to access, instatiate the WebflowClient and call the List Sites endpoint. For each authorized site, use the db.insertSiteAuthorization function to store the siteId and corresponding accessToken in the database for secure, future access.

Redirect the User to the Designer Extension using a deep link

main.js
server.js
database.js
jwt.js
.env

_115
const express = require("express");
_115
const cors = require("cors");
_115
const { WebflowClient } = require("webflow-api");
_115
const axios = require("axios");
_115
require("dotenv").config();
_115
_115
const app = express(); // Create an Express application
_115
const db = require("./database.js"); // Load DB Logic
_115
const jwt = require("./jwt.js")
_115
_115
var corsOptions = { origin: ["http://localhost:1337"] };
_115
_115
// Middleware
_115
app.use(cors(corsOptions)); // Enable CORS with the specified options
_115
app.use(express.json()); // Parse JSON-formatted incoming requests
_115
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_115
_115
// Redirect user to Webflow Authorization screen
_115
app.get("/authorize", (req, res) => {
_115
_115
const authorizeUrl = WebflowClient.authorizeURL({
_115
scope: ["sites:read","authorized_user:read"],
_115
clientId: process.env.WEBFLOW_CLIENT_ID,
_115
})
_115
res.redirect(authorizeUrl)
_115
})
_115
_115
// Optional: Redirect root to Webflow Authorization screen
_115
app.get("/", (req,res) =>{
_115
res.redirect("/authorize")
_115
})
_115
_115
// Exchange the authorization code for an access token and save to DB
_115
app.get("/callback", async (req, res) => {
_115
const { code } = req.query;
_115
_115
// Get Access Token
_115
const accessToken = await WebflowClient.getAccessToken({
_115
clientId: process.env.WEBFLOW_CLIENT_ID,
_115
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_115
code: code,
_115
});
_115
_115
// Instantiate the Webflow Client
_115
const webflow = new WebflowClient({ accessToken });
_115
_115
// Get site ID to pair with the authorization access token
_115
const sites = await webflow.sites.list();
_115
sites.sites.forEach((site) => {
_115
db.insertSiteAuthorization(site.id, accessToken);
_115
});
_115
_115
// Redirect URI with first site, can improve UX later for choosing a site
_115
// to redirect to
_115
const firstSite = sites.sites?.[0];
_115
if (firstSite) {
_115
const shortName = firstSite.shortName;
_115
res.redirect(
_115
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_115
);
_115
return;
_115
}
_115
_115
});
_115
_115
// Authenticate Designer Extension User via ID Token
_115
app.post("/token", async (req, res) => {
_115
const token = req.body.idToken; // Get token from request
_115
_115
// Resolve Session token by makeing a Request to Webflow API
_115
const APP_TOKEN = process.env.APP_TOKEN;
_115
const options = {
_115
method: "POST",
_115
url: "https://api.webflow.com/beta/token/resolve",
_115
headers: {
_115
accept: "application/json",
_115
"content-type": "application/json",
_115
authorization: `Bearer ${process.env.APP_TOKEN}`,
_115
},
_115
data: {
_115
idToken: token,
_115
},
_115
};
_115
const request = await axios.request(options);
_115
const user = request.data;
_115
_115
// Generate a Session Token
_115
const sessionToken = jwt.createSessionToken(user)
_115
_115
// Respond to user with sesion token
_115
res.json({ sessionToken });
_115
});
_115
_115
// Make authenticated request with user's session token
_115
app.get("/sites", jwt.authenticateToken ,async (req, res) => {
_115
_115
try {
_115
// Initialize Webflow Client and make request to sites endpoint
_115
const accessToken = req.accessToken
_115
const webflow = new WebflowClient({ accessToken });
_115
const data = await webflow.sites.list();
_115
console.log(accessToken)
_115
// Send the retrieved data back to the client
_115
res.json({ data });
_115
} catch (error) {
_115
console.error("Error handling authenticated request:", error);
_115
res.status(500).json({ error: "Internal server error" });
_115
}
_115
});
_115
_115
// Start the server
_115
const PORT = process.env.PORT || 3000;
_115
app.listen(PORT, () => {
_115
console.log(`Server is running on http://localhost:${PORT}`);
_115
});

This link allows the user to seamlessly continue within your App in the Webflow Designer. For now, the example below redirects to the first available site, but you could enhance the UX by allowing users to select a specific site before redirecting.

Integrate JWT for secure session management

main.js
server.js
database.js
jwt.js
.env

_71
import jwt from "jsonwebtoken";
_71
import db from "./database.js";
_71
_71
// Given a site ID, retrieve associated Access Token
_71
const retrieveAccessToken = (req, res, next) => {
_71
const idToken = req.body.idToken;
_71
const siteId = req.body.siteId;
_71
_71
if (!idToken) {
_71
return res.status(401).json({ message: "ID Token is missing" });
_71
}
_71
if (!siteId) {
_71
return res.status(401).json({ message: "Site ID is missing" });
_71
}
_71
_71
db.getAccessTokenFromSiteId(siteId, (error, accessToken) => {
_71
if (error) {
_71
return res.status(500).json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
};
_71
_71
const createSessionToken = (user) => {
_71
const sessionToken = jwt.sign({ user }, process.env.WEBFLOW_CLIENT_SECRET, {
_71
expiresIn: "24h",
_71
}); // Example expiration time of 1 hour}
_71
const decodedToken = jwt.decode(sessionToken);
_71
return {
_71
sessionToken,
_71
exp: decodedToken.exp,
_71
};
_71
};
_71
_71
// Middleware to authenticate and validate JWT, and fetch the access token given the user ID
_71
const authenticateSessionToken = (req, res, next) => {
_71
const authHeader = req.headers.authorization;
_71
const sessionToken = authHeader && authHeader.split(" ")[1]; // Extract the token from 'Bearer <token>'
_71
if (!sessionToken) {
_71
return res.status(401).json({ message: "Authentication token is missing" });
_71
}
_71
_71
// Verify the Token
_71
jwt.verify(sessionToken, process.env.WEBFLOW_CLIENT_SECRET, (err, user) => {
_71
if (err) {
_71
return res.status(403).json({ message: "Invalid or expired token" });
_71
}
_71
_71
// Use the user details to fetch the access token from the database
_71
db.getAccessTokenFromUserId(user.user.id, (error, accessToken) => {
_71
if (error) {
_71
return res
_71
.status(500)
_71
.json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
});
_71
};
_71
_71
export default {
_71
createSessionToken,
_71
retrieveAccessToken,
_71
authenticateSessionToken,
_71
};

With our database configured to handle access tokens, it’s time to implement JSON Web Tokens (JWT) to securely manage sessions and authenticate requests in the Data Client.

JWT will enable us to issue session tokens and validate them, allowing for a secure, stateless authentication flow.

Setting up JWT middleware

main.js
server.js
database.js
jwt.js
.env

_71
import jwt from "jsonwebtoken";
_71
import db from "./database.js";
_71
_71
// Given a site ID, retrieve associated Access Token
_71
const retrieveAccessToken = (req, res, next) => {
_71
const idToken = req.body.idToken;
_71
const siteId = req.body.siteId;
_71
_71
if (!idToken) {
_71
return res.status(401).json({ message: "ID Token is missing" });
_71
}
_71
if (!siteId) {
_71
return res.status(401).json({ message: "Site ID is missing" });
_71
}
_71
_71
db.getAccessTokenFromSiteId(siteId, (error, accessToken) => {
_71
if (error) {
_71
return res.status(500).json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
};
_71
_71
const createSessionToken = (user) => {
_71
const sessionToken = jwt.sign({ user }, process.env.WEBFLOW_CLIENT_SECRET, {
_71
expiresIn: "24h",
_71
}); // Example expiration time of 1 hour}
_71
const decodedToken = jwt.decode(sessionToken);
_71
return {
_71
sessionToken,
_71
exp: decodedToken.exp,
_71
};
_71
};
_71
_71
// Middleware to authenticate and validate JWT, and fetch the access token given the user ID
_71
const authenticateSessionToken = (req, res, next) => {
_71
const authHeader = req.headers.authorization;
_71
const sessionToken = authHeader && authHeader.split(" ")[1]; // Extract the token from 'Bearer <token>'
_71
if (!sessionToken) {
_71
return res.status(401).json({ message: "Authentication token is missing" });
_71
}
_71
_71
// Verify the Token
_71
jwt.verify(sessionToken, process.env.WEBFLOW_CLIENT_SECRET, (err, user) => {
_71
if (err) {
_71
return res.status(403).json({ message: "Invalid or expired token" });
_71
}
_71
_71
// Use the user details to fetch the access token from the database
_71
db.getAccessTokenFromUserId(user.user.id, (error, accessToken) => {
_71
if (error) {
_71
return res
_71
.status(500)
_71
.json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
});
_71
};
_71
_71
export default {
_71
createSessionToken,
_71
retrieveAccessToken,
_71
authenticateSessionToken,
_71
};

In jwt.js, start by importing the jsonwebtoken library and your custom database module. These imports will allow you to handle JWT creation and validation, as well as access and store authorization data in your database.

Retrieve the Access Token based on the Site ID

main.js
server.js
database.js
jwt.js
.env

_71
import jwt from "jsonwebtoken";
_71
import db from "./database.js";
_71
_71
// Given a site ID, retrieve associated Access Token
_71
const retrieveAccessToken = (req, res, next) => {
_71
const idToken = req.body.idToken;
_71
const siteId = req.body.siteId;
_71
_71
if (!idToken) {
_71
return res.status(401).json({ message: "ID Token is missing" });
_71
}
_71
if (!siteId) {
_71
return res.status(401).json({ message: "Site ID is missing" });
_71
}
_71
_71
db.getAccessTokenFromSiteId(siteId, (error, accessToken) => {
_71
if (error) {
_71
return res.status(500).json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
};
_71
_71
const createSessionToken = (user) => {
_71
const sessionToken = jwt.sign({ user }, process.env.WEBFLOW_CLIENT_SECRET, {
_71
expiresIn: "24h",
_71
}); // Example expiration time of 1 hour}
_71
const decodedToken = jwt.decode(sessionToken);
_71
return {
_71
sessionToken,
_71
exp: decodedToken.exp,
_71
};
_71
};
_71
_71
// Middleware to authenticate and validate JWT, and fetch the access token given the user ID
_71
const authenticateSessionToken = (req, res, next) => {
_71
const authHeader = req.headers.authorization;
_71
const sessionToken = authHeader && authHeader.split(" ")[1]; // Extract the token from 'Bearer <token>'
_71
if (!sessionToken) {
_71
return res.status(401).json({ message: "Authentication token is missing" });
_71
}
_71
_71
// Verify the Token
_71
jwt.verify(sessionToken, process.env.WEBFLOW_CLIENT_SECRET, (err, user) => {
_71
if (err) {
_71
return res.status(403).json({ message: "Invalid or expired token" });
_71
}
_71
_71
// Use the user details to fetch the access token from the database
_71
db.getAccessTokenFromUserId(user.user.id, (error, accessToken) => {
_71
if (error) {
_71
return res
_71
.status(500)
_71
.json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
});
_71
};
_71
_71
export default {
_71
createSessionToken,
_71
retrieveAccessToken,
_71
authenticateSessionToken,
_71
};

Create the retrieveAccessToken function to obtain the Access Token associated with a given siteId using the db.getAccssTokenFromSiteId function we created in our database module.

When the Designer Extension passes a siteId and idToken to our /token endpoint in the Data Client, we'll use this middleware to retreive the Access Token associated with the siteId and attach it to the request object. This way, subsequent steps in the endpoint have access to the token, allowing the Data Client to securely interact with Webflow’s API.

Create the Session Token from User Data

main.js
server.js
database.js
jwt.js
.env

_71
import jwt from "jsonwebtoken";
_71
import db from "./database.js";
_71
_71
// Given a site ID, retrieve associated Access Token
_71
const retrieveAccessToken = (req, res, next) => {
_71
const idToken = req.body.idToken;
_71
const siteId = req.body.siteId;
_71
_71
if (!idToken) {
_71
return res.status(401).json({ message: "ID Token is missing" });
_71
}
_71
if (!siteId) {
_71
return res.status(401).json({ message: "Site ID is missing" });
_71
}
_71
_71
db.getAccessTokenFromSiteId(siteId, (error, accessToken) => {
_71
if (error) {
_71
return res.status(500).json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
};
_71
_71
const createSessionToken = (user) => {
_71
const sessionToken = jwt.sign({ user }, process.env.WEBFLOW_CLIENT_SECRET, {
_71
expiresIn: "24h",
_71
}); // Example expiration time of 1 hour}
_71
const decodedToken = jwt.decode(sessionToken);
_71
return {
_71
sessionToken,
_71
exp: decodedToken.exp,
_71
};
_71
};
_71
_71
// Middleware to authenticate and validate JWT, and fetch the access token given the user ID
_71
const authenticateSessionToken = (req, res, next) => {
_71
const authHeader = req.headers.authorization;
_71
const sessionToken = authHeader && authHeader.split(" ")[1]; // Extract the token from 'Bearer <token>'
_71
if (!sessionToken) {
_71
return res.status(401).json({ message: "Authentication token is missing" });
_71
}
_71
_71
// Verify the Token
_71
jwt.verify(sessionToken, process.env.WEBFLOW_CLIENT_SECRET, (err, user) => {
_71
if (err) {
_71
return res.status(403).json({ message: "Invalid or expired token" });
_71
}
_71
_71
// Use the user details to fetch the access token from the database
_71
db.getAccessTokenFromUserId(user.user.id, (error, accessToken) => {
_71
if (error) {
_71
return res
_71
.status(500)
_71
.json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
});
_71
};
_71
_71
export default {
_71
createSessionToken,
_71
retrieveAccessToken,
_71
authenticateSessionToken,
_71
};

The createSessionToken function generates a session token using JWT based on user data from the resolved idToken.

This sessionToken, which includes the user’s details, will later be verified for secure access. The token is set to expire after a defined time (in this example, 24 hours), ensuring that the session remains secure and temporary.

Authenticate the Session Token to Validate User Access on Each Request

main.js
server.js
database.js
jwt.js
.env

_71
import jwt from "jsonwebtoken";
_71
import db from "./database.js";
_71
_71
// Given a site ID, retrieve associated Access Token
_71
const retrieveAccessToken = (req, res, next) => {
_71
const idToken = req.body.idToken;
_71
const siteId = req.body.siteId;
_71
_71
if (!idToken) {
_71
return res.status(401).json({ message: "ID Token is missing" });
_71
}
_71
if (!siteId) {
_71
return res.status(401).json({ message: "Site ID is missing" });
_71
}
_71
_71
db.getAccessTokenFromSiteId(siteId, (error, accessToken) => {
_71
if (error) {
_71
return res.status(500).json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
};
_71
_71
const createSessionToken = (user) => {
_71
const sessionToken = jwt.sign({ user }, process.env.WEBFLOW_CLIENT_SECRET, {
_71
expiresIn: "24h",
_71
}); // Example expiration time of 1 hour}
_71
const decodedToken = jwt.decode(sessionToken);
_71
return {
_71
sessionToken,
_71
exp: decodedToken.exp,
_71
};
_71
};
_71
_71
// Middleware to authenticate and validate JWT, and fetch the access token given the user ID
_71
const authenticateSessionToken = (req, res, next) => {
_71
const authHeader = req.headers.authorization;
_71
const sessionToken = authHeader && authHeader.split(" ")[1]; // Extract the token from 'Bearer <token>'
_71
if (!sessionToken) {
_71
return res.status(401).json({ message: "Authentication token is missing" });
_71
}
_71
_71
// Verify the Token
_71
jwt.verify(sessionToken, process.env.WEBFLOW_CLIENT_SECRET, (err, user) => {
_71
if (err) {
_71
return res.status(403).json({ message: "Invalid or expired token" });
_71
}
_71
_71
// Use the user details to fetch the access token from the database
_71
db.getAccessTokenFromUserId(user.user.id, (error, accessToken) => {
_71
if (error) {
_71
return res
_71
.status(500)
_71
.json({ error: "Failed to retrieve access token" });
_71
}
_71
// Attach access token in the request object so that you can make an authenticated request to Webflow
_71
req.accessToken = accessToken;
_71
_71
next(); // Proceed to next middleware or route handler
_71
});
_71
});
_71
};
_71
_71
export default {
_71
createSessionToken,
_71
retrieveAccessToken,
_71
authenticateSessionToken,
_71
};

The authenticateSessionToken function is middleware that validates the sessionToken provided in the authorization header of each request. By verifying the sessionToken, this function ensures that only authorized users can access protected routes. Upon successful validation, it retrieves the accessToken for the user and attaches it to the request, allowing further secure interactions with Webflow.

Export these functions once they are ready.

Using JWT middleware in the server

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

Once the JWT functions are set up, the next step is to import them into server.js to securely manage session tokens and authenticate requests.

Import JWT Middleware Functions

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

At the beginning of server.js, import the the JWT middleware.

Update the /token endpoint to authenticate the Designer Extension user

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

Now that you’ve set up the JWT and database functions, you can update the /token endpoint in server.js to securely authenticate the user. This endpoint will:

  1. Retrieve the accessToken associated with the user's siteId
  2. Get user details from the resolved idToken
  3. Generate a Session Token from user details
  4. Store the authorization details

See the steps below for more detail.

1. Retrieve the accessToken associated with the user's siteId

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

The jwt.retrieveAccessToken middleware fetches the accessToken associated with the siteId and attaches it to the request, ensuring secure access for the Webflow API call.

2. Get user details from the resolved idToken

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

Using the accessToken, this endpoint sends an authenticated request to Webflow’s Resolve ID Token endpoint, which validates the idToken received from the Designer Extension, and returns the following user details: id, email, firstName, and lastName.

3. Generate a Session Token

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

With the newly obtained user information, the jwt.createSessionToken function creates a Session Token for the authenticated user, establishing a secure, temporary access session.

4. Store Authorization Details

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

Finally, the retrieved accessToken is stored in the database and associated with the userId using db.insertUserAuthorization, enabling easy access and authorization for subsequent requests

Create a protected endpoint for authenticated Webflow requests

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

To demonstrate how to make secure, protected requests, let's set up an endpoint that requires a Session Token for access. We'll create a new /sites endpoint that will:

  1. Authenticate the Session Token
  2. Access Webflow data in the Data Client
  3. Return Webflow data to the Designer Extension

1. Authenticate the Session Token

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

This endpoint will use jwt.authenticateSessionToken middleware to verifiy the session token passed with the request, ensuring that only authorized users can access this endpoint. Once authenticated, the middleware retrieves the user's associated accessToken and attaches it to the request.

2. Access Webflow Data in the Data Client

server.js

_1

The Data Client then uses the accessToken attached to the request to initialize the Webflow client and fetch data from Webflow’s List Sites endpoint.

3. Return Webflow Data to the Designer Extension

main.js
server.js
database.js
jwt.js
.env

_128
const express = require("express");
_128
const cors = require("cors");
_128
const { WebflowClient } = require("webflow-api");
_128
const axios = require("axios");
_128
require("dotenv").config();
_128
_128
const app = express(); // Create an Express application
_128
const db = require("./database.js"); // Load DB Logic
_128
const jwt = require("./jwt.js")
_128
_128
var corsOptions = { origin: ["http://localhost:1337"] };
_128
_128
// Middleware
_128
app.use(cors(corsOptions)); // Enable CORS with the specified options
_128
app.use(express.json()); // Parse JSON-formatted incoming requests
_128
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded incoming requests with extended syntax
_128
_128
// Redirect user to Webflow Authorization screen
_128
app.get("/authorize", (req, res) => {
_128
_128
const authorizeUrl = WebflowClient.authorizeURL({
_128
scope: ["sites:read","authorized_user:read"],
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
})
_128
res.redirect(authorizeUrl)
_128
})
_128
_128
// Optional: Redirect root to Webflow Authorization screen
_128
app.get("/", (req,res) =>{
_128
res.redirect("/authorize")
_128
})
_128
_128
// Exchange the authorization code for an access token and save to DB
_128
app.get("/callback", async (req, res) => {
_128
const { code } = req.query;
_128
_128
// Get Access Token
_128
const accessToken = await WebflowClient.getAccessToken({
_128
clientId: process.env.WEBFLOW_CLIENT_ID,
_128
clientSecret: process.env.WEBFLOW_CLIENT_SECRET,
_128
code: code,
_128
});
_128
_128
// Instantiate the Webflow Client
_128
const webflow = new WebflowClient({ accessToken });
_128
_128
// Get site ID to pair with the authorization access token
_128
const sites = await webflow.sites.list();
_128
sites.sites.forEach((site) => {
_128
db.insertSiteAuthorization(site.id, accessToken);
_128
});
_128
_128
// Redirect URI with first site, can improve UX later for choosing a site
_128
// to redirect to
_128
const firstSite = sites.sites?.[0];
_128
if (firstSite) {
_128
const shortName = firstSite.shortName;
_128
res.redirect(
_128
`https://${shortName}.design.webflow.com?app=${process.env.WEBFLOW_CLIENT_ID}`
_128
);
_128
return;
_128
}
_128
_128
// Send Auth Complete Screen with Post Message
_128
const filePath = path.join(__dirname, "public", "authComplete.html");
_128
res.sendFile(filePath);
_128
});
_128
_128
// Authenticate Designer Extension User via ID Token
_128
app.post("/token", jwt.retrieveAccessToken, async (req, res) => {
_128
const token = req.body.idToken; // Get token from request
_128
_128
// Resolve ID token by making a request to Webflow API
_128
let sessionToken;
_128
try {
_128
const options = {
_128
method: "POST",
_128
url: "https://api.webflow.com/beta/token/resolve",
_128
headers: {
_128
Accept: "application/json",
_128
"Content-Type": "application/json",
_128
Authorization: `Bearer ${req.accessToken}`,
_128
},
_128
data: {
_128
idToken: token,
_128
},
_128
};
_128
const request = await axios.request(options);
_128
const user = request.data;
_128
_128
// Generate a Session Token
_128
const tokenPayload = jwt.createSessionToken(user);
_128
sessionToken = tokenPayload.sessionToken;
_128
const expAt = tokenPayload.exp;
_128
db.insertUserAuthorization(user.id, req.accessToken);
_128
// Respond to user with sesion token
_128
res.json({ sessionToken, exp: expAt });
_128
} catch (e) {
_128
console.error(
_128
"Unauthorized; user is not associated with authorization for this site",
_128
);
_128
res.status(401).json({
_128
error: "Error: User is not associated with authorization for this site",
_128
});
_128
}
_128
});
_128
_128
// Make authenticated request with user's session token
_128
app.get("/sites", jwt.authenticateSessionToken, async (req, res) => {
_128
try {
_128
// Initialize Webflow Client and make request to sites endpoint
_128
const accessToken = req.accessToken;
_128
const webflow = new WebflowClient({ accessToken });
_128
const data = await webflow.sites.list();
_128
// Send the retrieved data back to the client
_128
res.json({ data });
_128
} catch (error) {
_128
console.error("Error handling authenticated request:", error);
_128
res.status(500).json({ error: "Internal server error" });
_128
}
_128
});
_128
_128
_128
// Start the server
_128
const PORT = process.env.PORT || 3000;
_128
app.listen(PORT, () => {
_128
console.log(`Server is running on http://localhost:${PORT}`);
_128
});

After retrieving the data, the endpoint responds with the information, providing the user access to their Webflow sites in the Designer Extension.

Start your App and test your authentication flow

You're all set up to securely make calls to the Webflow Data API from a Designer Extension. To test out your App, run the following command to get started:

$ npm run dev

Once your App is up and running:

  1. Set Your Callback URI: Update the Webflow dashboard with the correct callback URI to ensure the OAuth flow can return to your app after authentication.
  2. Open your App in the Designer: Open a site in the Designer and open your test App with the corresponding CLIENT_ID
  3. Authorize in the Designer Extension: Open your Designer Extension and click the "Authorize" button to initiate the authentication flow.

Congratulations!

You have a working Hybrid app that can securely make requests to Webflow’s Data APIs.

Next steps

Add Elements to the Canvas in bulk

Create and manpulate complex hierarchical designs using the ElementBuilder APIs.

Add Custom Code to a Site

Learn how to register, apply, and manage custom scripts on a site or page using the Data APIs.

Publish your Hybrid App

Once you've finished developing your app, discover how to publish it and share it in the marketplace.

Securely connect your browser-based Designer Extension to your server-side Data Client.

In this tutorial we’ll build a Hybrid App that:

  • Authorizes sites upon App installation
  • Transmits a Site ID and ID Token
    Send a Site ID and ID Token from the Designer Extension to the Data Client for verification.
  • Resolves the ID Token on the Server Side
    The Data Client resolves the ID Token to get user details from Webflow and verify access.
  • Maps user authorization
    Establish a link between the user and their authorized sites, allowing for secure access to site data.
  • Creates a Session Token for the user
    Generate and send a session token to the Designer Extension to securely maintain the user’s session and enable authenticated requests.
  • Makes authenticated requests to Webflow’s Data APIs
    Use the session token and stored tokens to make authenticated API calls to Webflow’s Data APIs.

By the end, you’ll have a secure, fully integrated setup to handle user sessions and seamlessly make requests to external APIs.

Prerequisites

  • A Hybrid App with the following scopes: sites:read, authorized_user:read
  • The CLIENT_ID and CLIENT_SECRET for your App
  • An ngrok authentication token
  • An understanding of how to authenticate a Webflow User
  • Basic knowledge of Node.js and Express
  • Familiarity with building React Single-Page Applications

Set up your development environment

Clone the starter code

If you have the GitHub CLI installed, type the following command into your terminal.

$ gh repo clone Webflow-Examples/Hybrid-App-Authentication

Otherwise, you can clone the repository from GitHub.com

Install dependencies

This example contains both a Designer Extension and Data Client project. Input the following commands in your terminal to install all necessary dependencies for the example.

$ cd hybrid-app-authentication
npm install
npm run install-frontend
npm run install-backend

Add environment variables

Replace the example values in the .env.example file with your credentials. Rename the file to .env

Review Designer Extension

In this tutorial, most of our focus will be on setting up and configuring the Data Client. However, to kick off the authentication process, we need to first send an ID token and Site ID from the Designer Extension to the Data Client. This step will allow us to exchange these tokens for a session token, which is required for making authenticated requests to Webflow’s API from the browser.

To get started, review the exchangeAndVerifyIdToken function in the Designer Extension.

Retrieving the idToken from the Designer Extension.

The exchangeAndVerifyIdToken function initiates the authentication process by using the Webflow Designer API’s getIdToken and getSiteInfo methods to retrieve the idToken and siteId from the Designer Extension.

Exchanging the idToken and siteId for a Session Token

With the ID token and Site ID in hand, the Designer Extension then sends this information to an endpoint on the Data Client.

The Data Client verifies the ID token and returns a session token, which the Designer Extension will use to make authenticated requests to Webflow. This token ensures secure, temporary access and allows the Designer Extension to manage user sessions as we continue through the setup.

Set up your server

Switching to the Data Client, let’s quickly set up an Express server to handle incoming requests and prepare for adding authentication in Data Client/server.js.

Initialize Express

Create your server with Express, configure CORS to accept incoming requests from your Designer, and set up middleware for JSON and URL-encoded requests

Configure authorization flow

Add an endpoint on your server to handle the OAuth callback and retrieve an authorization_code from the URI’s query parameters. Then, create a /callback endpoint to exchange the code for an accessToken for your user.

For an in-depth explanation on setting up auth, check out the guide

Handling the ID Token and Site ID from the Designer Extension

In addition to retrieving an accessToken through OAuth, your Designer Extension will also receive an idToken and a siteId during the authentication process for the Designer Extension. These tokens enable you to verify and authorize user access securely.

To manage this, you’ll need to set up a new endpoint that:

  1. Validates the ID Token received from the Designer Extension using the Resolve ID Token endpoint.
  2. Retrieves an accessToken associated with the Site ID from a database
  3. Generates a Session Token for secure, temporary access.
  4. Stores the accessToken in your database, associating it with the user or site.
  5. Sends the Session Token to the Designer Extension

For now, we’ll set up the endpoint to receive the ID Token. We’ll add the database storage, access token retrieval, and session token generation in the next steps.

Set up your database to save and retrieve credentials

Now you've set up your server to retreive an access token, let’s configure a database to store user credentials and authorization details in database.js.

Set up the database schema and tables

First, you’ll create the necessary database schema to store site and user authorizations. This includes two main tables to associate site IDs and user IDs with their respective access tokens.

Store authorization data

Next, you’ll store the Site IDs and user access tokens in the database using functions that insert new records if they don’t already exist.

  1. Store Site Authorization Data with insertSiteAuthorization
    Use this function to pair a siteId with its access token when a site is initially authorized.
  2. Store User Authorization Data with insertUserAuthorization
    Use this function to pair a userId and access token.

Retrieve authorization data

Finally, you’ll retrieve the stored access tokens using functions designed to fetch access tokens based on siteId or userId.

  1. Retrieve Site Access Tokens with getAccessTokenFromSiteId
    This function retrieves the access token for a specific siteId. We'll use this function to obtain user details from Webflow's Resolve ID Token endpoint when a Designer Extension sends an idToken and siteId to our /token endpoint.
  2. Retrieve User Access Tokens with getAccessTokenFromUserId
    Use this function to get a user-specific access token, enabling authenticated access to the Webflow API based on the userId.

Export functions

Once these functions are ready, export them along with the database connection.

Configure authorization flow with token storage

To handle authorization effectively, you’ll need to store access tokens securely.

Import the database module into server.js

Exchange Aauthorization Code for Access Token and store it in the database

Update the /callback endpoint in server.js to exchange the code for an accessToken.

To get a list of Sites that the App is authorized to access, instatiate the WebflowClient and call the List Sites endpoint. For each authorized site, use the db.insertSiteAuthorization function to store the siteId and corresponding accessToken in the database for secure, future access.

Redirect the User to the Designer Extension using a deep link

This link allows the user to seamlessly continue within your App in the Webflow Designer. For now, the example below redirects to the first available site, but you could enhance the UX by allowing users to select a specific site before redirecting.

Integrate JWT for secure session management

With our database configured to handle access tokens, it’s time to implement JSON Web Tokens (JWT) to securely manage sessions and authenticate requests in the Data Client.

JWT will enable us to issue session tokens and validate them, allowing for a secure, stateless authentication flow.

Setting up JWT middleware

In jwt.js, start by importing the jsonwebtoken library and your custom database module. These imports will allow you to handle JWT creation and validation, as well as access and store authorization data in your database.

Retrieve the Access Token based on the Site ID

Create the retrieveAccessToken function to obtain the Access Token associated with a given siteId using the db.getAccssTokenFromSiteId function we created in our database module.

When the Designer Extension passes a siteId and idToken to our /token endpoint in the Data Client, we'll use this middleware to retreive the Access Token associated with the siteId and attach it to the request object. This way, subsequent steps in the endpoint have access to the token, allowing the Data Client to securely interact with Webflow’s API.

Create the Session Token from User Data

The createSessionToken function generates a session token using JWT based on user data from the resolved idToken.

This sessionToken, which includes the user’s details, will later be verified for secure access. The token is set to expire after a defined time (in this example, 24 hours), ensuring that the session remains secure and temporary.

Authenticate the Session Token to Validate User Access on Each Request

The authenticateSessionToken function is middleware that validates the sessionToken provided in the authorization header of each request. By verifying the sessionToken, this function ensures that only authorized users can access protected routes. Upon successful validation, it retrieves the accessToken for the user and attaches it to the request, allowing further secure interactions with Webflow.

Export these functions once they are ready.

Using JWT middleware in the server

Once the JWT functions are set up, the next step is to import them into server.js to securely manage session tokens and authenticate requests.

Import JWT Middleware Functions

At the beginning of server.js, import the the JWT middleware.

Update the /token endpoint to authenticate the Designer Extension user

Now that you’ve set up the JWT and database functions, you can update the /token endpoint in server.js to securely authenticate the user. This endpoint will:

  1. Retrieve the accessToken associated with the user's siteId
  2. Get user details from the resolved idToken
  3. Generate a Session Token from user details
  4. Store the authorization details

See the steps below for more detail.

1. Retrieve the accessToken associated with the user's siteId

The jwt.retrieveAccessToken middleware fetches the accessToken associated with the siteId and attaches it to the request, ensuring secure access for the Webflow API call.

2. Get user details from the resolved idToken

Using the accessToken, this endpoint sends an authenticated request to Webflow’s Resolve ID Token endpoint, which validates the idToken received from the Designer Extension, and returns the following user details: id, email, firstName, and lastName.

3. Generate a Session Token

With the newly obtained user information, the jwt.createSessionToken function creates a Session Token for the authenticated user, establishing a secure, temporary access session.

4. Store Authorization Details

Finally, the retrieved accessToken is stored in the database and associated with the userId using db.insertUserAuthorization, enabling easy access and authorization for subsequent requests

Create a protected endpoint for authenticated Webflow requests

To demonstrate how to make secure, protected requests, let's set up an endpoint that requires a Session Token for access. We'll create a new /sites endpoint that will:

  1. Authenticate the Session Token
  2. Access Webflow data in the Data Client
  3. Return Webflow data to the Designer Extension

1. Authenticate the Session Token

This endpoint will use jwt.authenticateSessionToken middleware to verifiy the session token passed with the request, ensuring that only authorized users can access this endpoint. Once authenticated, the middleware retrieves the user's associated accessToken and attaches it to the request.

2. Access Webflow Data in the Data Client

server.js

_1

The Data Client then uses the accessToken attached to the request to initialize the Webflow client and fetch data from Webflow’s List Sites endpoint.

3. Return Webflow Data to the Designer Extension

After retrieving the data, the endpoint responds with the information, providing the user access to their Webflow sites in the Designer Extension.

Start your App and test your authentication flow

You're all set up to securely make calls to the Webflow Data API from a Designer Extension. To test out your App, run the following command to get started:

$ npm run dev

Once your App is up and running:

  1. Set Your Callback URI: Update the Webflow dashboard with the correct callback URI to ensure the OAuth flow can return to your app after authentication.
  2. Open your App in the Designer: Open a site in the Designer and open your test App with the corresponding CLIENT_ID
  3. Authorize in the Designer Extension: Open your Designer Extension and click the "Authorize" button to initiate the authentication flow.

Congratulations!

You have a working Hybrid app that can securely make requests to Webflow’s Data APIs.

Next steps

Add Elements to the Canvas in bulk

Create and manpulate complex hierarchical designs using the ElementBuilder APIs.

Add Custom Code to a Site

Learn how to register, apply, and manage custom scripts on a site or page using the Data APIs.

Publish your Hybrid App

Once you've finished developing your app, discover how to publish it and share it in the marketplace.

main.js
server.js
database.js
jwt.js
ExpandClose

_145
import React, { useState, useEffect } from "react";
_145
import ReactDOM from "react-dom/client";
_145
import axios from "axios";
_145
import DataTable from "./DataTable";
_145
import { ThemeProvider, Button, Container, Typography } from "@mui/material";
_145
import theme from "./theme";
_145
_145
const App = () => {
_145
const [idToken, setIdToken] = useState("");
_145
const [sessionToken, setSessionToken] = useState("");
_145
const [user, setUser] = useState({});
_145
const [siteData, setSiteData] = useState([]);
_145
_145
const PORT = 3000;
_145
const API_URL = `http://localhost:${PORT}/`;
_145
_145
useEffect(() => {
_145
// Set Extension Size
_145
webflow.setExtensionSize("default");
_145
_145
// Function to exchange and verify ID token
_145
const exchangeAndVerifyIdToken = async () => {
_145
try {
_145
const idToken = await webflow.getIdToken();
_145
const siteInfo = await webflow.getSiteInfo();
_145
setIdToken(idToken);
_145
_145
// Resolve token by sending it to the backend server
_145
const response = await axios.post(API_URL + "token", {
_145
idToken: idToken,
_145
siteId: siteInfo.siteId,
_145
});
_145
_145
try {
_145
// Parse information from resolved token
_145
const sessionToken = response.data.sessionToken;
_145
const expAt = response.data.exp;
_145
const decodedToken = JSON.parse(atob(sessionToken.split(".")[1]));
_145
const firstName = decodedToken.user.firstName;
_145
const email = decodedToken.user.email;
_145
_145
// Store information in Local Storage
_145
localStorage.setItem(
_145
"wf_hybrid_user",
_145
JSON.stringify({ sessionToken, firstName, email, exp: expAt })
_145
);
_145
setUser({ firstName, email });
_145
setSessionToken(sessionToken);
_145
console.log(`Session Token: ${sessionToken}`);
_145
} catch (error) {
_145
console.error("No Token", error);
_145
}
_145
} catch (error) {
_145
console.error("Error fetching ID Token:", error);
_145
}
_145
};
_145
_145
// Check local storage for session token
_145
const localStorageUser = localStorage.getItem("wf_hybrid_user");
_145
if (localStorageUser) {
_145
const userParse = JSON.parse(localStorageUser);
_145
const userStoredSessionToken = userParse.sessionToken;
_145
const userStoredTokenExp = userParse.exp;
_145
if (userStoredSessionToken && Date.now() < userStoredTokenExp) {
_145
if (!sessionToken) {
_145
setSessionToken(userStoredSessionToken);
_145
setUser({ firstName: userParse.firstName, email: userParse.email });
_145
}
_145
} else {
_145
localStorage.removeItem("wf_hybrid_user");
_145
exchangeAndVerifyIdToken();
_145
}
_145
} else {
_145
exchangeAndVerifyIdToken();
_145
}
_145
_145
// Listen for message from the OAuth callback window
_145
const handleAuthComplete = (event) => {
_145
if (
_145
// event.origin === "http://localhost:3000" &&
_145
event.data === "authComplete"
_145
) {
_145
exchangeAndVerifyIdToken(); // Retry the token exchange
_145
}
_145
};
_145
_145
window.addEventListener("message", handleAuthComplete);
_145
_145
return () => {
_145
window.removeEventListener("message", handleAuthComplete);
_145
};
_145
}, [sessionToken]);
_145
_145
// Handle request for site data
_145
const getSiteData = async () => {
_145
const sites = await axios.get(API_URL + "sites", {
_145
headers: { authorization: `Bearer ${sessionToken}` },
_145
});
_145
setSiteData(sites.data.data.sites);
_145
};
_145
_145
// Open OAuth screen
_145
const openAuthScreen = () => {
_145
window.open("http://localhost:3000", "_blank", "width=600,height=400");
_145
};
_145
_145
return (
_145
<ThemeProvider theme={theme}>
_145
<div>
_145
{!user.firstName ? (
_145
// If no user is found, Send a Hello Stranger Message and Button to Authorize
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello Stranger</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={openAuthScreen}
_145
>
_145
Authorize App
_145
</Button>
_145
</Container>
_145
) : (
_145
// If a user is found send welcome message with their name
_145
<Container sx={{ padding: "20px" }}>
_145
<Typography variant="h1">👋🏾 Hello {user.firstName}</Typography>
_145
<Button
_145
variant="contained"
_145
sx={{ margin: "10px 20px" }}
_145
onClick={getSiteData}
_145
>
_145
Get Sites
_145
</Button>
_145
{siteData.length > 0 && <DataTable data={siteData} />}
_145
</Container>
_145
)}
_145
</div>
_145
</ThemeProvider>
_145
);
_145
};
_145
_145
// Render your App component inside the root
_145
const rootElement = document.getElementById("root");
_145
const root = ReactDOM.createRoot(rootElement);
_145
_145
root.render(<App />);