So You’ve Decided To Make A Provider…

Built-in allauth providers are well-documented, but adding new ones can be tricky. I took some notes as a reminder to myself, and decided to share.

Making a Provider for Wix

Wix uses Oauth2 access and refresh tokens. Start by modifying another Oauth2 provider. In providers.py:

from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider

class WixAccount(ProviderAccount):
    pass

class WixProvider(OAuth2Provider):
    id = 'wix'
    name = 'Wix'
    account_class = WixAccount

    def extract_uid(self, data):
        return str(data['id'])

provider_classes = [WixProvider]

urls.py:

from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import WixProvider

urlpatterns = default_urlpatterns(WixProvider)

Find the access URLs from the provider’s documentation. Like Wix, they may not be consistent. The authorize & access token URLs are unique, but you can pick any profile URL(s) with relevant details. Modify the header as needed – Wix uses the access token in the header as shown. In views.py:

from allauth.socialaccount.providers.oauth2.views import (
    OAuth2Adapter,
    OAuth2CallbackView,
    OAuth2LoginView,
)

from .provider import WixProvider

class WixOAuth2Adapter(OAuth2Adapter):
    provider_id = WixProvider.id
    access_token_url = 'https://www.wixapis.com/oauth/access'
    authorize_url = 'https://www.wix.com/installer/install'
    profile_url = 'https://www.wixapis.com/apps/v1/instance'
    supports_state = True

    def complete_login(self, request, app, token, **kwargs):
        resp = requests.get(
            self.profile_url,
            headers={'Authorization': token.token})
        return self.get_provider().sociallogin_from_response(
            request, extra_data
        )

oauth2_login = OAuth2LoginView.adapter_view(WixOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(WixOAuth2Adapter)

Due to an odd redirect, Wix needs a custom client. You may be able to use the default and omit this file, client.py:

import requests
from django.utils.http import urlencode

from allauth.socialaccount.providers.oauth2.client import (
    OAuth2Client,
    OAuth2Error
)

class WixOAuth2Client(OAuth2Client):
    def get_access_token(self, code):
        resp = requests.post(
            self.access_token_url,
            headers={'Content-Type': 'application/json'},
            json={
                'grant_type': 'authorization_code', # omit redirect_uri
                'client_id': self.consumer_key, # << add fields missing
                'client_secret': self.consumer_secret, # << from allauth
                'code': code
            }
        )
        access_token = None
        if resp.status_code in [200, 201]:
            access_token = resp.json()
        if not access_token or 'access_token' not in access_token:
            raise OAuth2Error(f'Wix access error: {resp.content}')
        return access_token

    def get_redirect_url(self, authorization_url, extra_params):
        params = {
            'appId': self.consumer_key, # instead of client_id
            'redirectUrl': self.callback_url # instead of redirect_uri
        }
        if self.state:
            params['state'] = self.state
        params.update(extra_params)
        return f'{authorization_url}?{urlencode(params)}'

Pick a field returned by the provider to use as the user ID. I opted for the user’s site name. In provider.py:

class WixProvider(OAuth2Provider):
    def extract_uid(self, data):
        return str(data['site']['siteDisplayName'])

Extra data allows additional profile info. To get necessary Wix data, I make two calls at login. In views.py:

class WixOAuth2Adapter(OAuth2Adapter):
    def complete_login(self, request, app, token, **kwargs):
        resp = requests.get(
            self.profile_url,
            headers={'Authorization': token.token}
        )
        extra_data = resp.json()
        resp = requests.get(
            self.members_url,
            headers={'Authorization': token.token}
        )
        extra_data.update(resp.json())
        return self.get_provider().sociallogin_from_response(
            request, extra_data
        )

BONUS: Refresh token

Wix tokens persist for 5 minutes, but renew with a refresh token. While not part of the allauth provider, here’s a useful Wix token-refreshing decorator function. Use the decorator on any Wix API calls, and the token will refresh if needed.

from django.conf import settings
from functools import wraps
from rauth import OAuth2Service

def fresh_wix(func):
    'refresh Wix token if necessary'
    @wraps(func)
    def fresh_func(self, *args):
        out = func(self, *args)
        if out.get('message') == 'internal error':
            self.refresh()
            return func(self, *args)
        return out
    return fresh_func

class WixAPI:
    access_token_url='https://www.wixapis.com/oauth/access'
    def __init__(self, access_token, refresh_token):
        self.data = {
            'client_id': settings.WIX_CLIENT_ID,
            'client_secret': settings.WIX_CLIENT_SECRET,
            'refresh_token': refresh_token,
            'grant_type': 'refresh_token'
        }
        self.service = OAuth2Service(
            client_id=settings.WIX_CLIENT_ID,
            client_secret=settings.WIX_CLIENT_SECRET,
            access_token_url=self.access_token_url,
        )
        self.session = self.service.get_session(access_token)

    def refresh(self):
        data = self.session.post(
	    self.access_token_url,
            json=self.data,
            headers={'Content-type': 'application/json'}
	).json()
        self.data['refresh_token'] = data['refresh_token']
        self.session = self.service.get_session(data['access_token'])

    @fresh_wix
    def do_stuff(self, url):
	return self.session.get(url) # Wix API call with fresh tokens

Find the code at github. Thanks for reading!

About scotty

Scotty Vercoe is an award-winning composer, performer, producer and activist.

Comments are closed.