wagnerdelima / drf-social-oauth2

drf-social-oauth2 makes it easy to integrate Django social authentication with major OAuth2 providers, i.e., Facebook, Twitter, Google, etc.
https://drf-social-oauth2.readthedocs.io/en/latest/
MIT License
270 stars 34 forks source link

convert-token returns invalid_client with hashed client secret #240

Closed Paul-Mo-Sys closed 1 month ago

Paul-Mo-Sys commented 1 month ago

Describe the bug When using a "Client Credenitals" app configured with a hashed client secret calling the convert-token always fails with error invalid_client

I believe that now the client secret is being set in the request by looking up the application rather than being required in the the body of the request this means that when trying to authenticate the client it is set to the three part encoded digest when the plaintext secret is required.

To Reproduce Steps to reproduce the behavior: Tested in a clean Django install with frozen requirements:

asgiref==3.8.1
certifi==2024.7.4
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==43.0.0
defusedxml==0.8.0rc2
Django==5.0.7
django-oauth-toolkit==2.4.0
djangorestframework==3.15.2
drf-social-oauth2==3.1.0
idna==3.7
jwcrypto==1.5.6
oauthlib==3.2.2
pycparser==2.22
PyJWT==2.8.0
python3-openid==3.2.0
pytz==2024.1
requests==2.32.3
requests-oauthlib==2.0.0
social-auth-app-django==5.4.2
social-auth-core==4.5.4
sqlparse==0.5.1
typing_extensions==4.12.2
tzdata==2024.1
urllib3==2.2.2

settings.py

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'django-insecure-ygbzvkp&jqk36ehq3xdzac!eyq5e%bvou#$w@i9x7$t2zr%g6#'
DEBUG = True
ALLOWED_HOSTS = []

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'oauth2_provider',
    'social_django',
    'drf_social_oauth2',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'socialBugReport.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'social_django.context_processors.backends',
                'social_django.context_processors.login_redirect',
            ],
        },
    },
]

WSGI_APPLICATION = 'socialBugReport.wsgi.application'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True

STATIC_URL = 'static/'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
        'drf_social_oauth2.authentication.SocialAuthentication',
    ),
}

AUTHENTICATION_BACKENDS = (
    'social_core.backends.google.GoogleOAuth2',
    'drf_social_oauth2.backends.DjangoOAuth2',
    'django.contrib.auth.backends.ModelBackend',
)
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'key' # redacted
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'secret' # redacted

SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile',
]

urls.py

from django.contrib import admin
from django.urls import path, re_path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r'^auth/', include('drf_social_oauth2.urls', namespace='drf'))
]

Create a Django Oauth Toolkit application with the following settings:

Client id: \<suggested value> test_client_id for this example Client Type: Confidential Authorization grant type: Client credentials Client secret: \<suggested value> test_client_secret for this example Hash client secret: checked

All other settings can be left blank.

Call /auth/convert-token/ with: grant_type=convert_token client_id=test_client_id backend=google-oauth2 token=\<googletoken\>

e.g. curl -X POST -d "grant_type=convert_token&client_id=test_client_id&backend=google-oauth2&token=<google_token>" http://localhost:8000/auth/convert-token

This will return 401 unauthorised with a body:

{
    "error": "invalid_client"
}

Note that the backend and token are irrelevant to the failure as this happens before they are used.

Expected behavior The authentication continues and returns the converted token as would happen if the application was saved with an un-hashed secret above

Desktop (please complete the following information):

Additional context Add any other context about the problem here.

wagnerdelima commented 1 month ago

You should not use the hashed version of the client secret. It is described in the following links: https://django-oauth-toolkit.readthedocs.io/en/latest/tutorial/tutorial_01.html#test-your-authorization-server, https://django-oauth-toolkit.readthedocs.io/en/latest/changelog.html#id1, and also at the https://drf-social-oauth2.readthedocs.io/en/latest/application.html documentation.