Part 3: API Configuration and User Registration

How To: Create a Flask API with JWT-Based Authentication (Part 3)

Photo by ZSun Fu on Unsplash

Project Structure

The chart below shows the folder structure for this section of the tutorial. In this post, we will work on all files marked as NEW CODE. Files that contain code from previous sections but will not be modified in this post are marked as NO CHANGES.

. (project root folder) |- src | |- flask_api_tutorial | |- api | | |- auth | | | |- __init__.py | | | |- business.py | | | |- dto.py | | | |- endpoints.py | | | | | |- widgets | | | |- __init__.py | | | | | |- __init__.py | | | |- models | | |- __init__.py | | |- user.py | | | |- util | | |- __init__.py | | |- datetime_util.py | | |- result.py | | | |- __init__.py | |- config.py | |- tests | |- __init__.py | |- conftest.py | |- test_auth_register.py | |- test_config.py | |- test_user.py | |- util.py | |- .env |- .gitignore |- .pre-commit-config.yaml |- pyproject.toml |- pytest.ini |- README.md |- run.py |- setup.py |- tox.ini
KEY:
FOLDER
NEW CODE
NO CHANGES
EMPTY FILE

Introduction

It’s finally time to start configuring the API! Keep in mind that the URL routes and the business logic that takes place when a user sends a GET, POST, PUT or DELETE request could all be accomplished using the functions, classes and decorators provided in Flask (that is, without using the Flask-RESTx extension). However, doing so would require considerably more code and would not give us the Swagger UI page to document and test the API.

Before we begin, let’s discuss what makes a REST API RESTful and make a decision to abide (or not to abide) by the requirements and constraints of REST.

Understanding REST

The term “REST” was introduced in 2000 by Roy Fielding in his doctoral thesis, titled Architectural Styles and the Design of Network-based Software Architectures. I strongly recommend trying to fully digest Fielding’s thesis. At the very least you should read Chapter 5 Representational State Transfer (REST).

What makes an API RESTful is much more than creating a set of URIs and implementing the response to GET, POST, PUT, etc. requests. First of all, REST does not require HTTP – REST is protocol-agnostic. HTTP just happens to be very popular and well-suited to REST systems. REST is truly about resource state, and how hypermedia defines the actions available to these resources. REST is also about the media types that the system uses to represent the resources.

I say these things because most so-called REST APIs and articles explaining how to design and construct a REST API are not truly RESTful. In this section of the tutorial we will implement the user registration and authentication processes. What is the correct way to design a REST API to perform these actions? Are these actions even appropriate for a REST API? The answer is not as straightforward or obvious as you might think.

User Authentication in a RESTful System

The following questions/topics are from stackoverflow:

I recommend skimming these discussions. What I took away from them is that there is nothing close to a consensus on the topic of the “correct” way to design a RESTful authentication system. The design that we will implement ensures that the client’s application state is never stored by the server, which adheres to the statelessness constraint of REST. However, the design I chose for the endpoint names clearly violates the naming requirements for RESTful URIs.

I believe that you should follow the tenets of REST as long as it makes sense for your application. For an authentication API, I believe the best design is to use routes with the verbs /register, /login, etc. A RESTful design where all routes strictly refer to a resource such as /session is far less intuitive, IMO.

Bearer Token Authentication

RFC6750 is the specification document that defines an authorization process where clients utilize tokens issued by a resource server to access protected resources. The tokens are exchanged via HTTP response fields and header fields. RFC6750 defines the required format and contents of these fields, as well as recommended best practices for mitigating possible security threats.

RFC6750 is a concrete implementation of the OAuth 2.0 Authorization Framework. OAuth 2.0 has its own specification document, RFC6749. To avoid duplicating work and to make maintenance of these reference docs easier, quite often terminology referred to in the Bearer Token Authentication spec references RFC6749 for the full definition/explanation. Because of this, I will refer to both documents and reproduce text whenever I am implementing something in order to fulfull a requirement for Bearer Token Authentication.

My intent is to adhere to the requirements in every possible case. Please let me know if you believe I have implemented anything incorrectly.

API Versioning

After you develop and release an API, it is important to remember that even minor changes such as a value in the request or response data changing from a string to an int could be a breaking change for clients. The best way to avoid causing frustration is to apply strict versioning with the promise that no breaking changes will be introduced within a major version.

There are several approaches to API versioning, but the one I prefer is the most explicit – embed the version number in the URL route. For example, the API route that registers a new user account will be /api/v1/auth/register. The /api/v1 prefix will apply to all API routes, and clients will expect that any tools or proceses that integrate with our API will continue to function as long as they use the same URI.

This post on the SparkNotes blog provides a nice summary of the types of changes that are breaking changes and the types that are not:

  • Breaking Changes (Very Bad)

    • A new required parameter
    • A new required key in POST bodies
    • Removal of an existing endpoint
    • Removal of an existing endpoint request method
    • A materially different internal behavior of an API call – such as a change to the default behavior.
  • NOT Breaking Changes (Good)

    • A new resource or API endpoint
    • A new optional parameter
    • A change to a non-public API endpoint
    • A new optional key in the JSON POST body
    • A new key returned in the JSON response body

API Configuration with Flask-RESTx

Just like every other extension, Flask-RESTx can be initialized with a Flask application object (i.e., api.init_app(app) – doing so would place the API at the website root). However, in most applications we would rather have the API routes configured with a prefix such as /api/v1 to enforce our versioning system.

The best way to accomplish this is with Flask blueprints. Typically, blueprints are used to factor a large, monolithic Flask application into logical groupings based on the functional areas of your website. Using a blueprint to isolate the API will allow us to define a url_prefix for the API endpoints.

api_bp Blueprint

In the src/flask_api_tutorial/api/__init__.py file, add the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
"""API blueprint configuration."""
from flask import Blueprint
from flask_restx import Api

api_bp = Blueprint("api", __name__, url_prefix="/api/v1")
authorizations = {"Bearer": {"type": "apiKey", "in": "header", "name": "Authorization"}}

api = Api(
    api_bp,
    version="1.0",
    title="Flask API with JWT-Based Authentication",
    description="Welcome to the Swagger UI documentation site!",
    doc="/ui",
    authorizations=authorizations,
)

