Role-Based Access Control in Flask
Implement role-based access control (RBAC) in Flask with this hands-on tutorial. Learn core concepts, step-by-step implementation, and secure best practices.
Focus: implement role-based access control in flask
You've built a Flask app where any logged-in user can delete accounts, edit admin settings, or view sensitive reports. That's the classic authorization gap — a single missing permission check turns a solid authentication system into a liability. In this lesson, you'll implement role-based access control (RBAC) in Flask so that every request is checked against the user's role, not just their login status. By the end, you'll have a clear, reusable pattern to protect your routes and data.
The problem this lesson solves
Authentication answers "Who are you?" — authorization answers "What are you allowed to do?" Most Flask tutorials stop at authentication: they check current_user.is_authenticated and call it a day. But that's a binary gate. In a real application, you have different types of users with different levels of trust:
- Admins who manage users and system settings.
- Editors who create and modify content.
- Viewers who can only read data.
Without a structured way to enforce these roles, developers resort to ad-hoc if user.role == 'admin' checks scattered across every route. This quickly becomes:
- Inconsistent — some routes check roles, others forget.
- Hard to maintain — every new route requires a decision about who should access it.
- Error-prone — a forgotten check exposes sensitive functionality.
Worse, these checks often happen inside the route logic after the request has already begun processing. That means you may be querying the database, rendering templates, or even performing side effects before discovering the user lacks permission — wasting resources and potentially leaking partial data.
This lesson gives you a centralized, declarative way to implement role-based access control in Flask: decorators that inspect the current user's role before your view function runs. The approach is simple, testable, and scales from a two-role demo to a multi-tenant production app.
Core concept / mental model
Think of RBAC as a gatekeeper at the entrance of every room in your application. The room is a route (e.g., /admin/dashboard), and the gatekeeper checks a visitor's badge (their role) against a set of allowed roles (e.g., ['admin']). If the badge doesn't match, the visitor is turned away with a clear response.
In Flask, this gatekeeper is implemented as a decorator — a function that wraps your view function and adds a permission check before the view runs. The decorator reads the current user's role, compares it to the roles you specify, and either calls the view or aborts with an appropriate HTTP error.
A useful analogy: authentication is the front door (everyone who enters needs a key), and authorization is the inner doors (even with a key, you can't enter every room — you need the right badge for that specific door).
Key concepts
- Role: An attribute on the user model that defines their permission level (e.g.,
admin,editor,viewer). - Permission: A specific action (e.g.,
create_post). In RBAC, permissions are often grouped by role. - Decorator: A Python function that wraps another function to add behavior — here, a permission check.
current_user: Flask-Login's proxy to the logged-in user object.
How it works step by step
Implementing RBAC in Flask follows five logical steps:
-
Store roles on the user model — Add a
rolecolumn to yourUsermodel. You can use a simple string, anEnum, or a separateRoletable for more complex systems. -
Create role constants — Define allowed roles as constants or an
Enumto avoid magic strings (e.g.,ROLE_ADMIN = 'admin'). -
Write a role-required decorator — The decorator reads
current_user.roleand compares it to the allowed roles. If the role is not allowed, abort with403 Forbidden(or redirect to a login page). -
Apply the decorator to routes — Decorate any view that needs restricted access with
@role_required('admin'). -
Test with different users — Simulate users with different roles to confirm the decorator works as expected.
The decorator must handle two special cases:
- Unauthenticated users:
current_usermight be an anonymous user — you should redirect to login (return a 401 or redirect tologin). - Authenticated but wrong role: Return
403 Forbiddento signal that the user could access the resource but is not allowed.
Hands-on walkthrough
Let's build a minimal Flask app with RBAC. We'll assume Flask-Login is already set up for authentication. Our focus is the authorization layer.
Step 1: Define roles and user model
Create a simple User model with a role field:
# models.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
db = SQLAlchemy()
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
role = db.Column(db.String(20), default='viewer', nullable=False)
Step 2: Create the role_required decorator
This is the heart of the implementation. It checks the current user's role before the view executes:
# decorators.py
from functools import wraps
from flask import abort, redirect, url_for
from flask_login import current_user
def role_required(*allowed_roles):
def decorator(view_func):
@wraps(view_func)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated:
# Redirect to login page (or return 401 for API)
return redirect(url_for('login'))
if current_user.role not in allowed_roles:
abort(403)
return view_func(*args, **kwargs)
return wrapped
return decorator
Step 3: Protect routes with the decorator
Apply the decorator to your routes, specifying which roles are allowed:
# app.py
from flask import Flask, render_template_string
from flask_login import login_user, login_required, logout_user, LoginManager, UserMixin
from models import db, User
from decorators import role_required
app = Flask(__name__)
app.secret_key = 'super-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db.init_app(app)
login_manager = LoginManager()
login_manager.login_view = 'login'
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
@app.route('/login')
def login():
# Simulate login as admin for demo
user = db.session.execute(db.select(User).filter_by(username='admin')).scalar()
if user:
login_user(user)
return 'Logged in as admin'
return 'User not found'
@app.route('/profile')
@login_required
def profile():
return 'Your profile (any logged-in user)'
@app.route('/admin')
@login_required
@role_required('admin')
def admin_dashboard():
return 'Admin dashboard — only admins see this'
if __name__ == '__main__':
with app.app_context():
db.create_all()
# Seed a user
if not db.session.execute(db.select(User)).scalar():
admin = User(username='admin', role='admin')
viewer = User(username='viewer1', role='viewer')
db.session.add_all([admin, viewer])
db.session.commit()
app.run(debug=True)
Expected output:
- Visiting
/adminwhen logged in asadminreturns "Admin dashboard — only admins see this". - If you log in as
viewer1, visiting/adminreturns a 403 Forbidden error.
Pro tip: For API endpoints, return a JSON 403 response instead of an HTML error page. You can modify the decorator to return
jsonify({'error': 'Forbidden'})with a 403 status.
Step 4: Advanced — multiple roles and dynamic permissions
Often you need to allow multiple roles or perform a custom check:
# decorators.py (extended)
from functools import wraps
from flask import abort, jsonify
from flask_login import current_user
def roles_required(*allowed_roles):
def decorator(view_func):
@wraps(view_func)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated:
return jsonify({'message': 'Authentication required'}), 401
if not set(current_user.roles).intersection(set(allowed_roles)):
return jsonify({'message': 'Forbidden'}), 403
return view_func(*args, **kwargs)
return wrapped
return decorator
# Usage: allow both 'admin' and 'editor'
@app.route('/edit')
@login_required
@roles_required('admin', 'editor')
def edit_content():
return 'Editing content'
Compare options / when to choose what
There are several ways to implement RBAC in Flask. The right choice depends on your app's complexity:
| Approach | Best for | Pros | Cons |
|---|---|---|---|
| Decorator with fixed roles | Simple apps (2–3 roles) | Easy to implement, readable, no dependencies | Roles are static per route; changes require code edits |
| Decorator with role constants | Slightly larger apps | Centralized role definitions; avoids typos | Still manual per-route decoration |
| Permission-based RBAC | Complex apps (many permissions) | Granular control; roles map to permissions | More setup; requires a permissions table |
| Third-party libraries (e.g., Flask-Principal, Flask-RBAC) | Large teams, complex policies | Battle-tested, flexible, often integration with Flask-Login | Extra dependency; learning curve |
When to choose what:
- Choose the simple decorator for most tutorials, prototypes, and small internal tools.
- Switch to a permission-based system when you have many overlapping roles (e.g., "editor can edit posts but not delete them").
- Use a third-party library when you need hierarchical roles, resource-level permissions, or integration with an external identity provider.
Pro tip: Keep role names as constants or an Enum. Avoid hardcoding strings like
'admin'in multiple route decorators — a single typo can open a security hole.
Troubleshooting & edge cases
Even a clean RBAC implementation can trip you up. Here are common issues and how to fix them:
1. current_user is None or an anonymous user
Symptom: AttributeError: 'AnonymousUserMixin' object has no attribute 'role'
Cause: Your decorator tries to access current_user.role before checking is_authenticated.
Fix: Always check current_user.is_authenticated first, as in the examples above.
2. The decorator doesn't apply to class-based views
Symptom: Decoration is applied but not working.
Cause: Flask's class-based views use MethodView, which requires decorating the dispatch_request method, not the class itself.
Fix: Apply the decorator to dispatch_request:
from flask.views import MethodView
class AdminView(MethodView):
@login_required
@role_required('admin')
def dispatch_request(self):
return 'Admin area'
app.add_url_rule('/admin', view_func=AdminView.as_view('admin'))
3. Circular imports
Symptom: Import errors like ImportError: cannot import name 'User'.
Cause: models.py imports the decorator and the decorator imports the model.
Fix: Put the decorator in its own module (decorators.py) that only imports from flask_login, not from your app or models.
4. Role names in the database don't match the decorator
Symptom: Users with role 'admin' still get 403.
Cause: The database stores 'Admin' or 'administrator' but the decorator expects 'admin'.
Fix: Normalize role names to lowercase or use a consistent Enum and ensure the database is seeded with the exact same values.
5. Decorator order matters
Symptom: @login_required doesn't work alongside @role_required.
Cause: The order of decorators can affect behavior. If @role_required is above @login_required, unauthenticated users get a 403 instead of being redirected to login.
Fix: Always apply @login_required below @role_required (or combine both checks in a single decorator). A common convention:
@app.route('/admin')
@role_required('admin')
@login_required
def admin():
...
6. Users can change their own role
Symptom: A user with the viewer role can update their own role to admin.
Cause: You allowed users to edit their profile without a role check.
Fix: Only admins can change roles; users should only see their own read-only role.
What you learned & what's next
You've now implemented role-based access control in Flask using a custom decorator. You can:
- Explain what RBAC is and why it's essential beyond basic authentication.
- Apply the
role_requireddecorator to protect routes for specific roles. - Compare different RBAC approaches and choose the right one for your app.
- Troubleshoot common pitfalls like decorator order and anonymous user access.
You've also connected this lesson to the broader Secure development track: controlling what users can do is just as important as verifying who they are. Next in the track, you'll likely explore session management or input validation, building on the secure pattern you've just established.
Pro tip: Always test your RBAC rules with at least two users of different roles — and include a test for unauthenticated access. Automated tests with
pytestand Flask's test client will catch regressions long before a user hits a 403 in production.
Now, open your own Flask project and add role checks to the routes that matter most. Your future self (and your security auditor) will thank you.
Practice recap
Extend the demo app by adding an editor role and a new route /publish that only admin and editor can access. Then write a small test using Flask's test client to verify that a viewer gets a 403, while an admin gets a 200. This hands-on exercise will cement the RBAC pattern you just learned.
Common mistakes
- Forgetting to check
current_user.is_authenticatedinside the decorator leads toAttributeErroronAnonymousUserMixin. - Hardcoding role strings in multiple places; a typo like 'admin ' with a space silently denies access.
- Decorator order: applying
@login_requiredbefore@role_requiredcan bypass the login redirect, causing 403 for unauthenticated users. - Checking roles inside the view function after expensive operations instead of using a decorator; repetitive and error-prone.
- Allowing users to change their own role (e.g., self-serve admin) without an administrative check.
Variations
- Use a
Rolemodel with a many-to-many relationship to the user for dynamic role assignment. - Adopt Flask-Principal or Flask-RBAC for permission-based control with resource-level checks.
- Implement a single decorator that combines login and role checks to avoid ordering issues.
Real-world use cases
- A content management system where editors can create posts but only admins can publish or delete them.
- A SaaS dashboard where customer support agents view tickets but only billing admins can access payment reports.
- An internal admin panel where only IT admins can reset user passwords or modify system configuration.
Key takeaways
- RBAC separates authentication (who you are) from authorization (what you can do).
- A decorator like
role_required('admin')centralizes permission checks and keeps routes clean. - Always check
current_user.is_authenticatedbefore accessing role attributes to avoid errors on anonymous users. - Use named constants or Enums for roles to prevent typos and improve maintainability.
- Decorator order matters —
@role_requiredmust appear above@login_requiredto redirect unauthenticated users properly. - Test your RBAC with at least two roles plus an unauthenticated case to catch regressions.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.