Skip to main content

Express

This page will help you get started with Auth using the Express.js framework.

tip

If you want to interact with a working version of the Auth + Express integration that we'll be building in this guide, you can checkout the following GitHub repository, or clone it with the command below:

npx thirdweb create app --template thirdweb-auth-express

Getting Started

To add support for Auth to your Express.js backend, you'll need to install the @thirdweb-dev/auth package as well as its peer dependencies (as well as any other packages you'll need for your wallet configuration, we'll need the ethers package for this example as we'll be using the PrivateKeyWallet):

npm install @thirdweb-dev/auth cookie-parser ethers@5

Now we can setup our Express.js server by using the @thirdweb-dev/auth/express entrypoint. First, we need to setup our Auth API by adding the auth router to our app:

import { ThirdwebAuth } from "@thirdweb-dev/auth/express";
import { PrivateKeyWallet } from "@thirdweb-dev/auth/evm";
import express from "express";

const PORT = 8000;
const app = express();

const { authRouter, authMiddleware, getUser } = ThirdwebAuth({
domain: process.env.THIRDWEB_AUTH_DOMAIN || "",
wallet: new PrivateKeyWallet(process.env.THIRDWEB_AUTH_PRIVATE_KEY || ""),
});

// Add the auth router to our app to set up the /auth/* endpoints
app.use("/auth", authRouter);

// Add the auth middleware to the rest of our app to allow user authentication on other endpoints
app.use(authMiddleware);

app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});

Here, we first configure our ThirdwebAuth instance with the domain of our app used for anti-phishing, as well as the admin wallet used to issue and verify JWTs. In this case, we use the PrivateKeyWallet from the @thirdweb-dev/auth/evm entrypoint, but there are a number of other supported wallet setups, which you can learn more about on the wallet configuration page:

