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

re #270 - Added middleware to refresh access tokens #278

Closed
wants to merge 2 commits into from

Conversation

daggaz
Copy link

@daggaz daggaz commented Feb 28, 2023

This PR addresses #270

@daggaz daggaz changed the title re#270 - Added middleware to refresh access tokens re #270 - Added middleware to refresh access tokens Feb 28, 2023
@codecov
Copy link

codecov bot commented Feb 28, 2023

Codecov Report

Merging #278 (9abbcbd) into master (80adc32) will increase coverage by 0.7%.
The diff coverage is 95.4%.

@@           Coverage Diff            @@
##           master    #278     +/-   ##
========================================
+ Coverage    86.3%   87.1%   +0.7%     
========================================
  Files          11      11             
  Lines         549     590     +41     
========================================
+ Hits          474     514     +40     
- Misses         75      76      +1     
Impacted Files Coverage Δ
django_auth_adfs/backend.py 87.1% <93.3%> (+0.7%) ⬆️
django_auth_adfs/config.py 88.0% <100.0%> (+<0.1%) ⬆️
django_auth_adfs/middleware.py 94.2% <100.0%> (+3.3%) ⬆️
django_auth_adfs/views.py 87.2% <0.0%> (+2.1%) ⬆️

@daggaz daggaz force-pushed the refresh-tokens branch 3 times, most recently from 1922fff to 412bec1 Compare March 1, 2023 11:47
Copy link
Member

@tim-schilling tim-schilling left a comment

Choose a reason for hiding this comment

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

Thanks for creating this PR! I like the idea of us supporting this out of the box. It seems relatively straightforward and a common use case. My preference is to have it be entirely optional so as to not break existing user's installations when upgrading. I could be convinced otherwise though.

Comment on lines +400 to +402
if request is None:
logger.debug("Authentication backend was called without request")
return
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't short-circuit this functionality if the request isn't provided. django_auth_adfs.rest_framework.AdfsAccessTokenAuthentication.authenticate() will call the authenticate backend without a request.

Copy link
Author

@daggaz daggaz Mar 15, 2023

Choose a reason for hiding this comment

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

This code is in AdfsAuthCodeBackend, not the base class.

In AdfsAuthCodeBackend, request is already required to not be None. I've just made it explicit.

Before these changes, If you attempt to use AdfsAuthCodeBackend without a request, you will get an exception when it tries to generate redirect_uri in exchange_auth_code().

On a separate point, I actually think exchange_auth_code() should be pulled up from the base class to AdfsAuthCodeBackend, as it's only called from there.

Copy link
Member

Choose a reason for hiding this comment

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

Agree

Comment on lines +421 to +428
def process_request(self, request):
now = datetime.now() + settings.REFRESH_THRESHOLD
expiry = datetime.fromisoformat(request.session['_adfs_token_expiry'])
if now > expiry:
try:
self._refresh_access_token(request, request.session['_adfs_refresh_token'])
except (PermissionDenied, HTTPError):
logout(request)
Copy link
Member

Choose a reason for hiding this comment

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

This logic should live in a middleware, not the authenticate backend.

Copy link
Author

Choose a reason for hiding this comment

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

I went back and forward on this myself a few times, moving the functionality between the middleware and the backend.

In the end I opted for the backend because then the session keys (such as _adfs_refresh_token) only appeared in one place rather than being spread through multiple files.

I'm open to moving it back to the middleware though.

Copy link
Member

Choose a reason for hiding this comment

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

Agree with @tim-schilling 😊

return user

def _process_adfs_response(self, request, adfs_response):
user = self.process_access_token(adfs_response['access_token'], adfs_response)
request.session['_adfs_access_token'] = adfs_response['access_token']
Copy link
Member

Choose a reason for hiding this comment

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

I don't see this session value being used anywhere. I think it can be removed.

Copy link
Author

Choose a reason for hiding this comment

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

It's not used, however in my use case I do not use the auto-generated claim to group mapping, and instead directly use the data in the access token.

ADFS can be configured to spit out many different things in the access token.

I'm sure there are many other use cases where having access the to original JWT would be valuable.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @tim-schilling here, I don't really want to just add the token to the session "for the sake of it".

user = self.process_access_token(adfs_response['access_token'], adfs_response)
request.session['_adfs_access_token'] = adfs_response['access_token']
expiry = datetime.now() + timedelta(seconds=adfs_response['expires_in'])
request.session['_adfs_token_expiry'] = expiry.isoformat()
Copy link
Member

Choose a reason for hiding this comment

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

This creates a dependency on django.contrib.session. I'm up in the air if we want to continue supporting that path or not.

We should add a system check to verify that the session app is installed to alert the developer sooner about a misconfiguration.

Copy link
Author

Choose a reason for hiding this comment

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

The base django.contrib.auth app requires django.contrib.session already. It's not an optional dependency.

if now > expiry:
try:
self._refresh_access_token(request, request.session['_adfs_refresh_token'])
except (PermissionDenied, HTTPError):
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about using logger.debug() here to capture the exception's information?

Copy link
Author

Choose a reason for hiding this comment

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

I agree

@JonasKs
Copy link
Member

JonasKs commented Mar 15, 2023

I'll try to review ASAP, but I'm pretty busy these days. If we're afraid of breaking, we'll have to major bump - I don't mind that 😊

@JonasKs
Copy link
Member

JonasKs commented Mar 30, 2023

Thanks for the good PR! And for the good review, @tim-schilling 😊 I'd love to get this merged this or next week.

@tim-schilling
Copy link
Member

Closed in favor of #343

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.

None yet

3 participants