Skip to content

Instantly share code, notes, and snippets.

@gordysc
Last active November 26, 2022 10:36
Show Gist options
  • Save gordysc/02a113aa0ce6a72fb7fbe328259b5ccf to your computer and use it in GitHub Desktop.
Save gordysc/02a113aa0ce6a72fb7fbe328259b5ccf to your computer and use it in GitHub Desktop.
Rails signed_id reimplemented in NodeJS
/*
* This was just a thought experiment of reimplementing the Rails signed_id's concept
* in NodeJS. All values below are examples and do not represent any real world data.
*
* This script might be useful for folks who need to support signed IDs that were
* generated by a Ruby on Rails application that is being ported to NodeJS.
*/
const { createHmac, pbkdf2Sync } = require("node:crypto");
const HASH_DIGEST_CLASS = "sha256";
const ITERATIONS = 1_000;
const KEY_LENGTH = 64;
const SALT = "active_record/signed_id";
const SEPARATOR = "--";
// Replace this w/ your Rails secret key base!
const SECRET_KEY_BASE =
"309a4e9f96e8a3c7d2c25fd70bf30df9825e354daccbf87924ced721164390f180acba48a39875f7c016eabf8ae8d02de7d1b98f2ed2dddf5823427b86438350";
// This is what the final result will be
const SIGNED_ID_TOKEN =
"eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1qYz0iLCJleHAiOm51bGwsInB1ciI6InVzZXIifX0=--67d391cc44ec6ca0c2f0ac5cf99145c658eae03827ebd74584ea1021967257de";
const secret = pbkdf2Sync(
SECRET_KEY_BASE,
SALT,
ITERATIONS,
KEY_LENGTH,
HASH_DIGEST_CLASS
);
const expiresAt = (expiresIn = null) => {
if (!expiresIn) return null;
const now = new Date().getTime();
const offset = 1_000 * expiresIn;
return new Date(now + offset).toISOString();
};
const encode64 = value => Buffer.from(value, "utf8").toString("base64");
const decode64 = value => Buffer.from(value, "base64").toString("utf8");
const encode = (value, pur, expiresIn = null) => {
const message = encode64(value);
const exp = expiresAt(expiresIn);
const _rails = { message, exp, pur };
const payload = JSON.stringify({ _rails });
return encode64(payload);
};
const generateSignature = payload => {
const hmac = createHmac("sha256", secret);
hmac.update(payload);
return hmac.digest("hex");
};
const signedId = (value, purpose, expiresIn = null) => {
const data = encode(value, purpose, expiresIn);
const signature = generateSignature(data);
return `${data}${SEPARATOR}${signature}`;
};
const decodedId = token => {
const segments = token.split(SEPARATOR);
// TODO: Add a check here if segments length is != 2
const payload = decode64(segments[0]);
// TODO: Add a check here to verify the payload is JSON
const data = JSON.parse(payload);
// TODO: Add a check here that the data is valid
const { message, exp, pur: model } = data._rails;
// Get the original model ID
const id = decode64(message);
// TODO: Recreate the signature here to verify nothing has been tampered w/
return { id, model, expiresAt: exp };
};
console.log("Original Token");
console.log(SIGNED_ID_TOKEN);
console.log("Generated Tokens");
const encoded = signedId("27", "user");
console.log(encoded);
console.log("Decoded Token");
const decoded = decodedId(encoded);
console.log(decoded);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment