jazzband / django-oauth-toolkit

OAuth2 goodies for the Djangonauts!
https://django-oauth-toolkit.readthedocs.io
Other
3.14k stars 794 forks source link

Impossible to swap models #634

Open gbataille opened 6 years ago

gbataille commented 6 years ago

Hey guys,

So I'm swapping the OAuth models on an application that is already live. All sorts of nice things there, but I'm getting around. I have however 2 comments

phillbaker commented 6 years ago

I think this comment: https://github.com/jazzband/django-oauth-toolkit/issues/605#issuecomment-397863421 offers a potential solution via using the run_before attribute in the migrations that create the replacement models?

gbataille commented 6 years ago

Hey @phillbaker,

no run_before does not solve anything. As the data model is crafted today, you cannot deploy one table before the other. What needs to happen is (for example, can be the other way around)

You basically need to manually amend the auto-generated migration

This is an extract of what I ended up with

operations = [
    migrations.CreateModel(
        name='AccessToken',
        fields=[
            ('id', models.BigAutoField(primary_key=True, serialize=False)),
            ('expires', models.DateTimeField()),
            ('scope', models.TextField(blank=True)),
            ('created', models.DateTimeField(auto_now_add=True)),
            ('updated', models.DateTimeField(auto_now=True)),
            ('token', models.TextField(unique=True)),
            ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
            ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='common_accesstoken', to=settings.AUTH_USER_MODEL)),
        ],
        options={
            'abstract': False,
            'swappable': 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL',
        },
    ),
    migrations.CreateModel(
        name='RefreshToken',
        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)),
            ('access_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='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='common_refreshtoken', to=settings.AUTH_USER_MODEL)),
            ('revoked', models.DateTimeField(null=True)),
        ],
        options={
            'abstract': False,
            'swappable': 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL',
        },
    ),
    migrations.AlterUniqueTogether(
        name='refreshtoken',
        unique_together=set([('token', 'revoked')]),
    ),
    migrations.AddField(
        model_name='accesstoken',
        name='source_refresh_token',
        field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL, related_name='refreshed_access_token'),
        preserve_default=False,
    )
]
agateblue commented 5 years ago

Same issue here. Trying to swap those models in my project via:


class Application(oauth2_models.AbstractApplication):
    pass

class Grant(oauth2_models.AbstractGrant):
    pass

class AccessToken(oauth2_models.AbstractAccessToken):
    pass

class RefreshToken(oauth2_models.AbstractRefreshToken):
    pass

raises this error when applying migrations:

oauth2_provider.RefreshToken.access_token: (fields.E304) Reverse accessor for 'RefreshToken.access_token' clashes with reverse accessor for 'RefreshToken.access_token'.
    HINT: Add or change a related_name argument to the definition for 'RefreshToken.access_token' or 'RefreshToken.access_token'.
oauth2_provider.RefreshToken.access_token: (fields.E305) Reverse query name for 'RefreshToken.access_token' clashes with reverse query name for 'RefreshToken.access_token'.
    HINT: Add or change a related_name argument to the definition for 'RefreshToken.access_token' or 'RefreshToken.access_token'.
oauth2_provider.RefreshToken.application: (fields.E304) Reverse accessor for 'RefreshToken.application' clashes with reverse accessor for 'RefreshToken.application'.
    HINT: Add or change a related_name argument to the definition for 'RefreshToken.application' or 'RefreshToken.application'.
users.RefreshToken.access_token: (fields.E304) Reverse accessor for 'RefreshToken.access_token' clashes with reverse accessor for 'RefreshToken.access_token'.
    HINT: Add or change a related_name argument to the definition for 'RefreshToken.access_token' or 'RefreshToken.access_token'.
users.RefreshToken.access_token: (fields.E305) Reverse query name for 'RefreshToken.access_token' clashes with reverse query name for 'RefreshToken.access_token'.
    HINT: Add or change a related_name argument to the definition for 'RefreshToken.access_token' or 'RefreshToken.access_token'.
users.RefreshToken.application: (fields.E304) Reverse accessor for 'RefreshToken.application' clashes with reverse accessor for 'RefreshToken.application'.
    HINT: Add or change a related_name argument to the definition for 'RefreshToken.application' or 'RefreshToken.application'.
fengyehong commented 5 years ago

Have the same problem when trying to swap AccessToken model, don't know how to solve it.

Alir3z4 commented 4 years ago

I have the same exact problem. Anyone has been able to find a solution for this yet ?

Alir3z4 commented 4 years ago

This is how I've fixed this.

I defined the models as follow:

# oauth/models.py

class Application(models.Model):
    pass

class Grant(models.Model):
    pass

class AccessToken(models.Model):
    pass

class RefreshToken(models.Model):
    pass

Then did makemigrations. Then, inherited the classes from OAuth abstract models:

# oauth/models.py

class Application(oauth2_models.AbstractApplication):
    pass

class Grant(oauth2_models.AbstractGrant):
    pass

class AccessToken(oauth2_models.AbstractAccessToken):
    pass

class RefreshToken(oauth2_models.AbstractRefreshToken):
    pass

Set the swapable models to point to these.

# settings.py

# OAuth
OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth.Application"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "oauth.AccessToken"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "oauth.RefreshToken"
OAUTH2_PROVIDER_GRANT_MODEL = "oauth.Grant"

Once again, did makemigrations and all goes good.

faxioman commented 4 years ago

We can conclude that the only working "out-of-the-box" swappable model is the Application model (which is the only covered by documentation). Probably, would be better to document this behaviour.

n2ygk commented 4 years ago

@Alir3z4 I'm trying the above and am still seeing the E.305 reverse accessor errors:

