Flask-Middleware / flask-security

Quick and simple security for Flask applications
MIT License
624 stars 155 forks source link

Private Access Tokens #922

Closed sr-verde closed 4 months ago

sr-verde commented 5 months ago

Hey,

I have been thinking about a problem for some time and am hoping for some help. In addition, I may also have a related feature request (therefore here as an issue, otherwise please move to the Discussions).

I have a website running with Flask Security. I’ll break my scenario down to a minimal example. So let's assume the following: I have a webite with a view for listing all my products, to get details for one product, and to delete a product. As a user, you can view all of these as normal webpages. Here is an example code for the listing:

@auth_required("session")
@product_bp.route("/", methods=["GET"])
def get_all():
    query = db.select(Product)
    query = db.session.scalars(query).all()

    return render_template(
        "product/list.html", products=query,
    )

I am now able to extend my app to act as a REST API (additionally).

@auth_required("session", "token")
@product_bp.route("/", methods=["GET"])
def get_all():
    query = db.select(Product)
    products = db.session.scalars(query).all()

    if want_json(request):
        return jsonify(products)

    return render_template(
        "product/list.html", products=products,
    )

My problem now is: I don't want to use this REST API for a VueJS appliaction or alike. I want to request the API using Private Access Tokens (for e.g. machine-to-machine communication or advanced users, which love curl). I can easily create auth tokens in my app using current_user.get_auth_token().

But sadly, I cannot specify any expiry date: They all have TOKEN_MAX_AGE since the tokens are not JWTs or alike but just the signed fs_token_uniquifier and the validity is hardcoded in the token validation but not encoded within the key.

My ideas:

  1. One option would be to add another or extend the existing auth mechanisms to include an expiry date within the token.
  2. Another option would be to allow a callable for TOKEN_MAX_AGE. The callable would be called with the token. My function could then return an individual expiry date for each token.

But I don't see how any of this can happen without changing Flask Security itself. I look forward to hearing your thoughts and am willing to contribute the necessary code.

jwag956 commented 5 months ago

Perfect timing - I am just starting a feature that allows embedding a session ID (probably using Flask-Session) in the authtoken - this would enable using the JSON API from applications like mobile that don't easily support cookies.

Adding an expiry time makes sense - doing a callable is easy - but it isn't that nice since at the time the token is de-serialized (and expiry checked), the user isn't yet known - so that would require new code just to check expiry (not difficult, just not very pretty).

Alternatively I have been thinking about just improving the authtoken format - include a version which would make it easier to add to - adding an expiry time to the authtoken would be pretty easy (much simpler than converting to JWT).

Another thought I have is to move to a JWT FORMAT (not pure client-side JWT) - there is good support in pyjwt for signing etc - it has expiry and other things built in. I am NOT suggesting moving away from session/server based authentication/authorization - just using JWT as a standard encoding mechanism.

I am hoping to start prototyping in the next few weeks and make this part of 5.4..

I will note that generate_auth_token() and validate_auth_token() are part of the UserMixin - which means you could simply create your own implementation and include an expiry time (at least for prototyping).

Thanks for the use case/note....

sr-verde commented 4 months ago

Yes, I totally agree not to use client-side JWTs.

I will note that generate_auth_token() and validate_auth_token() are part of the UserMixin - which means you could simply create your own implementation and include an expiry time (at least for prototyping).

Yeah, I forgot about that. Thanks. That helps! :+1:

jwag956 commented 4 months ago

I have been working on this - a question - why is MAX_AGE not sufficient? do you need different expiry for different users? or is this a 'how to refresh' issue - i.e. if it expires, how will the user get another one... This has come up numerous times in the past and I have some new thoughts on that as well...

Also - just re-reading your iniitial comment - using JSON is of course completely independent of how the user authenticates -session or auth_token.

sr-verde commented 4 months ago

I need different expiry for different auth tokens. Just like Private Access Tokens in Gitlab or Github. It is not a "how to refresh issue". Before a token expires, the user can login (via HTML) and click a new one.

Also - just re-reading your iniitial comment - using JSON is of course completely independent of how the user authenticates -session or auth_token.

Yes, of course. But I want to use it for machine-to-machine communication. So I want tokens with a short lifetime. But a user should set the lifetime to his or her needs. Additional, I want to be able to delete already generated tokens (invalidate them). I already implemented that for me (that needs a database lookup). But I don't think that is a feature you want for Flask Security?

jwag956 commented 4 months ago

Thanks for your answers - please look at #927 - might help. Besides per-token expire times - the ability to extend and verify has been simplified so that you don't have to re-create the entire token logic. For example in the augment_auth_token() routine you could add a field that a DB lookup key and in verify_auth_token you could look that up and see if it has been 'revoked'...

I am going to close this - feel free to re-open if there are other features you might need/want.

sr-verde commented 4 months ago

This is so nice of you and so fast. I’ll update my code. Thanks.