Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes #156 -- provide token refresh endpoint #158

Closed
wants to merge 1 commit into from
Closed

fixes #156 -- provide token refresh endpoint #158

wants to merge 1 commit into from

Conversation

sphrak
Copy link
Collaborator

@sphrak sphrak commented Jan 14, 2019

Hi,
First draft of whats discussed in #156, I ended up going for a simpler approach than integrating drf throttling due to the points laid out by @johnraz. However as suggested by #157 we should prolly take a look at that aswell since at least I would expect drf throttling to also apply to our views. (different issue tho).

As discussed these endpoints are optional, and they are not exposed by default unless TOKEN_TTL and AUTO_REFRESH are set. Where AUTO_REFRESH is False by default, thus off.

Let me know what you think

knox/views.py Outdated Show resolved Hide resolved
knox/views.py Show resolved Hide resolved
knox/urls.py Outdated
name='knox_refresh_all'
),
]
if knox_settings.TOKEN_TTL and knox_settings.AUTO_REFRESH:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check if DISABLE_REFRESH_ENDPOINT is set to true?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my comments above answers this question and my opinion on this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm I feel there is no real gain of this flexible url setup, if I miss something let me know. We make our lifes as devs harder in terms of testing etc but do we decrease security or anything if we have this url "avaiable" despite not allowing refreshes? The view could simply return a 401.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@belugame I agree, lets do that instead. Lets return 403 as per:

This status is similar to 401, but in this case, re-authenticating will make no difference. The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with the above + 403 👍

Copy link
Collaborator

@johnraz johnraz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, massive work here 👍
I do have a few comments regarding the design but nothing major I believe.
Also I skipped reviewing the tests for now as I wanted to focus on the design first.

docs/urls.md Outdated Show resolved Hide resolved
docs/urls.md Outdated Show resolved Hide resolved
docs/views.md Outdated
Optional view for refreshing the expiration date of a single token.
It accepts an empty `POST` request against `/refresh` to increase the `expiry` field of the
token by the amount set in `TOKEN_TTL`.
This endpoint is only exposed if `AUTO_REFRESH` and `TOKEN_TTL` are enabled.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that the endpoint would still be useful if one want's to disable AUTO_REFRESH but still want to be able to manually refresh a token.

I see the point of not enabling the endpoint if TOKEN_TTL is None as no token would ever expire.
So I would rephrase this a bit and make it clear that we clearly specify that it's only disabled when TOKEN_TTL is None.

Also see my comment below about this feature.

docs/views.md Outdated
token by the amount set in `TOKEN_TTL`.
This endpoint is only exposed if `AUTO_REFRESH` and `TOKEN_TTL` are enabled.

The functionality can be disabled entirely by setting `DISABLE_REFRESH_ENDPOINT=True` in `REST_KNOX` settings.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could get rid of the logic explained above by just having a ENABLE_REFRESH_ENDPOINT setting.
It would be easier to explain and would also make the code simpler.

The explanation would boil down to:

If you want the refresh endpoint, just opt-in by setting ENABLE_REFRESH_ENDPOINT to True.

Simple, straight, obvious, no magic.

knox/urls.py Outdated
name='knox_refresh_all'
),
]
if knox_settings.TOKEN_TTL and knox_settings.AUTO_REFRESH:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my comments above answers this question and my opinion on this.

knox/views.py Outdated
permission_classes = (IsAuthenticated,)