oauth.AccessToken.application: (fields.E304) Reverse accessor for 'AccessToken.application' clashes with reverse accessor for 'AccessToken.application'.
    HINT: Add or change a related_name argument to the definition for 'AccessToken.application' or 'AccessToken.application'.
etc.

What did I miss? Thanks.

n2ygk commented 4 years ago

Has this broken since release 1.1? https://gitmemory.com/issue/jazzband/django-oauth-toolkit/634/471959496

n2ygk commented 4 years ago

See https://docs.djangoproject.com/en/3.0/topics/db/models/#abstract-related-name Trying out a fix now...

ashiksl commented 4 years ago

I tried the above steps. Still facing the same problem( Error fields.E305) . Is there any workaround to fix this issue.

n2ygk commented 4 years ago

After spinning around a lot with E304's and so on, I got this to work but I don't believe this is truly a swappable set of models (but maybe it is). What I did:

  1. Defined my models that extend the built in ones. I only cared to extend AccessToken but had to make RefreshToken and Application as well due to the cross-references among them.
    
    from django.db import models
    from oauth2_provider import models as oauth2_models

class MyAccessToken(oauth2_models.AbstractAccessToken): """ extend the AccessToken model with the external introspection server response """ class Meta(oauth2_models.AbstractAccessToken.Meta): swappable = "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL"

introspection = models.TextField(null=True, blank=True)

class MyRefreshToken(oauth2_models.AbstractRefreshToken): """ extend the AccessToken model with the external introspection server response """ class Meta(oauth2_models.AbstractRefreshToken.Meta): swappable = "OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL"

class MyApplication(oauth2_models.AbstractApplication): class Meta(oauth2_models.AbstractApplication.Meta): swappable = "OAUTH2_PROVIDER_APPLICATION_MODEL"


2. Manually fixed up a migration to be similar to the one in oauth2_provider to work around circular references from MyAccessToken.source_refresh_token by deferring adding it until later.

3. My 'oauth' app in settings.INSTALLED_APPS:

INSTALLED_APPS = [ ... 'oauth2_provider', 'oauth', ... ]


4. Set my models in setting.OAUTH2_PROVIDER_...:

OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth.MyApplication" OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "oauth.MyAccessToken" OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "oauth.MyRefreshToken" OAUTH2_PROVIDER_GRANT_MODEL = "oauth2_provider.Grant"


5. Started with a totally empty set of migrations and then do a migrate. This basically ends up with the following tables:

... | oauth2_provider_grant | | oauth_myaccesstoken | | oauth_myapplication | | oauth_myrefreshtoken | ...

This is really not a swappable model as far as I understand what that means. But it was a way to extend the AccessToken model which I can than override the validator class for:

OAUTH2_PROVIDER = {

here's where we add the external introspection endpoint:

'RESOURCE_SERVER_INTROSPECTION_URL': OAUTH2_SERVER + '/as/introspect.oauth2',
'RESOURCE_SERVER_INTROSPECTION_CREDENTIALS': (
    os.environ.get('RESOURCE_SERVER_ID','demo'),
    os.environ.get('RESOURCE_SERVER_SECRET','demosecret')
),
'SCOPES': { k: '{} scope'.format(k) for k in OAUTH2_CONFIG['scopes_supported'] },
'OAUTH2_VALIDATOR_CLASS': 'oauth.oauth2_introspection.OAuth2Validator',  # my custom validator

}



I'm doing the above because I want to use some locally-added claims from my external OAuth2/OIDC AS introspection endpoint. This is kind of non-standard but my AS lets me configure added response fields.
armando-herastang commented 4 years ago

@n2ygk Could you elaborate on the migration file you customized?. I am trying to follow your workaround for this issue but I can't seem to get it done. The migration structure you did would be a good addition to this issue discussion.

n2ygk commented 4 years ago

@armando-herastang sure. Here it is. It's basically the same as the 0001 migration in DOT; I've just added an extra field to MyAccessToken.

# Generated by Django 3.0.3 on 2020-04-03 20:33

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import oauth2_provider.generators

class Migration(migrations.Migration):
    initial = True

    dependencies = [
        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),
        migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='MyApplication',
            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')], 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)),
                ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='oauth_myapplication', to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'abstract': False,
                'swappable': 'OAUTH2_PROVIDER_APPLICATION_MODEL',
            },
        ),
        migrations.CreateModel(
            name='MyAccessToken',
            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)),
                ('userinfo', models.TextField(blank=True, null=True)),
                ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='oauth_myaccesstoken_related_app', 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='oauth_myaccesstoken_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='oauth_myaccesstoken', to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'abstract': False,
                'swappable': 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL',
            },
        ),
        migrations.CreateModel(
            name='MyRefreshToken',
            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)),
                ('access_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oauth_myrefreshtoken_refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL)),
                ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='oauth_myrefreshtoken_related_app', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='oauth_myrefreshtoken', to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'abstract': False,
                'swappable': 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL',
                'unique_together': {('token', 'revoked')},
            },
        ),
        migrations.AddField(
            model_name='MyAccessToken',
            name='source_refresh_token',
            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oauth_myaccesstoken_refreshed_access_token', to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
        ),
    ]
armando-herastang commented 4 years ago

@n2ygk . Thanks for the quick response, but I am still getting the same issue. I want to do the same thing you did. I want to add a field that I will populate in a custom Oauth2Validator I wrote. I ended up using the migration you suggested, just included my additional field on the MyAccessToken, but I am still getting this when I run python manage.py migrate:

The field oauth2_provider.AccessToken.source_refresh_token was declared with a lazy reference to 'oauth.myrefreshtoken', but app 'oauth' isn't installed.
The field oauth2_provider.Grant.application was declared with a lazy reference to 'oauth.myapplication', but app 'oauth' isn't installed.
The field oauth2_provider.RefreshToken.access_token was declared with a lazy reference to 'oauth.myaccesstoken', but app 'oauth' isn't installed.
The field oauth2_provider.RefreshToken.application was declared with a lazy reference to 'oauth.myapplication', but app 'oauth' isn't installed.
n2ygk commented 4 years ago

