What is the best way to protect a flask endpoint? - python

I'm building a flask app where I have an end point called createUser. Now I want only admins (user type = admin) to create other users but not regular users. Using #login-required decorator will not prevent regular users from calling this end point.
One simple way this can be restricted is to check type of the user in createUser function and allow only if user is admin.
But in general what is the best way to protect flask end points and give only few users the access to end points?

generally speaking, people usually set up a decorator that will get the current_user and check their roles.
def role_required(role_name):
def decorator(func):
#wraps(func)
def authorize(*args, **kwargs):
if not current_user.has_role(role_name):
abort(401) # not authorized
return func(*args, **kwargs)
return authorize
return decorator
then in your views you can essentially do:
#app.route('/protected')
#role_required('admin')
def admin_view():
" this view is for admins only "
Alternatively, you can use flask-bouncer

Related

Flask Authentication Override for one Route

I'm new to python and working on a small code for flask webserver.
I need a solution to achieve the below scenario.
I have app.py which calls settings.py for flask user ID and password and also a route.py to handble the routes.
Now I want users to be asked for authentication only for few routes and other routes should override authentication and perform operation based on specific users/specific host.
Can this be achieved?
As of now its asking for authentication for all the routes
You can write a decorator. Decorators are usually called before the definition of a function you want to use (in this case whenever you want to authenticate your route).
For example:
def requires_auth():
def wrapper(f):
#wraps(f)
def decorated(*args, **kwargs):
if 'id' not in flask.session:
return unauthorized_abort()
else:
if flask.session['first_login']:
return f(*args, **kwargs)
else:
return flask.render_template('change_password.html')
return decorated
return wrapper
After writing this decorator, we can easily use authentication for particular routes only. This below code shows #require_auth() which will easily help for particular routes only.
#app.route('/data/get_data',methods=['GET'])
#requires_auth()
def get_data():
Although the above answer solves my query, I was looking for more specific scenario.
Here's what I changed,
I commented #app.config['BASIC_AUTH_FORCE'] = True in flask settings
Here is what changed in routes.py
Added
basic_auth = BasicAuth(app)
and I added this decorator
#basic_auth.required for the routes which requires authentication.
As a security measure, I am allowing Unauthenticated requests only from a specific host using headers.

How to use sessions to restrict access to the backend and only allow the admin to access it in flask? (Python)

I would like to know how to restrict access to certain pages of my website using sessions. In, summery I'd like the administrator to log in through the same login form as normal users but using the credentials he entered, he'll be redirected to the backend page where he'll have full access to both front-end web pages and back-end web pages. When a user logs in, they can only view front-end web pages. I'm trying to avoid a situation where a user logs in and then changes the URL in the search bar of the web browser from 127.0.0.7/homepage to 127.0.0.7/backend and is allowed access because he's logged in.
I'm using the code below to check if the user is logged in. If they are not then they're redirected to the login page. I'm using flask framework. Any suggestions are greatly appreciated.
Thank you.
app.route('/backend', methods=['POST', 'GET'])
def backend():
if 'userkey' in session:
# connect to database using pymysql
if cursor.rowcount == 0:
return render_template('backend.html', msg="No orders found...")
else:
rows = cursor.fetchall()
return render_template('backend.html', orderdata=rows)
elif 'userkey' not in session:
return redirect('/login')
else:
return redirect('/login')
You must already be setting session['userkey'] in your login view once the user has authenticated, you just need to add the additional information about the users admin status to get this working. It would also be much easier if you moved all of the if 'userkey' in session checks outside of your view function into a decorator so that it can easily be reused to protect all of your view functions. This pattern is shown in the flask documentation under the heading 'Login Required Decorator' http://flask.pocoo.org/docs/1.0/patterns/viewdecorators/.
In your login function you should query your database for the users admin status at the same time as retrieving the userkey and hashed password. Then simply set session["admin"] = True if the user is an admin or don't set this at all if they are not an admin.
The following code would then work for the decorator.
from functools import wraps
from flask import session
def login_required(status=None):
def login_decorator(func):
#wraps(func)
def wrapper(*args, **kwargs):
if 'userkey' in session and (status is None or status in session):
return func(*args, **kwargs)
else:
return redirect("/login")
return wrapper
return login_decorator
This decorator should then be applied to protect all of your view functions. It will then be executed prior to each view function either allowing the view to proceed if all the conditions are met or redirecting the user back to the login page if not. It takes an optional single argument status which in your case should be 'admin' for all of the back end views and should not be supplied for the front end views that do not require admin privileges. The functools.wraps decorator inside the function is just there to update the metadata of the wrapped function so that attributes like function.__name__ behave as expected. The 'userkey' in session check is the same as in your original code but we additionally check if a status was supplied and if so if it is also stored in the session. In your case the only value of status would be 'admin' but this pattern could easily be expanded to allow other groups that would each have access to different parts of the website.
Give that all the user checks are carried out in the decorator your backend view function can be simplified to
#app.route('/backend', methods=['POST', 'GET'])
#login_required("admin")
def backend():
# connect to database using pymysql
if cursor.rowcount == 0:
return render_template('backend.html', msg="No orders found...")
else:
rows = cursor.fetchall()
return render_template('backend.html', orderdata=rows)
The same decorator can be used to protect all of your front end view functions, just omit the "admin" argument.
#app.route('/frontend', methods=['POST', 'GET'])
#login_required()
def frontend():
# .....

