I'm using flask security to authenticate users. I've made sure the authentication works properly with the http_auth_required decorator - the user is being validated against the userstore (an SQLAlchemyUserDatastore in my case), and all is well.
I would like now to use my own authentication method (I'll be using a custom LDAP validation system), while still taking advantage of the things Flask-Security is giving me (things like current_user). I wrote a custom decorator that looks like this:
def authenticate_with_ldap(func):
#wraps(func)
def wrapper(*args, **kwargs):
if not request.authorization:
return unauthorized_user_handler()
user = user_datastore.get_user(request.authorization.username)
if not user or not authenticate_with_ldap(user.email, user.password):
return unauthorized_user_handler()
return func(*args, **kwargs)
return wrapper
However, when I look at the http_auth_required decorator I see that it uses a private function called _check_http_auth that is doing some stuff that I can't do on my own without accessing private members, like setting the user to the top of the request context stack and sending signals. The code looks like this:
def _check_http_auth():
auth = request.authorization or BasicAuth(username=None, password=None)
user = _security.datastore.find_user(email=auth.username)
if user and utils.verify_and_update_password(auth.password, user):
_security.datastore.commit()
app = current_app._get_current_object()
_request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id))
return True
return False
So my question is: what is the correct way to have a custom authentication method, while still utilizing Flask-Security to its fullest?
You can accomplish this with a quick monkey patch. Not ideal, but I'm not sure what else you can do until the Flask-Security team writes in a more elegant way to handle this.
import flask_security
def verify_and_update_password_custom(password, user):
return user.verify_password(password)
flask_security.forms.verify_and_update_password = verify_and_update_password_custom
I'm not sure if it is used anywhere else. The above works for my own purposes. If it does get called elsewhere, you would just need to monkeypatch it into place wherever that is.
Related
In Django Rest framework, we can verify permissions such as (isAuthenticated, isAdminUser...)
but how can we add our custom permissions and decide what django can do with those permissions?
I really want to understand what happens behind (I didn't find a documentation that explaint this):
#permission_classes([IsAdminUser])
Thank you
Write your own permissions, like this:
def permission_valid_token(func):
# first arg is the viewset, second is the request
def wrapper(*args, **kwargs):
valid_token, user_token = test_token_authorization(args[1].headers) # my function to validate the user
if not valid_token:
return Response(status=status.HTTP_401_UNAUTHORIZED)
return func(*args, **kwargs)
return wrapper
This is a permission that i'm using in app, probably you will have to change the valid_token part
And them you import in your views
from file_that_you_use.permissions import permission_valid_token
And you use as a decorator
class WeatherViewSet(viewsets.ViewSet):
#permission_valid_token
def list(self, request):
This is just a viewset for example, you can use generic viewsets or whatever you want.
If you are using VSCode, hover over #permission_classes([IsAdminUser]), click on Command (on your keyboard).
You can see what happens behind the scenes, play and create your custom Django version (not recommended) or you can overwrite the function.
I am using the Django rest framework JSON Web token API that is found here on github (https://github.com/GetBlimp/django-rest-framework-jwt/tree/master/).
I can successfully create tokens and use them to call protected REST APis. However, there are certain cases where I would like to delete a specific token before its expiry time. So I thought to do this with a view like:
class Logout(APIView):
permission_classes = (IsAuthenticated, )
authentication_classes = (JSONWebTokenAuthentication, )
def post(self, request):
# simply delete the token to force a login
request.auth.delete() # This will not work
return Response(status=status.HTTP_200_OK)
The request.auth is simply a string object. So, this is of course, not going to work but I was not sure how I can clear the underlying token.
EDIT
Reading more about this, it seems that I do not need to do anything as nothing is ever stored on the server side with JWT. So just closing the application and regenerating the token on the next login is enough. Is that correct?
The biggest disadvantage of JWT is that because the server does not save the session state, it is not possible to abolish a token or change the token's permissions during use. That is, once the JWT is signed, it will remain in effect until it expires, unless the server deploys additional logic.
So, you cannot invalidate the token even you create a new token or refresh it. Simply way to logout is remove the token from the client.
Yes, it's correct to say that JWT tokens are not stored in the database. What you want, though, is to invalidate a token based on user activity, which doesn't seem to be possible ATM.
So, you can do what you suggested in your question, or redirect the user to some token refreshing endpoint, or even manually create a new token.
Add this in Admin.py
class OutstandingTokenAdmin(token_blacklist.admin.OutstandingTokenAdmin):
def has_delete_permission(self, *args, **kwargs):
return True # or whatever logic you want
admin.site.unregister(token_blacklist.models.OutstandingToken)
admin.site.register(token_blacklist.models.OutstandingToken, OutstandingTokenAdmin)
from rest_framework_simplejwt.token_blacklist.admin import OutstandingTokenAdmin
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
class OutstandingTokenAdmin(OutstandingTokenAdmin):
def has_delete_permission(self, *args, **kwargs):
return True # or whatever logic you want
def get_actions(self, request):
actions = super(OutstandingTokenAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
admin.site.unregister(OutstandingToken)
admin.site.register(OutstandingToken, OutstandingTokenAdmin)
I am currently using code found here:
http://flask.pocoo.org/snippets/8/
And I decorate my function accordingly to have the admin authenticate when requesting a specific admin page. However, instead of requiring the admin to keep authenticating each time they admin page, I noticed that it somehow keeps track of the session and no longer requires authentication after successfully authenticating once. Is there some way to force flask to re-authenticate every time an admin requests the given decorated admin page?
Using the included snippet, there is no good way to force a user to log in every time they request the given page.
This is because that snippet is using HTTP Basic Auth and there is no good way to ask the browser to stop sending that header.
What you are looking for can be done with a custom decorator. You can use the sample below. Note that your case will be different, but you can use this as a guide.
from web import app, has_role
#app.route("/admin/my_page")
#login_required
#has_role(role="admin")
def admin_my_page():
//do stuff
Then, in your project init, or an include file you can add the following:
def has_role(role=None):
def _initial_decorator(view_func):
def _decorator(*args, **kwargs):
response = view_func(*args, **kwargs)
if g.user.user_level != role:
from flask import redirect, url_for
return redirect(url_for("no_access"))
return response
return wraps(view_func)(_decorator)
return _initial_decorator
This should at lease give you an idea of how to create a custom decorator, and then check for role permissions. You can expand this to however you need. You can put engine logic, or other checks to fit your project.
I wrote this, which seems to work just fine:
#app.route('/admin', methods=['GET','POST'])
#login_required
def admin():
if not current_user.role == ROLE_ADMIN:
flash('You do not have access to view this page.')
return redirect(url_for('index'))
...the rest of my code...
While trying to simplify things since I don't want to add these 3 lines to every area I want only visible to admins, I tried to put it in a function like so:
def admin_only():
if not current_user.role == ROLE_ADMIN:
flash('You do not have access to view this page.')
return redirect(url_for('index'))
and then put in my view function:
#app.route('/admin', methods=['GET','POST'])
#login_required
def admin():
admin_only()
...the rest of my code....
However this doesn't work as I expected. I get the flashed message, but it does not redirect like I thought it would.
So, two questions:
Why doesn't the returned redirect work?
Is there a better way to implement this functionality?
To actually answer your question. You should make the admin_only function a decorator and decorate the admin view method. The reason it does not redirect now is because you are not returning the redirect from the view.
def admin():
ret = admin_only()
if( not ret ):
return ret
....
That should fix your current issue, but it is not ideal and the functionality you wish should be moved to a decorator.
I also recommend the following:
Take a look at Flask-Principal it provides the ability to assign roles to users and then restrict access based on these roles to your views.
Along with Flask-Principal take a look at Flask-Security as it provides many useful security related Flask extensions and just makes it easier to use.
Example use:
#roles_required( "admin" )
def website_control_panel():
return "Only Admin's can see this."
Will ONLY allow users with the role admin attached to their account. Another use case is to allow a user to have one of many roles which can be specified with the roles_accepted and can be used as following:
#roles_accepted( "journalist", "editor" )
def edit_paper():
return render_template( "paper_editor.html", ... )
Will only allow users that have at least one of the journalist or editor roles tied to their account.
I'm using Django, and want to store data that is relevant only for the duration of a request, and not on the session.
Is it correct to add something to request.META, like:
request.META['acl'] = acl
In my situation, I am using Tastypie, with a custom authorization class, and need a way to pass data between functions... it seems like storing something on the request would be the right thing to do... I just don't know where to store such information. My class looks something like:
class MyAuthorization(Authorization):
def is_authorized(self, request, object=None):
acl = getMyAccessControlList(request.method,request.session['username'])
for permission in acl:
if permission in self.permissions[request.method]:
request.META['acl'] = acl
return True
return False
def apply_limits(self, request, object_class, rs):
if 'HAS_ALL_ACCESS' in request.META['acl']:
return rs
else if 'HAS_USER_ACCESS' in request.META['acl']:
rs = rs.filter(object_class.user==request.session['username'])
return rs
Futher, Tastypie creates a single REST resource object, with a single authorization class used by all threads, so it's not thread-safe to just put it on the authorization class.
UPDATE
As per Chris Pratt's feedback, no, it doesn't make sense to modify the request. Exploring further, it appears to be appropriate to modify the request initially through custom middleware, and then keep it constant for the rest of the request: https://docs.djangoproject.com/en/1.4/topics/http/middleware
In this case, the middleware will look something like:
class AccessControlListMiddleware(object):
def process_view(self,request,view_func,view_args,view_kwargs):
permissions = set()
for role in request.session['permissions']:
for permission in PERMISSION_LIST[request.method][role]:
permissions.add(permission)
request.acl = list(permissions)
No. Don't mess with the request object. Especially since these are methods on the same class, you should simply assign data to self:
self.acl = getMyAccessControlList(request.method,request.session['username'])
...
if 'HAS_ALL_ACCESS' in self.acl: