Repository: jazzband/django-oauth-toolkit

Issue:
Why are secrets in plain text?
Hello all,
I was digging in the data and I found out that all the secrets are stored in plain text in the DB. 

Why so?

reading this https://github.com/jazzband/django-oauth-toolkit/issues/276 it seems that the client_secret is used to generate/calculate some value, which one precisely? so for that, there's no way to hash it.

However, what about access_token and refresh tokens? why are not they hashed? in the end, they should be, somehow, as a password, so the real value is not needed (although you will have access to the real value each request if needed).




Discussion:
I think the main reason is that most of the time it's not required. With the multiple factors involved (you usually need client secret + password to get an access token) and them being random, often temporary, and not reused anywhere, stealing a bunch of client secrets doesn't give intruder much more than they already have.

But I can see some value in hashing and salting not just client secrets but also all the tokens and grants. Hashing access tokens by default is usually pointless (would make requests too slow) but in case of extreme paranoia (e.g. auth servers used by important resource servers) could be possible as well (even introspection should work, I think, token is always presented plain when something needs to be done), probably with some clever optimization to speed up verifying requests or perhaps just lighter hash algo as those tokens are short-lived and random enough.

I think it could be implemented as an extension over DOT or even as an optional model mixin or something here. Feel free to PR and, you know, occasionally ask questions about how to override one method or other.
The client credentials (2-legged) flow does not require an additional password, so in that sense the `client_id` and `client_secret` act like a username/password pair. Not hashing the secrets does feel like a gap, since a compromise of the database effectively gives you plain text passwords for all client credentials applications.

I'd be happy to work up a patch to address this. I did not see any mention of it elsewhere.
@IvanAnishchuk I took a first pass in #1020 -- let me know what you think.
will need more input for this, i personally dont think thats an big isseue. but lets wait
> For each registered application, you’ll need to store the public client_id and the private client_secret. Because these are essentially equivalent to a username and password, you should not store the secret in plain text, instead only store an encrypted or hashed version, to help reduce the likelihood of the secret leaking.

from https://www.oauth.com/oauth2-servers/client-registration/client-id-secret/

Resolving patch:
diff --git a/oauth2_provider/migrations/0006_alter_application_client_secret.py b/oauth2_provider/migrations/0006_alter_application_client_secret.py
new file mode 100644
--- /dev/null
+++ b/oauth2_provider/migrations/0006_alter_application_client_secret.py
@@ -0,0 +1,31 @@
+from django.db import migrations
+from django.contrib.auth.hashers import identify_hasher, make_password
+import logging
+import oauth2_provider.generators
+import oauth2_provider.models
+
+
+def forwards_func(apps, schema_editor):
+    """
+    Forward migration touches every application.client_secret which will cause it to be hashed if not already the case.
+    """
+    Application = apps.get_model('oauth2_provider', 'application')
+    applications = Application.objects.all()
+    for application in applications:
+        application.save(update_fields=['client_secret'])
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('oauth2_provider', '0005_auto_20211222_2352'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='application',
+            name='client_secret',
+            field=oauth2_provider.models.ClientSecretField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, help_text='Hashed on Save. Copy it now if this is a new secret.', max_length=255),
+        ),
+        migrations.RunPython(forwards_func),
+    ]
diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py
--- a/oauth2_provider/models.py
+++ b/oauth2_provider/models.py
@@ -6,6 +6,7 @@
 
 from django.apps import apps
 from django.conf import settings
+from django.contrib.auth.hashers import identify_hasher, make_password
 from django.core.exceptions import ImproperlyConfigured
 from django.db import models, transaction
 from django.urls import reverse
@@ -24,6 +25,20 @@
 logger = logging.getLogger(__name__)
 
 