There are a few important things to note about how we are configuring the api and api_bp objects:

  • Line 5: This is where we create the Flask blueprint object for our API. The first parameter, "api", is the name of the blueprint. All API endpoint names will be prefixed with this value (e.g., api.func_name). The url_prefix value makes all API routes begin with /api/v1 (e.g., api/v1/auth/login).

  • Lines 6, 14: The API will implement Bearer token authentication. Providing a value for authorizations to the Flask-RESTx Api constructor will allow the user to add a JWT to the header of all requests sent through the Swagger UI. Specifically, the Swagger UI will contain a button labeled "Authorize" that opens a modal dialog prompting the user for the value of the Bearer access token.

    Currently, Flask-RESTx only supports OpenAPI 2.0, which lacks sufficient configuration settings to accurately describe Bearer token authentication as a security scheme object. This is not the case in OpenAPI 3.0. Defining an apiKey named Bearer which is located in the Authorization field of the request header achieves nearly the same behavior as Bearer Token Authentication, and provides a dialog window on the Swagger UI page to send requests with the access token in the header.

  • Line 9: Passing the api_bp blueprint object to the Flask-RESTx Api constructor links the two objects and is how all API routes become prefixed with the url_prefix value from api_bp. Later, we will import the api_bp object in the run module and register the blueprint with the Flask application object to complete the process of configuring the API.

  • Lines 10-12: All of these string values are displayed in the Swagger UI.

  • Line 13: The doc value controls the URL path of the Swagger UI. With this value, the Swagger UI path is /api/v1/ui.

The next step in configuring the API is registering the api_bp blueprint with our Flask application. The correct place to do this is within the create_app method in the src/flask_api_tutorial/__init__.py file. Open this file and add the highlighted lines below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"""Flask app initialization via factory pattern."""
from flask import Flask
from flask_bcrypt import Bcrypt
from flask_cors import CORS
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

from flask_api_tutorial.config import get_config

cors = CORS()
db = SQLAlchemy()
migrate = Migrate()
bcrypt = Bcrypt()


def create_app(config_name):
    app = Flask("flask-api-tutorial")
    app.config.from_object(get_config(config_name))

    from flask_api_tutorial.api import api_bp

    app.register_blueprint(api_bp)

    cors.init_app(app)
    db.init_app(app)
    migrate.init_app(app, db)
    bcrypt.init_app(app)
    return app

The placement of the import statement is deliberate. To avoid a circular import, we do not want the app.api package to be imported unless the create_app method is invoked.

It’s a good idea to make sure that everything still works and we have not broken anything, so run the unit tests with tox. They should all pass. Then, run flask routes to see the new URL routes that have been registered in our application:

(flask-api-tutorial) flask-api-tutorial $ flask routes
Endpoint             Methods  Rule
-------------------  -------  --------------------------
api.doc              GET      /api/v1/ui
api.root             GET      /api/v1/
api.specs            GET      /api/v1/swagger.json
restplus_doc.static  GET      /swaggerui/<path:filename>
static               GET      /static/<path:filename>

The first four routes in the list were added by registering the api_bp blueprint with our application. Next, run flask run to start the development server and point your browser to http://localhost:5000/api/v1/ui (if you are using a different port or hostname on your dev machine, adjust accordingly).

You should see something similar to the screenshot below. Note that the URL path, API version, title and description are taken directly from values we provided to the Api constructor in the src/flask_api_tutorial/api/__init__.py file.

How To: Create a Flask API with JWT-Based Authentication (Part 3)

Figure 1 - Swagger UI without API Routes

API Namespaces

In the same way that we can organize our Flask project with blueprints, we can organize our Flask-RESTx API with namespace objects. Our API will contain two namespaces: auth_ns and widget_ns, which correspond to the flask_api_tutorial.api.auth and flask_api_tutorial.api.widgets packages, respectively. For now, we will focus on auth_ns, since this is the namespace that handles authentication requests.

Currently, the src/flask_api_tutorial/api/auth folder only contains the __init__.py file. We need to create 3 new files in the auth folder: business.py, dto.py and endpoints.py. Run the command below from the project root folder to create the files (or create them yourself however you wish):

(flask-api-tutorial) flask-api-tutorial $ cd src/flask_api_tutorial/api/auth && touch business.py dto.py endpoints.py
(flask-api-tutorial) flask-api-tutorial/src/flask_api_tutorial/api/auth $ ls -al
total 8
drwxr-xr-x  7 aaronluna  staff  224 Dec 30 01:20 .
drwxr-xr-x  5 aaronluna  staff  160 Dec 27 02:47 ..
-rw-r--r--  1 aaronluna  staff    0 Dec 29 13:05 __init__.py
-rw-r--r--  1 aaronluna  staff    0 Dec 30 01:20 business.py
-rw-r--r--  1 aaronluna  staff    0 Dec 30 01:20 dto.py
-rw-r--r--  1 aaronluna  staff    0 Dec 30 01:20 endpoints.py

All of these files are standard for any Flask-RESTx API namespace package that I create. These files perform specific roles that are common to request handling and response formatting:

  • business.py: This file contains the business logic that executes when an API endpoint in this namespace receives a valid request. (e.g., process registration request, process login request, etc.)

  • dto.py: DTO stands for data transfer object. This file will contain custom objects that parse and validate request data, and API model classes that will serialize our database model classes to JSON objects before sending them in an HTTP response.

  • endpoints.py: This file will contain Flask-RESTx Resource classes. Resources are the most important individual parts of a REST API. Each resource is an API endpoint, and the methods we add to each Resource class control the HTTP methods the endpoint responds to . The following method names are automatically mapped to the corresponding HTTP methods: get, post, put, delete, patch, options and head.

Before we begin implementing the API routes and business logic for the authentication processes, we need to understand how to parse the information sent by the client in a HTTP request.

Request Parsing and Response Marshalling

Flask-RESTx provides two different approaches for parsing and validating request data. Deciding which method to use will depend on the complexity of the data and the interface you provide to the client.

In many cases the source of an HTTP POST request is a form submission from a page. Another common scenario is an HTTP GET request that occurs when a client accesses a URL with a query string containing pertinent request data. In these cases, you should use the RequestParser class provided in the reqparse module.

If your API endpoint expects a complex object (e.g., a blog post containing full content and metadata) and the source of the reqeust data is NOT a web form (i.e., client is accessing the API programmatically), then you should use the fields module to document the format of the object and instruct the client to send the object as serialized JSON. The expected JSON format is defined as an API model, and is used both for validating request data and documenting the output format for objects returned from HTTP GET requests.

Documenting the expected format of request and response data has an additional benefit – the Swagger UI automatically documents the expected format for any API routes we specify.

Request Parser Configuration

Flask-RESTx provides the RequestParser class as a way to parse data from the Flask request object. For each value to be parsed, we add an instance of the Argument class to the RequestParser. The Argument class is very flexible and is configured by the parameters listed below:

  • name: The name of the argument to parse from the request.
  • default: The value to use if the argument is not found in the request, default value is None.
  • type: The type to convert the parsed argument to. This can be any primitive (e.g., int, str, etc.), but Flask-RESTx also provides more advanced types in the inputs module (e.g., email address, URL, etc.). You can also define your own custom data types.
  • required: By default, arguments that are added to a RequestParser that are not found in the request are set to the default value. If required=True, any request where the argument is not found will be aborted with HTTP exception 400 HTTPStatus.BAD_REQUEST.
  • location: Where to look on the Flask.request object for the argument (can be args, form, headers, json, values or files). The default behavior is to parse values from values and json. values is actually a dictionary that conbines args and form. Also, you can specify multiple locations with a list (e.g., ["form", "args"]), the last location in the list takes precedence in the result set.

    I'm sure that this explanation is confusing. I think that you should explicitly define the location on the request object where you expect the data to be found. Since this value is documented on the Swagger UI, it is beneficial to users of the API to know how their requests should be constructed.

  • help: If you provide a value, it will be pre-pended to any error message that is raised while parsing the argument.
  • nullable: Whether a null or None value is allowed. By default, this is True.

