51

I have a small API that I'd like to add authentication to. I'd like to be able to generate API keys for API consumers; the consumers can then use include the keys with their requests requests.

Is there a Flask library which does something like this? Or is there a typical way to do it? I did a search and I only really came upon this, which doesn't really go very much in depth. I'm looking for a library if there is one.

NickAldwin
  • 11,584
  • 12
  • 52
  • 67

3 Answers3

14

For authentication keys, create a random value and store that value in a database. random() provides insufficient entropy for things like this, so use os.urandom().

The link you posted to has a very good example of how to handle things with a decorator function. In the decorator function, check the appkey value is set in the request, verify it is valid in the database, and then return the function. If the appkey is invalid, raise AuthenticationError("Invalid appkey") and you're done.

The example you linked to is a bit confusing. I like the demonstration from How to make a chain of function decorators? better.

def checkAppKey(fn):
    def inner(*args, **kwargs): #appkey should be in kwargs
        try:
            AppKey.get(appkey)
        except KeyError:
            raise AuthenticationError("Invalid appkey")
            #Whatever other errors can raise up such as db inaccessible
        #We were able to access that API key, so pass onward.
        #If you know nothing else will use the appkey after this, you can unset it.
        return fn(*args, **kwargs)
    return inner
Community
  • 1
  • 1
Jeff Ferland
  • 17,832
  • 7
  • 46
  • 76
  • How do you prevent key collisions when simply 'creating a random value'? – jeffknupp Mar 15 '13 at 18:14
  • 1
    You rely on having a sufficiently good enough random number generator that the odds are those of a "Birthday Attack". With a large enough keyspace (I recommend 128 bits as standard), the odds of collision are so low that you'll probably encounter bit errors in memory first. http://en.wikipedia.org/wiki/Birthday_problem#Probability_table – Jeff Ferland Mar 15 '13 at 18:20
  • OK, but then you should probably specify that in your `random` and `os.urandom` calls (both of which take the desired length, a pretty important detail). – jeffknupp Mar 15 '13 at 18:29
  • 2
    I'm curious why you suggest that I use `random` instead of generating a UUID, which seems to be typical for API keys. – NickAldwin Mar 16 '13 at 22:36
  • 1
    (UUID(version 4))[http://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29] works just fine. All that matters is that you choose some format for representation that is sufficiently portable. – Jeff Ferland Mar 16 '13 at 23:02
  • @JeffFerland Why not make your own by generating 32 bits normally, then adding the 32 bits from the unix timestamp to get 64, and then Sha256 it? Run that through a big number calculator, and there's a 0.000000000000000000000000000000000000000000000000000000015% chance that the Sha256 hashes of any of the 18,446,744,073,709,551,616 possible keys that you can generate, will conflict. If you're really desperate, you could probably throw in millisecond, or microsecond latency to some random DNS server. Even with a terrible random number generator you're practically guaranteed a unique value. – Shayna Sep 14 '19 at 17:55
10

Here is an function that uses hashlib which has worked fairly well for me:

def generate_hash_key():
    """
    @return: A hashkey for use to authenticate agains the API.
    """
    return base64.b64encode(hashlib.sha256(str(random.getrandbits(256))).digest(),
                            random.choice(['rA', 'aZ', 'gQ', 'hH', 'hG', 'aR', 'DD'])).rstrip('==')

A possible solution to implement this in the app could be applying a decorator on each route that you want to protect.

Example:

def get_apiauth_object_by_key(key):
    """
    Query the datastorage for an API key.
    @param ip: ip address
    @return: apiauth sqlachemy object.
    """
    return model.APIAuth.query.filter_by(key=key).first()

def match_api_keys(key, ip):
    """
   Match API keys and discard ip
   @param key: API key from request
   @param ip: remote host IP to match the key.
   @return: boolean
   """
   if key is None or ip is None:
      return False
   api_key = get_apiauth_object_by_key(key)
   if api_key is None:
      return False
   elif api_key.ip == "0.0.0.0":   # 0.0.0.0 means all IPs.
      return True
   elif api_key.key == key and api_key.ip == ip:
      return True
   return False

def require_app_key(f):
   """
   @param f: flask function
   @return: decorator, return the wrapped function or abort json object.
   """

   @wraps(f)
   def decorated(*args, **kwargs):
      if match_api_keys(request.args.get('key'), request.remote_addr):
         return f(*args, **kwargs)
      else:
         with log_to_file:
            log.warning("Unauthorized address trying to use API: " + request.remote_addr)
         abort(401)
      return decorated

And then you can use the decorator as such:

@require_app_key
def delete_cake(version, cake_id):
   """
   Controller for API Function that gets a cake by ID
   @param cake_id: cake id
   @return: Response and HTTP code
   """

This example uses SQLAlchemy to store keys in database (You could use SQLite).

You can see the implementation here: https://github.com/haukurk/flask-restapi-recipe.

Haukur Kristinsson
  • 1,020
  • 9
  • 12
  • Good Stackoverflow answers tend to explain how to do something, rather than just link to an example. Explain what that example is doing right, rather than just saying that it is right. – Adam Jul 11 '14 at 20:15
  • Links can be helpful as supplemental information, but [link-only answers are strongly discouraged](http://meta.stackoverflow.com/a/8259/228805). Please include a summary of the linked information that's relevant to the question, and explain how it resolves the issue. – Adi Inbar Jul 14 '14 at 01:03
  • Don't use `random.getrandbits()` to generate your secret, the random number generator is not cryptographically strong. There's a big warning in the docs for the `random` module about that: https://docs.python.org/2/library/random.html – dbader Feb 17 '16 at 14:07
  • @Hakur Kristinsson How would this work in conjunction with Flask-JWT? Or do I not need this when I use Flask-JWT? Thanks! – user805981 May 17 '16 at 19:31
  • JWT would be a perfect fit here, just refine the match_api_keys function to validate the JWT token instead of querying a key store. @user805981 – Haukur Kristinsson Mar 24 '18 at 11:18
2

The "typical" way to generate an API key is to create a UUID (usually by creating an md5 hash of some subset of user information + somewhat random information (like current time)).

All API keys, however, should be UUIDs. The hexadecimal hash created by md5 meets this requirement, but there are certainly other methods.

Once you've created a key for the user, store it in the database as part of the user information and check that their key (stored in a cookie, usually) matches what you have. The actual mechanics of this are (somewhat) described in the page you linked to.

jeffknupp
  • 5,966
  • 3
  • 28
  • 29
  • 5
    *usually by creating an md5 hash of some subset of user information + somewhat random information (like current time)* No, don't do that. All I have to do is guess how old somebody's account is and then it's off to the races because the keyspace I have to test is now very, very small. – Jeff Ferland Mar 15 '13 at 18:09
  • How do you get the information that the time the key was created is part of the hash in the first place? And how do you know what part it contributes to? – jeffknupp Mar 15 '13 at 18:12
  • https://www.google.com/search?q=samy+how+i+met+your+girlfriend explains some examples of busting entropy, and a few other hacks besides. (Warning, he's a little strange, but it's a technically sound presentation.) – Jeff Ferland Mar 15 '13 at 18:18
  • Again, in the video, he relies on *knowing in advance* how the key was created (because he is familiar with how PHP's `session_start` generates its "random" value). If you don't have any clue how the hash was generated, how would you possibly divine that the time the key was created is part of the hash (and what part/how much of it)? – jeffknupp Mar 15 '13 at 18:26
  • 9
    Relying on nobody knowing how your key is generated or trying some relatively obvious patterns is poor practice. Always consider the **algorithm** to be known. – Jeff Ferland Mar 15 '13 at 18:27
  • OK, I agree with that (and am aware it's a central tenant in security research/implementation). Thanks for the info. – jeffknupp Mar 15 '13 at 18:33