28

Can someone share the best practices for creating a nonce for an OAuth request in Python?

Nayuki
  • 17,911
  • 6
  • 53
  • 80
charliesneath
  • 1,917
  • 3
  • 21
  • 36

5 Answers5

27

While this probably does not exist at the time of this question creation, Python 3.6 introduced the secrets module which is meant for generating cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets.

In this case, generating a nonce can be generated easily (here a base64 encoded string):

nonce = secrets.token_urlsafe()

Alternatives are token_bytes to get a binary token or token_hex to get an hexadecimal string.

gabuzo
  • 7,378
  • 4
  • 28
  • 36
  • I recently heard about nonce. As per my understanding nonce should be of type integer i.e number. But here you are trying to us base64 encoded string as nonce. So can nonce be a string also – Santhosh Feb 19 '20 at 00:41
  • @SanthoshYedidi `nonce` being a number depends on how the token provider implements it (it's not strictly part of the JWT standard). All the providers I've seen are happy to use strings. The important part is that it's a single-use value. Numbers that only ever increment up are good, but you have to solve the problem of a system reset. Using a big random string is generally easier. – Erdős-Bacon Sep 16 '20 at 22:58
  • yes, thats correct, difficult to keep track of increment – Santhosh Sep 17 '20 at 06:32
15

Here's how python-oauth2 does it:

def generate_nonce(length=8):
    """Generate pseudorandom number."""
    return ''.join([str(random.randint(0, 9)) for i in range(length)])

They also have:

@classmethod
def make_nonce(cls):
    """Generate pseudorandom number."""
    return str(random.randint(0, 100000000))

Additionally there is this issue entitled: "make_nonce is not random enough", which proposes:

def gen_nonce(length):
   """ Generates a random string of bytes, base64 encoded """
   if length < 1:
      return ''
   string=base64.b64encode(os.urandom(length),altchars=b'-_')
   b64len=4*floor(length,3)
   if length%3 == 1:
      b64len+=2
   elif length%3 == 2:
      b64len+=3
   return string[0:b64len].decode()

And also references CVE-2013-4347. TL;DR version, use os.urandom or the abstracted interface to it (SystemRandom).

I like my lambdas—and didn't want non-alphanumeric characters—so I used this:

lambda length: filter(lambda s: s.isalpha(), b64encode(urandom(length * 2)))[:length]
A T
  • 13,008
  • 21
  • 97
  • 158
  • BTW. uuid1 creates a unique key based on host and time: import uuid; uuid.uuid1(). It can be cast to a string & used if an alphanumeric nonce is desired. It also has a epoch UTC time component i.e. uuid.uuid1().time will return a long integer. – radtek Oct 05 '15 at 22:43
  • To get the string you can also just use `uuid1().get_hex()`. You probably want `uuid4` or `uuid5` though, more on the UUID standard that Python conforms with can be found in [RFC4122](http://tools.ietf.org/html/rfc4122.html) – A T Oct 07 '15 at 04:15
  • Sure, unless you want the seed to be current host and time, you use uuid1 for that: https://docs.python.org/2/library/uuid.html . get_hex() is useful thanks! – radtek Oct 08 '15 at 14:14
  • 1
    Note that if you're trying to generate anything with "nonce" in the name, it's IMPORTANT that you use `os.urandom()` and never, ever `random.random` or `random.randint`. Otherwise you might have serious security issues. – Aur Saraf Mar 03 '16 at 16:55
  • @AurSaraf can you further comment on the security implications when using `random.randint` to generate a "nonce"? Thx – radtek Mar 03 '16 at 18:55
  • 2
    "[nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce)" is a word invented by cryptographers that needed a new word to unambiguously mean "a value nobody can guess" (practically, a big enough cryptographically-secure random integer). They use them in many creative ways in their protocols, and always under the assumption that nobody can guess them. `random.randint` is easy for bad guys to guess, compromising security. https://www.cigital.com/papers/download/developer_gambling.php – Aur Saraf Mar 03 '16 at 19:34
  • Where does `floor(a, b)` come from? In Python3, `math.floor` takes a single argument. – kevr Jun 14 '21 at 16:43
  • Yeah that is odd… Python 2 had [one arg](https://docs.python.org/2/library/math.html#math.floor) ask Eric O'Connor (you'll find him on that reference) – A T Jun 15 '21 at 07:18
  • 1
    @kevr I'm kinda late to this answer, but for future readers: Eric meant 4*floor(length/3) (floor should be ceil, but that's another question). References: https://en.wikipedia.org/wiki/Base64#Output_padding and here the mathematical explanations https://stackoverflow.com/questions/13378815/base64-length-calculation – scmanjarrez Apr 01 '22 at 13:39
15

For most practical purposes this gives very good nonce:

import uuid
uuid.uuid4().hex
# 'b46290528cd949498ce4cc86ca854173'

uuid4() uses os.urandom() which is best random you can get in python.

Nonce should be used only once and hard to predict. Note that uuid4() is harder to predict than uuid1() whereas later is more globally unique. So you can achieve even more strength by combining them:

uuid.uuid4().hex + uuid.uuid1().hex
# 'a6d68f4d81ec440fb3d5ef6416079305f7a44a0c9e9011e684e2c42c0319303d'
andruso
  • 1,955
  • 1
  • 18
  • 26
  • 1
    If you assume that uuid1() is more globally unique because based on mac address, then you should know that mac addresses, in practice, are far from being unique. Some manufacturers use same mac address for a given batch/model... So based on that assumption your statement is just wrong. On the contrary. – comte Jun 07 '20 at 08:23
  • 1
    @comte not only. UUID1 also contains timestamp. MAC adds some level of uniqueness as long as SOME computers in the world have different MAC-s. [source](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address)) – andruso Aug 26 '20 at 15:21
4

Here's what rauth does. There's not really hard and fast rules here. The spec doesn't seem too opinionated. Your constraints are that the value, being a nonce, should be unique. Other than that, assuming the provider doesn't complain, you can use whatever method you like.

maxcountryman
  • 1,562
  • 1
  • 24
  • 51
  • Note that it now also calls `.encode('ascii')` before giving it to `sha1`. Probably for Python 3 compatibility? – A T Jan 28 '15 at 06:33
1

Here are a few ideas I got for emailage. The generate_nonce comes from their code, but I use generate_nonce_timestamp which I used uuid for. It gives me a random alpha-numeric string and a time stamp in seconds:

import random
import time
import uuid


def generate_nonce(length=8):
    """Generate pseudo-random number."""
    return ''.join([str(random.randint(0, 9)) for i in range(length)])


def generate_timestamp():
    """Get seconds since epoch (UTC)."""
    return str(int(time.time()))

def generate_nonce_timestamp():
    """Generate pseudo-random number and seconds since epoch (UTC)."""
    nonce = uuid.uuid1()
    oauth_timestamp, oauth_nonce = str(nonce.time), nonce.hex
    return oauth_nonce, oauth_timestamp

I like using uuid1, since it generates the uuid based on current host and time and has the time property that you can extract if you need both. For emailage, you need both the timestamp and the nonce.

Here is what you get:

>>> generate_nonce_timestamp()
('a89faa84-6c35-11e5-8a36-080027c336f0', '136634341422770820')

If you want to remove the -, use nonce.get_hex().

uuid1 - Generate a UUID from a host ID, sequence number, and the current time. More on uuid.

radtek
  • 34,210
  • 11
  • 144
  • 111