Finally, we add the authRouter to our app with the app.use("/auth", authRouter) line to set up all the /auth/* endpoints automatically. We'll also need to add the authMiddleware to any routes that we want to protect with Auth.

Usage

Authenticating the user on other routes

The getUser function can be used to authenticate the user on the server. It will return the user's address if the user is authenticated, or null if the user is not authenticated. It can be used in any API route that uses the authMiddleware:

src/index.ts
// Add the auth middleware to any routes where you want to use the getUser function
app.get("/secret", authMiddleware, async (req, res) => {
const user = await getUser(req);

if (!user) {
return res.status(401).json({
message: "Not authorized.",
});
}

return res.status(200).json({
message: "This is a secret... don't tell anyone.",
});
});

Alternatively, you can add the authMiddleware to the entire app if you want to use the getUser function across multiple routes.

src/index.ts
// Now getUser function will be available on all routes
app.use(authMiddleware);

app.get("/secret", async (req, res) => {
const user = await getUser(req);

if (!user) {
return res.status(401).json({
message: "Not authorized.",
});
}

return res.status(200).json({
message: "This is a secret... don't tell anyone.",
});
});

Validating the login request

By default, the Auth API will validate the login request by checking that the user requesting to login succesfully signed a valid sign-in with wallet message. However, this doesn't perform specific checks on the exact contents of the payload, aside from the domain used for anti-phishing.

If you want to add specific checks to enforce the exact data on the login payload signed by users, you can use the authOptions configuration:

src/index.ts
const { authRouter, authMiddleware, getUser } = ThirdwebAuth({
... // other config
authOptions: {
// Enforce that the user's login message has these exact values
statement: "I agree to the terms of service",
uri: "https://frontend.example.com",
resources: ["https://terms-of-service.example.com"],
version: "1",
chainId: "1",
},
});

Note that when you enforce these checks on the server-side, you'll also want to pass in the proper parameters to the login function on your client-side application to ensure that the login payload gets the correct format. You can see an example of how to do this in the React section.

Prevent replay attacks

Since the sign-in with wallet payload is used to login to your server, it's important to prevent third parties from being able to reuse old login payloads to falsely authenticate as other users. This reuse of old login payloads is called a replay attack.

Luckily, all sign-in with wallet payloads include a nonce field which is a random string generated when the request was created. If you are using a database, or have somewhere to store nonces, you can ensure that each nonce is only used once:

src/index.ts
const { authRouter, authMiddleware, getUser } = ThirdwebAuth({
... // other config
authOptions: {
validateNonce: async (nonce: string) => {
// Check in database or storage if nonce exists
const nonceExists = await dbExample.nonceExists(nonce);
if (nonceExists) {
throw new Error("Nonce has already been used!");
}

// Otherwise save nonce in database or storage for later validation
await dbExample.saveNonce(nonce);
}
},
});

Changing the token validity duration

By default, the JWTs issued by the server are valid for 5 hours (21600 seconds), after which the user will have to login again. If you want to change this duration, you can use the authOptions.tokenDurationInSeconds configuration:

src/index.ts
const { authRouter, authMiddleware, getUser } = ThirdwebAuth({
... // other config
authOptions: {
tokenDurationInSeconds: 60 * 60 * 24 * 7, // 1 week
},
});

The Auth API will set a cookie on the user's browser with the issued JWT token (learn more about cookie settings from the Mozilla cookie reference). This cookie is set to be httpOnly and secure (only sent over HTTPS), which is necessary for security purposes - and is also set to use the domain of your API, with a path of /, and SameSite=None.

If you want to overwrite these, settings, you can with the cookieOptions configuration. For example, you may want to set your domain to be less specific - like if you have your api on api.example.com and a client on client.example.com, you may want to set your domain to .example.com, or you want want to set your SameSite setting to strict if your API and client are on the same domain:

src/index.ts
const { authRouter, authMiddleware, getUser } = ThirdwebAuth({
... // other config
cookieOptions: {
domain: ".example.com",
path: "/api",
sameSite: "strict" // or "lax" or "none"
},
});

Saving users in your database

We can use the callbacks.onLogin function to remember users in a database whenever they login, enabling a traditional database enabled authentication flow:

pages/api/auth/[...thirdweb].ts
export const { authRouter, authMiddleware, getUser } = ThirdwebAuth({
... // other config
callbacks: {
onLogin: async (user: User) => {
// Create a new user entry if the user doesn't exist
if (!await dbExample.userExists(user.address)) {
await dbExample.createUser({ address: user.address });
}
},
}
});

Enhancing session data

When the server issues a JWT, the default JWT claims are added to the token (as elaborated in the how auth works section). However, you may want to add additional data to the JWT claims to serve as session data that will persist on the token, such as certain user level permissions or access information. You can do this by returning data from the callbacks.onLogin callback function which treats your return value as session data to store onto the JWT.

pages/api/auth/[...thirdweb].ts
export const { authRouter, authMiddleware, getUser } = ThirdwebAuth({
... // other config
callbacks: {
onLogin: async (address: string) => {
// You can populate data from your database or other sources
const role = await dbExample.getUserRole(address);

// Add arbitrary session data
const session = {
permissions: ["admin", role],
};

return session;
},
}
});

Enhancing user data

Alternatively, you can populate data about a user everytime user data is requested from the /user endpoint or getUser function. This data is not session-wide as it is not stored on the JWT, so it isn't persisted an can change between requests. It can be useful for populating data that is not stored on the JWT, such as user profile information. We can do this by returning data from the callbacks.onUser callback function.

pages/api/auth/[...thirdweb].ts
import type { User } from "@thirdweb/auth/express";

export const { authRouter, authMiddleware, getUser } = ThirdwebAuth({
... // other config
callbacks: {
onUser: async (user: User) => {
// You can populate data from your database or other sources
const data = await dbExample.getUserData(user.address);

return data;
},
}
});

Configuration

NameRequiredDefaultDescription
domain-The domain of your app, used for anti-phishing. See the sign-in with wallet overview for more details.
wallet-The wallet used to issue and verify JWTs. See the wallet configuration page for more details.
authOptions-Configuration used to validate login requests and JWTs
authOptions.statement-The required statement used on the login payload. See the sign-in with wallet overview for more details.
authOptions.uri-The required URI used on the login payload. See the sign-in with wallet overview for more details.
authOptions.version1The required version used on the login payload. See the sign-in with wallet overview for more details.
authOptions.chainId-The required chain ID used on the login payload. If set, smart contract wallets on other chains won't be able to login. See the sign-in with wallet overview for more details.
authOptions.resources-The required resources used on the login payload. See the sign-in with wallet overview for more details.
authOptions.validateNonce-A function to check if the nonce on a login payload is valid, used to prevent replay attacks.
authOptions.validateTokenId-A function to check if the JWT uid is still valid, used to logout users when using a setup with a database.
authOptions.tokenDurationInSeconds21600The number of seconds that each JWT is valid for. Defaults to 21600 (5 hours).
cookieOptions-Configuration used for all cookies set by the server
cookieOptions.domain-The domain value set on all cookies. See Mozilla cookie reference for more information.
cookieOptions.path/The path value set on all cookies. See Mozilla cookie reference for more information.
cookieOptions.sameSitenoneThe sameSite value set on all cookies. See Mozilla cookie reference for more information.
callbacks-Callback functions that let you add additional data and side-effects to the Auth flow. Especially useful when integrating databases.
callbacks.login.onLogin-Callback function that gets called whenever the user logs in. Can be used purely as a side-effect, or can additionally return data from the function to store in the user's session (on the JWT).
callbacks.user.onUser-Callback function that gets called whenever the user data is requested by the client or another endpoint. Can be used purely as a side-effect, or can additionally return data to be populated onto the user object (not session-wide, but request-specific data that can change, like profile information).
callbacks.logout.onLogout-Side-effect function that gets called whenever a user logs out.