djoser icon indicating copy to clipboard operation
djoser copied to clipboard

Allow configuration for case-insensitive emails

Open YPCrumble opened this issue 5 years ago • 12 comments

Thanks for writing Djoser! I really like the library and API.

The only thing I've found frustrating recently is that emails are automatically treated as case-sensitive per discussion in #87. What I've had to do is override all of the default serializers with the following method:

    def validate_email(self, value):
        return value.lower()

I suspect that a lot of sites using Djoser store emails as all-lowercase. Would you consider a PR that would apply something like the following code? This would incorporate a default value of CASE_SENSITIVE_EMAIL = True but also allow me to easily change it globally for Djoser in terms of storing email, user login, password reset, etc. Happy to submit if it's helpful. Let me know what you think!

    def validate_email(self, value):
        if settings.DJOSER['CASE_SENSITIVE_EMAIL'] is False:
            return value.lower()
        return value

YPCrumble avatar Oct 17 '19 20:10 YPCrumble

You brought up interesting topic. We want djoser to be very flexible while providing sane defaults. Your idea seems to adhere to these goals. So please do send a PR - I'll be more than happy to review it. But instead of if {setting} is False just write if not {setting} :wink:

dekoza avatar Oct 18 '19 05:10 dekoza

@YPCrumble, thanks for using djoser!

In general I believe this is a bit broader feature which I would call "data sanitization". This can be applied to username, email address, etc.

I've just found similar discussion in #146

haxoza avatar Oct 18 '19 10:10 haxoza

@haxoza makes sense. As I think through it, I can't think of a field other than email and username that would make sense for default sanitization. I also can't think of a sanitization method other than case-insensitivity that makes sense to include in Djoser.

If you agree my suggestion would be to simply add two optional settings:

DJOSER_CASE_SENSITIVE_EMAIL = True
DJOSER_CASE_SENSITIVE_USERNAME = True

If you agree I'm happy to make the PR.

YPCrumble avatar Dec 10 '19 15:12 YPCrumble

Is this a feature that's being considered? I would like to make username case insensitive, but I don't currently see any means of doing so. What are my options?

FendyPierre avatar Dec 29 '19 04:12 FendyPierre

I don't think we should do this on Djoser level as it wouldn't work correctly in Django Admin, unless you want to alter initial user input (username.lower() or sth).

Levi Payne shows how to use CaseInsensitiveFieldMixin on his blog.

So if you had such mixin

class CaseInsensitiveFieldMixin:
    """
    Field mixin that uses case-insensitive lookup alternatives if they exist.
    """
    LOOKUP_CONVERSIONS = {
        'exact': 'iexact',
        'contains': 'icontains',
        'startswith': 'istartswith',
        'endswith': 'iendswith',
        'regex': 'iregex',
    }

    def get_lookup(self, lookup_name):
        converted = self.LOOKUP_CONVERSIONS.get(lookup_name, lookup_name)
        return super().get_lookup(converted)  # noqa

you can easily create case insensitive email, without changing initial user input at all

class CaseInsensitiveEmailField(CaseInsensitiveFieldMixin, models.EmailField):
    def to_python(self, value):
        value = super().to_python(value)
        if isinstance(value, str):
            value = value.lower()
        return value

This solution will work for all cases.

If you see any downsides to this approach let me know.

tomwojcik avatar Feb 05 '21 08:02 tomwojcik

You can create a new serializer subclassing djoser's TokenCreateSerializer and override the validate method.

class LoginSerializer(TokenCreateSerializer):
    def validate(self, attrs):
        attrs["email"] = attrs["email"].lower()
        return super(LoginSerializer, self).validate(attrs)

Badredine-Kheddaoui avatar May 11 '21 08:05 Badredine-Kheddaoui

@Badredine-Kheddaoui could you explain a little more how you implemented this ? I've tried adding the serializer and then updated the DJOSER_SETTINGS with

DJOSER = { 
  'SERIALIZERS': {
          'token_create': 'app.users.serializers.LoginSerializer',
      }
}

but it doesn't seem to work. any thought's ?

Thanks

brsc2909 avatar Sep 10 '21 22:09 brsc2909

@brsc2909 you would have to override this for every serializer that can create an account, not just for login. Something like the following:

    'SERIALIZERS': {
        'password_reset': 'accounts.serializers.PasswordResetSerializer',
        'user_create': 'accounts.serializers.AccountCreateSerializer',
        'token_create': 'accounts.serializers.AccountTokenCreateSerializer',
        'login': 'accounts.serializers.AccountTokenCreateSerializer',
        'set_username': 'accounts.serializers.AccountSetUsernameSerializer',
    }

YPCrumble avatar Sep 11 '21 14:09 YPCrumble

@Badredine-Kheddaoui could you explain a little more how you implemented this ? I've tried adding the serializer and then updated the DJOSER_SETTINGS with

DJOSER = { 
  'SERIALIZERS': {
          'token_create': 'app.users.serializers.LoginSerializer',
      }
}

but it doesn't seem to work. any thought's ?

Thanks

Here's the serializer I implemented:

class LoginSerializer(TokenCreateSerializer):
    def validate(self, attrs):
        attrs["email"] = attrs.get("email").lower()
        return super(LoginSerializer, self).validate(attrs)

TokenCreateSerializer is djoser's

Badredine-Kheddaoui avatar Sep 11 '21 17:09 Badredine-Kheddaoui

initial user input (

What is wrong with altering the user's input email case?

Ou7law007 avatar Aug 25 '22 15:08 Ou7law007

Just in case someone else runs into a similar need anytime soon.

At first, I was worried that doing anything like this would break email standards. But it seems that while the original standard deemed that different casing meant different users, this is no longer the case for modern email providers (to keep from having emails spoofed).

I found the following answer to be helpful and worked for my situation.

https://stackoverflow.com/questions/50895643/removing-case-sensitivity-from-email-in-django-login-form#answer-58495709

class LowercaseEmailField(models.EmailField):
    """
    Override EmailField to convert emails to lowercase before saving.
    """
    def to_python(self, value):
        """
        Convert email to lowercase.
        """
        value = super(LowercaseEmailField, self).to_python(value)
        # Value can be None so check that it's a string before lowercasing.
        if isinstance(value, str):
            return value.lower()
        return value

Then there is no need to override the other forms, serializers, etc.

spockNinja avatar Feb 15 '23 19:02 spockNinja

I keep thinking about how best to migrate many existing users to new usernames, and if there's an OS opportunity for reuse...

aehlke avatar Feb 16 '23 14:02 aehlke