I recommend reading and fully understanding the documentation explaining how to specify input validation for advanced and custom data types. Also, please review the API documentation for the inputs module which contains a list of advanced data types that are available before you create your own custom validator.

Defining API Models

For POST and PUT requests that create a new resource or update an existing resource in a collection, you should instruct the client to send the resource as a JSON object in the request body. You can define the expected API model by creating a dict object where the keys are the names of the attributes on the JSON object and the values are a class that will validate and convert the attribute to the required data type.

In the same way that the inputs module provides primitive data types and a set of predefined data formats to specify the type of each RequestParser Argument, the fields module fulfills the same role for model objects. You can find a list of pre-defined fields in the API documentation. You can also easily create your own custom field by subclassing fileds.Raw, as shown in the Flask-RESTx docs.

To avoid duplicating code, if you need to define two models which represent the same ORM object but expose slightly different sets of attributes, you can inherit a model rather than defining the same set of fields twice.

Response Marshalling

API models can be used to document the output of an API operation as well as the expected request format. This is most often used to return a representation of an ORM object with a subset of the attributes defined by the actual ORM model. For example, the User class includes id and password_hash attributes which store the primary key for the database table and password hash used to verify a user’s password during login. There’s very little security risk to exposing these two values, but at the bare minimum they expose implementation details which the client has no need for.

We will see this shortly in the auth/user API route which inspects the access token of the current user and returns a representation of the User object as the HTTP response. The API model we define as the expected output of this API route omits the id and password_hash attributes from the response.

auth_ns Endpoints

Within the auth_ns namespace, we will create the four API endpoints listed in the table below:

Endpoint NameURL PathHTTP MethodAuthentication Process
api.auth_register/api/v1/auth/registerPOSTRegister new user
api.auth_login/api/v1/auth/loginPOSTAuthenticate user
api.auth_user/api/v1/auth/userGETGet logged-in user info
api.auth_logout/api/v1/auth/logoutPOSTBlacklist access token

We will implement each endpoint in the same way, following the steps listed below:

  1. Create request parsers/API models to validate request data and serialize response data.
  2. Define the business logic necessary to process the request if validation succeeds.
  3. Create a class that inherits from Resource and bind it to the API endpoint/URL route.
  4. Define the set of HTTP methods that the API endpoint will support and expose methods on the concrete Resource class for each. Methods named get, post, put, delete, patch, options or head will be called when the API endpoint receives a request of the same HTTP method type.

    If the API endpoint does not support the HTTP method, do not expose a method with the name of the HTTP method and the client will receive a response with status code 405 HTTPStatus.METHOD_NOT_ALLOWED.

  5. Document the Resource class and all methods as explained in the Flask-RESTx docs. Most of the content on the Swagger UI page is generated by decorating your concrete Resource classes and their methods.
  6. Utilize the business logic created in Step 2 within the approprate HTTP methods to process the request.
  7. Create unit tests to verify that the input validation provided by the request parsers/API models is working correctly, and verify the endpoint behaves as expected.

This process will result in an updated Swagger UI page containing a documented version of the API endpoint which allows clients to construct data in the expected format, send HTTP requests and inspect responses from the server.

api.auth_register Endpoint

The first resource we create will handle the process of registering a new user account. If this were a full-stack tutorial, we would probably create a registration form that calls this API endpoint when a user clicks the Submit button. I’m leaving all decisions regarding the front-end in your hands, therefore designing such a form is your task to conquer.

auth_reqparser Request Parser

When a new user attempts to register, what data is required? The way our User model is defined, the value for email must be unique (i.e., two users cannot register with the same email address). The only other value which is provided by the user is their password, which is not stored in the database (the actual password is ony needed to create the password_hash value and to authenticate a user attempting to login). Open src/flask_api_tutorial/api/auth/dto.py, add the content below and save the file:

"""Parsers and serializers for /auth API endpoints."""
from flask_restx.inputs import email
from flask_restx.reqparse import RequestParser


auth_reqparser = RequestParser(bundle_errors=True)
auth_reqparser.add_argument(
    name="email", type=email(), location="form", required=True, nullable=False
)
auth_reqparser.add_argument(
    name="password", type=str, location="form", required=True, nullable=False
)

The first thing to note here is the parameter bundle_errors=True when we instantiate auth_reqparser. This value is false by default, which means that only a single error is reported whenever the request data fails validation. I prefer to have all error messages reported for all arguments in our request parser.

Next, notice that we have specified type=email() for the email argument. This is a pre-defined type provided by Flask-RESTx that verifies that the value sent in the request is a valid email address. If a request includes a value of “213323 kjljk” for email, we expect that the user will not be registered and the response will include a status code indicating that a validation error occurred and a message explaining that the value for email is not valid.

The remaining parameters are the same for both arguments: location="form", required=True, nullable=False. The purpose of each parameter was explained previously, and should answer any questions you have about these settings.

Process Registration Request

Next, we need to create a function that performs the following actions:

  • Add a new user to the database
  • Issue an access token for the new user
  • Construct an HTTP response including the access token and send the response the client

For any response containing sensitive information (e.g., access tokens, credentials, etc), RFC6749 (OAuth 2.0) defines the required and optional fields in both the response body and header:

5.1 Successful Response

The authorization server issues an access token and optional refresh token, and constructs the response by adding the following parameters to the entity-body of the HTTP response:

access_token

REQUIRED. The access token issued by the authorization server.

token_type

REQUIRED. The type of the token issued as described in Section 7.1. Value is case insensitive.

expires_in

RECOMMENDED. The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. If omitted, the authorization server SHOULD provide the expiration time via other means or document the default value.

refresh_token

OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same authorization grant as described in Section 6.

scope

OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The scope of the access token as described by Section 3.3.

The parameters are included in the entity-body of the HTTP response using the "application/json" media type as defined by [RFC4627]. The parameters are serialized into a JavaScript Object Notation (JSON) structure by adding each parameter at the highest structure level. Parameter names and string values are included as JSON strings. Numerical values are included as JSON numbers. The order of parameters does not matter and can vary.

The authorization server MUST include the HTTP "Cache-Control" response header field [RFC2616] with a value of "no-store" in any response containing tokens, credentials, or other sensitive information, as well as the "Pragma" response header field [RFC2616] with a value of "no-cache".

For example:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
  "access_token": "2YotnFZFEjr1zCsicMWpAA",
  "token_type": "example",
  "expires_in": 3600,
  "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
  "example_parameter": "example_value"
}

Open src/flask_api_tutorial/api/auth/business.py, add the content below and save the file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"""Business logic for /auth API endpoints."""
from http import HTTPStatus