Do you have your 'oauth' app in INSTALLED_APPS? Here's my complete:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'cat_manager',  # my app
    'rest_framework_json_api',
    'rest_framework',
    'debug_toolbar',
    'corsheaders',
    'oauth2_provider',
    'oauth',  # my oauth2_provider extension
    'django_filters',
    'django_extensions',
    'simple_history',
    'django_s3_storage',
]

Make sure you also have this in settings (I'm not sure the grant one is needed):

OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth.MyApplication"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "oauth.MyAccessToken"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "oauth.MyRefreshToken"
OAUTH2_PROVIDER_GRANT_MODEL = "oauth2_provider.Grant"
armando-herastang commented 4 years ago

@n2ygk . I do, although I do have this app inside a couple of folders, and I do have it in the INSTALLED_APPS like this:

   ...
    'api.apps.oauth'
   ...

And it's name on apps.py:

class OauthConfig(AppConfig):
    name = 'api.apps.oauth'

Then, on my settings file:

OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL= "oauth.MyAccessToken"
OAUTH2_PROVIDER_APPLICATION_MODEL= "oauth.MyApplication"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL= "oauth.MyRefreshToken"
OAUTH2_PROVIDER_GRANT_MODEL= "oauth2_provider.Grant"

I notice oauth.My.... here, but I can´t change it to api.apps.oauth because I get:

String model references must be of the form 'app_label.ModelName'.

I'm sorry, but maybe I am missing something. Thanks for the help

n2ygk commented 4 years ago

@armando-herastang

String model references must be of the form 'app_label.ModelName'.

This is a really annoying feature of this stuff that I wasted a lot of time looking at. It looks like a typical string-style module import but if you dig into the code, you'll see it does a simple split(".") and the [0] entry is the app name and the [1] entry is the model name, I believe expected to be in app/models.py. Try moving your oauth extension up to top-level.

danlamanna commented 4 years ago

What is the status of this issue? I'm having a really rough time trying to swap out the ACCESS_TOKEN_MODEL.

Is what @faxioman said correct?

We can conclude that the only working "out-of-the-box" swappable model is the Application model (which is the only covered by documentation). Probably, would be better to document this behaviour.

If not, is there a set of reproducible steps that allow one to override the access token model?

armando-herastang commented 4 years ago

@danlamanna I am sorry, but I wasn't able to do it either.

SpencerPinegar commented 4 years ago

https://github.com/wq/django-swappable-models

Could someone add this alpha stealth django feature to the project? It seems it would allow these circular references to be handled out of the box and in the future all the models would easily be customizable. Looking for some feedback before this gets undertaken so custom access keys are easier to implement for others in the future.

SpencerPinegar commented 4 years ago

https://github.com/jazzband/django-oauth-toolkit/issues/871

denniel-sadian commented 3 years ago

I did what @Alir3z4 did, the makemigrations command worked fine, but the migrations command didn't. It just said this:

ValueError: The field oauth2_provider.AccessToken.application was declared with a lazy reference to 'accounts.application', but app 'accounts' doesn't provide model 'application'.
The field oauth2_provider.AccessToken.source_refresh_token was declared with a lazy reference to 'accounts.refreshtoken', but app 'accounts' doesn't provide model 'refreshtoken'.
The field oauth2_provider.Grant.application was declared with a lazy reference to 'accounts.application', but app 'accounts' doesn't provide model 'application'.
The field oauth2_provider.RefreshToken.access_token was declared with a lazy reference to 'accounts.accesstoken', but app 'accounts' doesn't provide model 'accesstoken'.
The field oauth2_provider.RefreshToken.application was declared with a lazy reference to 'accounts.application', but app 'accounts' doesn't provide model 'application'.

Please help me. I've been doing this since yesterday and weren't able to sleep properly. I keep on thinking about this.

My models:

class Application(AbstractApplication):
    """"""

class Grant(AbstractGrant):
    application = models.ForeignKey(
        oauth2_settings.APPLICATION_MODEL,
        on_delete=models.CASCADE
    )

class AccessToken(AbstractAccessToken):
    token = models.CharField(max_length=500, unique=True)
    application = models.ForeignKey(
        oauth2_settings.APPLICATION_MODEL,
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        related_name='access_tokens'
    )
    source_refresh_token = models.OneToOneField(
        # unique=True implied by the OneToOneField
        oauth2_settings.REFRESH_TOKEN_MODEL,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='refreshed_access_tokens',
    )

class RefreshToken(AbstractRefreshToken):
    token = models.CharField(max_length=500)
    application = models.ForeignKey(
        oauth2_settings.APPLICATION_MODEL,
        on_delete=models.CASCADE,
        related_name='refresh_tokens'
    )
    access_token = models.OneToOneField(
        'accounts.AccessToken',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name="refresh_tokens",
    )

My settings:

OAUTH2_PROVIDER_APPLICATION_MODEL = 'accounts.Application'

OAUTH2_PROVIDER_GRANT_MODEL = 'accounts.Grant'

OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'accounts.AccessToken'

OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'accounts.RefreshToken'

OAUTH2_PROVIDER = {
    'SCOPES': {
        'read': 'Read scope',
        'write': 'Write scope',
        'groups': 'Access to your groups'},
    'OAUTH2_SERVER_CLASS': 'outdoorevents.oauth2.CustomServer',
    'APPLICATION_MODEL': 'accounts.Application',
    'GRANT_MODEL': 'accounts.Grant',
    'ACCESS_TOKEN_MODEL': 'accounts.AccessToken',
    'REFRESH_TOKEN_MODEL': 'accounts.RefreshToken'
}
danlamanna commented 3 years ago

@n2ygk Given my personal experience and what some of the others in this issue are saying, it's only safe to conclude that dozens or hundreds of man hours have been wasted trying to configure these models over the last few years. It seems clear that these models aren't swappable in practice. Is there something we can do to prevent this from happening in the future? A warning when trying to configure these settings, a change in documentation, etc?

ironyinabox commented 3 years ago

This is still in issue, in case anyone thought it went away :D

ironyinabox commented 3 years ago

Hey guys, I just wanted to let everyone on this thread know that I think I found a hacky workaround using django.db.migrations.SeperateDatabaseAndState https://docs.djangoproject.com/en/3.1/ref/migration-operations/#separatedatabaseandstate

The major issue is django refuses to run the migration swapping the oauth2 models cause they don't exist yet, and the hacks you can do locally to make it work are not practical when releasing to prod. However, you can just lie to django apparently.

go into your initial migration (0001_initial.py), and add this to the operations

operations = [
    migrations.SeparateDatabaseAndState(
            state_operations=[
                migrations.CreateModel(
                    name='AccessToken',
                    fields=[
                        ('id', models.BigAutoField(primary_key=True, serialize=False)),
                    ],
                ),
            ... and whatever other models you want to use a swappable dependency with

It wont actually build the table, but django will think you did, so it wont fail it's pre-migrate checks later.

then, generate an empty migration in the same app and copy-paste the faked table create operations over into database_operations instead this time.

 operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[ # note the difference here
                migrations.CreateModel(
                    name='AccessToken',
                    fields=[
                        ('id', models.BigAutoField(primary_key=True, serialize=False)),
                    ],
                ),

this time, it will create the tables, but django wont be aware of it, so you wont get a "table exists" error or anything.

Now you should be able to swap the models in your settings, make and run migrations, and it'll work as you originally expected it to.

I have no idea what kind of unintended consequences could arise from lying to django this way, so use this workaround at your own risk.

michaeljaszczuk commented 2 years ago

If you still have problems here is solution that worked for me:

Summary:

1. Add to settings.py:

OAUTH2_PROVIDER = {
    'ACCESS_TOKEN_EXPIRE_SECONDS': 3600,
    'SCOPES_BACKEND_CLASS': 'custom_oauth.backend.DjangoScopes',
    'APPLICATION_MODEL': 'custom_oauth.Application',
    'ACCESS_TOKEN_MODEL': 'custom_oauth.AccessToken',
}
OAUTH2_PROVIDER_APPLICATION_MODEL = 'custom_oauth.Application'
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'custom_oauth.AccessToken'
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "custom_oauth.RefreshToken"
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "custom_oauth.IDToken"

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'custom_oauth',

    'oauth2_provider',
]

2. Implement models:

class Application(AbstractApplication):
    objects = ApplicationManager()

    class Meta(AbstractApplication.Meta):
        swappable = "OAUTH2_PROVIDER_APPLICATION_MODEL"

    def natural_key(self):
        return (self.client_id,)

class AccessToken(AbstractAccessToken):
    class Meta(AbstractAccessToken.Meta):
        swappable = "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL"

class RefreshToken(AbstractRefreshToken):
    """
    extend the AccessToken model with the external introspection server response
    """
    class Meta(AbstractRefreshToken.Meta):
        swappable = "OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL"

class IDToken(AbstractIDToken):
    """
    extend the AccessToken model with the external introspection server response
    """
    class Meta(AbstractIDToken.Meta):
        swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL"

3. Run makemigrations and migrate

What was most important? You need to implement and overwrite ALL models (Access, Refresh, ID, Application)

KhoaDauTay commented 2 years ago

@michaeljaszczuk Can you share detail about how to set up your custom model ?. I am flowing you but not I can't.

michaeljaszczuk commented 2 years ago

What exactly do you need to know? Have you seen my requirements? Clean db, no migrations ran and packages versions? If so,

  1. Add settings
  2. Add models
  3. Run makemigrations and migrate

That worked for me 🤔 Code is above... Let me know what is unclear and i will try to help!

guyskk commented 2 years ago

I confirm @michaeljaszczuk solution https://github.com/jazzband/django-oauth-toolkit/issues/634#issuecomment-994821666 works as expected.

n2ygk commented 2 years ago

Perhaps someone would submit a documentation PR for this?

binnev commented 2 years ago

I have spent the better part of 2 days just trying to customise the Application model, which is the one model people claim is extendible! Here's what I did. I'd really appreciate any help or explanation.

Approach 1: follow the docs

Starting with a brand new project, I follow these instructions here: https://django-oauth-toolkit.readthedocs.io/en/latest/advanced_topics.html to the letter

  1. define my custom app model

    # authentication/models.py
    ...
    class MyApplication(AbstractApplication):
       agree = models.BooleanField()
  2. run makemigrations (trying to update the settings first causes a bunch of ValueErrors)

    ValueError: The field oauth2_provider.AccessToken.application was declared with a lazy reference to 'authentication.myapplication', but app 'authentication' isn't installed.
    ...
  3. point to my custom model in the settings

    # settings.py
    OAUTH2_PROVIDER_APPLICATION_MODEL='authentication.MyApplication'
    INSTALLED_APPS = [
    ...
    'oauth2_provider',
       'authentication',
    ]
  4. Add the run_before dependency to the migration, because when releasing to production I obviously won't be able to manually fudge the migration order:

       run_before = [
           ('oauth2_provider', '0001_initial'),
       ]
  5. run migrate

    Here I am confronted with

    AttributeError: Manager isn't available; 'oauth2_provider.Application' has been swapped for 'authentication.MyApplication'

    This is because in oauth2_provider/migrations/0006_alter_application_client_secret.py there is a hard reference to the library's Application model; NOT the swappable oauth2_settings.APPLICATION_MODEL

    Application = apps.get_model('oauth2_provider', 'application')  # <--- hard reference
    applications = Application.objects.all()  # <-- this .objects call causes the manager error

So the conclusion appears to be: I can't swap the model before migrating oauth2_provider.

Approach 2: swap the model after the docs flow

  1. do all the steps of the previous flow, except adding OAUTH2_PROVIDER_APPLICATION_MODEL='authentication.MyApplication' to the settings.

  2. Now migrate succeeds

  3. add OAUTH2_PROVIDER_APPLICATION_MODEL='authentication.MyApplication' to the settings.

  4. Add the oauth views to urls.py to get the token view:

    urlpatterns = [
       ...
       path('oauth/', include('oauth2_provider.urls', namespace='oauth2_provider')),
    ]
  5. Create a MyApplication instance with grant_type="client credentials" and client_type="confidential"

  6. POST to the /oauth/token view, passing the MyApplication instance's credentials, correctly encoded in base64 etc, as basic auth Here I get:

    django.db.utils.IntegrityError: insert or update on table "oauth2_provider_accesstoken" violates foreign key constraint "oauth2_provider_acce_application_id_b22886e1_fk_oauth2_pr"
    DETAIL:  Key (application_id)=(1) is not present in table "oauth2_provider_application".

    The view tries to create an AccessToken for the MyApplication with id=1. But the AccessToken.application ForeignKey appears to point to table oauth2_provider_application; not table authentication_myapplication.

  7. Inspect the tables to check: using

    psql
    \d oauth2_provider_accesstoken

    here we see

    Foreign-key constraints:
       "oauth2_provider_acce_application_id_b22886e1_fk_oauth2_pr" FOREIGN KEY (application_id) REFERENCES oauth2_provider_application(id) DEFERRABLE INITIALLY DEFERRED

    So AccessToken.application does indeed point to oauth2_provider_application, not authentication_myapplication. This makes sense, because when we migrated oauth2_provider, we hadn't yet swapped the model in the settings.

binnev commented 2 years ago

I should add: I'm using django-oauth-toolkit v2.0. And I see that in our other project (which has django-oauth-toolkit = "<2") migration 0006 is not yet present. So this seems like a v2.0 thing.

I guess this might not affect you if you are upgrading from v1.x to v2.0 (because migrations 0001-0005 will have been run a long time ago). But it is definitely breaking new v2.0 setups.

binnev commented 2 years ago

When I try Approach 1 described above using oauth-toolkit v1.7.1, it just works.

There goes 2 days of my life :wave:

binnev commented 2 years ago

Just curious to see if setting up my custom Application in 1.7.1 and then upgrading to 2.0.0 would work -- but migration 0006 also causes an error in this case.

AttributeError: Manager isn't available; 'oauth2_provider.Application' has been swapped for 'authentication.MyApplication'
n2ygk commented 2 years ago

Just curious to see if setting up my custom Application in 1.7.1 and then upgrading to 2.0.0 would work -- but migration 0006 also causes an error in this case.

AttributeError: Manager isn't available; 'oauth2_provider.Application' has been swapped for 'authentication.MyApplication'

@binnev See pinned issue #1146 which is pending a fix. I hope to get to this in the coming week or so.

ippeiukai commented 2 years ago

:memo: what I did to setup custom models, for future reference:

  1. add empty shell of models to sample_identity_oauth2_provider.models.
  2. makemigrations
  3. add inheritance to the models in sample_identity_oauth2_provider.models.
  4. specify the custom models to OAUTH2PROVIDER*_MODEL in settings
  5. makemigrations
  6. merge two migrations in sample_identity_oauth2_provider manually, adjust dependencies etc.
  7. add DbRouter to skip migrations in oauth2_provider, add to DATABASE_ROUTERS in settings
  8. migrate

Now you can start customizing models in sample_identity_oauth2_provider.models.

Following are the end results that you could just copy to your codebase:

models.py ```Py from django.db import models from oauth2_provider.models import ( AbstractAccessToken, AbstractApplication, AbstractGrant, AbstractIDToken, AbstractRefreshToken, ) class OAuth2AccessToken(AbstractAccessToken): class Meta(AbstractAccessToken.Meta): db_table = "oauth2_provider_accesstoken__custom" class OAuth2RefreshToken(AbstractRefreshToken): class Meta(AbstractRefreshToken.Meta): db_table = "oauth2_provider_refreshtoken__custom" class OAuth2Application(AbstractApplication): class Meta(AbstractApplication.Meta): db_table = "oauth2_provider_application__custom" class OAuth2Grant(AbstractGrant): class Meta(AbstractGrant.Meta): db_table = "oauth2_provider_grant__custom" class OAuth2IdToken(AbstractIDToken): class Meta(AbstractIDToken.Meta): db_table = "oauth2_provider_idtoken__custom" ```
migrations/0001_initial.py ```Py # Generated by Django 3.2.13 on 2022-06-21 12:48 import uuid import django.db.models.deletion import django.utils.timezone import oauth2_provider.generators import oauth2_provider.models from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] run_before = [ ("oauth2_provider", "0001_initial"), ] operations = [ migrations.CreateModel( name="OAuth2AccessToken", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ], options={ "db_table": "oauth2_provider_accesstoken__custom", }, ), migrations.CreateModel( name="OAuth2Application", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ], options={ "db_table": "oauth2_provider_application__custom", }, ), migrations.CreateModel( name="OAuth2Grant", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ], options={ "db_table": "oauth2_provider_grant__custom", }, ), migrations.CreateModel( name="OAuth2RefreshToken", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ], options={ "db_table": "oauth2_provider_refreshtoken__custom", }, ), migrations.CreateModel( name="OAuth2IdToken", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ], options={ "db_table": "oauth2_provider_idtoken__custom", }, ), migrations.AddField( model_name="oauth2accesstoken", 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="oauth2accesstoken", name="created", field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="oauth2accesstoken", name="expires", field=models.DateTimeField(default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="oauth2accesstoken", 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="oauth2accesstoken", name="scope", field=models.TextField(blank=True), ), migrations.AddField( model_name="oauth2accesstoken", name="source_refresh_token", field=models.OneToOneField( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="refreshed_access_token", to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL, ), ), migrations.AddField( model_name="oauth2accesstoken", name="token", field=models.CharField(max_length=255, default="", unique=True), preserve_default=False, ), migrations.AddField( model_name="oauth2accesstoken", name="updated", field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name="oauth2accesstoken", name="user", field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="sample_identity_oauth2_provider_oauth2accesstoken", to=settings.AUTH_USER_MODEL, ), ), migrations.AddField( model_name="oauth2application", 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="oauth2application", 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"), ], default="", max_length=32, ), preserve_default=False, ), migrations.AddField( model_name="oauth2application", 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="oauth2application", 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="oauth2application", name="client_type", field=models.CharField( choices=[("confidential", "Confidential"), ("public", "Public")], default="", max_length=32 ), preserve_default=False, ), migrations.AddField( model_name="oauth2application", name="created", field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="oauth2application", name="name", field=models.CharField(blank=True, max_length=255), ), migrations.AddField( model_name="oauth2application", name="redirect_uris", field=models.TextField(blank=True, help_text="Allowed URIs list, space separated"), ), migrations.AddField( model_name="oauth2application", name="skip_authorization", field=models.BooleanField(default=False), ), migrations.AddField( model_name="oauth2application", name="updated", field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name="oauth2application", name="user", field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="sample_identity_oauth2_provider_oauth2application", to=settings.AUTH_USER_MODEL, ), ), migrations.AddField( model_name="oauth2grant", name="application", field=models.ForeignKey( default=0, on_delete=django.db.models.deletion.CASCADE, to="sample_identity_oauth2_provider.oauth2application" ), preserve_default=False, ), migrations.AddField( model_name="oauth2grant", name="claims", field=models.TextField(blank=True), ), migrations.AddField( model_name="oauth2grant", name="code", field=models.CharField(default="", max_length=255, unique=True), preserve_default=False, ), migrations.AddField( model_name="oauth2grant", name="code_challenge", field=models.CharField(blank=True, default="", max_length=128), ), migrations.AddField( model_name="oauth2grant", name="code_challenge_method", field=models.CharField( blank=True, choices=[("plain", "plain"), ("S256", "S256")], default="", max_length=10 ), ), migrations.AddField( model_name="oauth2grant", name="created", field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="oauth2grant", name="expires", field=models.DateTimeField(default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="oauth2grant", name="nonce", field=models.CharField(blank=True, default="", max_length=255), ), migrations.AddField( model_name="oauth2grant", name="redirect_uri", field=models.TextField(default=""), preserve_default=False, ), migrations.AddField( model_name="oauth2grant", name="scope", field=models.TextField(blank=True), ), migrations.AddField( model_name="oauth2grant", name="updated", field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name="oauth2grant", name="user", field=models.ForeignKey( default=0, on_delete=django.db.models.deletion.CASCADE, related_name="sample_identity_oauth2_provider_oauth2grant", to=settings.AUTH_USER_MODEL, ), preserve_default=False, ), migrations.AddField( model_name="oauth2idtoken", 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="oauth2idtoken", name="created", field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="oauth2idtoken", name="expires", field=models.DateTimeField(default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="oauth2idtoken", name="jti", field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name="JWT Token ID"), ), migrations.AddField( model_name="oauth2idtoken", name="scope", field=models.TextField(blank=True), ), migrations.AddField( model_name="oauth2idtoken", name="updated", field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name="oauth2idtoken", name="user", field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="sample_identity_oauth2_provider_oauth2idtoken", to=settings.AUTH_USER_MODEL, ), ), migrations.AddField( model_name="oauth2refreshtoken", name="access_token", field=models.OneToOneField( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="refresh_token", to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL, ), ), migrations.AddField( model_name="oauth2refreshtoken", name="application", field=models.ForeignKey( default=0, on_delete=django.db.models.deletion.CASCADE, to="sample_identity_oauth2_provider.oauth2application" ), preserve_default=False, ), migrations.AddField( model_name="oauth2refreshtoken", name="created", field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="oauth2refreshtoken", name="revoked", field=models.DateTimeField(null=True), ), migrations.AddField( model_name="oauth2refreshtoken", name="token", field=models.CharField(default="", max_length=255), preserve_default=False, ), migrations.AddField( model_name="oauth2refreshtoken", name="updated", field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name="oauth2refreshtoken", name="user", field=models.ForeignKey( default=0, on_delete=django.db.models.deletion.CASCADE, related_name="sample_identity_oauth2_provider_oauth2refreshtoken", to=settings.AUTH_USER_MODEL, ), preserve_default=False, ), migrations.AlterField( model_name="oauth2accesstoken", name="id", field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterField( model_name="oauth2application", name="id", field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterField( model_name="oauth2grant", name="id", field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterField( model_name="oauth2idtoken", name="id", field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterField( model_name="oauth2refreshtoken", name="id", field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterUniqueTogether( name="oauth2refreshtoken", unique_together={("token", "revoked")}, ), migrations.AlterModelTable( name="oauth2idtoken", table="oauth2_provider_idtoken__custom", ), ] ```
db_router.py ```Py class DisableOauth2ProviderMigrationsRouter: def db_for_read(self, model, **hints): return None def db_for_write(self, model, **hints): return None def allow_relation(self, obj1, obj2, **hints): return None def allow_migrate(self, db, app_label, model_name=None, **hints): if app_label == "oauth2_provider": return False return None ```
ShanWeera commented 2 years ago

On version 2.1.0. Not sure if this can help anyone else but adding

    run_before = [
        ('oauth2_provider', '0001_initial'),
    ]

to the Application model swap migration, and then specifying the new model with OAUTH2_PROVIDER_APPLICATION_MODEL prior (yes, despite the documentation explicitly mentioning not to do this) to running the migration worked for me.

R70YNS commented 2 years ago

Managed to get it working as per @michaeljaszczuk's suggestion above https://github.com/jazzband/django-oauth-toolkit/issues/634#issuecomment-994821666. Tried to get it working on a DB that wasn't empty to save data transfer but required empty DB to work properly.

iamriel commented 1 year ago

For anyone that's starting from a new app or haven't ran any migration for this app yet and want to swap models, here's the hacky way which works for me:

  1. remove/rename the oauth2_provider migrations directory from the site package
  2. create necessary models as well as set the model classes in settings
  3. run ./manage.py makemigrations custom_app_name
  4. update migration file and add

    run_before = [
        ("oauth2_provider", "0001_initial"),
    ]
  1. bring back the migrations directory from the site package
  2. run ./manage.py migrate

I have encountered all of the errors mentioned above and followed the fixes mentioned but nothing worked for me.

I'm using version 2.2.0 btw.

jeriox commented 1 year ago

Also stumbled upon this and the only think that worked was the solution in https://github.com/jazzband/django-oauth-toolkit/issues/634#issuecomment-994821666

twoodcock commented 1 year ago

Same issue here. Trying to swap those models in my project via:


class Application(oauth2_models.AbstractApplication):
    pass

class Grant(oauth2_models.AbstractGrant):
    pass

class AccessToken(oauth2_models.AbstractAccessToken):
    pass

class RefreshToken(oauth2_models.AbstractRefreshToken):
    pass

raises this error when applying migrations:

oauth2_provider.RefreshToken.access_token: (fields.E304) Reverse accessor for 'RefreshToken.access_token' clashes with reverse accessor for 'RefreshToken.access_token'.
  HINT: Add or change a related_name argument to the definition for 'RefreshToken.access_token' or 'RefreshToken.access_token'.
oauth2_provider.RefreshToken.access_token: (fields.E305) Reverse query name for 'RefreshToken.access_token' clashes with reverse query name for 'RefreshToken.access_token'.
  HINT: Add or change a related_name argument to the definition for 'RefreshToken.access_token' or 'RefreshToken.access_token'.
oauth2_provider.RefreshToken.application: (fields.E304) Reverse accessor for 'RefreshToken.application' clashes with reverse accessor for 'RefreshToken.application'.
  HINT: Add or change a related_name argument to the definition for 'RefreshToken.application' or 'RefreshToken.application'.
users.RefreshToken.access_token: (fields.E304) Reverse accessor for 'RefreshToken.access_token' clashes with reverse accessor for 'RefreshToken.access_token'.
  HINT: Add or change a related_name argument to the definition for 'RefreshToken.access_token' or 'RefreshToken.access_token'.
users.RefreshToken.access_token: (fields.E305) Reverse query name for 'RefreshToken.access_token' clashes with reverse query name for 'RefreshToken.access_token'.
  HINT: Add or change a related_name argument to the definition for 'RefreshToken.access_token' or 'RefreshToken.access_token'.
users.RefreshToken.application: (fields.E304) Reverse accessor for 'RefreshToken.application' clashes with reverse accessor for 'RefreshToken.application'.
  HINT: Add or change a related_name argument to the definition for 'RefreshToken.application' or 'RefreshToken.application'.

I got around this.

Step 1: Create your custom model with related fields set to None. In my case, I was creating (only) a JWTAccessToken model, so I set id_token = None and source_refresh_token = None.

Setting the related fields to non (temporarily) allows the migration to work without E304 and the migration succeded without having the other error I was getting: "[...]token was declared with a lazy reference to 'myappname.accesstoken', but app 'myappname' isn't installed".

Step 2: With that first migration completed on myappname, we are free to restore the related attributes. That is, in my case, remove id_token = None and source_refresh_token = None.

Before creating the final migration, change the settings file to swap the models. Note that I had to add values pointing OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'oauth2_provider.RefreshToken' and the equivalent for the id token model.

As has been noted elsewhere, squashmigrations is not going to work because between these 2 migrations, you have to change the settings file. As far as I know that can't be deployed as a migration.

AitorPo commented 1 year ago

Hi! I faced every issue that is explained here and after struggling for some hours I found a potential solid solution. Once you fix everything (as people explain here) you should arrive to the point when the manager is not available

AttributeError: Manager isn't available; 'oauth2_provider.Application' has been swapped for XXX

Well, that's good! Now, on the migration where your models are created I did not need the "run_first" statement but to fulfill the "dependencies". This is my migration (everything is needed because of the foreign keys between models)

`from django.conf import settings from django.db import migrations, models import django.db.models.deletion import oauth2_provider.generators import uuid

class Migration(migrations.Migration):

initial = True

dependencies = [
    migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
    migrations.swappable_dependency(settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
]

operations = [
    migrations.CreateModel(
        name='Application',
        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)),
            ('algorithm', models.CharField(blank=True, choices=[('', 'No OIDC support'), ('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='', max_length=5)),
        ],
        options={
            'abstract': False,
            'swappable': 'OAUTH2_PROVIDER_APPLICATION_MODEL',
        },
    ),
    migrations.CreateModel(
        name='AccessToken',
        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)),
        ],
        options={
            'abstract': False,
        },
    ),
    migrations.CreateModel(
        name='Grant',
        fields=[
            ('id', models.BigAutoField(primary_key=True, serialize=False)),
            ('code', models.CharField(max_length=255, unique=True)),
            ('expires', models.DateTimeField()),
            ('redirect_uri', models.TextField()),
            ('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)),
            ('nonce', models.CharField(blank=True, default='', max_length=255)),
            ('claims', models.TextField(blank=True)),
        ],
        options={
            'abstract': False,
        },
    ),
    migrations.CreateModel(
        name='IDToken',
        fields=[
            ('id', models.BigAutoField(primary_key=True, serialize=False)),
            ('jti', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='JWT Token ID')),
            ('expires', models.DateTimeField()),
            ('scope', models.TextField(blank=True)),
            ('created', models.DateTimeField(auto_now_add=True)),
            ('updated', models.DateTimeField(auto_now=True)),
        ],
        options={
            'abstract': False,
        },
    ),
    migrations.CreateModel(
        name='RefreshToken',
        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)),
            ('access_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL)),
            ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
        ],
        options={
            'abstract': False,
        },
    ),
]

`

First step: accomplished. The second step brings us to the "settings.py" file. I've had to add these definitions. `from oauth2_provider.settings import oauth2_settings OAUTH2_PROVIDER_APPLICATION_MODEL = 'your_app.Application' OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'your_app.AccessToken' OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'your_app.RefreshToken' OAUTH2_PROVIDER_ID_TOKEN_MODEL = 'your_app.IDToken' OAUTH2_PROVIDER_GRANT_MODEL = 'your_app.Grant'

"""THIS IS WHAT IS GOING TO MAP YOUR MODEL WITH THE MANAGER AND ENABLE IT INSTEAD OF CALLING THE ORIGINAL APP PLUS YOU WON'T NEED TO REGISTER your_app.Application, your_app.AccessToken, ... ON THE ADMIN BECAUSE EVERYTHING IS ALREADY MAPPED THROUGH THE PACKAGE""" oauth2_settings.APPLICATION_MODEL = 'your_app.Application' oauth2_settings.ACCESS_TOKEN_MODEL = 'your_app.AccessToken' oauth2_settings.REFRESH_TOKEN_MODEL = 'your_app.RefreshToken' oauth2_settings.ID_TOKEN_MODEL = 'your_app.IDToken' oauth2_settings.GRANT_MODEL = 'your_app.Grant'`

The "bug" here is that the package is retrieving the modules like this: APPLICATION_MODEL = getattr(settings, "OAUTH2_PROVIDER_APPLICATION_MODEL", "oauth2_provider.Application") And looks like is not working as good as should be. So the solution is: OVERRIDE those little annoying settings and your problems are gone.

Step 3: python manage.py runserver and enjoy happy coding :)

iaggocapitanio1 commented 6 months ago

If you still have problems here is solution that worked for me:

Summary:

  • My db is clear, no migrations made
  • I needed to overwrite ALL models
  • django-oauth-toolkit==1.5.0
  • Django==4.0

1. Add to settings.py:

OAUTH2_PROVIDER = {
    'ACCESS_TOKEN_EXPIRE_SECONDS': 3600,
    'SCOPES_BACKEND_CLASS': 'custom_oauth.backend.DjangoScopes',
    'APPLICATION_MODEL': 'custom_oauth.Application',
    'ACCESS_TOKEN_MODEL': 'custom_oauth.AccessToken',
}
OAUTH2_PROVIDER_APPLICATION_MODEL = 'custom_oauth.Application'
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'custom_oauth.AccessToken'
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "custom_oauth.RefreshToken"
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "custom_oauth.IDToken"

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'custom_oauth',

    'oauth2_provider',
]

2. Implement models:

class Application(AbstractApplication):
    objects = ApplicationManager()

    class Meta(AbstractApplication.Meta):
        swappable = "OAUTH2_PROVIDER_APPLICATION_MODEL"

    def natural_key(self):
        return (self.client_id,)

class AccessToken(AbstractAccessToken):
    class Meta(AbstractAccessToken.Meta):
        swappable = "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL"

class RefreshToken(AbstractRefreshToken):
    """
    extend the AccessToken model with the external introspection server response
    """
    class Meta(AbstractRefreshToken.Meta):
        swappable = "OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL"

class IDToken(AbstractIDToken):
    """
    extend the AccessToken model with the external introspection server response
    """
    class Meta(AbstractIDToken.Meta):
        swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL"

3. Run makemigrations and migrate

What was most important? You need to implement and overwrite ALL models (Access, Refresh, ID, Application)

If someone still are having trouble after do that, consider deleting the migrations in the virtual enviroment of the oauth2 package

ygag-basil commented 5 months ago

custom_oauth

ValueError: The field oauth2_provider.AccessToken.application was declared with a lazy reference to 'custom_oauth.application', but app 'custom_oauth' isn't installed. The field oauth2_provider.AccessToken.id_token was declared with a lazy reference to 'custom_oauth.idtoken', but app 'custom_oauth' isn't installed. The field oauth2_provider.AccessToken.source_refresh_token was declared with a lazy reference to 'custom_oauth.refreshtoken', but app 'custom_oauth' isn't installed. The field oauth2_provider.Grant.application was declared with a lazy reference to 'custom_oauth.application', but app 'custom_oauth' isn't installed. The field oauth2_provider.IDToken.application was declared with a lazy reference to 'custom_oauth.application', but app 'custom_oauth' isn't installed. The field oauth2_provider.RefreshToken.access_token was declared with a lazy reference to 'custom_oauth.accesstoken', but app 'custom_oauth' isn't installed. The field oauth2_provider.RefreshToken.application was declared with a lazy reference to 'custom_oauth.application', but app 'custom_oauth' isn't installed.

Giving above errors