Artur Siepietowski
Artur Siepietowski

Reputation: 1092

Django Two tables for users authorization

I have to implement a backward-compatible Django server with PHP app. Legacy app is using LegacyUser model for authorization which more or less like:

class LegacyUser(models.Model):
    id = models.BigAutoField(primary_key=True)
    email = models.CharField(max_length=255, unique=True)
    password = models.CharField(max_length=120, blank=True, null=True)
    ...
    other_data_fields
    ...

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []
    EMAIL_FIELD = 'email'

In a new system, I do not have to add new records of LegacyUser (but I can).

For now, the legacy system does not allow to create multiple users per Group. Actually LegacyUser should be considered as a group but I was implemented as a user.

Right now I have to implement multiple Users per each LegacyUser so I have added proper Django user for authorization like:

class User(AbstractUser):
    username = None
    email = models.EmailField(unique=True)
    legacy_user = models.ForeignKey(LegacyUser, on_delete=models.DO_NOTHING)
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['publisher']

    class Meta:
        managed = True
        db_table = 'user'

and in base.py settings:

...
AUTH_USER_MODEL = 'api.LegacyUser'

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'AUTH_HEADER_TYPES': ('Bearer',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'id',
}

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend', #Supports User
    'common.auth.backends.LegacyBackend'         #Supports LegacyUser
]
...

The new application should enable to login both LegacyUser and User. After authorization LegacyUser.id should be used as USER_ID_CLAIM.

This means if I have one LegacyUser like:

{
    "id": 1,
    "email": [email protected],
    "password": "hashed_password",
    ...
}

And two Users e.g.

{
    "id": 1,
    "email": [email protected],
    "password": "hashed_password",
    "legacy_user_id": 1,
    ...
}
{
    "id": 2,
    "email": [email protected],
    "password": "hashed_password",
    "legacy_user_id": 1,
    ...
}

Valuse of LegacyUser.id or User.legacy_user.id should be visible in request.

Whats more email field have to be unique altogether in LegacyUser and User

Is this possible to have two User Authorization models? AUTH_USER_MODEL only enables me to have one such a model

AUTH_USER_MODEL = 'api.LegacyUser'

The solution which came to my mind (to archive backward-compatible apps) is to copy current passwords and emails from LegacyUser to the new model User model and mark them as the most privileges.

Having synchronized passwords for both new and legacy system is also a must so I can use some stored procedure, that changes the password in both places on the update, in the database? For me, it smells very bad and maybe there is some other way to do this without synchronizing these passwords or just using two tables for authorization?

Edit: To solve this issue I have created MySQL view with unified user data needed only for authorization like:

CREATE OR REPLACE VIEW unified_user AS
    SELECT email as email, password as password, is_active as is_active, last_login as last_login
    FROM user
    UNION ALL
    SELECT email as email, password as password, 1 as is_active, null as last_login
    FROM legacy_user;

So synchronization of passwords will be done automatically as view will be updated

Upvotes: 0

Views: 2311

Answers (2)

Saiful Azad
Saiful Azad

Reputation: 1921

You need a flag to differentiate auth for legacy users and new user. Let me focus on Session-based auth.

AUTHENTICATION_BACKENDS = [
'common.auth.backends.CustomUserBackend'

]

And remove it django.contrib.auth.middleware.AuthenticationMiddleware from middlewares and place your CustomAuthenticationMiddleware.

MIDDLEWARE = [
   ...
    'Your CustomAuthenticationMiddleware',
   ....
]

at CustomAuthenticationMiddleware, attach your user based on the session variable.

implement those methods for class CustomUserBackend. Please read Auth

def get_user(self, user_id):
    # read is_legacy_user from session
    # Try to get user based on session is_legacy_user
    # Note that this method is called in CustomAuthenticationMiddleware middleware. 
    # and you have to read from 2 separate table. Session is your friend to differentiate which table we need to fetch.
def authenticate(self, request, email, password, is_legacy_user):
    if is_legacy_user:
       #write the logic for legacy user.
    else:
       #write the logic for new user.

From view call the authenticate function (from django.contrib.auth import authenticate).

For legacy user

user = authenticate(email, password, is_legacy_user=True)
# Store the user ID in session as well as is_legacy_user value

For new user

authenticate(email, password, is_legacy_user=False)
# Store the user ID in session as well as is_legacy_user value

I hope this is how you will solve the session-based auth issue. If that work; will move to Token-based.

Upvotes: 1

dotslash227
dotslash227

Reputation: 408

You want to look at [a https://docs.djangoproject.com/en/2.2/topics/auth/customizing/. You can have as many different ways to authenticate as you wish. Part of the information stored in the Session record is which authentication backend was used successfully. It's slightly involved, but they give you all of the control necessary to do pretty much whatever you like.

I used this on a system a number of years ago where the primary user/password information was coming from an external subscription management server. If the user/pass did not work on the normal User-auth system, I checked a different system. If it succeeded, I created a new User on the fly.

Upvotes: 0

Related Questions