from flask import current_app, jsonify
from flask_restx import abort

from flask_api_tutorial import db
from flask_api_tutorial.models.user import User


def process_registration_request(email, password):
    if User.find_by_email(email):
        abort(HTTPStatus.CONFLICT, f"{email} is already registered", status="fail")
    new_user = User(email=email, password=password)
    db.session.add(new_user)
    db.session.commit()
    access_token = new_user.encode_access_token()
    response = jsonify(
        status="success",
        message="successfully registered",
        access_token=access_token.decode(),
        token_type="bearer",
        expires_in=_get_token_expire_time(),
    )
    response.status_code = HTTPStatus.CREATED
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response


def _get_token_expire_time():
    token_age_h = current_app.config.get("TOKEN_EXPIRE_HOURS")
    token_age_m = current_app.config.get("TOKEN_EXPIRE_MINUTES")
    expires_in_seconds = token_age_h * 3600 + token_age_m * 60
    return expires_in_seconds if not current_app.config["TESTING"] else 5
  • Lines 12-13: The first thing we do is verify that the email address provided by the user has not been registered. If a User already exists with the same email address, the request is aborted.

    The abort function is provided by Flask-RESTx and is the correct way to abort a request received by an API endpoint. The first argument is the HTTP status code to include in the response. In this case, the appropriate response code is 409 HTTPStatus.CONFLICT. The remaining arguments are included in the response body.

  • Line 14-16: If the email address has not been registered, we proceed to create a User object with the provided email and password values, and then commit the new user to the database.

  • Line 17: The response needs to include an access token and we can issue one by calling encode_access_token on the user object.

  • Line 18: The specification cited above requires that any response containing an access token must have the Cache-Control and Pragma fields present in the header. The only way to add the necessary headers is by constructing the response object ourselves.

    Flask provides the jsonify function which takes either a dict, a list of arguments, or a list of keyword-arguments and converts the data to a JSON object (similar to calling json.dumps on an object). Finally, jsonify returns a response object with the JSON object as the response body.

  • Line 21: Per the specification, the access_token attribute is included as a parameter of the serialized JSON in the response body.

  • Line 22: Per the specification, the token_type attribute is included as a parameter of the serialized JSON in the response body.

  • Line 23: Per the specification, the expires_in attribute is included as a parameter of the serialized JSON in the response body.

    We calculate the lifespan of the access_token from the app.config values TOKEN_EXPIRE_HOURS and TOKEN_EXPIRE_MINUTES. If the app.config["TESTING"] flag is set, then five seconds is used as the lifespan of the token. Otherwise, the lifespan in seconds is calculated with TOKEN_EXPIRE_HOURS * 3600 + TOKEN_EXPIRE_MINUTES * 60.

  • Line 25: The most appropriate HTTP status code for a response indicating we have created a new resource is 201 HTTPStatus.CREATED.

  • Line 26-27: The final requirement in the specification cited above is that the response must include the HTTP Cache-Control response header field with a value of no-store, and the Pragma response header field with a value of no-cache

  • Line 28: After ensuring that all required response body and header elements have been created and populated correctly, we send the HTTP response containing the newly issued access_token to the client.

Next, we need to create the API endpoint and incorporate it with the auth_reqparser and process_registration_request function.

RegisterUser Resource

If you look back at all of the material covered in this tutorial, it’s amazing (IMO) that we haven’t written one line of code that that makes our Flask application perform one of the basic functions of a web server: URL routing. It’s time to fix that.

In an application that adheres to the principles of REST, each API endpoint (IOW, each URL) is a representation of a resource. Clients interact with resources by sending HTTP requests. The method type of the client’s request (e.g., GET, PUT, POST) is used to perform different operations, and the nature of the operation should be related to the. For this reason, when we need to add a URL route to the API, we define a class that inherits from the flask_restx.Resource base class.

