Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Decide when to refresh OAUTH2 token with Python Social Auth

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.

like image 513
Giovanni Di Milia Avatar asked May 09 '16 17:05

Giovanni Di Milia


People also ask

Do refresh tokens expire OAuth?

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.

How oauth2 refresh token works?

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.

How do I get a new refresh token oauth2?

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.

How do you refresh OAuth?

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.


2 Answers

I eventually figured this out. The reason I was initially confused was because there are actually two cases:

  1. When the user comes from a login, so when basically the pipeline get executed.
  2. When the token is refreshed calling the user social auth method 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.

like image 195
Giovanni Di Milia Avatar answered Oct 15 '22 04:10

Giovanni Di Milia


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?

like image 31
SCasey Avatar answered Oct 15 '22 06:10

SCasey