+class ClientSecretField(models.CharField):
+    def pre_save(self, model_instance, add):
+        secret = getattr(model_instance, self.attname)
+        try:
+            hasher = identify_hasher(secret)
+            logger.debug(f"{model_instance}: {self.attname} is already hashed with {hasher}.")
+        except ValueError:
+            logger.debug(f"{model_instance}: {self.attname} is not hashed; hashing it now.")
+            hashed_secret = make_password(secret)
+            setattr(model_instance, self.attname, hashed_secret)
+            return hashed_secret
+        return super().pre_save(model_instance, add)
+
+
 class AbstractApplication(models.Model):
     """
     An Application instance represents a Client on the Authorization server.
@@ -90,8 +105,12 @@ class AbstractApplication(models.Model):
     )
     client_type = models.CharField(max_length=32, choices=CLIENT_TYPES)
     authorization_grant_type = models.CharField(max_length=32, choices=GRANT_TYPES)
-    client_secret = models.CharField(
-        max_length=255, blank=True, default=generate_client_secret, db_index=True
+    client_secret = ClientSecretField(
+        max_length=255,
+        blank=True,
+        default=generate_client_secret,
+        db_index=True,
+        help_text=_("Hashed on Save. Copy it now if this is a new secret."),
     )
     name = models.CharField(max_length=255, blank=True)
     skip_authorization = models.BooleanField(default=False)
diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py
--- a/oauth2_provider/oauth2_validators.py
+++ b/oauth2_provider/oauth2_validators.py
@@ -12,6 +12,7 @@
 import requests
 from django.conf import settings
 from django.contrib.auth import authenticate, get_user_model
+from django.contrib.auth.hashers import check_password
 from django.core.exceptions import ObjectDoesNotExist
 from django.db import transaction
 from django.db.models import Q
@@ -123,7 +124,7 @@ def _authenticate_basic_auth(self, request):
         elif request.client.client_id != client_id:
             log.debug("Failed basic auth: wrong client id %s" % client_id)
             return False
-        elif request.client.client_secret != client_secret:
+        elif not check_password(client_secret, request.client.client_secret):
             log.debug("Failed basic auth: wrong client secret %s" % client_secret)
             return False
         else:
@@ -148,7 +149,7 @@ def _authenticate_request_body(self, request):
         if self._load_application(client_id, request) is None:
             log.debug("Failed body auth: Application %s does not exists" % client_id)
             return False
-        elif request.client.client_secret != client_secret:
+        elif not check_password(client_secret, request.client.client_secret):
             log.debug("Failed body auth: wrong client secret %s" % client_secret)
             return False
         else:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -16,6 +16,8 @@
 Application = get_application_model()
 UserModel = get_user_model()
 
+CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
+
 
 class OAuthSettingsWrapper:
     """
@@ -101,12 +103,14 @@ def application():
         client_type=Application.CLIENT_CONFIDENTIAL,
         authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
         algorithm=Application.RS256_ALGORITHM,
+        client_secret=CLEARTEXT_SECRET,
     )
 
 
 @pytest.fixture
 def hybrid_application(application):
     application.authorization_grant_type = application.GRANT_OPENID_HYBRID
+    application.client_secret = CLEARTEXT_SECRET
     application.save()
     return application
 
@@ -141,7 +145,7 @@ def oidc_tokens(oauth2_settings, application, test_user, client):
             "code": code,
             "redirect_uri": "http://example.org",
             "client_id": application.client_id,
-            "client_secret": application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "scope": "openid",
         },
     )
diff --git a/tests/test_authorization_code.py b/tests/test_authorization_code.py
--- a/tests/test_authorization_code.py
+++ b/tests/test_authorization_code.py
@@ -34,6 +34,7 @@
 
 URI_OOB = "urn:ietf:wg:oauth:2.0:oob"
 URI_OOB_AUTO = "urn:ietf:wg:oauth:2.0:oob:auto"
+CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
 
 
 # mocking a protected resource view
@@ -60,6 +61,7 @@ def setUp(self):
             user=self.dev_user,
             client_type=Application.CLIENT_CONFIDENTIAL,
             authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
+            client_secret=CLEARTEXT_SECRET,
         )
 
     def tearDown(self):
