Invalid_client when trying to authenticate with client_id and client_secret using django oauth toolkit and rest framework

I’m running a Django service that exposes endpoints via Django REST Framework. I want to secure these endpoints using Django OAuth Toolkit for authentication.

When I create an application from the admin panel, I use the following settings:

enter image description here

As shown in the screenshot, I disable client_secret hashing. With this configuration, everything works perfectly, and I can obtain an access token without any issues.

enter image description here

* Preparing request to http://localhost:8000/o/token/
* Current time is 2024-12-19T10:47:03.573Z
* Enable automatic URL encoding
* Using default HTTP version
* Enable timeout of 30000ms
* Enable SSL validation
* Found bundle for host localhost: 0x159f21ed0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#106) with host localhost
* Connected to localhost (127.0.0.1) port 8000 (#106)

> POST /o/token/ HTTP/1.1
> Host: localhost:8000
> User-Agent: insomnia/0.2.2
> Content-Type: application/x-www-form-urlencoded
> Accept: application/x-www-form-urlencoded, application/json
> Authorization: Basic MkVDdzAwWkN1cm1CRFc4TEFrSmpjSGt1bnh1OW9WZlRpY09DaGU5bDpKVTl6SURzd0Zob09JMzJhRjhUMVI2WnhYZDVVTU84TWwwbHdiZldNWFNxcHVuTGdsaXBLT2xLTFBNMTBublV1TGp3WGFWOVBPR2ZxYUpURzF5Smx2VGRMWHVPRzN0SVg4bE9tQ1N6U09lbTV4Z2ExaWZrNWRUdjVOYWdFV2djQQ==
> Content-Length: 29

| grant_type=client_credentials

* Mark bundle as not supporting multiuse

< HTTP/1.1 200 OK
< Date: Thu, 19 Dec 2024 10:47:03 GMT
< Server: WSGIServer/0.2 CPython/3.12.8
< Content-Type: application/json
< Cache-Control: no-store
< Pragma: no-cache
< djdt-store-id: b377d48fbdae4db989aabb760af12619
< Server-Timing: TimerPanel_utime;dur=28.13699999999919;desc="User CPU time", TimerPanel_stime;dur=2.7200000000000557;desc="System CPU time", TimerPanel_total;dur=30.856999999999246;desc="Total CPU time", TimerPanel_total_time;dur=56.63733399705961;desc="Elapsed time", SQLPanel_sql_time;dur=5.738208987168036;desc="SQL 3 queries", CachePanel_total_time;dur=0;desc="Cache 0 Calls"
< Vary: Accept-Language, Cookie
< Content-Language: en
< X-Frame-Options: DENY
< Content-Length: 118
< X-Content-Type-Options: nosniff
< Referrer-Policy: same-origin
< Cross-Origin-Opener-Policy: same-origin


* Received 118 B chunk
* Connection #106 to host localhost left intact

| {"access_token": "C5RbvIhZIp3y5zLnC7xrezfuzTGNwe", "expires_in": 36000, "token_type": "Bearer", "scope": "read write"}

However, when I enable client_secret hashing, I receive the error: {"error": "invalid_client"}.

enter image description here

enter image description here

* Preparing request to http://localhost:8000/o/token/
* Current time is 2024-12-19T10:50:41.587Z
* Enable automatic URL encoding
* Using default HTTP version
* Enable timeout of 30000ms
* Enable SSL validation
* Too old connection (217 seconds), disconnect it
* Connection 106 seems to be dead!
* Closing connection 106
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#107)

> POST /o/token/ HTTP/1.1
> Host: localhost:8000
> User-Agent: insomnia/0.2.2
> Content-Type: application/x-www-form-urlencoded
> Accept: application/x-www-form-urlencoded, application/json
> Authorization: Basic eHp0cktWNmh1bDJjdFJNWWMxNW82ZHhWRk4wZHJTNzkyMjNldTQ3VTp6cUJwbW1zVHQ4dU1PaEk4Y29FZ3U3eW9rUUNRYnpoVE1rN2Y3bkVYclFlYnl5cEdYU1JpOWNFSGlIUXk1UnJ4blpvNTFOWVBldWhoTERkeWF3VkNrYkJGN05KVzNKZjRua1Z4OEI0a3p1V2tHRU9XdHNOOUo0NWFQRTIybHkzNw==
> Content-Length: 29

| grant_type=client_credentials

* Mark bundle as not supporting multiuse

< HTTP/1.1 401 Unauthorized
< Date: Thu, 19 Dec 2024 10:50:41 GMT
< Server: WSGIServer/0.2 CPython/3.12.8
< Content-Type: application/json
< Cache-Control: no-store
< Pragma: no-cache
< WWW-Authenticate: Bearer error="invalid_client"
< djdt-store-id: fc37d8dc93364fa1ab78cd392ddcfe20
< Server-Timing: TimerPanel_utime;dur=20.312999999998027;desc="User CPU time", TimerPanel_stime;dur=3.6159999999991754;desc="System CPU time", TimerPanel_total;dur=23.928999999997203;desc="Total CPU time", TimerPanel_total_time;dur=44.46612499305047;desc="Elapsed time", SQLPanel_sql_time;dur=2.3827080003684387;desc="SQL 2 queries", CachePanel_total_time;dur=0;desc="Cache 0 Calls"
< Vary: Accept-Language, Cookie
< Content-Language: en
< X-Frame-Options: DENY
< Content-Length: 27
< X-Content-Type-Options: nosniff
< Referrer-Policy: same-origin
< Cross-Origin-Opener-Policy: same-origin


* Received 27 B chunk
* Connection #107 to host localhost left intact

| {"error": "invalid_client"}

Here’s my custom authentication class, which I copied from https://stackoverflow.com/a/65718714/15136864:

from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from oauth2_provider.models import Application


class OAuth2ClientCredentialAuthentication(OAuth2Authentication):

    """OAuth2Authentication doesn't allows credentials to belong to an application (client).
    This override authenticates server-to-server requests, using client_credential authentication.
    """

    def authenticate(self, request):
        authentication = super().authenticate(request)

        if authentication is not None:
            _, access_token = authentication
            if self._grant_type_is_client_credentials(access_token):
                authentication = access_token.application.user, access_token

        return authentication

    def _grant_type_is_client_credentials(self, access_token):
        return access_token.application.authorization_grant_type == Application.GRANT_CLIENT_CREDENTIALS

I used this class because, as mentioned in the referenced post, logging in using client credentials isn’t supported out of the box, which isn’t explicitly stated in the documentation.

Here's my configuration

REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    "DEFAULT_AUTHENTICATION_CLASSES": ("authentication.rest_framework.OAuth2ClientCredentialAuthentication",),
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
}

OAUTH2_PROVIDER_APPLICATION_MODEL = "authentication.Application"

OAUTH_2_PROVIDER = {
    "SCOPES": {"read": "Read scope", "write": "Write scope", "groups": "Access to your groups"},
}

Edit: I discovered that our application uses a custom password hasher:

PASSWORD_HASHERS = [
    "authentication.password.PBKDF2SHA256PasswordHasher",
]

It’s likely that Django OAuth Toolkit uses a different hasher, which might be causing the issue.

Back to Top