Create privileged users with Flask-HTTPAuth by chaining decorators --- losing context?

I'm trying to create a two-tiered authentication system with basic auth, using Flask-HTTPAuth. My application has two routes, a base route at / accessible to any logged in user, and an admin route at /admin accessible only to users who are (as you might expect) logged in as admins.
So I decided to implement this by chaining decorators, with the relevant part of the code as follows (where dbops is just a namespace that handles talking to the database):
#auth.verify_password
def verify_pw(lastname, password):
ln = lastname.lower()
if ln in dbops.list_users():
hashed_pw = dbops.find_hashed_password(ln)
return bcrypt.checkpw(password.encode('utf8'), hashed_pw.encode('utf8'))
return False
def must_be_admin(f):
#wraps(f)
def wrapper(*args, **kwargs):
if dbops.is_admin(auth.username()):
return f(*args, **kwargs)
return "Not authorized."
return wrapper
#core.route("/")
#auth.login_required
def dataentry():
return render_template("dataentry.html")
#core.route("/admin")
#must_be_admin
#auth.login_required
def admin():
return render_template("admin.html")
This works fine so long as anyone trying to log in as an admin user first visits the / route: it prompts for a username and password, and then the admin user can go to /admin and carry out logged-in admin tasks.
However, if an admin user first visits /admin it doesn't give a login prompt. It just throws, and after poking around in the debugger I've determined that auth.username() is returning an empty string. So, my guess is that for some reason, the inner decorator isn't being applied, hence the lack of a login prompt.
Does anyone know what might be going on here?
My first hypothesis was that this was an easy bug, because the inner function on the admin decorator wasn't being called until after the is_admin check. So I tried to fix that my calling the function---and thus presumably making auth.username() available--- before the check, as follows:
def must_be_admin(f):
#wraps(f)
def wrapper(*args, **kwargs):
dummy_to_get_username = f(*args, **kwargs)
if dbops.is_admin(auth.username()):
return dummy_to_get_username
return "Not authorized."
return wrapper
But that just caused the same behavior.
I see from this prior SO that the recommended way to do this from the library author is to just create two separate Flask-HTTPAuth objects. Which I can do, no problem. But clearly my mental model about how decorators are working are failing, so I'd like to solve this problem independent of getting the functionality I want working...
The correct order in which you apply decorators is sometimes hard to figure out without knowing what the decorator does, but unfortunately the wrong order will make the application behave incorrectly.
For decorators that do something "before" the view function runs, as in this case, you typically have to put the decorators in the order in which you want them to execute. So I think your code will do what you expect when you use Flask-HTTPAuth's login_required before your must_be_admin:
#core.route("/admin")
#auth.login_required
#must_be_admin
def admin():
return render_template("admin.html")
In this way, the credentials will be checked first, and if missing or invalid login_required will return a 401 error to the browser, which will make the login prompt appear. Only after credentials are determined to be valid you want to evaluate the admin decorator.

How to make a python decorator function in Flask with arguments (for authorization)