def post(self, request, format=None):
return Response(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do an explicit update on the expiry here:

  • If we don't, in the context of my previous comments, it will not work for people with AUTO_REFRESH=False and wanting to manually refresh.

  • this view should bypass MIN_REFRESH_INTERVAL as it is an explicit request for a refresh and not an implicit one triggered by AUTO_REFRESH.

Ideally, we should find a way to disable the behavior of AUTO_REFRESH in the authentication class when hitting this endpoint.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnraz I agree with you, ill go back to the drawing board a bit to try to make this more explicity and flexible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnraz I am not sure how to circumvent the auto refresh in auth without making it messy. I mean we could explicitly do it in the refresh view if AUTO_REFRESH is not set, but if we do that it will not bypass MIN_REFRESH_INTERVAL if the user happen to be using AUTO_REFRESH also.. which is kinda unexpected behaivor if you ask me..

Copy link
Collaborator

@johnraz johnraz Jan 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the auto-refresh flow today is:

  • hit a view with a token --> if MIN_REFRESH_INTERVAL allows it, update expiry and hit the DB with a write.
  • hit a view with a token --> if MIN_REFRESH_INTERVAL denies it, don't update expiry and don't write to the DB.

What we'd ideally want for the refresh view (whether or not auto-refresh is active) is:

  • hit the refresh view with a token --> refresh the token in every case by doing a single write to the DB

If auto-refresh is off, there is no issue.

If auto-refresh is on, hitting the refresh view will do this:

  • if MIN_REFRESH_INTERVAL is expired, the auto-refresh will update the expiry and write to the DB at authentication time, next the view will do it again, resulting in 2 writes for nothing.
  • if MIN_REFRESH_INTERVAL is not expired, only the view will write to the DB, 1 write everybody is happy.

The problem we need to solve is to avoid to have 2 consecutive useless writes to the DB...
Sadly I don't think we have access to the view or anything useful to identify the refresh view in the authenticate method (where AUTO_REFRESH happens)...

I would say, let the 2 DB writes be and add a note in the doc that if you use AUTO_REFRESH and the refresh view together, a double write could happen.

Copy link
Collaborator

@johnraz johnraz Jan 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking the above statement back.
The view can be accessed in the authenticate method by doing:

request.parser_context['view']

This means we could simply add a property to the class like:

class RefreshView():
    AUTO_REFRESH=False

And then check for that in the authenticate / authenticate_credentials method of the TokenAuthentication to disable auto-refresh "per view"

We can then advertise that as a sub-feature of the auto-refresh feature in the doc, if you want to enable/disable on a per view basis, use the property.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnraz, wonderful research, thank you. I have been experimenting with this approach, but it seems to be not as trivial as it initially seemed.

If we reference AUTO_REFRESH value in the authenticate/_credentials() methods it needs to be accessible on every view which kinda sucks because then we need to add it to each view or do some weird complex thing of trying to validate who or which part of the code is calling us -- which I dont think we should do since this is a library.

I dont like that so maybe we should make a KnoxAPIView class that extends from APIView with the only difference this attribute AUTO_REFRESH is added as a field attribute like auto_refresh or so.

But I am not sure if its a good approach, though I would like to be able to do away with things like if knox_settings.TOKEN_TTL and only have if token_ttl or auto_refresh directly as an attribute in the class. Maybe im overthinking, but thats the current status. I will ponder this some more.

Copy link
Collaborator

@johnraz johnraz Jan 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we reference AUTO_REFRESH value in the authenticate/_credentials() methods it needs to be accessible on every view

I think we could solve this with something like:

view = request.parser_context['view']
if getattr(view, AUTO_REFRESH, knox_settings.AUTO_REFRESH):
    self.renew_token(auth_token)

instead of the current:
https://github.com/James1345/django-rest-knox/blob/c1972fcddf86179680522d6af1b463b02c49b00c/knox/auth.py#L77-L78

That way the order of precedence would be:
Class attribute's value first and next settings value, which is what we want.

edit: I fixed the code sample above.

Copy link
Collaborator Author

@sphrak sphrak Jan 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a very nice solution, I like it. This PR does add some complexity to testing, I think I will have to mock some parts which tests the TokenAuthentication class directly because in that context there wont be any view. Is it okay if I mock it? no tests currently employs mocking.

edit: turns out its not needed, nvm the mocking stuff.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! Glad you made it work 👍

knox/views.py Outdated Show resolved Hide resolved
knox/views.py Show resolved Hide resolved
@johnraz
Copy link
Collaborator

johnraz commented Jan 14, 2019

Oh and regarding throttling, I think it's a picky subject.
I like the idea of making it easier on people to enable it but at the same time it can be easily broken.
eg: by a bad proxy configuration.
We need to make sure we think it through and document it properly, I suggest we get to the bottom of #157 and then move on and decide based on the outcome.

Copy link
Collaborator Author

@sphrak sphrak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnraz I have addressed the suggestions you made and it turned out pretty neat I think. There is still a test missing for proving that there are in fact no double db writes but I am working on it. Other than that I think its mostly ready.

Cheers

knox/auth.py Outdated
@@ -37,6 +37,8 @@ class TokenAuthentication(BaseAuthentication):
model = AuthToken

def authenticate(self, request):

view = getattr(request, 'parser_context', None)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We either do this, or mock -- because otherwise tests will fail when we test the TokenAuthentication directly where there is no actual view calling it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm against making exception in the library code to make the tests happy.
A non complete request passed here should raise and not be swallowed.

I don't believe you'd need mock here, changing the way we do the request should be enough.

Also shouldn't this be:

view = getattr(request, 'parser_context', None)["view"]

so without the workaround it should be:
view = request.parser_context["view"]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instructions a tad unclear, would you like me to rewrite those tests?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the tests shouldn't be failing yes and I guess this is because we use request factory with too few arguments.

knox/views.py Outdated Show resolved Hide resolved
Copy link
Collaborator

@johnraz johnraz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are getting there, I still have a few comments though. Keep it up 👍

docs/settings.md Outdated Show resolved Hide resolved
docs/views.md Outdated Show resolved Hide resolved
knox/auth.py Outdated
@@ -37,6 +37,8 @@ class TokenAuthentication(BaseAuthentication):
model = AuthToken

def authenticate(self, request):

view = getattr(request, 'parser_context', None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm against making exception in the library code to make the tests happy.
A non complete request passed here should raise and not be swallowed.

I don't believe you'd need mock here, changing the way we do the request should be enough.

Also shouldn't this be:

view = getattr(request, 'parser_context', None)["view"]

so without the workaround it should be:
view = request.parser_context["view"]

knox/auth.py Outdated Show resolved Hide resolved
url(r'logoutall/', views.LogoutAllView.as_view(), name='knox_logoutall'),
url(r'logout/$', views.LogoutView.as_view(), name='knox_logout'),
url(r'logout/all/$', views.LogoutAllView.as_view(), name='knox_logout_all'),
url(r'refresh/$', views.TokenRefreshView.as_view(), name='knox_refresh')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the refresh... I would like token/refresh better I think.
@belugame thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm for me depends on the outcome of our discussion :) creating a new one for me would not be a "refresh". If we keep the same token I would prefer the verb "extend". "extend" I feel would fit nicely in with the others logout, login

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense 👍

if knox_settings.TOKEN_TTL is None:
raise ValueError(
'Value of \'TOKEN_TTL\' cannot be \'None\' in this context.'
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will trigger a 500, I think we should make a note about the error code in the doc.

Also I would rather make this check at application loading time so it blows when you try to run the server and not when someone tries to hit the view... I'm not sure how to achieve that easily.

I'm not going to block if it stays as it is with a note in the doc though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error is documented here: https://github.com/James1345/django-rest-knox/pull/158/files#diff-1819b1daaccb3d358620ade9c67e9118R66

But I will add a note saying it will cause a HTTP 500 if improperly configured. But I agree, I'd rather have it fail immediatly at runtime rather than when a user tries to refresh, but am unaware of how.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me, we can investigate settings checking at runtime later on.

tests/tests.py Outdated Show resolved Hide resolved
tests/tests.py Show resolved Hide resolved
tests/tests.py Outdated Show resolved Hide resolved
tests/tests.py Outdated Show resolved Hide resolved
@johnraz
Copy link
Collaborator

johnraz commented Jan 18, 2019

@sphrak to fix the current conflict, just remove docs/changes 😉

@belugame
Copy link
Collaborator

@sphrak @johnraz Vetos against releasing current master? I want to get your work out of the door, as things seem to be stuck at the moment. We will never run out of version numbers :)

@sphrak
Copy link
Collaborator Author

sphrak commented Mar 27, 2019

@belugame I say go for it, release it. I unfortunatly won't have time for a couple more months to work on the stuck #156 issue right now. I will make a comment at least if anyone else want to work on it in the meantime.

@johnraz
Copy link
Collaborator

johnraz commented Mar 27, 2019

Go ahead yes 👍 😊

@belugame
Copy link
Collaborator

4.0.0 is on pypi
https://pypi.org/project/django-rest-knox/4.0.0/

@sphrak sphrak closed this Dec 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants