85

Is there a good way to do this in Django without rolling my own authentication system? I want the username to be the user's email address instead of them creating a username.

Please advise

1

12 Answers 12

35

For anyone else wanting to do this, I'd recommend taking a look at django-email-as-username which is a pretty comprehensive solution, that includes patching up the admin and the createsuperuser management commands, amongst other bits and pieces.

Edit: As of Django 1.5 onwards you should consider using a custom user model instead of django-email-as-username.

Sign up to request clarification or add additional context in comments.

2 Comments

If you decide that a custom user model is the best option, you might want to check this tutorial on the caktus group blog, it resembles this example given in django docs but take cares of some details needed for a production environment (e.g. Permissions).
For Django 1.5 and above, the django-custom-user app contains a canned custom user model that implements this.
29

Here's what we do. It isn't a "complete" solution, but it does much of what you're looking for.

from django import forms from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User class UserForm(forms.ModelForm): class Meta: model = User exclude = ('email',) username = forms.EmailField(max_length=64, help_text="The person's email address.") def clean_email(self): email = self.cleaned_data['username'] return email class UserAdmin(UserAdmin): form = UserForm list_display = ('email', 'first_name', 'last_name', 'is_staff') list_filter = ('is_staff',) search_fields = ('email',) admin.site.unregister(User) admin.site.register(User, UserAdmin) 

3 Comments

Works for me. Although I can see this being confusing for future maintainers.
that is probably the smartest answer I have ever seen in SO.
on admin Create User this: exclude = ('email',) throw me an error that key['email'] is missing from UserForm, that is obvious.
22

Here is one way to do it so that both username and email are accepted:

from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.forms import ValidationError class EmailAuthenticationForm(AuthenticationForm): def clean_username(self): username = self.data['username'] if '@' in username: try: username = User.objects.get(email=username).username except ObjectDoesNotExist: raise ValidationError( self.error_messages['invalid_login'], code='invalid_login', params={'username':self.username_field.verbose_name}, ) return username 

Don't know if there is some setting to set the default Authentication form but you can also override the url in urls.py

url(r'^accounts/login/$', 'django.contrib.auth.views.login', { 'authentication_form': EmailAuthenticationForm }, name='login'), 

Raising the ValidationError will prevent 500 errors when an invalid email is submitted. Using the super's definition for "invalid_login" keeps the error message ambiguous (vs a specific "no user by that email found") which would be required to prevent leaking whether an email address is signed up for an account on your service. If that information is not secure in your architecture it might be friendlier to have a more informative error message.

2 Comments

