Random token generator

Session and bearer tokens from the operating system CSPRNG, with the entropy in bits shown as you change the length.

G
More options
up to 10,000

Constraints

How many bits a token needs

The OWASP Session Management Cheat Sheet sets the floor at 64 bits of entropy for a session identifier, with a recommended identifier length of 128 bits. NIST SP 800-63B-4 approaches it from the other side and says that a secret below 64 bits must be rate-limited, which is the same boundary stated as a requirement on the server rather than on the value.

Sixty-four bits is a floor, not a target, and in practice 128 is where you should sit. The reason is not that 64 bits is easy to guess in the abstract - it is not - but that the 64-bit case depends on the rate limit actually being there, on every code path, forever. Rate limits get disabled during incidents, bypassed by an internal caller, or applied per IP against an attacker with many. At 128 bits none of that matters, and the cost of the extra bits is eleven characters.

The default here is 32 alphanumeric characters, which is 190.53 bits. That is more than you need and is deliberately so: for a token, the length is invisible to the user and the extra characters cost nothing.

Reaching a target entropy in each common alphabet
TargetHex charactersAlphanumeric (62)base64url (64)Bytes
64 bits1611118
112 bits28191914
128 bits32222216
192 bits48333224
256 bits64434332

Entropy is a property of how the value was made, not of how it looks. A 32-character token picked by a human, or produced by Math.random(), has nothing like 190 bits regardless of its length.

A token is a bearer credential

Bearer means exactly what it says: whoever holds the value is treated as the user, with no further proof. There is no key exchange, no signature over the request, and nothing tying the token to the machine presenting it. Every property you want from a token follows from that single fact.

It must travel only over TLS, because it is replayable in full by anyone who sees it. It must not appear in a URL, because URLs are written to access logs, sent in the Referer header to third-party sites, kept in browser history, and copied into bug reports. Putting a secret in a query string is a design decision that outlives the request. One of the fifteen generators we audited, random.org, does exactly this: its generation form is a GET, so your value travels in the query string and comes back in the response body, and both ends are ordinary server-loggable HTTP.

It should be storable in a way that survives a database leak. Store the SHA-256 of the token rather than the token itself. Verification then hashes what was presented and compares digests, the lookup is by a fixed-width key, and an attacker with a copy of your token table has a list of hashes rather than a set of live logins. This costs one hash per request and is the single highest-value change most token systems can make.

Expiry, rotation and revocation

Entropy protects against guessing. It does nothing at all against a token that has leaked, and leaking is by far the more common failure. Expiry is what limits the damage, so give every token the shortest lifetime the workflow tolerates: minutes for a password reset link, an hour or less for an access token, days for a refresh token, and a hard ceiling even on long-lived API keys.

A token that has been used for a one-time purpose should be invalidated on use, not left to expire. Password reset and email verification links are the classic case, and the classic bug is that the link keeps working after the password has been changed.

Revocation is where opaque random tokens beat self-contained ones. A random token means nothing without your database, so deleting the row revokes it instantly. A signed token such as a JWT carries its own claims and stays valid until it expires, so revoking it needs a denylist, which is the state you were trying to avoid. If you need instant revocation, a random token backed by a lookup is the simpler design.

Comparing tokens without leaking the answer

String equality in every mainstream language short-circuits at the first differing byte. That makes the comparison take slightly longer when more of the prefix matches, and the difference is measurable often enough to be worth removing. An attacker who can time your endpoint can in principle recover the token one byte at a time, turning an infeasible search over the whole value into a linear one.

The fix is a constant-time comparison, and every standard library has one. Node has crypto.timingSafeEqual, Python has hmac.compare_digest, Go has subtle.ConstantTimeCompare, PHP has hash_equals, Java has MessageDigest.isEqual, and .NET has CryptographicOperations.FixedTimeEquals. It is one function call and there is no reason not to use it.

There is an honest caveat. Over a network, jitter usually buries the signal, and a remote timing attack against a well-conditioned comparison is difficult. But the mitigation is free, the analysis of whether you are safe is not free, and the same code often ends up in a local context where the signal is clean. Comparing digests rather than raw tokens gives you a second benefit here: the lengths are always equal and the values compared are not the secret.

What we do differently

Every character is drawn with rejection sampling against the largest exact multiple of the alphabet size, so the distribution is uniform rather than nearly uniform. Two of the fifteen audited competitors do this correctly, so it is table stakes rather than a differentiator, but it is worth stating because none of the fifteen ever mentions the subject.

