sanic-jwt icon indicating copy to clipboard operation
sanic-jwt copied to clipboard

Logout

Open amor71 opened this issue 5 years ago • 3 comments

I'm aware of the debate on if and how to implement 'logout' functionality for JWT. Two enhancements that come into mind are adding 'delete_refresh_token' to the framework, and creating a skeleton for blacklisting. If there's a decision on an optimal way forward, I could implement and create a pull request if it helps.

amor71 avatar Mar 27 '19 10:03 amor71

While I am not off the bat opposed to the idea, I think it needs to be thought thru some. Inherently because JWTs are stateless, there is nothing to logout from.

I do see the usage in perhaps having an endpoint that would delete the refresh token, but I see this more as a feature that the developer would want to implement as a custom endpoint. Afterall, "logout" could mean a lot of different things for different applications. Your proposed use case probably does warrant an example though on how that would look and be setup. Something like this:

from sanic_jwt import BaseEndpoint
from sanic import responses


class Logout(BaseEndpoint):

    async def post(self, request, *args, **kwargs):
        user_id = request.app.auth.extract_user_id(request)
        key = f'refresh_token_{user_id}'
        await aredis.delete(key)

        response = responses.json({"OK": True})

return response
Initialize(
    ...
    class_views=(("/logout" Logout),),
)

Now... tell me more about the idea you have for blacklisting? Configurable to blacklist on different criteria? Perhaps IP, user_id, etc? Maybe blacklisted for a time period only? What about white listing? An interesting idea. One that might best be served as an example as well and not a core feature.

ahopkins avatar Mar 27 '19 12:03 ahopkins

Thank you, while JWT is server-less, the server is still responsible to issue & sign the token. I would assume that in most cases, the token will be used as authorization to certain services, and not just authentication (there is a bigger discussion on aaa & micro-services but that is off-topic).

The “issuing״ authority has certain responsibility on the life-span including revocation of the token and/or the refresh-token, regardless of the server-less approach.

I would think there are two main use-cases: logout (let's not forget cookies) and an abrupt change in authorization - such as depromotion of user privileges.

In the logout case, I think the framework should provide the option to delete the refresh token, at least as a “hint” or even promote as best-practices - not just as an example.

The blacklist is intended to close the authorization gap between refreshes. I understand there could be way around it using the x5c header but that seems like an overly dramatic solution.

I'm not sure you want to go down the path of IPs and whitelist, my reasoning is simple: nothing lives in a vacuum, including RESTful APIs, and it's better to leave IP and equivalents to other components in the architecture, such as the reverse-proxy.

My suggestion is: i. Include delete-refresh-token as part of if the initialization and core end-points (short term) ii. Augment current design with blacklisting (mid-term)

Sorry for the long response. Really like what you’re doing here!

On Mar 27, 2019, at 1:05 PM, Adam Hopkins [email protected] wrote:

While I am not off the bat opposed to the idea, I think it needs to be thought thru some. Inherently because JWTs are stateless, there is nothing to logout from.

I do see the usage in perhaps having an endpoint that would delete the refresh token, but I see this more as a feature that the developer would want to implement as a custom endpoint. Afterall, "logout" could mean a lot of different things for different applications. Your proposed use case probably does warrant an example though on how that would look and be setup. Something like this:

from sanic_jwt import BaseEndpoint from sanic import responses

class Register(BaseEndpoint):

async def post(self, request, *args, **kwargs):
    user_id = request.app.auth.extract_user_id(request)
    key = f'refresh_token_{user_id}'
    await aredis.delete(key)

    response = responses.json({"OK": True})

return response Initialize( ... class_views=(("/logout" Logout),), ) Now... tell me more about the idea you have for blacklisting? Configurable to blacklist on different criteria? Perhaps IP, user_id, etc? Maybe blacklisted for a time period only? What about white listing? An interesting idea. One that might best be served as an example as well and not a core feature.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or mute the thread.

amor71 avatar Mar 28 '19 11:03 amor71

So, I have been thinking more and more about this. While I certainly do not think this idea would be a good thing to enable by default, I also do not see the harm in adding it since it potentially can make things easier for developers (instead of coming up with their own solutions to this).

I can think of five different ways that this could be achieved.

  1. The simplest solution would be something like what @amor71 suggested: provide an endpoint to delete refresh_token. While this does not per se disable usage of the access_token it does take a step in the direction. It probably requires the most work for the developer to complete the cycle to achieve a true "logout".
  2. Same as the first, but with some sort of a flag (ex Initialize(enable_logout=True,...)) that then makes the system require the refresh_token to be present on each request. If it cannot be validated (because it has been deleted), then no access. This sort of perverts the usage of a refresh_token, but could work.
  3. One thing that could be done now, is create some sort of a CustomClaim that embeds a session id into the payload, and then verifies it on each request. One problem with this is that CustomClaim does not currently support async (doing so requires a lot of change to the API). If this were to be an adopted practice built into Sanic JWT, that would be a must.
  4. Another solution would be to add access_token to a temp in memory store for duration of expiration. Again, this would require something like: Initialize(enable_logout=True,...). After "logging out" (which means adding the token to a blacklist), it is checked on each subsequent request. To prevent a memory leak, we would need to implement some sort of periodic process to clean the store of expired tokens (which wouldn't work anyway).
  5. The last idea would be to operate this thru scopes. The "logout" endpoint would need to set a flag (probably with a handler, potentially with an in memory store) that disable access by way of scope. This one would be best served with the addition of sscopes to replace the scopes module in Sanic JWT (which I am planning to do at some point) since it has scope negation in it. See #103

Thoughts?

ahopkins avatar Jan 02 '20 12:01 ahopkins