I am not using auth.views.login. Using custom this is my url url(r'accounts/login', 'login_view',) . If i am giving EmailAuthenticationForm, then the error is login_view() got an unexpected keyword argument 'authentication_form'
This worked cleanly for me, I actually used my custom user profile (since in my architecture User is not guaranteed to have email address attached). Seems much nicer than customizing the user model or making the username equal to an email (since that allows keeping friend's email private while exposing username for instance).
8

Django now provides a full example of an extended authentication system with admin and form: https://docs.djangoproject.com/en/stable/topics/auth/customizing/#a-full-example

You can basically copy/paste it and adapt (I didn't need the date_of_birth in my case).

It is actually available since Django 1.5 and is still available as of now (django 1.7).

1 Comment

I follower the djangoproject tutorial and it works perfectly
8

If you're going to extend user model, you will have to implement custom user model anyway.

Here is an example for Django 1.8. Django 1.7 would require a little bit more work, mostly changing default forms (just take a look at UserChangeForm & UserCreationForm in django.contrib.auth.forms - that's what you need in 1.7).

user_manager.py:

from django.contrib.auth.models import BaseUserManager from django.utils import timezone class SiteUserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): today = timezone.now() if not email: raise ValueError('The given email address must be set') email = SiteUserManager.normalize_email(email) user = self.model(email=email, is_staff=False, is_active=True, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, password, **extra_fields): u = self.create_user(email, password, **extra_fields) u.is_staff = True u.is_active = True u.is_superuser = True u.save(using=self._db) return u 

models.py:

from mainsite.user_manager import SiteUserManager from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import PermissionsMixin class SiteUser(AbstractBaseUser, PermissionsMixin): email = models.EmailField(unique=True, blank=False) is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) is_staff = models.BooleanField(default=False) USERNAME_FIELD = 'email' objects = SiteUserManager() def get_full_name(self): return self.email def get_short_name(self): return self.email 

forms.py:

from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.forms import UserChangeForm, UserCreationForm from mainsite.models import SiteUser class MyUserCreationForm(UserCreationForm): class Meta(UserCreationForm.Meta): model = SiteUser fields = ("email",) class MyUserChangeForm(UserChangeForm): class Meta(UserChangeForm.Meta): model = SiteUser class MyUserAdmin(UserAdmin): form = MyUserChangeForm add_form = MyUserCreationForm fieldsets = ( (None, {'fields': ('email', 'password',)}), ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser',)}), ('Groups', {'fields': ('groups', 'user_permissions',)}), ) add_fieldsets = ( (None, { 'classes': ('wide',), 'fields': ('email', 'password1', 'password2')} ), ) list_display = ('email', ) list_filter = ('is_active', ) search_fields = ('email',) ordering = ('email',) admin.site.register(SiteUser, MyUserAdmin) 

settings.py:

AUTH_USER_MODEL = 'mainsite.SiteUser' 

2 Comments

I've test your approach and works, but in my case I had add the username field to SiteUser model, because, when I execute the python manage.py makemigrations ... command I get this output: ERRORS: <class 'accounts.admin.UserAdmin'>: (admin.E033) The value of 'ordering[0]' refers to 'username', which is not an attribute of 'accounts.User'.
I've add the username field to my User model with their null=True attribute. In this paste bin entry I did want show the implementation. pastebin.com/W1PgLrD9
2

Other alternatives look too complex for me, so I wrote a snippet that allows to authenticate using username, email, or both, and also enable or disable case sensitive. I uploaded it to pip as django-dual-authentication.

from django.contrib.auth.backends import ModelBackend from django.contrib.auth import get_user_model from django.conf import settings ################################### """ DEFAULT SETTINGS + ALIAS """ ################################### try: am = settings.AUTHENTICATION_METHOD except: am = 'both' try: cs = settings.AUTHENTICATION_CASE_SENSITIVE except: cs = 'both' ##################### """ EXCEPTIONS """ ##################### VALID_AM = ['username', 'email', 'both'] VALID_CS = ['username', 'email', 'both', 'none'] if (am not in VALID_AM): raise Exception("Invalid value for AUTHENTICATION_METHOD in project " "settings. Use 'username','email', or 'both'.") if (cs not in VALID_CS): raise Exception("Invalid value for AUTHENTICATION_CASE_SENSITIVE in project " "settings. Use 'username','email', 'both' or 'none'.") ############################ """ OVERRIDDEN METHODS """ ############################ class DualAuthentication(ModelBackend): """ This is a ModelBacked that allows authentication with either a username or an email address. """ def authenticate(self, username=None, password=None): UserModel = get_user_model() try: if ((am == 'email') or (am == 'both')): if ((cs == 'email') or cs == 'both'): kwargs = {'email': username} else: kwargs = {'email__iexact': username} user = UserModel.objects.get(**kwargs) else: raise except: if ((am == 'username') or (am == 'both')): if ((cs == 'username') or cs == 'both'): kwargs = {'username': username} else: kwargs = {'username__iexact': username} user = UserModel.objects.get(**kwargs) finally: try: if user.check_password(password): return user except: # Run the default password hasher once to reduce the timing # difference between an existing and a non-existing user. UserModel().set_password(password) return None def get_user(self, username): UserModel = get_user_model() try: return UserModel.objects.get(pk=username) except UserModel.DoesNotExist: return None 

Comments

1

Latest version of django-registration allows some nice customisation and might do the job - docs here https://bitbucket.org/ubernostrum/django-registration/src/fad7080fe769/docs/backend-api.rst

Comments

1
 if user_form.is_valid(): # Save the user's form data to a user object without committing. user = user_form.save(commit=False) user.set_password(user.password) #Set username of user as the email user.username = user.email #commit user.save() 

working perfectly... for django 1.11.4

Comments

0

you can also find an interesting discussion on this topic at the below link :

http://groups.google.com/group/django-users/browse_thread/thread/c943ede66e6807c/2fbf2afeade397eb#2fbf2afeade397eb

Comments

0

The easiest way is to lookup the username based on the email in the login view. That way you can leave everything else alone:

from django.contrib.auth import authenticate, login as auth_login def _is_valid_email(email): from django.core.validators import validate_email from django.core.exceptions import ValidationError try: validate_email(email) return True except ValidationError: return False def login(request): next = request.GET.get('next', '/') if request.method == 'POST': username = request.POST['username'].lower() # case insensitivity password = request.POST['password'] if _is_valid_email(username): try: username = User.objects.filter(email=username).values_list('username', flat=True) except User.DoesNotExist: username = None kwargs = {'username': username, 'password': password} user = authenticate(**kwargs) if user is not None: if user.is_active: auth_login(request, user) return redirect(next or '/') else: messages.info(request, "<stvrong>Error</strong> User account has not been activated..") else: messages.info(request, "<strong>Error</strong> Username or password was incorrect.") return render_to_response('accounts/login.html', {}, context_instance=RequestContext(request)) 

In your template set the next variable accordingly, i.e.

<form method="post" class="form-login" action="{% url 'login' %}?next={{ request.GET.next }}" accept-charset="UTF-8"> 

And give your username / password inputs the right names, i.e. username, password.

UPDATE:

Alternatively, the if _is_valid_email(email): call can be replaced with if '@' in username. That way you can drop the _is_valid_email function. This really depends on how you define your username. It will not work if you allow the '@' character in your usernames.

6 Comments

This code is buggy, because username can also have a '@' symbol, so if '@' is present, it is not necessary an email.
depends on you really, I don't allow username to have @ symbol. If you do you can add another filter query to search through User object by username. PS. username can also be an email, so you have to be careful with how you design your user management.
Also check out, from django.core.validators import validate_email . You can do a try except ValidationError block with validate_email('[email protected]') . It might still be buggy depending on your app.
Of course, you're right, it depends on how the login logic is set up. I mentioned that because the possibility of '@' in username is django's default. Someone can copy your code and have problems when user with username 'Gladi@tor' cannot login because login code thinks it's an email.
Good call. I hope people understand what it is they are copying. I'll add in the email validation and comment out the current validation, leave it as an alternative. I guess the only other reason why I didn't use the validation apart from my user names not having the @, is that it makes the code less dependent on django. Things do tend to move around from version to version. Cheers!
|
0

I think the most quickly way is to create a form inherit from UserCreateForm, and then override the username field with forms.EmailField. Then for every new registration user, they need to signon with their email address.

For example:

urls.py

... urlpatterns += url(r'^signon/$', SignonView.as_view(), name="signon") 

views.py

from django.contrib.auth.models import User from django.contrib.auth.forms import UserCreationForm from django import forms class UserSignonForm(UserCreationForm): username = forms.EmailField() class SignonView(CreateView): template_name = "registration/signon.html" model = User form_class = UserSignonForm 

signon.html

... <form action="#" method="post"> ... <input type="email" name="username" /> ... </form> ... 

1 Comment

Downvoted for a couple of reasons. This answer is a better way of doing the same thing. And why subclass when you can merely define what widget to use directly in the UserCreationForm class? And please do not recommend writing out <input … when, surely, {{form.username}} is better.
0

Not sure if people are trying to accomplish this, but I found nice (and clean) way to only ask for the email and then set the username as the email in the view before saving.

My UserForm only requires the email and password:

class UserForm(forms.ModelForm): password = forms.CharField(widget=forms.PasswordInput()) class Meta: model = User fields = ('email', 'password') 

Then in my view I add the following logic:

if user_form.is_valid(): # Save the user's form data to a user object without committing. user = user_form.save(commit=False) user.set_password(user.password) #Set username of user as the email user.username = user.email #commit user.save() 

1 Comment

Couldn't storing the email in the username cause issues because the username is 30 chars while email is 75 chars?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.