Movatterモバイル変換


[0]ホーム

URL:


Sitemap
Open in app

Press enter or click to view image in full size
Secure your Alchemy, QuickNode, and Infura endpoints

Problem

Ever wondered how to protect your node provider API key and URLs for services like Alchemy in your frontend dApp? You’re not alone — it’s a common challenge for many developers. While there are plenty of discussions, such asthis one,this one,this one,this one,this one, andthis one, most of them fall short of providing effective solution.

Press enter or click to view image in full size
😱 How do I protect my provider API in my React app? 😱

Frontend dApps face aunique challenge that backend services don’t:making node RPC calls directly from the browser. When instantiating anEthers.js provider or awagmi config, most people expose their API key on the frontend:

// https://wagmi.sh/react/api/createConfig
const config = createConfig({
chains: [mainnet],
client({ chain }) {
return createClient({
chain,
transport: http("https://eth-mainnet.g.alchemy.com/v2/<YOUR-API-KEY>") // Your API key is exposed
});
},
});

// https://docs.ethers.org/v5/api/providers/api-providers/#AlchemyProvider
const alchemyProvider = () => {
return new providers.AlchemyProvider(
"mainnet",
<YOUR-API-KEY> // Your API key is exposed
);
};

So, how can frontend dApps protect their node provider API key? Here are some common suggestions:

  • Add your app’s domain to a referrer allowlist on Alchemy or QuickNode
  • Lock down your API key to a set of IPs
  • Use a backend to proxy the RPC calls

Unfortunately, none of these methods are truly effective. Allowing only your app’s domain to access the RPC can be bypassed by spoofing the referrer header. Locking down the API key to specific IPs is unrealistic — you can’t allowlist all your users’ IPs. Proxying RPC calls through a backend hides the API key, but abusers can still exploit your proxy.

Solution

An effective solution is to use short-livedJSON Web Tokens (JWTs) for authentication. In this approach, your app generates a short-lived JWT and includes it with the RPC request. These JWTs are securely created by your backend using a private key. Since the tokens are short-lived and can only be generated by you, they remain safe on the client side.

You can see the full implementation of the solution in thelive demohereand its code in the GitHub repositoryhere.Clone therepository to follow along.The demo app is bootstrapped withwagmi’sNext.js example.This guide assumes you’re usingNext.js, but the approach can be adapted for any other framework.

Step 1: Generate a Public & Private Key Pair

Start by generating a public & private key pair usingOpenSSL.

Generate a Private Key:

openssl genpkey -algorithm RSA -out private_key.pem

Generate the Public Key:

openssl rsa -pubout -in private_key.pem -out public_key.pem

This will create two files:public_key.pem andprivate_key.pem. The private key must be kept secure and never shared outside your team. The generated keys should resemble the followingmocked examples:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyQ2v0e7xWt9RsYT0GpPQ
fg36YmPGlzSh50bHT2lmgKcIqk3C9GRl4gImJb75Lo5P9sMGB1PzV6lhScjqA2BG
5KXhN+NwCsQZ1YQ9yPT9KDhcdzlx35xLft2kctAjbhT/6eXXF4M9yOmClA0mRsUN
LbCx5sSRxgD7cOFAXwrWc7yqPpETVqIN2jcFyErwE5zznAJIb3hwxZ6sAkwlP3mS
MYgOl+oSk+N+lvO7vAoeSy9g1VR0hI9LbqOGGyWRywo7nAe7Vjl5lYxOYV9gFG0v
-----END PUBLIC KEY-----
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAzQ8X7hY9sTZ8FHt9LbKyT9V1MQZXjz9U0GrHWBoHGeUIAdCn
ZfbCQ3dKCV5xtHiyXkrjUj9DZ8OAjHZsT9MFlBDnL2z0TB7hr4Vc2V1BLd+2oHeM
KslHXOXTdRmfR9poXq6pHwgTR5x8EJNtvpReE8zQlwRmfHlZXcPmZpbkE+LdToxf
0HHeIt4OB+CrwDs6CQPCPMhCVAvwZLlv2t612KhbZlmB1UjbBhzjlg0oq9GpW4A8
3aodC4DAs6jmlPZIiJ98kHrL8gWnhCRNrfCPeJUCgYBSM+/M5hvZy43bFuSREzPN
zywm5ys7kjqfVmIm5+7cyhDOyNCNl0Pf5Q1x+ItmMki9PVuqGjdB5mFehRkcQlBo
46U1cQKBgQCJvEn1LYChLQf0osZY0kswjCZZb2DypFssAe4PrvVoHzX7Zni+3uGV
VvmbSClYy2sAHC3GYgiIpmxrPaNPf58AX8Y1UJfiPV3JSuzdTVMgTLFgSkMnD5HQ
so5XYTUSrPf3cG4fB1TlyUwUYFbZ+FcNZ4nmUp8BLFRH0VSaYN/hgw==
-----END RSA PRIVATE KEY-----

