Repository: jazzband/django-oauth-toolkit

Issue:
A limit on redirect_uri only up to 255 chars
**Describe the bug**
Cannot handle `redirect_uri` longer then 255 chars. [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.1.1) recommends to design system to be capable to work with URI at least to 8000 chars long.

> It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets.

**To Reproduce**
Send a request to `AuthorizationView` with `redirect_uri` longer than 255 chars.

**Expected behavior**
To save `redirect_uri` into `Grant` model.

**Version**
1.3.3

<!-- Have you tested with the latest version and/or master branch? -->
<!-- Replace '[ ]' with '[x]' to indicate that. -->
- [X] I have tested with the latest published release and it's still a problem.
- [x] I have tested with the master branch and it's still a problem.



Discussion:
@shaddeus thanks for raising this issue, can you provide a failing test case?
@MattBlack85 Thanks for super fast response. Yes, I can provide failing test case (unit test, yes?). I will try to send a merge request with fix.
I am going to change `AbstractGrant.redirect_uri` to `models.TextField()`. Would it be OK?
@shaddeus sounds like a plan!
Unfortunatelly I have a problem to write a corrent unit test for this. The problem depends on DB representation `models.CharField`. SQLite transform `VARCHAR(255)` into `TEXT`data type.
https://sqlite.org/datatype3.html#affinity_name_examples

So the unit test passes always. The test must be run on another DB engine to fail.

Should I append this "useless" test or not?
@shaddeus I'd say let's add it, we may want to discuss then if we should test against another DB or that would be enough

Resolving patch:
diff --git a/oauth2_provider/migrations/0003_auto_20201211_1314.py b/oauth2_provider/migrations/0003_auto_20201211_1314.py
new file mode 100644
--- /dev/null
+++ b/oauth2_provider/migrations/0003_auto_20201211_1314.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.4 on 2020-12-11 13:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('oauth2_provider', '0002_auto_20190406_1805'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='grant',
+            name='redirect_uri',
+            field=models.TextField(),
+        ),
+    ]
diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py
--- a/oauth2_provider/models.py
+++ b/oauth2_provider/models.py
@@ -220,7 +220,7 @@ class AbstractGrant(models.Model):
     code = models.CharField(max_length=255, unique=True)  # code comes from oauthlib
     application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)
     expires = models.DateTimeField()
-    redirect_uri = models.CharField(max_length=255)
+    redirect_uri = models.TextField()
     scope = models.TextField(blank=True)
 
     created = models.DateTimeField(auto_now_add=True)


Test patch:
diff --git a/tests/test_models.py b/tests/test_models.py
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -22,10 +22,15 @@
 UserModel = get_user_model()
 
 
-class TestModels(TestCase):
+class BaseTestModels(TestCase):
     def setUp(self):
         self.user = UserModel.objects.create_user("test_user", "test@example.com", "123456")
 
+    def tearDown(self):
+        self.user.delete()
+
+
+class TestModels(BaseTestModels):
     def test_allow_scopes(self):
         self.client.login(username="test_user", password="123456")
         app = Application.objects.create(
@@ -103,10 +108,7 @@ def test_scopes_property(self):
     OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL="tests.SampleRefreshToken",
     OAUTH2_PROVIDER_GRANT_MODEL="tests.SampleGrant",
 )
-class TestCustomModels(TestCase):
-    def setUp(self):
-        self.user = UserModel.objects.create_user("test_user", "test@example.com", "123456")
-
+class TestCustomModels(BaseTestModels):
     def test_custom_application_model(self):
         """
         If a custom application model is installed, it should be present in
@@ -237,7 +239,21 @@ def test_custom_grant_model_not_installed(self):
         oauth2_settings.GRANT_MODEL = "oauth2_provider.Grant"
 
 
-class TestGrantModel(TestCase):
+class TestGrantModel(BaseTestModels):
+    def setUp(self):
+        super().setUp()
+        self.application = Application.objects.create(
+            name="Test Application",
+            redirect_uris="",
+            user=self.user,
+            client_type=Application.CLIENT_CONFIDENTIAL,
+            authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
+        )
+
+    def tearDown(self):
+        self.application.delete()
+        super().tearDown()
+
     def test_str(self):
         grant = Grant(code="test_code")
         self.assertEqual("%s" % grant, grant.code)
@@ -247,11 +263,26 @@ def test_expires_can_be_none(self):
         self.assertIsNone(grant.expires)
         self.assertTrue(grant.is_expired())
 
+    def test_redirect_uri_can_be_longer_than_255_chars(self):
+        long_redirect_uri = "http://example.com/{}".format("authorized/" * 25)
+        self.assertTrue(len(long_redirect_uri) > 255)
+        grant = Grant.objects.create(
+            user=self.user,
+            code="test_code",
+            application=self.application,
+            expires=timezone.now(),
+            redirect_uri=long_redirect_uri,
+            scope="",
+        )
+        grant.refresh_from_db()
+
+        # It would be necessary to run test using another DB engine than sqlite
+        # that transform varchar(255) into text data type.
+        # https://sqlite.org/datatype3.html#affinity_name_examples
+        self.assertEqual(grant.redirect_uri, long_redirect_uri)
 
-class TestAccessTokenModel(TestCase):
-    def setUp(self):
-        self.user = UserModel.objects.create_user("test_user", "test@example.com", "123456")
 
+class TestAccessTokenModel(BaseTestModels):
     def test_str(self):
         access_token = AccessToken(token="test_token")
         self.assertEqual("%s" % access_token, access_token.token)
@@ -273,15 +304,15 @@ def test_expires_can_be_none(self):
         self.assertTrue(access_token.is_expired())
 
 
-class TestRefreshTokenModel(TestCase):
+class TestRefreshTokenModel(BaseTestModels):
     def test_str(self):
         refresh_token = RefreshToken(token="test_token")
         self.assertEqual("%s" % refresh_token, refresh_token.token)
 
 
-class TestClearExpired(TestCase):
+class TestClearExpired(BaseTestModels):
     def setUp(self):
-        self.user = UserModel.objects.create_user("test_user", "test@example.com", "123456")
+        super().setUp()
         # Insert two tokens on database.
         app = Application.objects.create(
             name="test_app",