How to generate secure random secrets in Node.js

Generating a secret is three lines of code, and almost every way of getting it wrong looks identical to getting it right. Here is the correct approach and the failure modes to avoid.

By Sk Md Rakib · Published · Updated · 8 min read

Math.random is never acceptable

Math.random is a fast, deterministic pseudo-random generator seeded from a small internal state. It was designed for simulations and animation, not for adversaries. Given enough consecutive outputs, its future values can be predicted — and the same is true of any library that builds ids on top of it.

Node.js ships the correct primitive: crypto.randomBytes, which draws from the operating system's cryptographically secure entropy pool. In the browser the equivalent is crypto.getRandomValues. If a token, key, nonce, salt, password-reset code or session identifier must be unguessable, it must come from one of those two.

How many bytes do you actually need?

For anything long-lived and attacker-visible, 32 random bytes — 256 bits — is the answer that never needs revisiting: API keys, session secrets, signing keys, webhook secrets. For short-lived values such as a CSRF token or a password-reset code with a fifteen-minute expiry, 16 bytes (128 bits) is comfortably sufficient.

The number to avoid is a human-chosen one. A hand-typed 'random' string carries perhaps 30 bits of entropy no matter how jumbled it looks, and short secrets padded out with predictable prefixes are no stronger than the random part they contain.

  • 32 bytes: signing keys, API keys, session secrets, anything persistent.
  • 16 bytes: expiring codes, CSRF tokens, nonces.
  • Never: a string you typed yourself, or one derived from a timestamp.

Choosing an encoding

Random bytes are not printable, so they need encoding before they go into a header, a URL or an environment variable. Hex is unambiguous and doubles the length. Base64url is more compact and safe in URLs and filenames, which makes it the usual choice for tokens that travel in links.

Encoding never changes the entropy — 32 bytes are 256 bits whether you print them as 64 hex characters or 43 Base64url characters. Do not slice the encoded string to shorten it, because that does reduce entropy, silently and by more than people expect.

Storing and rotating

Secrets belong in the environment or a secret manager, never in the repository and never in a client bundle. Anything reachable by the browser is public by definition, however obscure the variable name. Add a rotation path from the start: support two valid signing keys at once so you can introduce a new one, let old tokens expire, then retire the old key without downtime.

Store API keys the way you store passwords: hash them before persisting, show the plaintext to the user once at creation, and compare using a constant-time function. A leaked database of hashed keys is an inconvenience; a leaked database of plaintext keys is an incident.

Common ways this goes wrong

Seeding a generator with the current time makes output reproducible by anyone who can guess the second. Deriving a secret from a user identifier makes every secret guessable from public data. Reusing one shared secret across environments means a staging leak compromises production. Comparing secrets with === leaks length and prefix information through timing — use crypto.timingSafeEqual.

If you need a secret right now without wiring up a script, our Password Generator produces high-entropy values in your browser using the Web Crypto API and reports the entropy in bits, and the UUID Generator covers identifiers that need to be unique but not secret. Neither transmits anything, which is the only property that makes a browser tool acceptable for this job at all.

// tools referenced in this guide

// more guides