Secure randomness for tokens and secrets

Generate unpredictable tokens with the secrets module, and see why the random module is not for security.

Python
import random
import secrets
import string

first = random.Random(1234)
second = random.Random(1234)
print([first.randrange(10) for _ in range(5)] == [second.randrange(10) for _ in range(5)])

token = secrets.token_urlsafe(32)
alphabet = set(string.ascii_letters + string.digits + "-_")
print(len(token), set(token) <= alphabet)
print(secrets.token_urlsafe(32) != token)
print(secrets.compare_digest(token, token))
Output
True
43 True
True
True

Explanation

Many security mechanisms depend on values an attacker cannot guess: session identifiers, password-reset tokens, API keys, and cryptographic keys. If the numbers are predictable, the protection is worthless, however strong the rest of the system is.

Python's random module uses the Mersenne Twister, which is a statistical generator designed for simulations. Its output is determined by its internal state, and given enough output an attacker can recover that state and predict later values. The first part of the example shows the extreme case: two generators with the same seed produce the same sequence. For anything security-related use the secrets module, which draws from the operating system's cryptographically secure random source.

secrets.token_urlsafe(32) returns 32 random bytes encoded with URL-safe base64, which is 43 characters. Thirty-two bytes is 256 bits of randomness, far beyond what can be guessed. Tokens should also be stored hashed when they are long-lived, compared in constant time, given an expiry, and used once when they grant a sensitive action such as resetting a password.

Explore the dry run
Guided walkthroughReady
1import random
2import secrets
3import string
4
5first = random.Random(1234)
6second = random.Random(1234)
7print([first.randrange(10) for _ in range(5)] == [second.randrange(10) for _ in range(5)])
8
9token = secrets.token_urlsafe(32)
10alphabet = set(string.ascii_letters + string.digits + "-_")
11print(len(token), set(token) <= alphabet)
12print(secrets.token_urlsafe(32) != token)
13print(secrets.compare_digest(token, token))

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Two generators created with the same seed will follow exactly the same sequence.—
—
Identical sequences: random is deterministic, so anyone who learns the seed or state can predict it.—
True
32 bytes of operating-system randomness, encoded for use in URLs.—
True
32 bytes encode to 43 URL-safe characters, with no padding.—
True
43 True
A new call gives a new, unpredictable value.—
True
43 True
True
Use compare_digest to compare secrets in constant time.—
True
43 True
True
True

Cost and common mistakes

Generating a token is O(n) in its length. It reads from the operating system's secure random generator.

  • Never use random for passwords, tokens, or keys. It is predictable, even when it looks random.
  • Seeding a security-sensitive generator with the time or a small number lets an attacker enumerate the possibilities.
  • Secrets such as tokens should not be logged or placed in URLs that may be stored in server logs or browser history.

Try it yourself

How long is secrets.token_urlsafe(16)?

Reveal answer and reasoning

22 characters. 16 bytes is 128 bits and base64url without padding uses ceil(16 × 8 / 6) = 22 characters.

Further reading