@@ -677,7 +679,7 @@ def test_basic_auth(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -699,7 +701,7 @@ def test_refresh(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -744,7 +746,7 @@ def test_refresh_with_grace_period(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -795,7 +797,7 @@ def test_refresh_invalidates_old_tokens(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -827,7 +829,7 @@ def test_refresh_no_scopes(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -855,7 +857,7 @@ def test_refresh_bad_scopes(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -881,7 +883,7 @@ def test_refresh_fail_repeating_requests(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -911,7 +913,7 @@ def test_refresh_repeating_requests(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -948,7 +950,7 @@ def test_refresh_repeating_requests_non_rotating_tokens(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -977,7 +979,7 @@ def test_basic_auth_bad_authcode(self):
             "code": "BLAH",
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -989,7 +991,7 @@ def test_basic_auth_bad_granttype(self):
         self.client.login(username="test_user", password="123456")
 
         token_request_data = {"grant_type": "UNKNOWN", "code": "BLAH", "redirect_uri": "http://example.org"}
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -1014,7 +1016,7 @@ def test_basic_auth_grant_expired(self):
             "code": "BLAH",
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -1049,7 +1051,7 @@ def test_basic_auth_wrong_auth_type(self):
             "redirect_uri": "http://example.org",
         }
 
-        user_pass = "{0}:{1}".format(self.application.client_id, self.application.client_secret)
+        user_pass = "{0}:{1}".format(self.application.client_id, CLEARTEXT_SECRET)
         auth_string = base64.b64encode(user_pass.encode("utf-8"))
         auth_headers = {
             "HTTP_AUTHORIZATION": "Wrong " + auth_string.decode("utf-8"),
@@ -1070,7 +1072,7 @@ def test_request_body_params(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
         }
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data)
@@ -1445,7 +1447,7 @@ def test_code_exchange_succeed_when_redirect_uri_match(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org?foo=bar",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -1480,7 +1482,7 @@ def test_code_exchange_fails_when_redirect_uri_does_not_match(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org?foo=baraa",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -1520,7 +1522,7 @@ def test_code_exchange_succeed_when_redirect_uri_match_with_multiple_query_param
             "code": authorization_code,
             "redirect_uri": "http://example.com?bar=baz&foo=bar",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -1565,7 +1567,7 @@ def test_oob_as_html(self):
             "code": authorization_code,
             "redirect_uri": URI_OOB,
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
         }
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data)
@@ -1605,7 +1607,7 @@ def test_oob_as_json(self):
             "code": authorization_code,
             "redirect_uri": URI_OOB_AUTO,
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
         }
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data)
@@ -1681,7 +1683,7 @@ def test_id_token_code_exchange_succeed_when_redirect_uri_match_with_multiple_qu
             "code": authorization_code,
             "redirect_uri": "http://example.com?bar=baz&foo=bar",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -1715,7 +1717,7 @@ def test_id_token(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "scope": "openid",
         }
 
@@ -1761,7 +1763,7 @@ def test_resource_access_allowed(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -1819,7 +1821,7 @@ def test_id_token_resource_access_allowed(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
diff --git a/tests/test_client_credential.py b/tests/test_client_credential.py
--- a/tests/test_client_credential.py
+++ b/tests/test_client_credential.py
@@ -1,6 +1,5 @@
 import json
 from unittest.mock import patch
-from urllib.parse import quote_plus
 
 import pytest
 from django.contrib.auth import get_user_model
@@ -24,6 +23,8 @@
 AccessToken = get_access_token_model()
 UserModel = get_user_model()
 
+CLEARTEXT_SECRET = "abcdefghijklmnopqrstuvwxyz1234567890"
+
 
 # mocking a protected resource view
 class ResourceView(ProtectedResourceView):
@@ -44,6 +45,7 @@ def setUp(self):
             user=self.dev_user,
             client_type=Application.CLIENT_PUBLIC,
             authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
+            client_secret=CLEARTEXT_SECRET,
         )
 
     def tearDown(self):
@@ -55,35 +57,28 @@ def tearDown(self):
 class TestClientCredential(BaseTest):
     def test_client_credential_access_allowed(self):
         """
-        Request an access token using Client Credential Flow
+        Request an access token using Client Credential Flow with hashed secrets
         """
+        self.assertNotEqual(self.application.client_secret, CLEARTEXT_SECRET)
+
         token_request_data = {
             "grant_type": "client_credentials",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
 
-        content = json.loads(response.content.decode("utf-8"))
-        access_token = content["access_token"]
-
-        # use token to access the resource
-        auth_headers = {
-            "HTTP_AUTHORIZATION": "Bearer " + access_token,
-        }
-        request = self.factory.get("/fake-resource", **auth_headers)
-        request.user = self.test_user
-
-        view = ResourceView.as_view()
-        response = view(request)
-        self.assertEqual(response, "This is a protected resource")
+        # secret mismatch should return a 401
+        auth_headers = get_basic_auth_header(self.application.client_id, "not-the-secret")
+        response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
+        self.assertEqual(response.status_code, 401)
 
     def test_client_credential_does_not_issue_refresh_token(self):
         token_request_data = {
             "grant_type": "client_credentials",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -93,7 +88,7 @@ def test_client_credential_does_not_issue_refresh_token(self):
 
     def test_client_credential_user_is_none_on_access_token(self):
         token_request_data = {"grant_type": "client_credentials"}
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -122,7 +117,7 @@ def test_extended_request(self):
         token_request_data = {
             "grant_type": "client_credentials",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
 
@@ -174,12 +169,11 @@ def test_client_resource_password_based(self):
             user=self.dev_user,
             client_type=Application.CLIENT_CONFIDENTIAL,
             authorization_grant_type=Application.GRANT_PASSWORD,
+            client_secret=CLEARTEXT_SECRET,
         )
 
         token_request_data = {"grant_type": "password", "username": "test_user", "password": "123456"}
-        auth_headers = get_basic_auth_header(
-            quote_plus(self.application.client_id), quote_plus(self.application.client_secret)
-        )
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
diff --git a/tests/test_commands.py b/tests/test_commands.py
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -1,6 +1,7 @@
 from io import StringIO
 
 from django.contrib.auth import get_user_model
+from django.contrib.auth.hashers import check_password
 from django.core.management import call_command
 from django.core.management.base import CommandError
 from django.test import TestCase
@@ -83,7 +84,7 @@ def test_application_created_with_client_secret(self):
         )
         app = Application.objects.get()
 
-        self.assertEqual(app.client_secret, "SECRET")
+        self.assertTrue(check_password("SECRET", app.client_secret))
 
     def test_application_created_with_client_id(self):
         call_command(
diff --git a/tests/test_hybrid.py b/tests/test_hybrid.py
--- a/tests/test_hybrid.py
+++ b/tests/test_hybrid.py
@@ -30,6 +30,8 @@
 RefreshToken = get_refresh_token_model()
 UserModel = get_user_model()
 
+CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
+
 
 # mocking a protected resource view
 class ResourceView(ProtectedResourceView):
@@ -62,6 +64,7 @@ def setUp(self):
             client_type=Application.CLIENT_CONFIDENTIAL,
             authorization_grant_type=Application.GRANT_OPENID_HYBRID,
             algorithm=Application.RS256_ALGORITHM,
+            client_secret=CLEARTEXT_SECRET,
         )
         self.application.save()
 
@@ -829,7 +832,7 @@ def test_basic_auth(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -850,7 +853,7 @@ def test_basic_auth_bad_authcode(self):
             "code": "BLAH",
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -862,7 +865,7 @@ def test_basic_auth_bad_granttype(self):
         self.client.login(username="hy_test_user", password="123456")
 
         token_request_data = {"grant_type": "UNKNOWN", "code": "BLAH", "redirect_uri": "http://example.org"}
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -887,7 +890,7 @@ def test_basic_auth_grant_expired(self):
             "code": "BLAH",
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -922,7 +925,7 @@ def test_basic_auth_wrong_auth_type(self):
             "redirect_uri": "http://example.org",
         }
 
-        user_pass = "{0}:{1}".format(self.application.client_id, self.application.client_secret)
+        user_pass = "{0}:{1}".format(self.application.client_id, CLEARTEXT_SECRET)
         auth_string = base64.b64encode(user_pass.encode("utf-8"))
         auth_headers = {
             "HTTP_AUTHORIZATION": "Wrong " + auth_string.decode("utf-8"),
@@ -943,7 +946,7 @@ def test_request_body_params(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
         }
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data)
@@ -1056,7 +1059,7 @@ def test_code_exchange_succeed_when_redirect_uri_match(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org?foo=bar",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -1091,7 +1094,7 @@ def test_code_exchange_fails_when_redirect_uri_does_not_match(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org?foo=baraa",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -1126,7 +1129,7 @@ def test_code_exchange_succeed_when_redirect_uri_match_with_multiple_query_param
             "code": authorization_code,
             "redirect_uri": "http://example.com?bar=baz&foo=bar",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -1163,7 +1166,7 @@ def test_id_token_code_exchange_succeed_when_redirect_uri_match_with_multiple_qu
             "code": authorization_code,
             "redirect_uri": "http://example.com?bar=baz&foo=bar",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -1200,7 +1203,7 @@ def test_resource_access_allowed(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -1239,7 +1242,7 @@ def test_id_token_resource_access_allowed(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -1351,7 +1354,7 @@ def test_id_token_nonce_in_token_response(oauth2_settings, test_user, hybrid_app
             "code": code,
             "redirect_uri": "http://example.org",
             "client_id": hybrid_application.client_id,
-            "client_secret": hybrid_application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "scope": "openid",
         },
     )
@@ -1422,7 +1425,7 @@ def test_claims_passed_to_code_generation(
             "code": code,
             "redirect_uri": "http://example.org",
             "client_id": hybrid_application.client_id,
-            "client_secret": hybrid_application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "scope": "openid",
         },
     )
diff --git a/tests/test_introspection_view.py b/tests/test_introspection_view.py
--- a/tests/test_introspection_view.py
+++ b/tests/test_introspection_view.py
@@ -17,6 +17,8 @@
 AccessToken = get_access_token_model()
 UserModel = get_user_model()
 
+CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
+
 
 @pytest.mark.usefixtures("oauth2_settings")
 @pytest.mark.oauth2_settings(presets.INTROSPECTION_SETTINGS)
@@ -35,6 +37,7 @@ def setUp(self):
             user=self.test_user,
             client_type=Application.CLIENT_CONFIDENTIAL,
             authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
+            client_secret=CLEARTEXT_SECRET,
         )
 
         self.resource_server_token = AccessToken.objects.create(
@@ -281,7 +284,7 @@ def test_view_post_notexisting_token(self):
 
     def test_view_post_valid_client_creds_basic_auth(self):
         """Test HTTP basic auth working"""
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
         response = self.client.post(
             reverse("oauth2_provider:introspect"), {"token": self.valid_token.token}, **auth_headers
         )
@@ -301,9 +304,7 @@ def test_view_post_valid_client_creds_basic_auth(self):
 
     def test_view_post_invalid_client_creds_basic_auth(self):
         """Must fail for invalid client credentials"""
-        auth_headers = get_basic_auth_header(
-            self.application.client_id, self.application.client_secret + "_so_wrong"
-        )
+        auth_headers = get_basic_auth_header(self.application.client_id, f"{CLEARTEXT_SECRET}_so_wrong")
         response = self.client.post(
             reverse("oauth2_provider:introspect"), {"token": self.valid_token.token}, **auth_headers
         )
@@ -316,7 +317,7 @@ def test_view_post_valid_client_creds_plaintext(self):
             {
                 "token": self.valid_token.token,
                 "client_id": self.application.client_id,
-                "client_secret": self.application.client_secret,
+                "client_secret": CLEARTEXT_SECRET,
             },
         )
         self.assertEqual(response.status_code, 200)
@@ -340,7 +341,7 @@ def test_view_post_invalid_client_creds_plaintext(self):
             {
                 "token": self.valid_token.token,
                 "client_id": self.application.client_id,
-                "client_secret": self.application.client_secret + "_so_wrong",
+                "client_secret": f"{CLEARTEXT_SECRET}_so_wrong",
             },
         )
         self.assertEqual(response.status_code, 403)
diff --git a/tests/test_oauth2_validators.py b/tests/test_oauth2_validators.py
--- a/tests/test_oauth2_validators.py
+++ b/tests/test_oauth2_validators.py
@@ -15,6 +15,7 @@
 from oauth2_provider.oauth2_validators import OAuth2Validator
 
 from . import presets
+from .utils import get_basic_auth_header
 
 
 try:
@@ -28,6 +29,8 @@
 AccessToken = get_access_token_model()
 RefreshToken = get_refresh_token_model()
 
+CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
+
 
 @contextlib.contextmanager
 def always_invalid_token():
@@ -51,7 +54,7 @@ def setUp(self):
         self.validator = OAuth2Validator()
         self.application = Application.objects.create(
             client_id="client_id",
-            client_secret="client_secret",
+            client_secret=CLEARTEXT_SECRET,
             user=self.user,
             client_type=Application.CLIENT_PUBLIC,
             authorization_grant_type=Application.GRANT_PASSWORD,
@@ -69,7 +72,7 @@ def test_authenticate_request_body(self):
         self.request.client_secret = "wrong_client_secret"
         self.assertFalse(self.validator._authenticate_request_body(self.request))
 
-        self.request.client_secret = "client_secret"
+        self.request.client_secret = CLEARTEXT_SECRET
         self.assertTrue(self.validator._authenticate_request_body(self.request))
 
     def test_extract_basic_auth(self):
@@ -86,26 +89,22 @@ def test_extract_basic_auth(self):
 
     def test_authenticate_basic_auth(self):
         self.request.encoding = "utf-8"
-        # client_id:client_secret
-        self.request.headers = {"HTTP_AUTHORIZATION": "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=\n"}
+        self.request.headers = get_basic_auth_header("client_id", CLEARTEXT_SECRET)
         self.assertTrue(self.validator._authenticate_basic_auth(self.request))
 
     def test_authenticate_basic_auth_default_encoding(self):
         self.request.encoding = None
-        # client_id:client_secret
-        self.request.headers = {"HTTP_AUTHORIZATION": "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=\n"}
+        self.request.headers = get_basic_auth_header("client_id", CLEARTEXT_SECRET)
         self.assertTrue(self.validator._authenticate_basic_auth(self.request))
 
     def test_authenticate_basic_auth_wrong_client_id(self):
         self.request.encoding = "utf-8"
-        # wrong_id:client_secret
-        self.request.headers = {"HTTP_AUTHORIZATION": "Basic d3JvbmdfaWQ6Y2xpZW50X3NlY3JldA==\n"}
+        self.request.headers = get_basic_auth_header("wrong_id", CLEARTEXT_SECRET)
         self.assertFalse(self.validator._authenticate_basic_auth(self.request))
 
     def test_authenticate_basic_auth_wrong_client_secret(self):
         self.request.encoding = "utf-8"
-        # client_id:wrong_secret
-        self.request.headers = {"HTTP_AUTHORIZATION": "Basic Y2xpZW50X2lkOndyb25nX3NlY3JldA==\n"}
+        self.request.headers = get_basic_auth_header("client_id", "wrong_secret")
         self.assertFalse(self.validator._authenticate_basic_auth(self.request))
 
     def test_authenticate_basic_auth_not_b64_auth_string(self):
@@ -116,7 +115,6 @@ def test_authenticate_basic_auth_not_b64_auth_string(self):
 
     def test_authenticate_basic_auth_invalid_b64_string(self):
         self.request.encoding = "utf-8"
-        # client_id:wrong_secret
         self.request.headers = {"HTTP_AUTHORIZATION": "Basic ZHVtbXk=:ZHVtbXk=\n"}
         self.assertFalse(self.validator._authenticate_basic_auth(self.request))
 
@@ -140,7 +138,7 @@ def test_client_authentication_required(self):
         self.assertTrue(self.validator.client_authentication_required(self.request))
         self.request.headers = {}
         self.request.client_id = "client_id"
-        self.request.client_secret = "client_secret"
+        self.request.client_secret = CLEARTEXT_SECRET
         self.assertTrue(self.validator.client_authentication_required(self.request))
         self.request.client_secret = ""
         self.assertFalse(self.validator.client_authentication_required(self.request))
@@ -327,7 +325,7 @@ def setUp(self):
         self.validator = OAuth2Validator()
         self.application = Application.objects.create(
             client_id="client_id",
-            client_secret="client_secret",
+            client_secret=CLEARTEXT_SECRET,
             user=self.user,
             client_type=Application.CLIENT_PUBLIC,
             authorization_grant_type=Application.GRANT_PASSWORD,
diff --git a/tests/test_password.py b/tests/test_password.py
--- a/tests/test_password.py
+++ b/tests/test_password.py
@@ -14,6 +14,8 @@
 Application = get_application_model()
 UserModel = get_user_model()
 
+CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
+
 
 # mocking a protected resource view
 class ResourceView(ProtectedResourceView):
@@ -33,6 +35,7 @@ def setUp(self):
             user=self.dev_user,
             client_type=Application.CLIENT_PUBLIC,
             authorization_grant_type=Application.GRANT_PASSWORD,
+            client_secret=CLEARTEXT_SECRET,
         )
 
     def tearDown(self):
@@ -51,7 +54,7 @@ def test_get_token(self):
             "username": "test_user",
             "password": "123456",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
@@ -70,7 +73,7 @@ def test_bad_credentials(self):
             "username": "test_user",
             "password": "NOT_MY_PASS",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 400)
@@ -83,7 +86,7 @@ def test_password_resource_access_allowed(self):
             "username": "test_user",
             "password": "123456",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
diff --git a/tests/test_scopes.py b/tests/test_scopes.py
--- a/tests/test_scopes.py
+++ b/tests/test_scopes.py
@@ -18,6 +18,8 @@
 Grant = get_grant_model()
 UserModel = get_user_model()
 
+CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
+
 
 # mocking a protected resource view
 class ScopeResourceView(ScopedProtectedResourceView):
@@ -67,6 +69,7 @@ def setUp(self):
             user=self.dev_user,
             client_type=Application.CLIENT_CONFIDENTIAL,
             authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
+            client_secret=CLEARTEXT_SECRET,
         )
 
     def tearDown(self):
@@ -123,7 +126,7 @@ def test_scopes_save_in_access_token(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -159,7 +162,7 @@ def test_scopes_protection_valid(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -201,7 +204,7 @@ def test_scopes_protection_fail(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -243,7 +246,7 @@ def test_multi_scope_fail(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -285,7 +288,7 @@ def test_multi_scope_valid(self):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
@@ -326,7 +329,7 @@ def get_access_token(self, scopes):
             "code": authorization_code,
             "redirect_uri": "http://example.org",
         }
-        auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret)
+        auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET)
 
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         content = json.loads(response.content.decode("utf-8"))
diff --git a/tests/test_token_revocation.py b/tests/test_token_revocation.py
--- a/tests/test_token_revocation.py
+++ b/tests/test_token_revocation.py
@@ -13,6 +13,8 @@
 RefreshToken = get_refresh_token_model()
 UserModel = get_user_model()
 
+CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
+
 
 class BaseTest(TestCase):
     def setUp(self):
@@ -26,6 +28,7 @@ def setUp(self):
             user=self.dev_user,
             client_type=Application.CLIENT_CONFIDENTIAL,
             authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
+            client_secret=CLEARTEXT_SECRET,
         )
 
     def tearDown(self):
@@ -46,7 +49,7 @@ def test_revoke_access_token(self):
 
         data = {
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "token": tok.token,
         }
         url = reverse("oauth2_provider:revoke-token")
@@ -93,7 +96,7 @@ def test_revoke_access_token_with_hint(self):
 
         data = {
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "token": tok.token,
             "token_type_hint": "access_token",
         }
@@ -115,7 +118,7 @@ def test_revoke_access_token_with_invalid_hint(self):
 
         data = {
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "token": tok.token,
             "token_type_hint": "bad_hint",
         }
@@ -139,7 +142,7 @@ def test_revoke_refresh_token(self):
 
         data = {
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "token": rtok.token,
         }
 
@@ -164,7 +167,7 @@ def test_revoke_refresh_token_with_revoked_access_token(self):
         for token in (tok.token, rtok.token):
             data = {
                 "client_id": self.application.client_id,
-                "client_secret": self.application.client_secret,
+                "client_secret": CLEARTEXT_SECRET,
                 "token": token,
             }
 
@@ -194,7 +197,7 @@ def test_revoke_token_with_wrong_hint(self):
 
         data = {
             "client_id": self.application.client_id,
-            "client_secret": self.application.client_secret,
+            "client_secret": CLEARTEXT_SECRET,
             "token": tok.token,
             "token_type_hint": "refresh_token",
         }