Creating a secure REST service

We can decompose application security into two considerations: authentication and authorization. We need to authenticate who the user is and we also need to be sure that the user is authorized to execute a particular function. There are a variety of techniques available for offering a secure RESTful service. All of them depend on using SSL. It's essential to create proper certificates and use them to ensure that all data transmissions are encrypted.

The details of setting up a certificate for SSL encryption are outside the scope of this book. The OpenSSL toolkit can be used to create self-signed certificates. The Flask application can then use these certificates as part of a testing or development environment.

When HTTP over SSL (HTTPS) is used, then the handling of credentials and authentication can be simplified. Without HTTPS, the credentials must be encrypted in some way to be sure they aren't sniffed in transmission. With HTTPS, credentials such as usernames and passwords can be included in headers by using a simple base-64 serialization of the bytes.

There are a number of ways of handling authentication; here are two common techniques:

  • The HTTP Authorization header can be used (the header's purpose is authentication, but it's called Authorization). The Basic type is used to provide username and password. The Bearer type can be used to provide an OAuth token. This is often used by the presentation layer where a human supplies their password. 
  • The HTTP Api-Key header can be used to provide a role or authorization. This is often provided by an API client application, and is configured by trusted administrators. This can involve relatively simple authorization checks.

We'll show the simpler approach of working with internal Api-Key headers first. It's best to use a library such as https://authlib.org for dealing with user credentials and the Authorization header. While the essential rules for handling passwords are generally simple, it's easier to use a well-respected package.

Using an Api-Key header for authorization checks is something that's designed to fit within the Flask framework. The general approach requires three elements:

  • A repository of valid Api-Key values, or an algorithm for validating a key.
  • An init_app() function to do any one-time preparation. This might include reading a file, or opening a database.
  • A decorator for the application view functions.

Each of these elements is shown in the following example:

from functools import wraps
from typing import Callable, Set

VALID_API_KEYS: Set[str] = set()

def init_app(app):
global VALID_API_KEYS
if app.env == "development":
VALID_API_KEYS = {"read-only", "admin", "write"}
else:
app.logger.info("Loading from {app.config['VALID_KEYS']}")
raw_lines = (
Path(app.config['VALID_KEYS'])
.read_text()
.splitlines()
)
VALID_API_KEYS = set(filter(None, raw_lines))

def valid_api_key(view_function: Callable) -> Callable:

@wraps(view_function)
def confirming_view_function(*args, **kw):
api_key = request.headers.get("Api-Key")
if api_key not in VALID_API_KEYS:
current_app.logger.error(f"Rejecting Api-Key:{api_key!r}")
abort(HTTPStatus.UNAUTHORIZED)
return view_function(*args, **kw)

return confirming_view_function

VALID_API_KEYS is a global module that contains the current set of valid values for the Api-Keys header. It has a type hint suggesting that it is a set of string values. We're using this instead of a more formal database. In many cases, this is all that's required to confirm that the Api-Key values sent by a client application meets the authorization requirement.

The init_app() function will load this global module from a file. For development, a simple set of Api-Key values are provided. This allows simple unit tests to proceed.

valid_api_key() is a decorator. It's used to make a consistent check that can be used in all view function definitions to be sure that an Api-Key header is present and the value is known. It's a common practice to use the Flask g object to save this information for use in later logging messages. We might also add a line such as g.api_key = api_key in this function to save the value for the duration of the interaction.

Here's how we would modify a view function to use this new decorator:

@roll.route("/roll/<identifier>", methods=["GET"])
@valid_api_key
def get_roll(identifier) -> Tuple[Dict[str, Any], HTTPStatus]:
if identifier not in SESSIONS:
abort(HTTPStatus.NOT_FOUND)

return jsonify(
roll=SESSIONS[identifier], identifier=identifier, status="OK"
), HTTPStatus.OK

This is a copy of the get_roll() function shown in an earlier example. The change is to add the @valid_api_key authorization decorator after the route decorator. This provides assurance that only requests with an Api-Key header in the short list of valid keys will be able to retrieve the roll values.

A parameterized version of the @valid_api_key decorator is a bit more complex.

Hashing user passwords will be discussed in the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset