Implementing Flask-Login with multiple User Classes
Question:
I am writing an app that has multiple classes that function as Users (for example, a School Account and a Staff account). I’m trying to use Flask-Login to make this easy but I’m not quite sure how to make it, so that when a user logs in I can have my app check to see whether or not the username belongs to a School account or Staff account, and then log in appropriately.
I know how to figure out which type of account it belongs to (since all usernames must be unique). But after that I’m not sure how to tell the app that I want it to login that specific user.
Right now, I only have one universal login page. Is it easier if I make separate login pages for Staff accounts and School accounts? I’m using a MySQL database through Flask-SQLAlchemy.
Answers:
This is an example of what you could do. I don’t have experience using Flask-SQLAlchemy, but the how shouldn’t be much more different. The example below uses SQLAlchemy directly.
First you define a user class that inherits from Base
so that it can be mapped by ORM (Declarative)
class User(Base):
__tablename__ = 'user_table'
id = Column(Integer, primary_key=True)
email = Column(String(45), unique=True)
name = Column(String(45))
pwd = Column(String(8))
user_role = Column(String(15))
__mapper_args__ = {
'polymorphic_identity': 'user_table',
'polymorphic_on': user_role
}
Once your parent class class is ready, set a different class for each of the roles that you want to have.
class SchoolAccount(User):
__tablename__ = 'school_account'
id = Column(Integer, ForeignKey('user_table.id'), primary_key=True)
representative_name = Column(String(45))
__mapper_args__ = {
'polymorphic_identity': 'school_account'
}
Using Flask-Login you login the user and limit access based on roles.
Here is an example of a login system with two different roles. This is a nice tutorial for flask, flask-sqlalchemy, flask-login: http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins
You can define each User with a specific role. For example, user ‘x’ can be SCHOOL while user ‘y’ can be ‘STAFF’.
class User(db.Model):
__tablename__ = 'User'
id = db.Column(db.Integer,primary_key=True)
username = db.Column(db.String(80),unique=True)
pwd_hash = db.Column(db.String(200))
email = db.Column(db.String(256),unique=True)
is_active = db.Column(db.Boolean,default=False)
urole = db.Column(db.String(80))
def __init__(self,username,pwd_hash,email,is_active,urole):
self.username = username
self.pwd_hash = pwd_hash
self.email = email
self.is_active = is_active
self.urole = urole
def get_id(self):
return self.id
def is_active(self):
return self.is_active
def activate_user(self):
self.is_active = True
def get_username(self):
return self.username
def get_urole(self):
return self.urole
Flask-login however does not have the concept of user roles yet and I wrote my own version of login_required decorator to override that. So you might want to use something like:
def login_required(role="ANY"):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return current_app.login_manager.unauthorized()
urole = current_app.login_manager.reload_user().get_urole()
if ( (urole != role) and (role != "ANY")):
return current_app.login_manager.unauthorized()
return fn(*args, **kwargs)
return decorated_view
return wrapper
Then, you can use this decorator on a view function like:
@app.route('/school/')
@login_required(role="SCHOOL")
def restricted_view_for_school():
pass
I had to modify codegeek’s code a bit to get it working for me, so I figured I’d drop it here in case it can help anyone else:
from functools import wraps
login_manager = LoginManager()
...
def login_required(role="ANY"):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return login_manager.unauthorized()
if ((current_user.role != role) and (role != "ANY")):
return login_manager.unauthorized()
return fn(*args, **kwargs)
return decorated_view
return wrapper
An option would be Flask User as mentioned by @lv10 answer. It implements an abstraction to User that handles a lot of things, one of them is Role-based authentication.
The code will remain basically the same as in @codegeek answer, but “urole” property on User class should be renamed to “roles” and role have to be an class with “name” property (you can copy-paste from documentation). You’ll not need define login_required, as it’s already implemented as roles_required. So, instead of @login_required(role='rolename')
we have @roles_required('rolename')
.
I just started to use this API for security reasons and it’s being an great experience. I highly recommend to anyone with problems with passwords, user authentication and user roles.
Following accepted solution (by @codegeek), in case if you want multiple roles tagged against each routes with users having one/multiple roles, you may try below —
login_manager = LoginManager()
def role_required(role=[]):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated:
return login_manager.unauthorized()
if all(x != role1 for role1 in role for x in current_user.role) and (all(role1 != "ANY" for role1 in role)): # One user may have multiple roles
return render_template("Unauthorized_Access.html")
return fn(*args, **kwargs)
return decorated_view
return wrapper
where —
dummy user database —
users = {'User_1': {'role': ['school', 'staff']}, 'User_2': {'role': ['admin']},
'User_3': {'role': ['staff', 'admin']}}
User Class —
class User(UserMixin):
role = None
User Loader (Callback to reload the user object)
@login_manager.user_loader
def user_loader(userid):
user = User()
user.id = userid
user.role = users[userid]['role']
return user
I am writing an app that has multiple classes that function as Users (for example, a School Account and a Staff account). I’m trying to use Flask-Login to make this easy but I’m not quite sure how to make it, so that when a user logs in I can have my app check to see whether or not the username belongs to a School account or Staff account, and then log in appropriately.
I know how to figure out which type of account it belongs to (since all usernames must be unique). But after that I’m not sure how to tell the app that I want it to login that specific user.
Right now, I only have one universal login page. Is it easier if I make separate login pages for Staff accounts and School accounts? I’m using a MySQL database through Flask-SQLAlchemy.
This is an example of what you could do. I don’t have experience using Flask-SQLAlchemy, but the how shouldn’t be much more different. The example below uses SQLAlchemy directly.
First you define a user class that inherits from Base
so that it can be mapped by ORM (Declarative)
class User(Base):
__tablename__ = 'user_table'
id = Column(Integer, primary_key=True)
email = Column(String(45), unique=True)
name = Column(String(45))
pwd = Column(String(8))
user_role = Column(String(15))
__mapper_args__ = {
'polymorphic_identity': 'user_table',
'polymorphic_on': user_role
}
Once your parent class class is ready, set a different class for each of the roles that you want to have.
class SchoolAccount(User):
__tablename__ = 'school_account'
id = Column(Integer, ForeignKey('user_table.id'), primary_key=True)
representative_name = Column(String(45))
__mapper_args__ = {
'polymorphic_identity': 'school_account'
}
Using Flask-Login you login the user and limit access based on roles.
Here is an example of a login system with two different roles. This is a nice tutorial for flask, flask-sqlalchemy, flask-login: http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins
You can define each User with a specific role. For example, user ‘x’ can be SCHOOL while user ‘y’ can be ‘STAFF’.
class User(db.Model):
__tablename__ = 'User'
id = db.Column(db.Integer,primary_key=True)
username = db.Column(db.String(80),unique=True)
pwd_hash = db.Column(db.String(200))
email = db.Column(db.String(256),unique=True)
is_active = db.Column(db.Boolean,default=False)
urole = db.Column(db.String(80))
def __init__(self,username,pwd_hash,email,is_active,urole):
self.username = username
self.pwd_hash = pwd_hash
self.email = email
self.is_active = is_active
self.urole = urole
def get_id(self):
return self.id
def is_active(self):
return self.is_active
def activate_user(self):
self.is_active = True
def get_username(self):
return self.username
def get_urole(self):
return self.urole
Flask-login however does not have the concept of user roles yet and I wrote my own version of login_required decorator to override that. So you might want to use something like:
def login_required(role="ANY"):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return current_app.login_manager.unauthorized()
urole = current_app.login_manager.reload_user().get_urole()
if ( (urole != role) and (role != "ANY")):
return current_app.login_manager.unauthorized()
return fn(*args, **kwargs)
return decorated_view
return wrapper
Then, you can use this decorator on a view function like:
@app.route('/school/')
@login_required(role="SCHOOL")
def restricted_view_for_school():
pass
I had to modify codegeek’s code a bit to get it working for me, so I figured I’d drop it here in case it can help anyone else:
from functools import wraps
login_manager = LoginManager()
...
def login_required(role="ANY"):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return login_manager.unauthorized()
if ((current_user.role != role) and (role != "ANY")):
return login_manager.unauthorized()
return fn(*args, **kwargs)
return decorated_view
return wrapper
An option would be Flask User as mentioned by @lv10 answer. It implements an abstraction to User that handles a lot of things, one of them is Role-based authentication.
The code will remain basically the same as in @codegeek answer, but “urole” property on User class should be renamed to “roles” and role have to be an class with “name” property (you can copy-paste from documentation). You’ll not need define login_required, as it’s already implemented as roles_required. So, instead of @login_required(role='rolename')
we have @roles_required('rolename')
.
I just started to use this API for security reasons and it’s being an great experience. I highly recommend to anyone with problems with passwords, user authentication and user roles.
Following accepted solution (by @codegeek), in case if you want multiple roles tagged against each routes with users having one/multiple roles, you may try below —
login_manager = LoginManager()
def role_required(role=[]):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated:
return login_manager.unauthorized()
if all(x != role1 for role1 in role for x in current_user.role) and (all(role1 != "ANY" for role1 in role)): # One user may have multiple roles
return render_template("Unauthorized_Access.html")
return fn(*args, **kwargs)
return decorated_view
return wrapper
where —
dummy user database —
users = {'User_1': {'role': ['school', 'staff']}, 'User_2': {'role': ['admin']},
'User_3': {'role': ['staff', 'admin']}}
User Class —
class User(UserMixin):
role = None
User Loader (Callback to reload the user object)
@login_manager.user_loader
def user_loader(userid):
user = User()
user.id = userid
user.role = users[userid]['role']
return user