developers

OpenFGA for Python Flask Applications

In this post, we will focus on adding fine-grained authorization to your Python Flask application using OpenFGA.

Fine-grained authorization (FGA) refers to the capability of granting individual users permission to perform particular actions on specific resources. Effective FGA systems enable the management of permissions for a large number of objects and users. These permissions can undergo frequent changes as the system dynamically adds objects and adjusts access permissions for its users.

OpenFGA is an open-source Relationship-Based Access Control (ReBAC) system designed by Okta for developers and adopted by the Cloud Native Computing Foundation (CNCF). It offers scalability and flexibility and supports the implementation of RBAC and ABAC authorization models. By moving authorization logic outside the application code, OpenFGA makes it simpler to evolve authorization policies as complexity grows.

Flask provides a simple framework for rapidly creating web applications in Python. Using add-ons like SQLAlchemy and authentication with Auth0, you can save development time and focus your efforts on your application’s core functionality.

In this guide, we will build a simple application that shows how to incorporate OpenFGA, allowing you to leverage the benefits of fine-grained authorization with Flask.

Prerequisites

Before we start, let's be sure you have the following installed on your development machine:

Once you have Python installed, you can install the necessary packages using

pip3
.

On some systems, the command

pip
may be used in place of
pip3
:

pip3 install Flask Flask-SQLAlchemy python-dotenv requests openfga_sdk authlib

You’ll also need an OpenFGA server instance running. You can use a managed instance like OktaFGA or set up a local instance with Docker with:

docker run -p 8080:8080 openfga/openfga run

Project Setup

Let's set up the project structure:

flask_openfga_tutorial/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── index.html
│   │   ├── resource.html
├── config.py
├── model.fga
├── run.py
└── requirements.txt
  • app/
    : This directory contains our Flask application code, including our database model, our route handlers, and our functions that interact with OpenFGA.
  • templates/
    : Contains HTML (jinja2) templates for our web interface.
  • config.py
    : Configuration settings for Flask and OpenFGA.
  • model.fga
    : Our OpenFGA authorization model.
  • run.py
    : The entry point for our Flask application.
  • requirements.txt
    : Lists our project dependencies.

Configuring Flask, SQLAlchemy, and AuthLib

config.py

In our

config.py
, we will define the configuration for our Flask application. We will read sensitive values using
os.getenv()
, which allows us to use a
.env
file in our project directory or from environment variables.

# config.py

import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY = os.getenv('SECRET_KEY')
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///db.sqlite3')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    FGA_API_URL = os.getenv('FGA_API_URL', 'http://localhost:8080')
    FGA_STORE_ID = os.getenv('FGA_STORE_ID')
    FGA_MODEL_ID = os.getenv('FGA_MODEL_ID')
    AUTH0_CLIENT_ID = os.getenv('AUTH0_CLIENT_ID')
    AUTH0_CLIENT_SECRET = os.getenv('AUTH0_CLIENT_SECRET')
    AUTH0_DOMAIN = os.getenv('AUTH0_DOMAIN')

app/init.py

Next, we will initialize Flask, SQLAlchemy, OAuth, and our OpenFGA Client in

app/__init__.py
:

# app/__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from authlib.integrations.flask_client import OAuth
from openfga_sdk.client import ClientConfiguration
from openfga_sdk.sync import OpenFgaClient
from config import Config
import os


db = SQLAlchemy()
oauth = OAuth()

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)
    oauth.init_app(app)

    # Configure and initialize the Auth0 Client
    oauth.register(
        "auth0",
        client_id=app.config["AUTH0_CLIENT_ID"],
        client_secret=app.config["AUTH0_CLIENT_SECRET"],
        client_kwargs={
            "scope":"openid profile email",
        },
        server_metadata_url=f'https://{app.config["AUTH0_DOMAIN"]}/.well-known/openid-configuration'
    )
    
    #Configure and initialize the OpenFGA Client
    configuration = ClientConfiguration(
        api_url=app.config['FGA_API_URL'],
        store_id=app.config['FGA_STORE_ID'],
        authorization_model_id=app.config['FGA_MODEL_ID'],
    )

    app.fga_client = OpenFgaClient(configuration)
    app.fga_client.read_authorization_models()

    from .routes import main as main_blueprint
    app.register_blueprint(main_blueprint)

    with app.app_context():
        db.create_all()

    return app

Creating Our Launch Script

run.py

Our

run.py
, in the root directory of our project, is the script we will call in order to run our Flask application:

# run.py

from app import create_app

app = create_app()

if __name__ == '__main__':
    app.run(debug=True)

Defining the Database Models

app/models.py

We will define the

Resource
model in
app/models.py
to keep track of the resources users create.

# app/models.py

from . import db
import uuid