According to the table defining the API endpoints for the auth_ns namespace, users can register for a new account by sending a POST request to /api/v1/auth/register. To create this API endpoint, open src/flask_api_tutorial/api/auth/endpoints.py, add the content below and save the file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""API endpoint definitions for /auth namespace."""
from http import HTTPStatus

from flask_restx import Namespace, Resource

from flask_api_tutorial.api.auth.dto import auth_reqparser
from flask_api_tutorial.api.auth.business import process_registration_request

auth_ns = Namespace(name="auth", validate=True)


@auth_ns.route("/register", endpoint="auth_register")
class RegisterUser(Resource):
    """Handles HTTP requests to URL: /api/v1/auth/register."""

    @auth_ns.expect(auth_reqparser)
    @auth_ns.response(int(HTTPStatus.CREATED), "New user was successfully created.")
    @auth_ns.response(int(HTTPStatus.CONFLICT), "Email address is already registered.")
    @auth_ns.response(int(HTTPStatus.BAD_REQUEST), "Validation error.")
    @auth_ns.response(int(HTTPStatus.INTERNAL_SERVER_ERROR), "Internal server error.")
    def post(self):
        """Register a new user and return an access token."""
        request_data = auth_reqparser.parse_args()
        email = request_data.get("email")
        password = request_data.get("password")
        return process_registration_request(email, password)
  • Line 9: Flask-RESTx Namespace objects are used to group a related set of API endpoints in the same way that Flask Blueprint objects can be used to group related URL routes.

    In this file, we will use the auth_ns object repeatedly as a decorator. Most of these have an effect on the behavior of the class or method they decorate, some do not. All of these decorators have one thing in common — they all produce some sort of documentation on the Swagger UI page.

    These decorators can inform clients about the expected format of request and response data or the set of possible HTTP status codes that the client can expect to receive from the server in a response. Also, docstrings for HTTP methods are rendered on the Swagger UI page and should be used to provide a short description of the method's purpose.

    Check out the Flask-RESTx docs for examples of using decorators to document the Swagger UI page (if you need even more info it's probably in the API documentation).

  • Line 12: The route decorator is used to decorate a class that inherits from Resource. Here, the @auth_ns.route decorator registers the RegisterUser resource with the auth_ns namespace.

    The first argument ("/register") is the URL route to register. The endpoint parameter overrides the default value for the endpoint name. I like to specify this value to enforce a consistent naming scheme for all endpoints within the same namespace.

  • Line 13: The RegisterUser class, which adds the "/register" endpoint to the auth_ns namespace, inherits from the Resource base class.

  • Line 16: The expect decorator is used to specify the data that the server expects the client to send in the HTTP request. The first argument can be either a request parser or an API model that defines the expected input model. The optional second argument is a bool value named validate. If validate=True, the request data will be checked to make sure it matches the expected input model.

    You can also control validation behavior for an entire namespace, which we did when the auth_ns namespace was created in Line 9. You can also define this behavior for the entire API when instantiating the api object, or by setting the value of the app configuration setting RESTPLUS_VALIDATE. You can override the validation behavior for each method using the expect decorator.

    We are using the auth_reqparser we created in src/flask_api_tutorial/api/auth/dto.py. In the Swagger UI, this renders a form with textboxes for the email and password values and also enforces the rules we configured for each argument. If we had used an API model, the Swagger UI instead renders a single textbox and an example of the expected JSON.

  • Line 17-20: The response decorator is solely for documentation purposes, removing these lines would have no impact on the behavior of this API endpoint. Still, you should document all of the response codes that can possibly be received from this endpoint. The second argument is a string value explaining why the client's request resulted in the response code that was sent, and is included in the Swagger UI page.

  • Line 21: Since the only supported HTTP method for this endpoint is POST, the only method exposed by the RegisterUser class is named post.

  • Line 22: This docstring will be rendered on the Swagger UI page.

  • Line 23-25: In order to access the email and password values provided by the user, we call the parse_args method on the auth_reqparer object. This method returns a dict object containing the validated arguments.

  • Line 26: Finally, we call the method we created to process a registration request and provide the email and password provided by the user.

Add auth_ns Namespace to api

In order to register the auth_ns namespace with the api object, open src/flask_api_tutorial/api/__init__.py and add the highlighted lines (Line 5 and Line 19):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
"""API blueprint configuration."""
from flask import Blueprint
from flask_restx import Api

from flask_api_tutorial.api.auth.endpoints import auth_ns

api_bp = Blueprint("api", __name__, url_prefix="/api/v1")
authorizations = {"Bearer": {"type": "apiKey", "in": "header", "name": "Authorization"}}

api = Api(
    api_bp,
    version="1.0",
    title="Flask API with JWT-Based Authentication",
    description="Welcome to the Swagger UI documentation for the Widget API",
    doc="/ui",
    authorizations=authorizations,
)

api.add_namespace(auth_ns, path="/auth")

The path parameter in the add_namespace method sets the prefix for all endpoints in the auth_ns namepsace. This, along with the url_prefix value in Line 8, is why all URL routes in the auth_ns namespace begin with /api/v1/auth.

We can verify that our route has been correctly registered by running flask routes:

flask-api-tutorial $ flask routes
Endpoint             Methods  Rule
-------------------  -------  --------------------------
api.auth_register    POST     /api/v1/auth/register
api.doc              GET      /api/v1/ui
api.root             GET      /api/v1/
api.specs            GET      /api/v1/swagger.json
restplus_doc.static  GET      /swaggerui/<path:filename>
static               GET      /static/<path:filename>

The presence of the api.auth_register endpoint in the list of routes confirms a number of things:

  • The RegisterUser resource supports HTTP POST requests, and no other method types.
  • RegisterUser is within the auth_ns namespace.
  • Because the api_bp Blueprint object is linked with the api object AND registered with the app Flask application object, adding the auth_ns Namespace object to the api object with the add_namespace method automatically registers all routes within the namespace with the Flask application object.

Start the development server by running flask run and point your browser to http://localhost:5000/api/v1/ui to check out the Swagger UI:

How To: Create a Flask API with JWT-Based Authentication (Part 3)

Figure 2 - Swagger UI with api.auth_register endpoint

You can click anywhere on the green bar to expand the component. It might not seem like a huge deal, but everything you see was automatically generated by Flask-RESTx (from the api object, auth_ns object, auth_reqparser, RegisterUser, etc):

How To: Create a Flask API with JWT-Based Authentication (Part 3)

Figure 3 - api.auth_register endpoint expanded

If you’d like to send a request, click the Try It Out button. Then, enter any valid email address and any value for password and click Execute:

How To: Create a Flask API with JWT-Based Authentication (Part 3)

Figure 4 - api.auth_register endpoint ready to test

You should receive a response with status code 201 HTTPStatus.CREATED if the email address is formatted correctly (this is the only validation process being performed by auth_reqparser):

How To: Create a Flask API with JWT-Based Authentication (Part 3)

Figure 5 - New user successfully registered (Swagger UI)

The Swagger UI helpfully provides a Curl textbox that contains the exact request that was submitted to the server based on the values you provided. cURL is a ubiquitous tool and you can copy and paste the contents of the textbox into any terminal if you would like to test your API from the command-line.

If you attempt to register with an email address that already exists in the database, you should receive a response with status code 409 HTTPStatus.CONFLICT. You can also test the API with a command-line tool (e.g., httpie, curl, wget, etc):

flask-api-tutorial $ http -f :5000/api/v1/auth/register email=user@test.com password=123456

POST /api/v1/auth/login HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 37
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5000
User-Agent: HTTPie/2.0.0

email=user%40test.com&password=123456

HTTP/1.0 409 CONFLICT
Access-Control-Allow-Origin: *
Content-Length: 79
Content-Type: application/json
Date: Sat, 03 Aug 2019 23:20:29 GMT
Server: Werkzeug/0.16.1 Python/3.7.6

{
  "message": "user@test.com is already registered",
  "status": "fail",
}

The CLI examples I provide in this tutorial will NOT use cURL I prefer httpie because the syntax is much cleaner and more intuitive. The options for styling and formatting the output are a huge plus as well.

Here’s an example of a successful request using httpie. Note that on the command-line or Swagger UI the response from the server is always formatted as JSON:

flask-api-tutorial $ http -f :5000/api/v1/auth/register email=user2@test.com password=123456

POST /api/v1/auth/login HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 37
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5000
User-Agent: HTTPie/2.0.0

email=user2%40test.com&password=123456

HTTP/1.0 201 CREATED
Access-Control-Allow-Origin: *
Content-Length: 79
Content-Type: application/json
Date: Sat, 03 Aug 2019 23:20:29 GMT
Server: Werkzeug/0.16.1 Python/3.7.6

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODI5MzQwMzMsImlhdCI6MTU4MjkzMzEzMywic3ViIjoiNjcwOTVlZDUtZjdhYS00MGE3LTgzZGUtNzQ1YmMzYjA5NDFmIiwiYWRtaW4iOmZhbHNlfQ.ylvNfoWwhI-NRU2WS65t4ti6sTbOEDQcJYIQC6ua0Do",
  "expires_in": 900,
  "message": "successfully registered",
  "status": "success",
  "token_type": "bearer"
}

Everything appears to be working correctly for the /register endpoint. Next, we will figure out how to create unit tests that interact with the API.

Unit Tests: test_auth_register.py

Before we start writing test cases for the newly-created endpoint, we need to add a function to tests/util.py. Open the file and add the lines highlighted below (Lines 2, 8-13):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
"""Shared functions and constants for unit tests."""
from flask import url_for

EMAIL = "new_user@email.com"
PASSWORD = "test1234"


def register_user(test_client, email=EMAIL, password=PASSWORD):
    return test_client.post(
        url_for("api.auth_register"),
        data=f"email={email}&password={password}",
        content_type="application/x-www-form-urlencoded",
    )

Let’s take a moment and discuss the register_user function. Understanding how we are using this method to test our API is absolutely vital to successfully completing this project:

  • Line 8: This function is not a test case (since the name does not begin with test_). register_user takes a Flask test client instance, and values for email and password as parameters. The test client instance must always be passed when using this function, but email and password will use the default values if none are specified.

  • Line 9: The Flask test client allows us to make HTTP requests. In order to register a new user, we must send a POST request to the api.auth_register endpoint. To do so, we call the test client's post method. The test client is capable of sending requests for all HTTP method types: get, post, put, delete, patch, options, head and trace.

  • Line 10: The first argument to the post method is the URL is the target of our request. Since the target URL is within our Flask application, we can dynamically construct the URL using the url_for function. This is really useful because it allows us to create links within our application without hardcoding any part of the path. All we need to do to use the url_for function is provide the name of the API endpoint, and voila, the URL is dynamically generated and provided to the post method.

  • Line 11: For a POST request, the server expects the data to be sent in the request body. Since we are simulating a form submission, we must format the data as a series of name/value pairs, each pair separated by an ampersand (&), and for each pair, the name is separated from the value by an equals (=) sign.

  • Line 12: This is how we specify the value of the Content-Type HTTP header. The value of this header is very important because it tells the server what type of data is being sent. The value application/x-www-form-urlencoded tells the server that the request contains form data encoded as URL parameters.

Wow, that required a lot more detail than I originally anticipated. I hope it helps you understand how the test client is used within our test cases to interact with the API. Next, create a new file test_auth_register.py in the tests folder, add the content below and save the file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""Unit tests for api.auth_register API endpoint."""
from http import HTTPStatus

