Authenticate with JWT
This tutorial demonstrates how to create and apply a JSON Web Token (JWT) to authenticate an
eth_blockNumber
API request
with Node.js.
Developers can configure the expiry time and scope of JWTs to enhance the security profile of their dapps.
Prerequisites
- Node version 20+
- A text editor (for example, VS Code)
- An Infura account
Steps
1. Initiate your project
Create a new directory for your project:
mkdir infura-jwt-demo
Next, change into the infura-jwt-demo
directory:
cd infura-jwt-demo
Initialize a new Node.js project:
npm init -y
Install the required dependencies:
npm install axios jsonwebtoken dotenv
2. Create a key pair
2.1. Generate your private key
Generate your private key using the RSA (PKCS #8) or ES256 algorithm:
- RSA
- ES256
openssl genpkey \
-algorithm RSA \
-out private_key.pem \
-pkeyopt rsa_keygen_bits:2048 \
-outform PEM
openssl ecparam \
-genkey \
-name prime256v1 \
-noout \
-out private_key.pem
2.2. Generate your public key
Generate your public key from your RSA or ES256 private key:
- RSA
- ES256
openssl rsa \
-in private_key.pem \
-pubout \
-out public_key.pem
openssl ec \
-in private_key.pem \
-pubout \
-out public_key.pem
3. Set up your .env
file
3.1. Update the MetaMask Developer dashboard
In the MetaMask Developer dashboard, under API Keys, select the key you want to use for authentication. Go to its Settings tab. Under Requirements, fill out these fields:
- JWT PUBLIC KEY NAME - Provide a unique name for your JWT public key, which can help you manage multiple keys.
- JWT PUBLIC KEY - Paste the entire contents of the
public_key.pem
file.
Optionally, you can check REQUIRE JWT FOR ALL REQUESTS. If this option is not checked, you can make calls using your key without a JWT in the request header, however, invalid or expired tokens will result in the call being rejected.
3.2. Create your .env
file
At the root of your Node.js project, create an environment file:
touch .env
Add the following details to .env
:
- Syntax
- Example
INFURA_API_KEY=<YOUR-API-KEY>
JWT_KEY_ID=<YOUR-JWT-KEY-ID>
INFURA_NETWORK_URL=<NETWORK-URL>
INFURA_API_KEY=8c101...6b26c77d8
JWT_KEY_ID=777e2caf...826bc4303f
INFURA_NETWORK_URL=https://sepolia.infura.io/v3/
Replace the following values in the .env
file:
<YOUR-API-KEY>
with your API key from the MetaMask Developer dashboard.<YOUR-JWT-KEY-ID>
with the JWT's key ID. This is generated by Infura, and you can find it in the MetaMask Developer dashboard. The code in Step 4 applies this ID to the JWT header to allow Infura to identify which key was used to sign the JWT.<NETWORK-URL>
with the URL of an Infura network for which your key has access rights, and that supports the methodeth_blockNumber
.
Before pushing code to a public repository, add .env
to your .gitignore
file. This reduces the likelihood that keys are exposed in public repositories.
Note that .gitignore
ignores only untracked files. If your .env
file was committed in the past, it's tracked by Git. Untrack the file by deleting it and running git rm --cached .env
, then include it in .gitignore
.
4. Create and apply your JWT
Create a new file named call.js
:
touch call.js
Add the following to call.js
:
require("dotenv").config();
const axios = require("axios");
const jwt = require("jsonwebtoken");
const fs = require("fs");
function getAlgorithm(privateKey) {
if (privateKey.includes("BEGIN RSA PRIVATE KEY") || privateKey.includes("BEGIN PRIVATE KEY")) {
return "RS256";
} else if (privateKey.includes("BEGIN EC PRIVATE KEY")) {
return "ES256";
} else {
throw new Error("Unsupported key type");
}
}
// Function to generate the JWT
function generateJWT() {
const privateKey = fs.readFileSync("private_key.pem", "utf8");
const algorithm = getAlgorithm(privateKey);
const token = jwt.sign(
{},
privateKey,
{
algorithm: algorithm, // Dynamically set the algorithm based on the key type
keyid: process.env.JWT_KEY_ID,
audience: "infura.io",
expiresIn: "1h",
header: {
typ: "JWT"
}
}
);
return token;
}
// Function to authenticate with Infura and get the latest block number
async function getBlockNumber() {
const jwtToken = generateJWT();
const url = `${process.env.INFURA_NETWORK_URL}${process.env.INFURA_API_KEY}`;
const data = {
jsonrpc: "2.0",
method: "eth_blockNumber",
params: [],
id: 1
};
try {
const response = await axios.post(url, data, {
headers: {
Authorization: "Bearer " + jwtToken,
"Content-Type": "application/json"
}
});
console.log("Block number:", response.data.result);
} catch (error) {
console.error("Error fetching block number:", error.response ? error.response.data : error.message);
}
}
// Call the function to get latest block
getBlockNumber();
Next, run the code:
node call.js
Your console outputs the response, for example:
Block number: 0x61fc48
This script:
- Generates a JWT with a 1 hour expiry that is only valid on
infura.io
. - Applies this JWT to form the header of a
getBlockNumber
call. - Submits the API call.
(Optional) Examine the curl equivalent
If you want to better understand the code applied in Step 4, you can examine the equivalent curl request.
Create a new file named curl.js
:
touch curl.js
Add the following to curl.js
:
require("dotenv").config();
const jwt = require("jsonwebtoken");
const fs = require("fs");
function getAlgorithm(privateKey) {
if (privateKey.includes("BEGIN RSA PRIVATE KEY") || privateKey.includes("BEGIN PRIVATE KEY")) {
return "RS256";
} else if (privateKey.includes("BEGIN EC PRIVATE KEY")) {
return "ES256";
} else {
throw new Error("Unsupported key type");
}
}
// Function to generate the JWT
function generateJWT() {
const privateKey = fs.readFileSync("private_key.pem", "utf8");
const algorithm = getAlgorithm(privateKey);
const token = jwt.sign(
{},
privateKey,
{
algorithm: algorithm, // Dynamically set the algorithm based on the key type
keyid: process.env.JWT_KEY_ID,
audience: "infura.io",
expiresIn: "1h",
header: {
typ: "JWT"
}
}
);
return token;
}
// Generate the JWT and print the curl command
async function printCurlRequest() {
const jwtToken = generateJWT();
console.log("Generated JWT:", jwtToken);
const curlRequest = `
curl -X POST ${process.env.INFURA_NETWORK_URL}${process.env.INFURA_API_KEY} \\
-H "Authorization: Bearer ${jwtToken}" \\
-H "Content-Type: application/json" \\
-d '{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": [],
"id": 1
}'
`;
console.log("Equivalent curl request:");
console.log(curlRequest);
}
// Call the function to print the curl request
printCurlRequest();
Next, run the code:
node curl.js
Your console outputs the curl request, for example:
curl -X POST https://sepolia.infura.io/v3/<YOUR-API-KEY> \
-H "Authorization: Bearer pqJhbG**JWT-token**2Ud5o2Q" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": [],
"id": 1
}'
You can run this request yourself to make the call. Your console outputs the response, for example:
{"jsonrpc":"2.0","id":1,"result":"0x61fc48"}
Next steps
Consider following these next steps:
- Configure your JWT to control its scope.
-
Decode your JWT: Copy the JWT provided in the console by the optional curl equivalent step, and paste it into the Encoded field in jwt.io.
-
Add a layer of verification to your call by applying the JWT's FINGERPRINT provided in the MetaMask Developer dashboard.
noteThe JWT's fingerprint is a hash of the public key, used to ensure the key's integrity and authenticity.