Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jwt based token auth to the engine api calls #3777

Merged
merged 10 commits into from
Feb 28, 2022
6 changes: 3 additions & 3 deletions packages/cli/src/options/beaconNodeOptions/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ export type ExecutionEngineArgs = {
};

export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] {
let jwtSecretHex;
let jwtSecret;
dapplion marked this conversation as resolved.
Show resolved Hide resolved
if (args["jwt-secret"]) {
twoeths marked this conversation as resolved.
Show resolved Hide resolved
jwtSecretHex = extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim());
jwtSecret = extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim());
}
return {
urls: args["execution.urls"],
timeout: args["execution.timeout"],
jwtSecretHex,
jwtSecret,
};
}

Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/util/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export function extractJwtHexSecret(jwtSecretContents: string): string {
const hexPattern = new RegExp(/^(0x|0X)?(?<jwtSecret>[a-fA-F0-9]+)$/, "g");
const jwtSecretHexMatch = hexPattern.exec(jwtSecretContents);
const jwtSecretHex = jwtSecretHexMatch?.groups?.jwtSecret;
if (!jwtSecretHex || jwtSecretHex.length != 64) {
throw Error(`Need a valid 256 bit hex encoded secret ${jwtSecretHex} ${jwtSecretContents}`);
const jwtSecret = jwtSecretHexMatch?.groups?.jwtSecret;
if (!jwtSecret || jwtSecret.length != 64) {
throw Error(`Need a valid 256 bit hex encoded secret ${jwtSecret} ${jwtSecretContents}`);
}
return jwtSecretHex;
return jwtSecret;
}
2 changes: 1 addition & 1 deletion packages/lodestar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"interface-datastore": "^5.1.2",
"it-all": "^1.0.2",
"it-pipe": "^1.1.0",
"jwt-simple": "^0.5.6",
"jwt-simple": "0.5.6",
"libp2p": "^0.32.4",
"libp2p-bootstrap": "^0.13.0",
"libp2p-gossipsub": "^0.11.1",
Expand Down
14 changes: 5 additions & 9 deletions packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {AbortController, AbortSignal} from "@chainsafe/abort-controller";

import {ErrorAborted, TimeoutError} from "@chainsafe/lodestar-utils";
import {IJson, IRpcPayload, ReqOpts} from "../interface";
import {encodeJwtToken} from "../../util/jwt";
import {encodeJwtToken} from "./jwt";
/**
* Limits the amount of response text printed with RPC or parsing errors
*/
Expand Down Expand Up @@ -41,7 +41,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
timeout?: number;
/** If returns true, do not fallback to other urls and throw early */
shouldNotFallback?: (error: Error) => boolean;
jwtSecretHex?: string;
jwtSecret?: Uint8Array;
}
) {
// Sanity check for all URLs to be properly defined. Otherwise it will error in loop on fetch
Expand All @@ -53,9 +53,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
throw Error(`JsonRpcHttpClient.urls[${i}] is empty or undefined: ${url}`);
}
}
if (this.opts?.jwtSecretHex) {
this.jwtSecret = Buffer.from(this.opts.jwtSecretHex, "hex");
}
this.jwtSecret = opts?.jwtSecret;
}

/**
Expand Down Expand Up @@ -127,14 +125,12 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
}

try {
let headers;
const headers = {"Content-Type": "application/json"};
if (this.jwtSecret) {
/** ELs have a tight +-5 second freshness check on token's iat i.e. issued at */
const token = encodeJwtToken({iat: Math.floor(new Date().getTime() / 1000)}, this.jwtSecret);
// eslint-disable-next-line @typescript-eslint/naming-convention
headers = {"Content-Type": "application/json", Authorization: `Bearer ${token}`};
} else {
headers = {"Content-Type": "application/json"};
Object.assign(headers, {Authorization: `Bearer ${token}`});
dapplion marked this conversation as resolved.
Show resolved Hide resolved
}

const res = await fetch(url, {
Expand Down
6 changes: 4 additions & 2 deletions packages/lodestar/src/executionEngine/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
export type ExecutionEngineHttpOpts = {
urls: string[];
timeout?: number;
jwtSecretHex?: string;
/** 256 bit jwt secret in hex format without the leading 0x */
dapplion marked this conversation as resolved.
Show resolved Hide resolved
jwtSecret?: string;
};

export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = {
Expand All @@ -51,7 +52,8 @@ export class ExecutionEngineHttp implements IExecutionEngine {
rpc ??
new JsonRpcHttpClient(opts.urls, {
signal,
...opts,
timeout: opts.timeout,
jwtSecret: opts.jwtSecret ? Buffer.from(opts.jwtSecret, "hex") : undefined,
dapplion marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand Down
8 changes: 4 additions & 4 deletions packages/lodestar/test/sim/merge-interop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/u
// 10 ttd / 2 difficulty per block = 5 blocks * 5 sec = 25 sec
const terminalTotalDifficultyPreMerge = 10;
const TX_SCENARIOS = process.env.TX_SCENARIOS?.split(",") || [];
const jwtSecretHex = "dc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d";
const jwtSecret = "dc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d";
dapplion marked this conversation as resolved.
Show resolved Hide resolved

describe("executionEngine / ExecutionEngineHttp", function () {
this.timeout("10min");
Expand Down Expand Up @@ -79,7 +79,7 @@ describe("executionEngine / ExecutionEngineHttp", function () {
...process.env,
TTD,
DATA_DIR,
JWT_SECRET_HEX: jwtSecretHex,
JWT_SECRET_HEX: `0x${jwtSecret}`,
},
});

Expand Down Expand Up @@ -156,7 +156,7 @@ describe("executionEngine / ExecutionEngineHttp", function () {
}

const controller = new AbortController();
const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl], jwtSecretHex}, controller.signal);
const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl], jwtSecret}, controller.signal);

// 1. Prepare a payload

Expand Down Expand Up @@ -320,7 +320,7 @@ describe("executionEngine / ExecutionEngineHttp", function () {
sync: {isSingleNode: true},
network: {discv5: null},
eth1: {enabled: true, providerUrls: [jsonRpcUrl]},
executionEngine: {urls: [engineApiUrl], jwtSecretHex},
executionEngine: {urls: [engineApiUrl], jwtSecret},
},
validatorCount: validatorClientCount * validatorsPerClient,
logger: loggerNodeA,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {expect} from "chai";
import {encodeJwtToken, decodeJwtToken} from "../../../src/util/jwt";
import {encodeJwtToken, decodeJwtToken} from "../../../src/eth1/provider/jwt";

describe("ExecutionEngine / jwt", () => {
it("encode/decode correctly", () => {
const jwtSecret = Buffer.from(Array.from({length: 32}, () => Math.round(Math.random() * 255)));
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6784,7 +6784,7 @@ just-extend@^4.0.2:
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==

jwt-simple@^0.5.6:
[email protected]:
version "0.5.6"
resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.6.tgz#3357adec55b26547114157be66748995b75b333a"
integrity sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==
Expand Down