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:
- Python 3.x
- Flask
- SQLAlchemy
- authlib
- requests
- OpenFGA SDK for Python
Once you have Python installed, you can install the necessary packages using
pip3
.On some systems, the command
may be used in place ofpip
: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
: This directory contains our Flask application code, including our database model, our route handlers, and our functions that interact with OpenFGA.app/
: Contains HTML (jinja2) templates for our web interface.templates/
: Configuration settings for Flask and OpenFGA.config.py
: Our OpenFGA authorization model.model.fga
: The entry point for our Flask application.run.py
: Lists our project dependencies.requirements.txt
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
in "Allowed Callback URLs"http://localhost:3000/callback
Allowed Logout URLs Enter
in "Allowed Logout URLs"http://localhost:3000/
Allowed Web Origins Enter
in "Allowed Web Origins"http://localhost:3000
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
route.login
- 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
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
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
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"
: A unique value used internally to secure session data in Flask.SECRET_KEY
: In our example, we are using SQLite, but you can also use a MySQL or PostgreSQL connection string.DATABSE_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.FGA_API_URL
: The client ID from our application page in the Auth0 Dashboard.AUTH0_CLIENT_ID
: The client secret from our application page in the Auth0 Dashboard.AUTH0_CLIENT_SECRET
: The domain from our application page in the Auth0 Dashboard.AUTH0_DOMAIN
: This is the store ID that was returned when you created a store in your OpenFGA instance.FGA_STORE_ID
: This is the model ID that was returned when you ranFGA_MODEL_ID
to write your model.fga write
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
relation.viewer
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.