from flask_api_tutorial.models.user import User
from tests.util import EMAIL, register_user

SUCCESS = "successfully registered"


def test_auth_register(client, db):
    response = register_user(client)
    assert response.status_code == HTTPStatus.CREATED
    assert "status" in response.json and response.json["status"] == "success"
    assert "message" in response.json and response.json["message"] == SUCCESS
    assert "token_type" in response.json and response.json["token_type"] == "bearer"
    assert "expires_in" in response.json and response.json["expires_in"] == 5
    assert "access_token" in response.json
    access_token = response.json["access_token"]
    result = User.decode_access_token(access_token)
    assert result.success
    user_dict = result.value
    assert not user_dict["admin"]
    user = User.find_by_public_id(user_dict["public_id"])
    assert user and user.email == EMAIL

In the previous post, I explained the meaning and purpose of the app and client test fixtures. In order to send and receive HTTP requests from our API, the test function must include client as a parameter.

Let’s go through the test_auth_register function and explain what is being tested:

  • Line 5: The `register_user` function which we just dissected and documented is imported.

  • Line 7: This string value will occur multiple times in the test cases we created in this test file, but it will not occur in any other test cases, so we do not need to refactor it and move it to the `tests/util.py` file.

  • Line 10: test_auth_register is a test case, and client and db are test fixtures defined in conftest.py. The reason for invoking the client fixture is obvious — we need it to test the API. However, the reason for invoking db is not so obvious since it isn't actually being called in the test function. This fixture initializes the database by creating tables for each database model class (the only model class at this point is User).

    In this test case, we are sending a request to register a new user and expecting the request to succeed. This will only work if the database has been initialized and the site_user table exists in the database since the SQLAlchemy extension will attempt to execute a INSERT INTO site_user... SQL statement.

    BOTTOM LINE  Invoking the db fixture is necessary for any test cases that add or modify database objects.

  • Line 11: We start off the test case by submitting the registration request with the default values. This is really the only action performed in this test case, the rest of the code just verifies the server response to the registration request.

  • Line 12: Next, we verify that the HTTP status code of the server response is 201 HTTPStatus.CREATED which indicates that a new user was created in the database.

  • Line 13-14: These two lines verify that the status and message attributes exist in the response JSON and that the values indicate that the user was successfully registered.

  • Line 15: This assert statement verifies that the token_type attribute exists in the response JSON and that the value is bearer.

  • Line 16: This assert statement verifies that the expires_in attribute exists in the response JSON and that the value is 5.

  • Line 17-18: Next, we verify that the access_token exists and retrieve the access_token.

  • Line 19-22: Next, we call User.decode_access_token and verify the operation was successful. Then, we retrieve the user_dict and verify that the token (for the user that we just registered) does not have administrator priveleges.

  • Line 23-24: The next thing we do is call User.find_by_public_id with the public_id value decoded from access_token. This verifies that the user we registered actually exists within the database. Using the object from the database, we verify that the email address for the user matches the value submitted in the original HTTP request.

test_auth_register verifies the “happy path” for the api.auth_register endpoint. We obviously need to test scenarios where a registration request is not successful, as well. Before we create the next test case, update test_auth_register.py to import the PASSWORD value from tests/util.py (Line 5) and define a new string value (Line 8):

1
2
3
4
5
6
7
8
"""Unit tests for api.auth_register API endpoint."""
from http import HTTPStatus

from flask_api_tutorial.models.user import User
from tests.util import EMAIL, PASSWORD, register_user

SUCCESS = "successfully registered"
EMAIL_ALREADY_EXISTS = f"{EMAIL} is already registered"

To verify the expected behavior when a registration request is sent for an email address that has already been registered, add the content below and save the file:

28
29
30
31
32
33
34
35
36
37
38
39
def test_auth_register_email_already_registered(client, db):
    user = User(email=EMAIL, password=PASSWORD)
    db.session.add(user)
    db.session.commit()
    response = register_user(client)
    assert response.status_code == HTTPStatus.CONFLICT
    assert (
        "message" in response.json and response.json["message"] == EMAIL_ALREADY_EXISTS
    )
    assert "token_type" not in response.json
    assert "expires_in" not in response.json
    assert "access_token" not in response.json
  • Line 29-32: The first thing we do in this test case is manually create a User instance and add it to the database. Then, we send the same registration request that was sent in the previous test case.

  • Line 33: Since a User already exists in the database with the same email address that is sent in the registration request, the response code 409 (HTTPStatus.CONFLICT) indicates that the request could not be completed, but the user might be able to resolve the source of the conflict and resubmit the request.

  • Line 34-37: Next, we verify that "status" and "message" attributes exist in the JSON object sent in the response body and the value for each indicates that the registration request was not successful.

  • Line 38-40: The final three lines verify that the "token_type", "expires_in" and "access_token" attributes are not present in the JSON object sent in the response body.

The last test case we will cover at this point is where the client submits an email address that is not in the correct format (the final version of the API will have more test cases). Let’s take a look at the actual response that is sent if the email value in a registration request is not a valid email (the example below uses httpie):

flask-api-tutorial $ http -f :5000/api/v1/auth/register email="first last" password=123456

POST /api/v1/auth/register HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 32
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5000
User-Agent: HTTPie/2.0.0

email=first+last&password=123456

HTTP/1.0 400 BAD REQUEST
Access-Control-Allow-Origin: *
Content-Length: 127
Content-Type: application/json
Date: Fri, 02 Aug 2019 17:45:40 GMT
Server: Werkzeug/0.16.1 Python/3.7.6

