Last active
November 26, 2022 10:36
-
-
Save gordysc/02a113aa0ce6a72fb7fbe328259b5ccf to your computer and use it in GitHub Desktop.
Rails signed_id reimplemented in NodeJS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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