I used a flask snippet for my flask-login that checks that a user is logged in:
from functools import wraps
def logged_in(f):
#wraps(f)
def decorated_function(*args, **kwargs):
if session.get('logged_in') is not None:
return f(*args, **kwargs)
else:
flash('Please log in first.', 'error')
return redirect(url_for('login'))
return decorated_function
And I decorate views like so:
#app.route('/secrets', methods=['GET', 'POST'])
#logged_in
def secrets():
error = None
I'd like to do something similar for authorization, too. Right now, I have many views to check that a user owns a resource, let's say the hotdogs resource.
If the logged_in user is the owner of that particular hotdog, he can edit and manage his hotdogs. If he isn't, I kick him out to the unauthorized screen.
#app.route('/<hotdog>/addmustard/',methods=["GET"])
#logged_in
def addmustard(hotdog):
if not (authorizeowner(hotdog)):
return redirect(url_for('unauthorized'))
do_stuff()
authorizeowner() takes a hotdog as input and checks that the recorded hotdog owner matches the owner name listed in the session variable.
I tried making a owns_hotdog wrapper/decorator function similar to my logged in one, but it complained that it didn't accept arguments. How can I achieve something similar? Something like...
def owns_hotdog(f):
#wraps(f)
def decorated_function(*args, **kwargs):
if not authorizeowner(hotdog):
return f(*args, **kwargs)
else:
flash('Please log in first.', 'error')
return redirect(url_for('login'))
return decorated_function
From the error message, decorator seems not to be receiving the hotdog argument that Flask views have access to from the variable in the route. My hope is for something like...
#app.route('/<hotdog>/addmustard/',methods=["GET"])
#logged_in
#owns_hotdog(hotdog)
def addmustard(hotdog):
do_stuff()
Everything works with my current authorizeowner(hotdog) function, but it just seems cleaner to have this in place as a wrapper on top of my route, rather than as the first line inside the route.
Some other notes:
I know that Flask-Security and Flask-Principal can manage
authorization for me. Unfortunately, I'm using an unsupported
database back-end and am unable to use these extensions. So, I'm
forced to do authentication without them.
If you see any glaring holes in doing authorization this way, please let me know!
Here's how to do it:
from functools import update_wrapper
def owns_hotdog(hotdog):
def decorator(fn):
def wrapped_function(*args, **kwargs):
# First check if user is authenticated.
if not logged_in():
return redirect(url_for('login'))
# For authorization error it is better to return status code 403
# and handle it in errorhandler separately, because the user could
# be already authenticated, but lack the privileges.
if not authorizeowner(hotdog):
abort(403)
return fn(*args, **kwargs)
return update_wrapper(wrapped_function, fn)
return decorator
#app.errorhandler(403)
def forbidden_403(exception):
return 'No hotdogs for you!', 403
When decorator takes arguments, it's not really a decorator, but a factory function which returns the real decorator.
But if I were you, I would use Flask-Login for authentication and augment it with custom decorators and functions as yours to handle authorization.
I looked into Flask-Principal, but found it overly complicated for my tastes. Haven't checked Flask-Security, but I believe it uses Flask-Principal for authorization. Overall I think that Flask-Login with some custom code is enough most of the time.

Best way to make Flask-Login's login_required the default

Like this question: Best way to make Django's login_required the default
I'm using Flask-Login's login_required decorator now. Is there anyway to make it the default behavior in Flask?
I did this in my instruments project. I use the before_request decorator:
#app.before_request
def check_valid_login():
login_valid = 'user' in session # or whatever you use to check valid login
if (request.endpoint and
'static' not in request.endpoint and
not login_valid and
not getattr(app.view_functions[request.endpoint], 'is_public', False) ) :
return render_template('login.html', next=request.endpoint)
and I then created an is_public() decorator for the few places that would need to be accessible without login:
def public_endpoint(function):
function.is_public = True
return function
If you are using blueprints and need to protect an entire blueprint with a login, you can make the entire before_request to require login.
This is what I use for my CMS blueprint:
#cms.before_request
#login_required
def before_request():
if g.user.role != ROLE_ADMIN:
abort(401)
If you need only to check if the user is logged in (and not if the user has privileges) you can simply pass the function
This is a follow up ( bit more pythonic but thats debatable ) to #MalphasWats
already great answer.
Also includes an important security fix suggested by #nonagon.
Explanation of the vulnerability with 'static' in request.endpoint:
Imagine that there is route which can be user defiened in some way, like a profile link for example.
If the user sets his name lets say Static Joe, then:
"Static Joe" --slugifys--> /usr/profiles/static_joe.
This way making this route public. This is just asking for trouble.
Here is the route guard function which is appened before every request handling:
#app.before_request
def check_route_access():
if any([request.endpoint.startswith('static/'),
current_user.is_authenticated, # From Flask-Login
getattr(app.view_functions[request.endpoint],'is_public',False)]):
return # Access granted
else:
return redirect(url_for('users.login_page'))
( Flask-Login is an excellent module and makes session handling a breeze )
And here is the decorator ( #public_route ) which you can use to allow access to special pages that need public access by default. (register page, login page):
def public_route(decorated_function):
decorated_function.is_public = True
return decorated_function
I had to secure a REST API and I have solved finally like this:
#app.before_request
#auth.login_required
def login_required_for_all_request():
pass
(Actually I used also the connexion framework so I had to use: #app.app.before_request )

Categories