How can I refresh the token with social-auth-app-django?

5.8k views Asked by At

I use Python Social Auth - Django to log in my users.

My backend is Microsoft, so I can use Microsoft Graph but I don't think that it is relevant.

Python Social Auth deals with authentication but now I want to call the API and for that, I need a valid access token. Following the use cases I can get to this:

social = request.user.social_auth.get(provider='azuread-oauth2')
response = self.get_json('https://graph.microsoft.com/v1.0/me',
                         headers={'Authorization': social.extra_data['token_type'] + ' ' 
                                                   + social.extra_data['access_token']})

But the access token is only valid for 3600 seconds and so I need to refresh, I guess I can do it manually but there must be a better solution. How can I get an access_token refreshed?

3

There are 3 answers

0
NBajanca On BEST ANSWER

Using load_strategy() at social.apps.django_app.utils:

social = request.user.social_auth.get(provider='azuread-oauth2')
strategy = load_strategy()
social.refresh_token(strategy)

Now the updated access_token can be retrieved from social.extra_data['access_token'].

The best approach is probably to check if it needs to be updated (customized for AzureAD Oauth2):

def get_azuread_oauth2_token(user):
    social = user.social_auth.get(provider='azuread-oauth2')
    if social.extra_data['expires_on'] <= int(time.time()):
        strategy = load_strategy()
        social.refresh_token(strategy)
    return social.extra_data['access_token']

This is based on the method get_auth_tokenfrom AzureADOAuth2. I don't think this method is accessible outside the pipeline, please answer this question if there is any way to do it.

Updates

Update 1 - 20/01/2017

Following an Issue to request an extra data parameter with the time of the access token refresh, it is now possible to check if the access_token needs to be updated in every backend.

In future versions (>0.2.1 for the social-auth-core) there will be a new field in extra data:

'auth_time': int(time.time())

And so this works:

def get_token(user, provider):
    social = user.social_auth.get(provider=provider)
    if (social.extra_data['auth_time'] + social.extra_data['expires']) <= int(time.time()):
        strategy = load_strategy()
        social.refresh_token(strategy)
    return social.extra_data['access_token']

Note: According to OAuth 2 RFC all responses should (it's a RECOMMENDED param) provide an expires_in but for most backends (including the azuread-oauth2) this value is being saved as expires. Be careful to understand how your backend behaves! An Issue on this exists and I will be update the answer with the relevant info when it exists.

Update 2 - 17/02/17

Additionally, there is a method in UserMixin called access_token_expired (code) that can be used to assert if the token is valid or not (note: this method doesn't work for race conditions, as pointed out in this anwser by @SCasey).

Update 3 - 31/05/17

In Python Social Auth - Core v1.3.0 get_access_token(self, strategy) was introduced in storage.py.

So now:

from social_django.utils import load_strategy

social = request.user.social_auth.get(provider='azuread-oauth2')
response = self.get_json('https://graph.microsoft.com/v1.0/me',
                     headers={'Authorization': '%s %s' % (social.extra_data['token_type'], 
                                                          social.get_access_token(load_strategy())}

Thanks @damio for pointing it out.

1
damio On

.get_access_token(strategy) refresh the token automatically if it's expired. You can use it like that:

from social_django.utils import load_strategy
#...
social = request.user.social_auth.get(provider='google-oauth2')
access_token = social.get_access_token(load_strategy())
5
SCasey On

@NBajanca's update is almost correct for version 1.0.1.

extra_data['expires_in']

is now

extra_data['expires']

So the code is:

def get_token(user, provider):
    social = user.social_auth.get(provider=provider)
    if (social.extra_data['auth_time'] + social.extra_data['expires']) <= int(time.time()):
        strategy = load_strategy()
        social.refresh_token(strategy)
    return social.extra_data['access_token']

I'd also recommend subtracting an arbitrary amount of time from that calc, so that we don't run into a race situation where we've checked the token 0.01s before expiry and then get an error because we sent the request after expiry. I like to add 10 seconds just to be safe, but it's probably overkill:

def get_token(user, provider):
    social = user.social_auth.get(provider=provider)
    if (social.extra_data['auth_time'] + social.extra_data['expires'] - 10) <= int(time.time()):
        strategy = load_strategy()
        social.refresh_token(strategy)
    return social.extra_data['access_token']

EDIT @NBajanca points out that expires_in is technically correct per the Oauth2 docs. It seems that for some backends, this may work. The code above using expires is what works with provider="google-oauth2" as of v1.0.1