I finally figured it out. The reason I get confused at first is because there are actually two cases:
- When the user comes from the entrance, therefore, when the pipeline is mostly executed.
- When the token is updated by calling
refresh_token social auth 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 to the pipeline right before 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 a new field to the additional data.
SOCIAL_AUTH_EDXORG_EXTRA_DATA = ['updated_at']
For the second case:
I refresh_token method of my backend to add an 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 base class, I added an extra field to retrieve 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 a timestamp when the access token was created ( updated_at ) and the number of seconds it will be valid ( expires_in ).
NOTE. updated_at is approximate since 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 update 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 useful for other people.