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.
By default, access tokens are valid for 60 days and programmatic refresh tokens are valid for a year. The member must reauthorize your application when refresh tokens expire.
Step 1 − First, the client authenticates with the authorization server by giving the authorization grant. Step 2 − Next, the authorization server authenticates the client, validates the authorization grant and issues the access token and refresh token to the client, if valid.
Use the code you get after a user authorizes your app to get an access token and refresh token. The access token will be used to authenticate requests that your app makes. Access tokens are short lived, so you can use the refresh token to get a new access token when the current access token expires.
To use the refresh token, make a POST request to the service's token endpoint with grant_type=refresh_token , and include the refresh token as well as the client credentials if required.
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.
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?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With