Nowadays the engineering community has many products for authentication in their frameworks. Lots of them have built-in features for authentication and a lot of libraries available for social sign-in. We have the Django framework, Flask, and python-social-auth to build almost everything we need to authenticate users in the pythonic world.

In this article, I'll show you an example of how to add everything we need for the user's authentication without writing lots of lines of code. The code used in this blog post is available on GitHub. We'll use Flaskflask cookie-cutterdockerdocker-composePostgresOry Kratos and Ory Keto.

Let's take a look at the login flow of our application using Ory Kratos and Ory Keto.

What we will use in our project

Setting up Ory Kratos

Ory Kratos will be responsible for storing identity data such as email/login and password. Using the quickstart guide we need to copy the contents of contrib/quickstart/kratos/email-password to the root of your project and then add the following content to the docker-compose:

  postgres-kratos:
    image: postgres:9.6
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=kratos
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=kratos
    networks:
      - intranet

  kratos-migrate:
    image: oryd/kratos:v0.8.0-alpha.3
    links:
      - postgres-kratos:postgres-kratos
    environment:
      - DSN=postgres://kratos:secret@postgres-kratos:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
    networks:
      - intranet
    volumes:
      - type: bind
        source: ./kratos
        target: /etc/config/kratos
    command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes

  kratos:
    image: oryd/kratos:v0.8.0-alpha.3
    links:
      - postgres-kratos:postgres-kratos
    environment:
      - DSN=postgres://kratos:secret@postgres-kratos:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
    ports:
      - '4433:4433'
      - '4434:4434'
    volumes:
      - type: bind
        source: ./kratos
        target: /etc/config/kratos
    networks:
      - intranet
    command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier

  kratos-selfservice-ui-node:
    image: oryd/kratos-selfservice-ui-node:v0.8.0-alpha.3
    environment:
      - KRATOS_PUBLIC_URL=http://kratos:4433/
      - KRATOS_BROWSER_URL=http://127.0.0.1:4433/
    networks:
      - intranet
    ports:
      - "4455:3000"
    restart: on-failure


  mailslurper:
    image: oryd/mailslurper:latest-smtps
    ports:
      - '4436:4436'
      - '4437:4437'
    networks:
      - intranet

  postgres-keto:

Setting up Ory Keto

You can get familiar with the concepts of Ory Keto reading the quickstart guide. These articles can give you a brief introduction to it. Since we need to manage access to the home page, we need to create a folder keto at the root of our project and have a keto/keto.yml file with the following content:

version: v0.7.0-alpha.1

log:
  level: debug

namespaces:
  - name: app
    id: 1

serve:
  read:
    host: 0.0.0.0
    port: 4466
  write:
    host: 0.0.0.0
    port: 4467

We need the following containers:

version: "3.7"

x-default-volumes: &default_volumes
  volumes:
    - ./:/app
    - node-modules:/app/node_modules
    - ./dev.db:/tmp/dev.db