class Resource(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    uuid = db.Column(db.String(36), unique=True, nullable=False, default=lambda: str(uuid.uuid4()))
    name = db.Column(db.String(120), nullable=False)
    owner = db.Column(db.String(255), nullable=False)

Setting Up the OpenFGA Model

Next, we will create our authorization model in the file

model.fga
. This model will define the types of relationships that can exist between our users and objects:

model
  schema 1.1

type user
  relations
    define owner: [user]

type resource
  relations
    define owner: [user]
    define viewer: owner

This model defines two types,

user
and
resource
, and establishes an
owner
relationship. The
viewer
relation is defined to be the same as the
owner
, meaning only owners can view the resource.

Writing the Model to OpenFGA

We will use the OpenFGA CLI to write our new model for our OpenFGA store. Check the OpenFGA docs to learn how to use the CLI to create a store where you can write your model. Remember, models, are immutable, so anytime you make changes; you need to write the updated model and update the model ID used in your application.

First, we will create a store where we'll write our model:

fga store create --name "FGA Flask Demo Store"

The result of that command will include our

store id
, which we will use in our next command to write the model.

fga model write --store-id <store_id> --file model.fga

Configuring Authentication

This app uses Auth0 for authentication, so to get started, we will need to configure our application to enable authentication.

Create an Auth0 Account

If you do not have an Auth0 account, create one here for free.

Configure an application

Use the interactive selector to create a new Auth0 application.

  • Application Type For Application Type, select "Single Page Application."

  • Allowed Callback URLs Enter

    http://localhost:3000/callback
    in "Allowed Callback URLs"

  • Allowed Logout URLs Enter

    http://localhost:3000/
    in "Allowed Logout URLs"

  • Allowed Web Origins Enter

    http://localhost:3000
    in "Allowed Web Origins"

By default, your app will have two "Connections" enabled to provide user authentication data.

  • google-oauth2 - This option allows users to log in with Google. There is no need to configure user accounts with this option
  • Username-Password-Authentication - This option allows you to create user accounts in the Auth0 dashboard by specifying usernames and passwords

Collect required credentials

From your application's page in the Auth0 Dashboard, copy your app's

Domain
,
Client ID
, and
Client Secret
, which we will use later when we configure our application.

Implementing Routes with AUTH0 and Openfga

app/routes.py

Next, create the application routes in

app/routes.py
:

# app/routes.py

from flask import Blueprint, request, render_template, redirect, url_for, flash, session, current_app
from .models import Resource, db
from urllib.parse import quote_plus, urlencode
from openfga_sdk.client.models import ClientTuple, ClientWriteRequest, ClientCheckRequest
from app import oauth
import uuid
from functools import wraps

main = Blueprint('main', __name__)

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        # This decorated function can be applied to route handers and will ensure that a valid user session is active.
        # If the requestor is not logged in it will redirect their browser to the home page
        if 'user_info' not in session:
            return redirect(url_for('main.login'))
        return f(*args, **kwargs)
    return decorated_function

@main.route("/login")
def login():
    # Login function redirects to the Auth0 login page for our app
    return oauth.auth0.authorize_redirect(
        redirect_uri=url_for("main.callback", _external=True)
    )

@main.route("/callback", methods=["GET", "POST"])
def callback():
    # The callback function that Auth0 will redirect users to after authentication
    try:
        token = oauth.auth0.authorize_access_token()
        session["user_info"] = token['userinfo']    
        session['user_email'] = token['userinfo']['email']
    except Exception as e:
        return redirect(url_for("main.index"))
    return redirect(url_for("main.index"))
    
    

@main.route("/logout")
def logout():
    # Log a user out of the app, clear the session and redirect them to the Auth0 logout url for our app
    session.clear()
    return redirect(
        "https://" + current_app.config["AUTH0_DOMAIN"]
        + "/v2/logout?"
        + urlencode(
            {
                "returnTo": url_for("main.index", _external=True),
                "client_id": current_app.config["AUTH0_CLIENT_ID"],
            },
            quote_via=quote_plus,
        )
    )

@main.route('/')
@login_required
def index():
    resources = Resource.query.all()
    return render_template('index.html', resources=resources, user_info=session.get['user_info'])

@main.route('/create_resource', methods=['POST'])
@login_required
def create_resource():
    resource_name = request.form.get('name')

    resource = Resource(name=resource_name, owner=session.get['user_email'])
    db.session.add(resource)
    db.session.commit()

    # Create a tuple in OpenFGA
    fga_client = current_app.fga_client
    write_request = ClientWriteRequest(
        writes=[
            ClientTuple(
                user=f"user:{session.get['user_email']}",
                relation="owner",
                object=f"resource:{resource.uuid}",
            ),
        ],
    )
    fga_client.write(write_request)

    return redirect(url_for('main.resource', resource_uuid=resource.uuid))

@main.route('/resource/<resource_uuid>')
@login_required
def resource(resource_uuid):
    resource = Resource.query.filter_by(uuid=resource_uuid).first()
    if not resource:
        flash('Resource not found.')
        return redirect(url_for('main.index'))

    # Check permission using OpenFGA
    fga_client = current_app.fga_client
    check_request = ClientCheckRequest(
        user=f"user:{session.get['user_email']}",
        relation="viewer",
        object=f"resource:{resource.uuid}",
    )
    response = fga_client.check(check_request)

    if not response.allowed:
        flash('You do not have permission to view this resource.')
        return redirect(url_for('main.index'))

    return render_template('resource.html', resource=resource)
  • User Login: Users can register or log in using Auth0 via the
    login
    route.
  • Resource Creation: Users can create resources, and ownership is established by writing a tuple to OpenFGA.
  • Resource Viewing: Access to resources is controlled by checking permissions with OpenFGA.

Creating our Web Templates

Base Template:
templates/base.html

Create a base template for consistent layout, our other templates will inherit their top level layout from this template.

<!-- templates/base.html -->

<!doctype html>
<html lang="en">
<head>
    <title>{% block title %}OpenFGA Tutorial{% endblock %}</title>
</head>
<body>
    {% with messages = get_flashed_messages() %}
      {% if messages %}
        <ul>
        {% for message in messages %}
          <li>{{ message }}</li>
        {% endfor %}
        </ul>
      {% endif %}
    {% endwith %}
    {% block content %}{% endblock %}
    <a href="/logout">Log Out</a>
</body>
</html>

Index template:
templates/index.html

This page extends our base template and provides the UI elements for the main page of our application.

<!-- templates/index.html -->

{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h2>Welcome, {{ user_info.name }}!</h2>

<h3>Create a Resource</h3>
<form method="POST" action="{{ url_for('main.create_resource') }}">
    <input type="text" name="name" placeholder="Resource Name" required>
    <button type="submit">Create</button>
</form>

<h3>Resources</h3>
<ul>
    {% for resource in resources %}
        <li><a href="{{ url_for('main.resource', resource_uuid=resource.uuid) }}">{{ resource.name }}</a></li>
    {% else %}
        <li>No resources available.</li>
    {% endfor %}
</ul>
{% endblock %}

Resource template:
templates/resource.html

This page extends our base template and displays a list of resources a user has access to.

<!-- templates/resource.html -->

{% extends "base.html" %}
{% block title %}Resource Details{% endblock %}
{% block content %}
<h2>Resource: {{ resource.name }}</h2>
<p>Owned by: {{ resource.owner }}</p>
<a href="{{ url_for('main.index') }}">Back to Home</a>
{% endblock %}

Testing the Application

Configuration variables

Our application expects several variables to be set as environment variables or in a .env file in our project directory. For this example, we will create a .env file with the following content, including the model and store we created earlier and our Auth0 application details:

SECRET_KEY="your_secret_key"
DATABASE_URL="sqlite:///db.sqlite3"
FGA_API_URL="http://localhost:8080"
AUTH0_CLIENT_ID=""
AUTH0_CLIENT_SECRET=""
AUTH0_DOMAIN=""
FGA_STORE_ID="your fga store id"
FGA_MODEL_ID="your fga model id"
  • SECRET_KEY
    : A unique value used internally to secure session data in Flask.
  • DATABSE_URL
    : In our example, we are using SQLite, but you can also use a MySQL or PostgreSQL connection string.
  • FGA_API_URL
    : The URL to connect to your OpenFGA instance. The example above assumes you are running OpenFGA in Docker locally using the command provided earlier.
  • AUTH0_CLIENT_ID
    : The client ID from our application page in the Auth0 Dashboard.
  • AUTH0_CLIENT_SECRET
    : The client secret from our application page in the Auth0 Dashboard.
  • AUTH0_DOMAIN
    : The domain from our application page in the Auth0 Dashboard.
  • FGA_STORE_ID
    : This is the store ID that was returned when you created a store in your OpenFGA instance.
  • FGA_MODEL_ID
    : This is the model ID that was returned when you ran
    fga write
    to write your model.

Run the Flask App

Start the application:

python run.py

Get Started

  • Open your browser and navigate to
    http://127.0.0.1:5000/
    .
  • You will be redirected via our login route handler to log in via Auth0. You can log in via one of the OAuth providers you selected or with user accounts created in the Auth0 Dashboard.

Create a Resource

  • After logging in, create a resource by entering a name and clicking "Create".
  • The resource will be added to the database, and an ownership tuple will be written to OpenFGA.

View the Resource

  • Click on the resource name to view its details.
  • OpenFGA checks whether you have permission to view the resource based on the
    viewer
    relation.

Test Access Control

  • Log out and create a new user either by logging in with a different OAuth account or by adding another set of login credentials in the Auth0 dashboard.
  • Try to access the resource created by the first user by entering its URL directly.
  • You should receive a message stating you do not have permission to view the resource.

Next Steps

This guide presented a basic example of how OpenFGA can be used within a Python Flask application to manage authorization and access to resources. You can find a more detailed example application that implements a web application allowing users to share text files and folders and covers topics including parent-child relationships and sharing resources with others in this project.