The entropy figure updates live and is computed under whatever constraints are active, not from the naive length times log2 alphabet formula. Constraints such as requiring at least one character of each class break the independence assumption that formula rests on, and the honest number is lower.

Nothing is transmitted. Generation happens in the page and the Network tab stays empty while you generate.

Doing this in your own code

Node 16+
import { randomBytes, createHash, timingSafeEqual } from "node:crypto";

// 32 bytes = 256 bits, rendered as 43 base64url characters.
const token = randomBytes(32).toString("base64url");

// Store the digest, not the token. A database leak then yields hashes
// rather than live credentials, and the lookup is by a fixed-width key.
const stored = createHash("sha256").update(token).digest();

function matches(presented, stored) {
  const h = createHash("sha256").update(presented).digest();
  return h.length === stored.length && timingSafeEqual(h, stored);
}

timingSafeEqual throws if the two buffers differ in length, so check the length first rather than letting it throw.

Python 3.6+
import hashlib, hmac, secrets

token = secrets.token_urlsafe(32)                  # 43 chars, 256 bits
stored = hashlib.sha256(token.encode()).digest()

def matches(presented: str, stored: bytes) -> bool:
    h = hashlib.sha256(presented.encode()).digest()
    return hmac.compare_digest(h, stored)

hmac.compare_digest is the constant-time comparison. == on bytes short-circuits at the first difference.

Go
import (
    "crypto/rand"
    "crypto/sha256"
    "crypto/subtle"
    "encoding/base64"
)

b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
    panic(err)
}
token := base64.RawURLEncoding.EncodeToString(b)

sum := sha256.Sum256([]byte(token))
ok := subtle.ConstantTimeCompare(sum[:], stored) == 1
PHP 7+
$token  = rtrim(strtr(base64_encode(random_bytes(32)), "+/", "-_"), "=");
$stored = hash("sha256", $token, true);

// hash_equals is constant time. == and === are not.
if (hash_equals($stored, hash("sha256", $presented, true))) {
    // authenticated
}
Java 17+
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

byte[] b = new byte[32];
new SecureRandom().nextBytes(b);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(b);

// MessageDigest.isEqual compares in time independent of the input.
// String.equals does not.
boolean ok = MessageDigest.isEqual(digestOf(presented), stored);
C# / .NET 6+
using System.Security.Cryptography;
using System.Buffers.Text;   // Base64Url lives here, .NET 9 and later

byte[] b = RandomNumberGenerator.GetBytes(32);

// .NET 6 and later: encode, then swap the two characters yourself.
string token = Convert.ToBase64String(b)
    .Replace('+', '-').Replace('/', '_').TrimEnd('=');

// .NET 9 and later, same result without the string surgery.
string token9 = Base64Url.EncodeToString(b);

// Constant-time comparison:
bool ok = CryptographicOperations.FixedTimeEquals(presented, stored);

System.Buffers.Text.Base64Url arrived in .NET 9. FixedTimeEquals has been there since .NET Core 2.1.

Questions

How long should a session token be?

Aim for 128 bits of entropy, which is 22 alphanumeric characters, 22 base64url characters or 32 hex characters. OWASP sets the floor at 64 bits of entropy with a recommended 128-bit identifier length, and 64 bits is only safe while rate limiting is genuinely in place on every path.

Should I use a UUID as a session token?

A version 4 UUID carries 122 bits from a secure source and is acceptable. A version 1 or version 7 UUID is not, because a large part of the value is a predictable timestamp. A purpose-built random token is still preferable: it is shorter for the same entropy and it does not look like an identifier that someone will later be tempted to expose.

Is a JWT a random token?

No. A JWT is a signed container for claims, and the security comes from the signature and the secret used to make it, not from randomness in the token itself. What should be random is the signing secret, which for HMAC-SHA-256 should be at least 32 bytes.

Why store a hash of the token instead of the token?

So that a leak of your database does not hand over working credentials. Verification hashes the presented token and compares digests, which costs one SHA-256 per request. Unlike a password, a token has full entropy already, so a fast hash is appropriate and a slow KDF is unnecessary.

Do I really need a constant-time comparison?

The remote timing signal is usually small and hard to exploit. The fix is one function call from your standard library, and reasoning carefully about whether you are safe costs more than just using it. Use hmac.compare_digest, timingSafeEqual, hash_equals or the equivalent.

What character set should a token use?

Alphanumeric or base64url. Both survive URLs, headers, cookies and shell arguments without escaping. Avoid symbols in tokens: they buy about half a bit per character and cost you an escaping bug somewhere downstream.

Related generators