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
pip
may be used in place ofpip3
:
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 ranfga 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.