Traefik + Basic Auth + React Preflight

In the last few days, I needed to configure two Docker containers, one running a static React website (served by nginx) and another one running a Python Flask-based API, both behind Basic Auth (in the future, OAuth, but one step at a time) provided by Traefik.

I discovered a couple of different things, both from Python Flask configuration to run behind a proxy, on having react to authenticate on its calls to the API, and to allow Traefik to have CORS working together with Basic Auth, and still allowing the Preflight requests performed by React (more details below).

Python & Flask-based API

The Python service, developed with Flask, was using Flask RestX. It was behind Traefik, and was giving some trouble for different reasons. One that had impact in the Python implementation was the fact that Traefik was receiving HTTPS requests, sending them to Flask, that was returning redirects for HTTP. To make the Flask application aware of the Traefik proxy, we added the FlaskBehindProxy module.

from flask import Flask
from flask_behind_proxy import FlaskBehindProxy


def create_app(config_name):
    app = Flask(__name__)
    # ...
    FlaskBehindProxy(app)
    # ...
    return app

This makes the app aware of HTTP headers as the X-Forwarded-For. Details will be shown below. This code was the only change required in the Python code itself.

React Static App

The front-end website is running statically. For that, React is compiled, so that the resulting code can be cleanly deployed in a container running only the nginx web server.

We were having a problem with the calls to the Python code, as it is configured behind a basic authentication configuration in Traefik. We tried to include the credentials in the API URL, encoding as https://user:password@server, but it wasn’t working. We couldn’t really understand why.

The solution ended up to be the injection of code on every Axios requests:

const token = Buffer.from(`username:password`, 'utf8').toString('base64');

axios.interceptors.request.use(function (config) {
    config.headers.Authorization = "Basic " + token;
    return config;
});

While this isn’t the nicer code, it does the trick. React is able to compile this code properly, so it runs smoothly without a node server.

Traefik Configurations

As for the Traefik configuration, there are some different kinds of details. Note that I am running these containers using Docker Swarm. Your configuration might need to be adapted if you are using any other service orchestration platform.

My configuration for this API is as follows:

  frontend-api:
    image: register-for-api
    networks:
      - traefik-public
    deploy:
      labels:
        traefik.enable: "true"
        traefik.docker.network: traefik-public
        traefik.constraint-label: traefik-public

        traefik.http.routers.api-http.rule: Host(`api.url.com`)
        traefik.http.routers.api-http.entrypoints: http
        traefik.http.routers.api-http.middlewares: https-redirect

        traefik.http.routers.api-https.rule: Host(`api.url.com`) && Method(`GET`,`POST`,`PUT`,`DELETE`,`PATCH`,`HEAD`)
        traefik.http.routers.api-https.entrypoints: https
        traefik.http.routers.api-https.tls: "true"
        traefik.http.routers.api-https.tls.certresolver: le
        traefik.http.routers.api-https.middlewares: api-auth,api-head

        traefik.http.routers.api-https2.rule: Host(`api.url.com`) && Method(`OPTIONS`)
        traefik.http.routers.api-https2.entrypoints: https
        traefik.http.routers.api-https2.tls: "true"
        traefik.http.routers.api-https2.tls.certresolver: le
        traefik.http.routers.api-https2.middlewares: api-head

        traefik.http.middlewares.api-auth.basicauth.users: "admin:$$apr1$$m.3e4nD/$$ndsw/42fXKF35gdJ4YxWo1"

        traefik.http.middlewares.api-head.headers.accesscontrolallowmethods: "POST,GET,HEAD,DELETE,PATCH,PUT,OPTIONS"
        traefik.http.middlewares.api-head.headers.accesscontrolallowheaders: "*"
        traefik.http.middlewares.api-head.headers.accesscontrolalloworiginlist: "*"
        traefik.http.middlewares.api-head.headers.accesscontrolmaxage: 1000
        traefik.http.middlewares.api-head.headers.addvaryheader: "true"
        traefik.http.middlewares.api-head.headers.accessControlAllowCredentials: "true"
        traefik.http.middlewares.api-head.headers.sslredirect: "true"
        traefik.http.middlewares.api-head.headers.sslproxyheaders.X-Forwarded-Proto: "https"

        traefik.http.services.api.loadbalancer.server.port: 5000

So, the basic idea is:

  1. Configure the service as a standard docker service, with the proper Traefik configuration to use the Traefik network.
  2. Then, the non HTTP entry point is configured to redirect to HTTPS
  3. HTTPS is configured in two groups, one for all HTTP methods, except for OPTIONS. This way, all these methods can be configured with two middlewares: the basic authentication and the CORS. The OPTIONS method is configured with CORS but without authentication.
  4. In the CORS configuration, add access control variables to configure which methods, headers, and origins are allowed. Do not forget to set the SSL Proxy Headers, so that the Flask knows it is running behind a proxy.

While this configuration is properly working, I am not sure everything listed is really needed. Nevertheless, it is working.

Please send corrections and feedback, in case this helps you.