services:
  oathkeeper:
    image: oryd/oathkeeper:v0.38
    depends_on:
      - kratos
    ports:
      - 8080:4455
      - 4456:4456
    command:
      serve proxy -c "/etc/config/oathkeeper/oathkeeper.yml"
    environment:
      - LOG_LEVEL=debug
    restart: on-failure
    networks:
      - intranet
    volumes:
      - ./oathkeeper:/etc/config/oathkeeper

  flask:
    build:
      context: .
    image: "kratos_app_example-development"
    environment:
      - FLASK_APP=autoapp.py
      - FLASK_ENV=development
    networks:
      - intranet
    restart: on-failure
    volumes:
      - type: bind
        source: ./
        target: /app

  postgres-kratos:
    image: postgres:9.6
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=kratos
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=kratos
    networks:
      - intranet

  kratos-migrate:
    image: oryd/kratos:v0.8.0-alpha.3
    links:
      - postgres-kratos:postgres-kratos
    environment:
      - DSN=postgres://kratos:secret@postgres-kratos:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
    networks:
      - intranet
    volumes:
      - type: bind
        source: ./kratos
        target: /etc/config/kratos
    command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes

  kratos:
    image: oryd/kratos:v0.8.0-alpha.3
    links:
      - postgres-kratos:postgres-kratos
    environment:
      - DSN=postgres://kratos:secret@postgres-kratos:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
    ports:
      - '4433:4433'
      - '4434:4434'
    volumes:
      - type: bind
        source: ./kratos
        target: /etc/config/kratos
    networks:
      - intranet
    command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier

  kratos-selfservice-ui-node:
    image: oryd/kratos-selfservice-ui-node:v0.8.0-alpha.3
    environment:
      - KRATOS_PUBLIC_URL=http://kratos:4433/
      - KRATOS_BROWSER_URL=http://127.0.0.1:4433/
    networks:
      - intranet
    ports:
      - "4455:3000"
    restart: on-failure


  mailslurper:
    image: oryd/mailslurper:latest-smtps
    ports:
      - '4436:4436'
      - '4437:4437'
    networks:
      - intranet

  postgres-keto:
    image: postgres:9.6
    ports:
      - "15432:5432"
    environment:
      - POSTGRES_USER=keto
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=keto
    networks:
      - intranet

  keto-migrate:
    image: oryd/keto:v0.7.0-alpha.1
    volumes:
      - type: bind
        source: ./keto
        target: /home/ory
    environment:
      - LOG_LEVEL=debug
      - DSN=postgres://keto:secret@postgres-keto:5432/keto?sslmode=disable&max_conns=20&max_idle_conns=4
    command: ["migrate", "up", "-y"]
    restart: on-failure
    depends_on:
      - postgres-kratos
    networks:
      - intranet

  keto-perms:
    image: oryd/keto:v0.7.0-alpha.1
    volumes:
      - type: bind
        source: ./keto
        target: /home/ory
    environment:
      - KETO_WRITE_REMOTE=keto:4467
      - KETO_READ_REMOTE=keto:4466
      - LOG_LEVEL=debug
      - DSN=postgres://keto:secret@postgres-keto:5432/keto?sslmode=disable&max_conns=20&max_idle_conns=4
    depends_on:
      - postgres-kratos
    networks:
      - intranet

  keto:
    image: oryd/keto:v0.7.0-alpha.1
    volumes:
      - type: bind
        source: ./keto
        target: /home/ory
    ports:
      - '4466:4466'
      - '4467:4467'
    depends_on:
      - keto-migrate
    environment:
      - DSN=postgres://keto:secret@postgres-keto:5432/keto?sslmode=disable&max_conns=20&max_idle_conns=4
    networks:
      - intranet
    command: serve


volumes:
  node-modules:
  kratos-sqlite:

networks:
  intranet:

Working with policies

Keto has configured namespace app to use in Flask application. Following the guide Check whether a User has Access to Something I decided to implement simple permission policy for the demo project:

Pros

Cons

Flask part

HTTP_STATUS_FORBIDDEN = 403


@blueprint.route("/", methods=["GET", "POST"])
def home():
    """Home page."""

    if 'ory_kratos_session' not in request.cookies:
        return redirect(settings.KRATOS_UI_URL)

    response = requests.get(
        f"{settings.KRATOS_EXTERNAL_API_URL}/sessions/whoami",
        cookies=request.cookies
    )
    active = response.json().get('active')
    if not active:
        abort(HTTP_STATUS_FORBIDDEN)

    email = response.json().get('identity', {}).get('traits', {}).get('email').replace('@', '')

    # Check permissions

    response = requests.get(
        f"{settings.KETO_API_READ_URL}/check",
        params={
            "namespace": "app",
            "object": "homepage",
            "relation": "read",
            "subject_id": email,
        }
    )
    if not response.json().get("allowed"):
        abort(HTTP_STATUS_FORBIDDEN)

    return render_template("public/home.html")

@blueprint.route("/oathkeeper", methods=["GET", "POST"])
def oathkeeper():
    """ An example route to demo oathkeeper integration with Kratos """
    return {"message": "greetings"}

Nota bene