{
  "errors": {
    "email": "first last is not a valid email"
  },
  "message": "Input payload validation failed"
}

You might notice that none of the code we wrote for the api.auth_register endpoint generated the response above. That is because this response was automatically generated by Flask-RESTx based on the auth_reqparser we configured in src/flask_api_tutorial/api/auth/dto.py.

The response above DOES NOT have an attribute named status, because Flask-RESTx generated the response rather than any of the code that was written for this tutorial.

Whenever a request is rejected because of one or more RequestParser arguments failed validation, the format of the response will always contain a message attribute equal to “Input payload validation failed” and a an errors attribute with the value being another embedded list. The embedded list contains an entry for each argument in the parser that failed validation, with the name of the argument as the attribute name and the value equal to a message describing the failure that occurred.

The information above should make writing the test case pretty easy. Since the error message for an invalid parameter will occur in nearly every set of test cases we create, we need to add it to tests/util.py (Line 6):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
"""Shared functions and constants for unit tests."""
from flask import url_for

EMAIL = "new_user@email.com"
PASSWORD = "test1234"
BAD_REQUEST = "Input payload validation failed"


def register_user(test_client, email=EMAIL, password=PASSWORD):
    return test_client.post(
        url_for("api.auth_register"),
        data=f"email={email}&password={password}",
        content_type="application/x-www-form-urlencoded",
    )

We need to import this value into test_auth_register.py (Line 5):

1
2
3
4
5
6
7
8
"""Unit tests for api.auth_register API endpoint."""
from http import HTTPStatus

from flask_api_tutorial.models.user import User
from tests.util import EMAIL, PASSWORD, BAD_REQUEST, register_user

SUCCESS = "successfully registered"
EMAIL_ALREADY_EXISTS = f"{EMAIL} is already registered"

Next, add the content below to test_auth_register.py and save the file:

42
43
44
45
46
47
48
49
50
51
52
53
def test_auth_register_invalid_email(client):
    invalid_email = "first last"
    response = register_user(client, email=invalid_email)
    assert response.status_code == HTTPStatus.BAD_REQUEST
    assert "message" in response.json and response.json["message"] == BAD_REQUEST
    assert "token_type" not in response.json
    assert "expires_in" not in response.json
    assert "access_token" not in response.json
    assert "errors" in response.json
    assert "password" not in response.json["errors"]
    assert "email" in response.json["errors"]
    assert response.json["errors"]["email"] == f"{invalid_email} is not a valid email"

I don’t think there’s anything that needs to be explained since most of it is the same as the previous test case, and the difference in the response JSON was explained thoroughly.

There are quite a few more test cases that we need to create for the api.auth_register endpoint. I will not go through any more at this point, since you can find the full set in the github repo. Also, trying to define the necessary test coverage yourself is a very worthwhile exercise.

You should run tox to make sure the new test cases all pass and that nothing else broke because of the changes:

(flask-api-tutorial) flask-api-tutorial $ tox
GLOB sdist-make: /Users/aaronluna/Projects/flask-api-tutorial/setup.py
py37 create: /Users/aaronluna/Projects/flask-api-tutorial/.tox/py37
py37 installdeps: black, flake8, pydocstyle, pytest, pytest-black, pytest-clarity, pytest-dotenv, pytest-flake8, pytest-flask
py37 inst: /Users/aaronluna/Projects/flask-api-tutorial/.tox/.tmp/package/1/flask-api-tutorial-0.1.zip
py37 installed: alembic==1.4.0,aniso8601==8.0.0,appdirs==1.4.3,attrs==19.3.0,bcrypt==3.1.7,black==19.10b0,certifi==2019.11.28,cffi==1.14.0,chardet==3.0.4,Click==7.0,entrypoints==0.3,flake8==3.7.9,Flask==1.1.1,flask-api-tutorial==0.1,Flask-Bcrypt==0.7.1,Flask-Cors==3.0.8,Flask-Migrate==2.5.2,flask-restx==0.1.1,Flask-SQLAlchemy==2.4.1,idna==2.9,importlib-metadata==1.5.0,itsdangerous==1.1.0,Jinja2==2.11.1,jsonschema==3.2.0,Mako==1.1.1,MarkupSafe==1.1.1,mccabe==0.6.1,more-itertools==8.2.0,packaging==20.1,pathspec==0.7.0,pluggy==0.13.1,py==1.8.1,pycodestyle==2.5.0,pycparser==2.19,pydocstyle==5.0.2,pyflakes==2.1.1,PyJWT==1.7.1,pyparsing==2.4.6,pyrsistent==0.15.7,pytest==5.3.5,pytest-black==0.3.8,pytest-clarity==0.3.0a0,pytest-dotenv==0.4.0,pytest-flake8==1.0.4,pytest-flask==0.15.1,python-dateutil==2.8.1,python-dotenv==0.12.0,python-editor==1.0.4,pytz==2019.3,regex==2020.2.20,requests==2.23.0,six==1.14.0,snowballstemmer==2.0.0,SQLAlchemy==1.3.13,termcolor==1.1.0,toml==0.10.0,typed-ast==1.4.1,urllib3==1.25.8,wcwidth==0.1.8,Werkzeug==0.16.1,zipp==3.0.0
py37 run-test-pre: PYTHONHASHSEED='1825844209'
py37 run-test: commands[0] | pytest
================================================= test session starts ==================================================
platform darwin -- Python 3.7.6, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /Users/aaronluna/Projects/flask-api-tutorial/.tox/py37/bin/python
cachedir: .tox/py37/.pytest_cache
rootdir: /Users/aaronluna/Projects/flask-api-tutorial, inifile: pytest.ini
plugins: clarity-0.3.0a0, black-0.3.8, dotenv-0.4.0, flask-0.15.1, flake8-1.0.4
collected 52 items

