Using Flask-Security Roles with Flask-JWT REST API
Asked Answered
C

1

29

I am building a Flask-based REST API and using Flask-JWT to handle JWT auth. I also want to use the built in roles management with Flask-Security. However, Flask-Security's @roles_required() decorator assumes I am showing a Flask view when it fails.

Here is my token endpoint (which is working as I want):

$ http POST localhost:5000/auth/token username='test' password='test'
HTTP/1.0 200 OK
Content-Length: 192
Content-Type: application/json
Date: Sun, 08 Nov 2015 17:45:46 GMT
Server: Werkzeug/0.10.4 Python/3.5.0

{
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NDcwMDQ3NDYsIm5iZiI6MTQ0NzAwNDc0NiwiZXhwIjoxNDQ3MDA1MDQ2LCJpZGVudGl0eSI6MX0.RFIeaLuvJNM9fDjFYFQ7sh_WaDVU-_aM7e46tVJzlBQ"
}

Here is a successful response to a resource that does not have any role requirement (using only @jwt_required) This is also working as I want:

$http GET localhost:5000/protected Authorization:'JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NDcwMDQ3NDYsIm5iZiI6MTQ0NzAwNDc0NiwiZXhwIjoxNDQ3MDA1MDQ2LCJpZGVudGl0eSI6MX0.RFIeaLuvJNM9fDjFYFQ7sh_WaDVU-_aM7e46tVJzlBQ'
HTTP/1.0 200 OK
Content-Length: 25
Content-Type: text/html; charset=utf-8
Date: Sun, 08 Nov 2015 17:46:24 GMT
Server: Werkzeug/0.10.4 Python/3.5.0

<models.User[email=test]>

When I do the same for a resource that has roles required (such as admin in this example), it seems to assume I have a page to display such as /login which I do not since it is a headless REST API:

$ http GET localhost:5000/admin Authorization:'JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NDcwMDQ3NDYsIm5iZiI6MTQ0NzAwNDc0NiwiZXhwIjoxNDQ3MDA1MDQ2LCJpZGVudGl0eSI6MX0.RFIeaLuvJNM9fDjFYFQ7sh_WaDVU-_aM7e46tVJzlBQ'
HTTP/1.0 302 FOUND
Content-Length: 209
Content-Type: text/html; charset=utf-8
Date: Sun, 08 Nov 2015 17:46:43 GMT
Location: http://localhost:5000/
Server: Werkzeug/0.10.4 Python/3.5.0
Set-Cookie: session=eyJfZmxhc2hlcyI6W3siIHQiOlsiZXJyb3IiLCJZb3UgZG8gbm90IGhhdmUgcGVybWlzc2lvbiB0byB2aWV3IHRoaXMgcmVzb3VyY2UuIl19XX0.CSEcAw.pjwXLeSWUsORXR-OU5AfFvq6ESg; HttpOnly; Path=/

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="/">/</a>.  If not click the link.

I know Flask-Security uses Flask-Principal behind the scene for its roles management (@roles_required, etc.) and it ties into the RoleMixin and UserMixin for the datastore which is very nice. However, if there is no way to get Flask-Security to just allow the resource through without using my JWT header, then maybe the best bet is to build my own decorators which uses Flask-Principal to manage the roles.

Does anyone have any experience with this? The idea is that the entire front end can and will be built in whatever language we need and that means it may not be Flask's templates/views which is what Flask-Security appears to be doing.

Thank you for any insight anyone can provide!

Corset answered 8/11, 2015 at 17:58 Comment(2)
Did you ever get anywhere with this?Riedel
@MikeDavlantes we ended up not getting a chance to do this as I had to move on to a new company, apologies :/Corset
W
8

Instead of a redirect, you would want to respond with an HTTP status code 403.

Your best bet is indeed to create your own decorator to manage the roles, and move away from using Flask-Security entirely.

The author of Flask-Security has mentioned that there are better ways to secure APIs, and it makes even more sense as the library is not maintained.

Flask-JWT or Flask-JWT-Extended are perfect candidates for this task. The former would require a bit more boilerplate to get things going. There is a stale PR suggesting an API to support roles, that you could use to create your own decorator if you decide to go with Flask-JWT.

The Flask-JWT-Extended docs suggest a simpler solution that might fit your case. You should follow the custom decorators section of the documentation for the full example, but here's the decorator in the nutshell:

from functools import wraps

from flask import jsonify
from flask_jwt_extended import (
    verify_jwt_in_request, get_jwt_claims
)

def admin_required(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        verify_jwt_in_request()
        claims = get_jwt_claims()
        if claims['roles'] != 'admin':
            return jsonify(msg='Admins only!'), 403
        else:
            return fn(*args, **kwargs)
    return wrapper

This code looks for a roles claim in the JWT, and returns a 403 response if it is not admin.

Wille answered 7/7, 2020 at 10:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.