Authenticating an internal API with Flask I'm developing an internal API using Flask, although due to limitations with our platform the endpoints will be accessible over the public internet. It will only have a very small number of users and it's not likely that this will increase much in the future. They will query the service programatically from a backend.

As only a small number of users will be consuming the API I think it makes sense to have a single key that can be used to access it. This will keep the authentication process simple and as the API doesn't provide access to anything really sensitive I don't believe we need to go too extreme on security. I've written the following:

app_config.py

import yaml
config = yaml.safe_load(open('config.yml'))


auth.py

from functools import wraps
from flask import request
from app_config import config

def valid_auth(func):
    @wraps(func)
    def func_wrapper(*args, **kwargs):
        if 'x-api-key' not in request.headers:
            return("Credentials not present in request", 401)
        elif request.headers['x-api-key'] != config['api_key']:
            return ("Credentials not valid", 401)
        else:
            return func(*args, **kwargs)
    return func_wrapper


main.py

import api_module as api
from flask import Flask, request
from auth import valid_auth

app = Flask(__name__)

@app.route('/route1')
@valid_auth
def api_function():
    #do api stuff here


Essentially the process is:


API key is stored in config.yml on API server
(SSL) Request from backend includes the key in a header called x-api-key
A wrapper function checks that the key sent in this header matches the key in the configuration before executing any API functions. If not, the user gets an error.


Does this seem like a reasonable approach to authentication for an internal API that will be used by a small number of users and that doesn't provide access to any sensitive information? Any general suggestions for how this process might be improved?
Just because you don't have many people using this API now, this does not mean that this will always be the case.

In addition, you might want to discriminate between the users of the API for various reasons:


To know how many there actually are
To know which user/application is generating all those requests suddenly
To rate-throttle some of them if needed
To disallow access for someone who abused the API or just left the company or an obsolete application


I would at least set this up in a very barebone way to have different API keys. For now you can just hardcode them in a dictionary in the config file, but this allows you to easily extract them to a database at some future point (which may never come). You can manually add users when needed (if it is only a few), or write a page that adds a user (if the manual task becomes too much).

In your config just have something like this:

config.yml

api_keys:
    08zEk8IC0le3I0kPwSF1g4XU9R5WgbpUq2vZkZ0pkQU: User 1
    ZtRE7FXwZdtCLMfFHWPTom7_d-4XFbXEkHR5bIdG2TM: User 2
    Wg1vaDs8uqFbYtNDsJ8H3gKjl_oI0T_O6Jg8qNLWJcU: App 1
    ...


Your code needs to be only minimally changed:

auth.py

def valid_auth(func):
    @wraps(func)
    def func_wrapper(*args, **kwargs):
        if 'x-api-key' not in request.headers:
            return "Credentials not present in request", 401
        elif request.headers['x-api-key'] not in config['api_keys']:
            return "Credentials not valid", 401
        else:
            return func(*args, **kwargs)
    return func_wrapper


Note that () around tuples are not needed in return since it is an expression and not a function.



Returning a tuple of message, status code to raise in one case and the result of the function in another case can also be a potential source of bugs. If the using code expects the return value to be a single value, checking for there being two and those two being a string and an int is not very fool-proof. Even worse if the function actually also returns a string and int tuple.

Instead, raise exceptions, which you can then deal with in api_function. Use custom classes inheriting from Exception:

class NoCredentials(Exception):
    status_code = 401

class WrongCredentials(Exception):
    status_code = 401

...
if 'x-api-key' not in request.headers:
    raise NoCredentials("Credentials not present in request")


This page in the official documentation explains in more detail how to use custom exceptions correctly in flask.