Repository: jazzband/django-oauth-toolkit

Issue:
2.0.0: Client Secret Hasher Migration vs. Swapped Application Models
### Describe the bug

The forward migration introduced in #1093 to hash all previously saved client secrets runs on apps that use a custom application model and produces the following error:

```
AttributeError: Manager isn't available; 'oauth2_provider.Application' has been swapped for 'oauth.Application'
```

### To Reproduce

Define a custom application model in `settings.py`:

```
OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth.Application"
```

Migrate the database:

```
$ python manage.py migrate
```

### Expected behavior

Migrations run when using a swappable application model without throwing an error.

### Version 2.0

- [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:

Resolving patch:
diff --git a/oauth2_provider/migrations/0006_alter_application_client_secret.py b/oauth2_provider/migrations/0006_alter_application_client_secret.py
--- a/oauth2_provider/migrations/0006_alter_application_client_secret.py
+++ b/oauth2_provider/migrations/0006_alter_application_client_secret.py
@@ -1,6 +1,5 @@
 from django.db import migrations
-from django.contrib.auth.hashers import identify_hasher, make_password
-import logging
+from oauth2_provider import settings
 import oauth2_provider.generators
 import oauth2_provider.models
 
@@ -9,8 +8,8 @@ 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()
+    Application = apps.get_model(settings.APPLICATION_MODEL)
+    applications = Application._default_manager.all()
     for application in applications:
         application.save(update_fields=['client_secret'])
 


Test patch:
diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py
--- a/tests/migrations/0001_initial.py
+++ b/tests/migrations/0001_initial.py
@@ -1,9 +1,6 @@
-# Generated by Django 2.2.6 on 2019-10-24 20:21
+# Generated by Django 4.0.4 on 2022-05-27 21:07
 
-from django.conf import settings
 from django.db import migrations, models
-import django.db.models.deletion
-import oauth2_provider.generators
 
 
 class Migration(migrations.Migration):
@@ -11,112 +8,41 @@ class Migration(migrations.Migration):
     initial = True
 
     dependencies = [
-        migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
-        migrations.swappable_dependency(settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL),
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        migrations.swappable_dependency(settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
+    ]
+
+    run_before = [
+        ('oauth2_provider', '0001_initial'),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='SampleGrant',
+            name='BaseTestApplication',
             fields=[
-                ('id', models.BigAutoField(primary_key=True, serialize=False)),
-                ('code', models.CharField(max_length=255, unique=True)),
-                ('expires', models.DateTimeField()),
-                ('redirect_uri', models.CharField(max_length=255)),
-                ('scope', models.TextField(blank=True)),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('updated', models.DateTimeField(auto_now=True)),
-                ('code_challenge', models.CharField(blank=True, default='', max_length=128)),
-                ('code_challenge_method', models.CharField(blank=True, choices=[('plain', 'plain'), ('S256', 'S256')], default='', max_length=10)),
-                ('custom_field', models.CharField(max_length=255)),
-                ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tests_samplegrant', to=settings.AUTH_USER_MODEL)),
-                ("nonce", models.CharField(blank=True, max_length=255, default="")),
-                ("claims", models.TextField(blank=True)),
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
             ],
-            options={
-                'abstract': False,
-            },
         ),
         migrations.CreateModel(
-            name='SampleApplication',
+            name='SampleAccessToken',
             fields=[
-                ('id', models.BigAutoField(primary_key=True, serialize=False)),
-                ('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)),
-                ('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated')),
-                ('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
-                ('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32)),
-                ('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)),
-                ('name', models.CharField(blank=True, max_length=255)),
-                ('skip_authorization', models.BooleanField(default=False)),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('updated', models.DateTimeField(auto_now=True)),
-                ('custom_field', models.CharField(max_length=255)),
-                ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tests_sampleapplication', to=settings.AUTH_USER_MODEL)),
-                ('algorithm', models.CharField(max_length=5, choices=[('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='RS256')),
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
             ],
-            options={
-                'abstract': False,
-            },
         ),
         migrations.CreateModel(
-            name='SampleAccessToken',
+            name='SampleApplication',
             fields=[
-                ('id', models.BigAutoField(primary_key=True, serialize=False)),
-                ('token', models.CharField(max_length=255, unique=True)),
-                ('expires', models.DateTimeField()),
-                ('scope', models.TextField(blank=True)),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('updated', models.DateTimeField(auto_now=True)),
-                ('custom_field', models.CharField(max_length=255)),
-                ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
-                ('source_refresh_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s_refreshed_access_token', to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL)),
-                ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tests_sampleaccesstoken', to=settings.AUTH_USER_MODEL)),
-                ('id_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to=settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL)),
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
             ],
-            options={
-                'abstract': False,
-            },
         ),
         migrations.CreateModel(
-            name='BaseTestApplication',
+            name='SampleGrant',
             fields=[
-                ('id', models.BigAutoField(primary_key=True, serialize=False)),
-                ('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)),
-                ('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated')),
-                ('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
-                ('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32)),
-                ('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)),
-                ('name', models.CharField(blank=True, max_length=255)),
-                ('skip_authorization', models.BooleanField(default=False)),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('updated', models.DateTimeField(auto_now=True)),
-                ('allowed_schemes', models.TextField(blank=True)),
-                ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tests_basetestapplication', to=settings.AUTH_USER_MODEL)),
-                ('algorithm', models.CharField(max_length=5, choices=[('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='RS256')),
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
             ],
-            options={
-                'abstract': False,
-            },
         ),
         migrations.CreateModel(
             name='SampleRefreshToken',
             fields=[
-                ('id', models.BigAutoField(primary_key=True, serialize=False)),
-                ('token', models.CharField(max_length=255)),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('updated', models.DateTimeField(auto_now=True)),
-                ('revoked', models.DateTimeField(null=True)),
-                ('custom_field', models.CharField(max_length=255)),
-                ('access_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s_refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL)),
-                ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tests_samplerefreshtoken', to=settings.AUTH_USER_MODEL)),
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
             ],
-            options={
-                'abstract': False,
-                'unique_together': {('token', 'revoked')},
-            },
         ),
     ]
diff --git a/tests/migrations/0002_swapped_models.py b/tests/migrations/0002_swapped_models.py
new file mode 100644
--- /dev/null
+++ b/tests/migrations/0002_swapped_models.py
@@ -0,0 +1,346 @@
+# Generated by Django 4.0.4 on 2022-05-27 21:12
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import oauth2_provider.generators
+import oauth2_provider.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        migrations.swappable_dependency(settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL),
+        ('tests', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='algorithm',
+            field=models.CharField(blank=True, choices=[('', 'No OIDC support'), ('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='', max_length=5),
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='allowed_schemes',
+            field=models.TextField(blank=True),
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='authorization_grant_type',
+            field=models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='client_id',
+            field=models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True),
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            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.AddField(
+            model_name='basetestapplication',
+            name='client_type',
+            field=models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='created',
+            field=models.DateTimeField(auto_now_add=True),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='name',
+            field=models.CharField(blank=True, max_length=255),
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='redirect_uris',
+            field=models.TextField(blank=True, help_text='Allowed URIs list, space separated'),
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='skip_authorization',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='updated',
+            field=models.DateTimeField(auto_now=True),
+        ),
+        migrations.AddField(
+            model_name='basetestapplication',
+            name='user',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='application',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='created',
+            field=models.DateTimeField(auto_now_add=True),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='custom_field',
+            field=models.CharField(max_length=255),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='expires',
+            field=models.DateTimeField(),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='id_token',
+            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to=settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL),
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='scope',
+            field=models.TextField(blank=True),
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='source_refresh_token',
+            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s_refreshed_access_token', to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='token',
+            field=models.CharField(max_length=255, unique=True),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='updated',
+            field=models.DateTimeField(auto_now=True),
+        ),
+        migrations.AddField(
+            model_name='sampleaccesstoken',
+            name='user',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='algorithm',
+            field=models.CharField(blank=True, choices=[('', 'No OIDC support'), ('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='', max_length=5),
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='authorization_grant_type',
+            field=models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='client_id',
+            field=models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True),
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            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.AddField(
+            model_name='sampleapplication',
+            name='client_type',
+            field=models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='created',
+            field=models.DateTimeField(auto_now_add=True),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='custom_field',
+            field=models.CharField(max_length=255),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='name',
+            field=models.CharField(blank=True, max_length=255),
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='redirect_uris',
+            field=models.TextField(blank=True, help_text='Allowed URIs list, space separated'),
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='skip_authorization',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='updated',
+            field=models.DateTimeField(auto_now=True),
+        ),
+        migrations.AddField(
+            model_name='sampleapplication',
+            name='user',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='application',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='claims',
+            field=models.TextField(blank=True),
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='code',
+            field=models.CharField(max_length=255, unique=True),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='code_challenge',
+            field=models.CharField(blank=True, default='', max_length=128),
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='code_challenge_method',
+            field=models.CharField(blank=True, choices=[('plain', 'plain'), ('S256', 'S256')], default='', max_length=10),
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='created',
+            field=models.DateTimeField(auto_now_add=True),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='custom_field',
+            field=models.CharField(max_length=255),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='expires',
+            field=models.DateTimeField(),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='nonce',
+            field=models.CharField(blank=True, default='', max_length=255),
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='redirect_uri',
+            field=models.TextField(),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='scope',
+            field=models.TextField(blank=True),
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='updated',
+            field=models.DateTimeField(auto_now=True),
+        ),
+        migrations.AddField(
+            model_name='samplegrant',
+            name='user',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplerefreshtoken',
+            name='access_token',
+            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s_refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL),
+        ),
+        migrations.AddField(
+            model_name='samplerefreshtoken',
+            name='application',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplerefreshtoken',
+            name='created',
+            field=models.DateTimeField(auto_now_add=True),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplerefreshtoken',
+            name='custom_field',
+            field=models.CharField(max_length=255),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplerefreshtoken',
+            name='revoked',
+            field=models.DateTimeField(null=True),
+        ),
+        migrations.AddField(
+            model_name='samplerefreshtoken',
+            name='token',
+            field=models.CharField(default=1, max_length=255),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='samplerefreshtoken',
+            name='updated',
+            field=models.DateTimeField(auto_now=True),
+        ),
+        migrations.AddField(
+            model_name='samplerefreshtoken',
+            name='user',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='basetestapplication',
+            name='id',
+            field=models.BigAutoField(primary_key=True, serialize=False),
+        ),
+        migrations.AlterField(
+            model_name='sampleaccesstoken',
+            name='id',
+            field=models.BigAutoField(primary_key=True, serialize=False),
+        ),
+        migrations.AlterField(
+            model_name='sampleapplication',
+            name='id',
+            field=models.BigAutoField(primary_key=True, serialize=False),
+        ),
+        migrations.AlterField(
+            model_name='samplegrant',
+            name='id',
+            field=models.BigAutoField(primary_key=True, serialize=False),
+        ),
+        migrations.AlterField(
+            model_name='samplerefreshtoken',
+            name='id',
+            field=models.BigAutoField(primary_key=True, serialize=False),
+        ),
+        migrations.AlterUniqueTogether(
+            name='samplerefreshtoken',
+            unique_together={('token', 'revoked')},
+        ),
+    ]
diff --git a/tests/settings.py b/tests/settings.py
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -54,6 +54,7 @@
                 "django.template.context_processors.debug",
                 "django.template.context_processors.i18n",
                 "django.template.context_processors.media",
+                "django.template.context_processors.request",
                 "django.template.context_processors.static",
                 "django.template.context_processors.tz",
                 "django.contrib.messages.context_processors.messages",
diff --git a/tests/settings_swapped.py b/tests/settings_swapped.py
new file mode 100644
--- /dev/null
+++ b/tests/settings_swapped.py
@@ -0,0 +1,6 @@
+from .settings import *  # noqa
+
+
+OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "tests.SampleAccessToken"
+OAUTH2_PROVIDER_APPLICATION_MODEL = "tests.SampleApplication"
+OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "tests.SampleRefreshToken"