run.py::FLAKE8 PASSED                                                                                            [  1%]
run.py::BLACK PASSED                                                                                             [  3%]
setup.py::FLAKE8 PASSED                                                                                          [  5%]
setup.py::BLACK PASSED                                                                                           [  7%]
src/flask_api_tutorial/__init__.py::FLAKE8 PASSED                                                                [  9%]
src/flask_api_tutorial/__init__.py::BLACK PASSED                                                                 [ 11%]
src/flask_api_tutorial/config.py::FLAKE8 PASSED                                                                  [ 13%]
src/flask_api_tutorial/config.py::BLACK PASSED                                                                   [ 15%]
src/flask_api_tutorial/api/__init__.py::FLAKE8 PASSED                                                            [ 17%]
src/flask_api_tutorial/api/__init__.py::BLACK PASSED                                                             [ 19%]
src/flask_api_tutorial/api/auth/__init__.py::FLAKE8 PASSED                                                       [ 21%]
src/flask_api_tutorial/api/auth/__init__.py::BLACK PASSED                                                        [ 23%]
src/flask_api_tutorial/api/auth/business.py::FLAKE8 PASSED                                                       [ 25%]
src/flask_api_tutorial/api/auth/business.py::BLACK PASSED                                                        [ 26%]
src/flask_api_tutorial/api/auth/dto.py::FLAKE8 PASSED                                                            [ 28%]
src/flask_api_tutorial/api/auth/dto.py::BLACK PASSED                                                             [ 30%]
src/flask_api_tutorial/api/auth/endpoints.py::FLAKE8 PASSED                                                      [ 32%]
src/flask_api_tutorial/api/auth/endpoints.py::BLACK PASSED                                                       [ 34%]
src/flask_api_tutorial/api/widgets/__init__.py::FLAKE8 PASSED                                                    [ 36%]
src/flask_api_tutorial/api/widgets/__init__.py::BLACK PASSED                                                     [ 38%]
src/flask_api_tutorial/models/__init__.py::FLAKE8 PASSED                                                         [ 40%]
src/flask_api_tutorial/models/__init__.py::BLACK PASSED                                                          [ 42%]
src/flask_api_tutorial/models/user.py::FLAKE8 PASSED                                                             [ 44%]
src/flask_api_tutorial/models/user.py::BLACK PASSED                                                              [ 46%]
src/flask_api_tutorial/util/__init__.py::FLAKE8 PASSED                                                           [ 48%]
src/flask_api_tutorial/util/__init__.py::BLACK PASSED                                                            [ 50%]
src/flask_api_tutorial/util/datetime_util.py::FLAKE8 PASSED                                                      [ 51%]
src/flask_api_tutorial/util/datetime_util.py::BLACK PASSED                                                       [ 53%]
src/flask_api_tutorial/util/result.py::FLAKE8 PASSED                                                             [ 55%]
src/flask_api_tutorial/util/result.py::BLACK PASSED                                                              [ 57%]
tests/__init__.py::FLAKE8 PASSED                                                                                 [ 59%]
tests/__init__.py::BLACK PASSED                                                                                  [ 61%]
tests/conftest.py::FLAKE8 PASSED                                                                                 [ 63%]
tests/conftest.py::BLACK PASSED                                                                                  [ 65%]
tests/test_auth_register.py::FLAKE8 PASSED                                                                       [ 67%]
tests/test_auth_register.py::BLACK PASSED                                                                        [ 69%]
tests/test_auth_register.py::test_auth_register PASSED                                                           [ 71%]
tests/test_auth_register.py::test_auth_register_email_already_registered PASSED                                  [ 73%]
tests/test_auth_register.py::test_auth_register_invalid_email PASSED                                             [ 75%]
tests/test_config.py::FLAKE8 PASSED                                                                              [ 76%]
tests/test_config.py::BLACK PASSED                                                                               [ 78%]
tests/test_config.py::test_config_development PASSED                                                             [ 80%]
tests/test_config.py::test_config_testing PASSED                                                                 [ 82%]
tests/test_config.py::test_config_production PASSED                                                              [ 84%]
tests/test_user.py::FLAKE8 PASSED                                                                                [ 86%]
tests/test_user.py::BLACK PASSED                                                                                 [ 88%]
tests/test_user.py::test_encode_access_token PASSED                                                              [ 90%]
tests/test_user.py::test_decode_access_token_success PASSED                                                      [ 92%]
tests/test_user.py::test_decode_access_token_expired PASSED                                                      [ 94%]
tests/test_user.py::test_decode_access_token_invalid PASSED                                                      [ 96%]
tests/util.py::FLAKE8 PASSED                                                                                     [ 98%]
tests/util.py::BLACK PASSED                                                                                      [100%]

=================================================== warnings summary ===================================================
src/flask_api_tutorial/api/auth/business.py::FLAKE8
  /Users/aaronluna/Projects/flask-api-tutorial/.tox/py37/lib/python3.7/site-packages/flask_restx/model.py:12: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3,and in 3.9 it will stop working
    from collections import OrderedDict, MutableMapping

src/flask_api_tutorial/api/auth/business.py::FLAKE8
  /Users/aaronluna/Projects/flask-api-tutorial/.tox/py37/lib/python3.7/site-packages/flask_restx/api.py:28: DeprecationWarning: The import 'werkzeug.cached_property' is deprecated and will be removed in Werkzeug 1.0. Use 'from werkzeug.utils import cached_property' instead.
    from werkzeug import cached_property

src/flask_api_tutorial/api/auth/business.py::FLAKE8
  /Users/aaronluna/Projects/flask-api-tutorial/.tox/py37/lib/python3.7/site-packages/flask_restx/swagger.py:12: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3,and in 3.9 it will stop working
    from collections import OrderedDict, Hashable

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=========================================== 52 passed, 3 warnings in 14.47s ============================================
_______________________________________________________ summary ________________________________________________________
  py37: commands succeeded
  congratulations :)

The warning that is generated from Flask-RESTx is a very minor issue with the manner in which one of their modules is importing a type from the standard library. This has no effect on the operation of the API and will be fixed very soon in an upcoming release. I will update this test result when it has been fixed.

Checkpoint

Once again, we only implemented a small number of features from the requirements list. It’s not too surprising if you realize we only created one of the four auth_ns endpoints in this section. I think we have fully satisfied one requirement: New users can register by providing an email address and password, and partially satisfied one more since the JWT is sent in the registration and login response: JWT is sent in access_token field of HTTP response after successful authentication with email/password.

User Management/JWT Authentication

New users can register by providing an email address and password

Existing users can obtain a JWT by providing their email address and password

JWT contains the following claims: time the token was issued, time the token expires, a value that identifies the user, and a flag that indicates if the user has administrator access

JWT is sent in access_token field of HTTP response after successful authentication with email/password

JWTs must expire after 1 hour (in production)

JWT is sent by client in Authorization field of request header

Requests must be rejected if JWT has been modified

Requests must be rejected if JWT is expired

If user logs out, their JWT is immediately invalid/expired

If JWT is expired, user must re-authenticate with email/password to obtain a new JWT

API Resource: Widget List

All users can retrieve a list of all widgets

All users can retrieve individual widgets by name

Users with administrator access can add new widgets to the database

Users with administrator access can edit existing widgets

Users with administrator access can delete widgets from the database

The widget model contains a "name" attribute which must be a string value containing only lowercase-letters, numbers and the "-" (hyphen character) or "_" (underscore character).

The widget model contains a "deadline" attribute which must be a datetime value where the date component is equal to or greater than the current date. The comparison does not consider the value of the time component when this comparison is performed.

URL and datetime values must be validated before a new widget is added to the database (and when an existing widget is updated).

The widget model contains a "name" field which must be a string value containing only lowercase-letters, numbers and the "-" (hyphen character) or "_" (underscore character).

Widget name must be validated before a new widget is added to the database (and when an existing widget is updated).

If input validation fails either when adding a new widget or editing an existing widget, the API response must include error messages indicating the name(s) of the fields that failed validation.

In the next section we will create the remaining three auth_ns endpoints, so the entire set of JWT authentication requirements should be complete at the next checkpoint. Questions/comments are appreciated!