Reputation: 14041
I believe this is mostly a question about best practices.
I have an OAUTH2 provider that issues access tokens (valid for 10 hours) as long as refresh tokens.
I found here that it is pretty easy to refresh the access token but I cannot understand how to decide when it is time to refresh.
The easy answer is probably "when it does not work any more", meaning when I get a HTTP 401 from the backend. The problem with this solution is that it is not that efficient, plus I can only assume I got a 401 because the token has expired.
I my django app I found that the user social auth
has an Extra data
field containing something like this:
{
"scope": "read write",
"expires": 36000,
"refresh_token": "xxxxxxxxxxxxx",
"access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"token_type": "Bearer"
}
but I am not sure how to use the expires
field.
So my question is: how do I know if an access token has expired and I need to refresh it?
EDIT: I just found this comment that seems relevant, but I cannot understand how to plug this new function in the pipeline in order to work during the token refresh.
Upvotes: 10
Views: 3548
Reputation: 41
Currently, the extra_data
field now has an auth_time
. You can use this along with expires
to determine the validity of the access_token
as such:
if (social.extra_data['auth_time'] + social.extra_data['expires'] - 10) <= int(time.time()):
from social_django.utils import load_strategy
strategy = load_strategy()
social.refresh_token(strategy)
The extra "10" seconds is in there to prevent a race condition where an access_token
might expire before further code is executed.
More detail is given in this question: How can I refresh the token with social-auth-app-django?
Upvotes: 2
Reputation: 14041
I eventually figured this out. The reason I was initially confused was because there are actually two cases:
refresh_token
To solve the first case
I created a new function for the pipeline:
def set_last_update(details, *args, **kwargs): # pylint: disable=unused-argument
"""
Pipeline function to add extra information about when the social auth
profile has been updated.
Args:
details (dict): dictionary of informations about the user
Returns:
dict: updated details dictionary
"""
details['updated_at'] = datetime.utcnow().timestamp()
return details
in the settings I added it in the pipeline right before the load_extra_data
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'social.pipeline.user.create_user',
'social.pipeline.social_auth.associate_user',
# the following custom pipeline func goes before load_extra_data
'backends.pipeline_api.set_last_update',
'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details',
'backends.pipeline_api.update_profile_from_edx',
'backends.pipeline_api.update_from_linkedin',
)
and, still in the settings I added the new field in the extra data.
SOCIAL_AUTH_EDXORG_EXTRA_DATA = ['updated_at']
For the second case:
I overwrote the refresh_token
method of my backend to add the extra field.
def refresh_token(self, token, *args, **kwargs):
"""
Overridden method to add extra info during refresh token.
Args:
token (str): valid refresh token
Returns:
dict of information about the user
"""
response = super(EdxOrgOAuth2, self).refresh_token(token, *args, **kwargs)
response['updated_at'] = datetime.utcnow().timestamp()
return response
Still in the backend class, I added an extra field to extract the expires_in
field coming from the server.
EXTRA_DATA = [
('refresh_token', 'refresh_token', True),
('expires_in', 'expires_in'),
('token_type', 'token_type', True),
('scope', 'scope'),
]
At this point I have the timestamp when the access token has been created (updated_at
) and the amount of seconds it will be valid (expires_in
).
NOTE: the updated_at
is an approximation, because it is created on the client and not on the provider server.
Now the only thing missing is a function to check if it is time to refresh the access token.
def _send_refresh_request(user_social):
"""
Private function that refresh an user access token
"""
strategy = load_strategy()
try:
user_social.refresh_token(strategy)
except HTTPError as exc:
if exc.response.status_code in (400, 401,):
raise InvalidCredentialStored(
message='Received a {} status code from the OAUTH server'.format(
exc.response.status_code),
http_status_code=exc.response.status_code
)
raise
def refresh_user_token(user_social):
"""
Utility function to refresh the access token if is (almost) expired
Args:
user_social (UserSocialAuth): a user social auth instance
"""
try:
last_update = datetime.fromtimestamp(user_social.extra_data.get('updated_at'))
expires_in = timedelta(seconds=user_social.extra_data.get('expires_in'))
except TypeError:
_send_refresh_request(user_social)
return
# small error margin of 5 minutes to be safe
error_margin = timedelta(minutes=5)
if datetime.utcnow() - last_update >= expires_in - error_margin:
_send_refresh_request(user_social)
I hope this can be helpful for other people.
Upvotes: 4