Convert the Private Key to PKCS8 Format

Now, convert the private key to PKCS8 format and store it in an environment variable to sign tokens. Use the following command:

openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private_key.pem -out privatekey-pkcs8.pem

We will store the contents ofprivatekey-pkcs8.pem in an environment variable calledALCHEMY_PRIVATE_KEY_PKCS8 tosign JWTs in our Next.js app.

Step 2: Set Public Key in Provider Dashboard

Navigate to “Apps” in your Alchemy dashboard, find your app, and click “Import Public Key”. A modal should pop up — enter a name for your public key and paste the key from the file.

Press enter or click to view image in full size
Press enter or click to view image in full size
Import your public key

After it’s set up, you’ll see yourKEY ID — note this down, as you’ll need it later.

Press enter or click to view image in full size
Note your Key ID, we’ll use it in our code

Step 3: Utility to Create New JWTs

Install thejose package before setting up the API routes. Let’s usejose for the demo app because it isedge runtime compatible.

npm install jose

First, let’s create a utility function insrc/lib/generateJWT.ts, to create our short-lived JWTs:

import { SignJWT, importPKCS8 } from "jose";

export async function generateJWT() {
if (!process.env.ALCHEMY_PRIVATE_KEY_PKCS8 || !process.env.ALCHEMY_KEY_ID) {
throw new Error(
"Missing required environment variables for JWT generation."
);
}

const privateKeyPEM = process.env.ALCHEMY_PRIVATE_KEY_PKCS8;

const privateKey = await importPKCS8(privateKeyPEM, "RS256");

const token = await new SignJWT({})
.setProtectedHeader({ alg: "RS256", kid: process.env.ALCHEMY_KEY_ID })
.setExpirationTime("5m") // JWT expires in 5 minutes
.sign(privateKey);

return token;
}

Step 4: Generate Initial JWT

Then we’ll generate the initial JWT inlayout.tsx which is a Reactserver component. This component only renders on the server:

export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const jwt = await generateJWT();

return (
<html lang="en">
<body>
<Providers initialJwt={jwt}>{children}</Providers>
</body>
</html>
);
}

Step 5: Issue Fresh JWTs

Create a new API route that generates and returns a fresh JWT. Since each JWT is short-lived and expires after 5 minutes, our app will require a new token periodically. Below is an example of what thesrc/app/api/get-jwt/route.ts file should contain:

import { NextResponse } from "next/server";
import { generateJWT } from "@/app/lib/generate-jwt";

export async function POST(req: Request) {
try {
const nodeProviderJwt = await generateJWT();
const response = NextResponse.json({ nodeProviderJwt });
return response;
} catch (error: any) {
console.error(error.message);
return NextResponse.json({ error: error.message }, { status: 403 });
}
}

Here’s thegenerateJWT:

import { SignJWT, importPKCS8 } from "jose";

