Search code examples
pythondjangopython-3.xpython-social-auth

Issue creating a django user with python-social-auth with a custom User model and UserManager


Seems like this isn't a unique problem, but I'm missing something in the solution. I'm using python-social-auth and logging in with Google. All seems to be going well, until it gets to the create_user part of the pipeline. I do have a custom User model and UserManager. On my User model I do have a role property that is hooked up to some choices. When social auth kicks in and logs someone in, it does call create_user in my User manager, however it's only passing email, and no additional fields. I was attempting to hook into the pipeline and add the required role property by adding it to the details social auth dict, but that doesn't seem to have any effect. How should I go about hooking into the create user property to add fields that won't exist as far as social auth is concerned?

User Model

class User(AbstractBaseUser, PermissionsMixin):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    date_joined = models.DateTimeField(default=timezone.now)
    email = models.EmailField(_("email address"), unique=True)
    first_name = models.CharField(max_length=240, blank=True)
    last_name = models.CharField(max_length=240, blank=True)
    role = models.IntegerField(choices=RoleChoices.choices)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    objects = UserManager()

    def __str__(self):
        return self.email

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}".strip()

And my UserManager:

class UserManager(BaseUserManager):
    """
    Custom user model manager where email is the unique identifiers
    for authentication instead of usernames.
    """

    def create_user(self, email, password, **extra_fields):
        """
        Create and save a User with the given email and password.
        """
        if not email:
            raise ValueError(_("The Email must be set"))

        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)

        if password is not None:
            user.set_password(password)

        user.save()
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        """
        Create and save a SuperUser with the given email and password.
        """
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)
        extra_fields.setdefault("is_active", True)
        extra_fields.setdefault("role", 1)

        if extra_fields.get("is_staff") is not True:
            raise ValueError(_("Superuser must have is_staff=True."))
        if extra_fields.get("is_superuser") is not True:
            raise ValueError(_("Superuser must have is_superuser=True."))
        return self.create_user(email, password, **extra_fields)

The Social auth config:

# Social Auth Config
AUTHENTICATION_BACKENDS = (
    'social_core.backends.google.GoogleOAuth2',
    'django.contrib.auth.backends.ModelBackend',
)

LOGIN_URL = 'login'
LOGOUT_URL = 'logout'
LOGIN_REDIRECT_URL = 'admin'
SOCIAL_AUTH_POSTGRES_JSONFIELD = True
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = os.getenv('GOOGLE_CLIENT_ID')
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = os.getenv('GOOGLE_CLIENT_SECRET')
SOCIAL_AUTH_USER_MODEL = 'search.User'
SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL = True
SOCIAL_AUTH_GOOGLE_OAUTH2_IGNORE_DEFAULT_SCOPE = True
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
    'https://www.googleapis.com/auth/calendar',
    'https://www.googleapis.com/auth/calendar.readonly',
    'https://www.googleapis.com/auth/userinfo.profile',
    'profile',
    'email'
]

SOCIAL_AUTH_PIPELINE = (
    'social_core.pipeline.social_auth.social_details',
    'social_core.pipeline.social_auth.social_uid',
    'social_core.pipeline.social_auth.auth_allowed',
    'social_core.pipeline.social_auth.social_user',
    'social_core.pipeline.user.get_username',
    'search.socialauth.add_role',
    'social_core.pipeline.user.create_user',
    'social_core.pipeline.social_auth.associate_user',
    'social_core.pipeline.social_auth.load_extra_data',
    'social_core.pipeline.user.user_details',
)

And finally the add_role function:

from .choices import RoleChoices


def add_role(**kwargs):
    kwargs['details']['role'] = RoleChoices.ARTIST
    return kwargs


Solution

  • The reason this isn't working is that the create_user function explicitly filters the contents of details to include only keys specified in a USER_FIELDS setting. This defaults to

    USER_FIELDS = ['username', 'email']
    

    so anything else with just be ignored. It doesn't appear to be documented, but you should be able to override this by creating a setting as follows:

    SOCIAL_AUTH_USER_FIELDS = ['username', 'email', 'role']
    

    Which will then ensure your role is passed through to the user instance.

    The rest of your pipeline and configuration looks fine.