Django.fun

Django DRF + Allauth: OAuth2Error: Error retrieving access token on production build

We are integrating DRF (dj_rest_auth) and allauth with the frontend application based on React. Recently, the social login was added to handle login through LinkedIn, Facebook, Google and GitHub. Everything was working good on localhost with each of the providers. After the staging deployment, I updated the secrets and social applications for a new domain. Generating the URL for social login works fine, the user gets redirected to the provider login page and allowed access to login to our application, but after being redirected back to the frontend page responsible for logging in - it results in an error: (example for LinkedIn, happens for all of the providers)

allauth.socialaccount.providers.oauth2.client.OAuth2Error:
Error retrieving access token:
b'{"error":"invalid_redirect_uri","error_description":"Unable to retrieve access token: appid/redirect uri/code verifier does not match authorization code. Or authorization code expired. Or external member binding exists"}'

Our flow is:

go to frontend page -> click on provider's icon ->
redirect to {BACKEND_URL}/rest-auth/linkedin/url/ to make it a POST request (user submits the form) ->
login on provider's page ->
go back to our frontend page {frontend}/social-auth?source=linkedin&code={the code we are sending to rest-auth/$provider$ endpoint}&state={state}->
confirm the code & show the profile completion page

The adapter definition (same for every provider):

class LinkedInLogin(SocialLoginView):
    adapter_class = LinkedInOAuth2Adapter
    client_class = OAuth2Client

    @property
    def callback_url(self):
        return self.request.build_absolute_uri(reverse('linkedin_oauth2_callback'))

Callback definition:

def linkedin_callback(request):
    params = urllib.parse.urlencode(request.GET)
    return redirect(f'{settings.HTTP_PROTOCOL}://{settings.FRONTEND_HOST}/social-auth?source=linkedin&{params}')

URLs:

path('rest-auth/linkedin/', LinkedInLogin.as_view(), name='linkedin_oauth2_callback'),
path('rest-auth/linkedin/callback/', linkedin_callback, name='linkedin_oauth2_callback'),
path('rest-auth/linkedin/url/', linkedin_views.oauth2_login),

Frontend call to send the access_token/code:

  const handleSocialLogin = () => {
    postSocialAuth({
      code: decodeURIComponent(codeOrAccessToken),
      provider: provider
    }).then(response => {
      if (!response.error) return history.push(`/complete-profile?source=${provider}`);

      NotificationManager.error(
        `There was an error while trying to log you in via ${provider}`,
        "Error",
        3000
      );

      return history.push("/login");
    }).catch(_error => {
      NotificationManager.error(
        `There was an error while trying to log you in via ${provider}`,
        "Error",
        3000
      );

      return history.push("/login");
    });
  }

Mutation:

const postSocialUserAuth = builder => builder.mutation({
  query: (data) => {
    const payload = {
      code: data?.code,
    };

    return {
      url: `${API_BASE_URL}/rest-auth/${data?.provider}/`,
      method: 'POST',
      body: payload,
    }
  }

Callback URLs and client credentials are set for the staging environment both in our admin panel (Django) and provider's panel (i.e. developers.linkedin.com)

Again - everything from this setup is working ok in the local environment.

IMPORTANT We are using two different domains for the backend and frontend - frontend has a different domain than a backend

The solution was to completely change the callback URL generation

For anyone looking for a solution in the future:

class LinkedInLogin(SocialLoginView):
    adapter_class = CustomAdapterLinkedin
    client_class = OAuth2Client

    @property
    def callback_url(self):
        callback_url = reverse('linkedin_oauth2_callback')
         site = Site.objects.get_current()
         return f"{settings.HTTP_PROTOCOL}://{site}{callback_url}"

Custom adapter:

class CustomAdapterLinkedin(LinkedInOAuth2Adapter):
    def get_callback_url(self, request, app):
        callback_url = reverse(provider_id + "_callback")
        site = Site.objects.get_current()

        return f"{settings.HTTP_PROTOCOL}://{site}{callback_url}"

It is important to change your routes therefore for URL generation:

path('rest-auth/linkedin/url/', OAuth2LoginView.adapter_view(CustomAdapterLinkedin))

I am leaving this open since I think this is not expected behaviour.

Tutorials

Современный Python: начинаем проект с pyenv и poetry

Настройка проекта Python — виртуальные среды и управление пакетами

Использование requests в Python — тайм-ауты, повторы, хуки

Понимание декораторов в Python

ProcessPoolExecutor в Python: полное руководство

map() против submit() с ProcessPoolExecutor в Python

Понимание атрибутов, словарей и слотов в Python

Полное руководство по slice в Python

Выпуск Django 4.0

Безопасное развертывание приложения Django с помощью Gunicorn, Nginx и HTTPS

Автоматический повтор невыполненных задач Celery

Django REST Framework и Elasticsearch

Докеризация Django с помощью Postgres, Gunicorn и Nginx

Асинхронные задачи с Django и Celery

Релизы безопасности Django: 3.2.4, 3.1.12 и 2.2.24

Выпуски исправлений ошибок Django: 3.2.3, 3.1.11 и 2.2.23

Эффективное использование сериализаторов Django REST Framework

Выпуски безопасности Django: 3.2.2, 3.1.10 и 2.2.22

Выпущенные релизы безопасности Django: 3.2.1, 3.1.9 и 2.2.21

Обработка периодических задач в Django с помощью Celery и Docker

View all tutorials →