export async function generateJWT() {
if (!process.env.ALCHEMY_PRIVATE_KEY_PKCS8 || !process.env.ALCHEMY_KEY_ID) {
throw new Error(
"Missing required environment variables for JWT generation."
);
}

const privateKeyPEM = process.env.ALCHEMY_PRIVATE_KEY_PKCS8;

const privateKey = await importPKCS8(privateKeyPEM, "RS256");

const token = await new SignJWT({})
.setProtectedHeader({ alg: "RS256", kid: process.env.ALCHEMY_KEY_ID })
.setExpirationTime("5m")
.sign(privateKey);

return token;
}

Step 6: Client Side Setup

Our initial short-lived JWT expires in 5 minutes, so we’ll set an interval to fetch a new JWT every 4 minutes inproviders.tsx. First, let's create a React hook calleduseToken to handle this:

import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";

const FETCH_INTERVAL = 4 * 60 * 1000; // 4 minutes

async function queryFn() {
const response = await fetch("/api/get-jwt", {
method: "POST",
cache: "no-store",
});

const data = await response.json();

if (!response.ok) {
throw new Error(`❌ Failed to refresh token: ${response.statusText}`);
} else {
return data;
}
}

export default function useToken() {
const [isEnabled, setIsEnabled] = useState(false);

useEffect(() => {
const timeoutId = setTimeout(() => {
setIsEnabled(true);
}, FETCH_INTERVAL);

return () => clearTimeout(timeoutId); // Cleanup the timer on unmount
}, []);

const { data } = useQuery({
queryFn,
enabled: isEnabled,
queryKey: ["get-jwt"],
refetchInterval: FETCH_INTERVAL,
refetchIntervalInBackground: true,
});

useEffect(() => {
if (data?.nodeProviderJwt) {
localStorage.setItem("node-provider-jwt", data.nodeProviderJwt);
}
}, [data]);
}

Then use it inproviders.tsx:

"use client";

import React, { ReactNode, useState } from "react";
import { WagmiProvider } from "wagmi";
import { getWagmiConfig } from "./config";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

export function Providers(props: { children: ReactNode; initialJwt: string }) {
const [config] = useState(() => {
return getWagmiConfig(props.initialJwt);
});

return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</WagmiProvider>
);
}

Now, we can securely fetch a new JWTs as long as the user keeps the app open.

Step 8: Set Up JWT Handling

Now that we have valid short-lived JWTs in our app and a secure way to refresh them, we’ll use these JWTs to securely access the Node provider by passing them directly to the provider. First, define thegetConfig function to configure wagmi with your chains, and connectors, passing the JWT in theAuthorization header the thehttp transport:

import { mainnet } from "wagmi/chains";
import { http, createConfig } from "wagmi";
import { metaMask, walletConnect, coinbaseWallet } from "wagmi/connectors";

const connectors = [
metaMask(),
coinbaseWallet(),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),
];

export const getWagmiConfig = (initialJwt: string) => {
const config = createConfig({
chains: [mainnet, base],
connectors,
transports: {
[mainnet.id]: http("https://eth-mainnet.g.alchemy.com/v2", {
onFetchRequest: (_, init) => {
return {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${initialJwt}`,
},
};
},
})
},
});

return config;
};

Conclusion

By leveraging short-lived JWTs, you can protect your node provider URLs. Here’s a summary of the key components involved in this approach:

  1. JWT Generation: The backend generates a JWT when the dApp initializes, with a short expiry time (e.g., 5 minutes), and includes it in the server-rendered content. The JWT is signed using your private key.
  2. API Requests: The JWT is passed via theAuthorization header, and the node provider verifies its authenticity by checking the signature against the public key submitted through their dashboard, confirming it was signed by your backend with the private key.
  3. Scheduled Refresh: The frontend automatically refreshes the JWT one minute before it expires (i.e., every 4 minutes), keeping the dApp active as long as it’s in use.

To explore the code and see the solution in action, check out the live demohere and the GitHub repositoryhere.

--

--

henryzhu.eth
henryzhu.eth

Written by henryzhu.eth

@0xproject • prev, design systems @zendesk • eng @shipt

Responses (1)


[8]ページ先頭

©2009-2025 Movatter.jp