Django all_auth and custom form - django

I'm using django allauth for social login/signup. Also, I've my own signup form as an alternate login/signup. Following are the fields that I'm fetching from the user in the alternate form.
class Profile(models.Model):
col1 = models.CharField(max_length=50, blank=True, null=True)
col2 = models.CharField(max_length=50, blank=True, null=True)
user = models.OneToOneField(User)
So, when the user signs up, it asks for additional fields as well(col1, col2), apart from username, email and password.
Following is the signup view.
user = User.objects.create_user(username, user_email, user_pass)
Profile.objects.create(user=user, col1=col1, col2=col2)
return
So, whenever the user signs up via the alternate form, the above view is called up.
Now, in contrast, when the user signs up from social account FB, it does not ask for extra info, ie col1/col2. It directly signs up without asking for extra info, neither I want it to ask.
I then create a row in Profile model post signup using signals.
#receiver(user_signed_up)
def create_profile(request, user, sociallogin=None, **kwargs):
if sociallogin:
if sociallogin.account.provider == 'facebook':
data = sociallogin.account.extra_data
col1 = data.get('col1')
col2 = data.get('col2')
Profile.objects.create(user=user, col1=col1, col2=col2)
So, (1) my problem is when creating a user using alternate form, no record is inserted in allauth tables, which i find weird.
(2) Consider, I signed up using alternate form using E1 as email id. Now I signup via allauth(FB) with the same id, it throws an error.
(3) How do I send confirmation mail to the users who signed up in alternate form using all_auth.

I played around with the library a bit and finally found the solution to my question. I'm pasting it over here for other's to review.
Add a signal pre_social_login that'll check for conditions.
class MySocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin=None, **kwargs):
if sociallogin:
user = User.objects.filter(email=email).first()
# If user already exists in custom local form, then login directly.
# Save/Update his details in social login tables.
if user:
# create or update social login tables to maintain the uniformity in the code.
token = sociallogin.token.token
socialaccount = SocialAccount.objects.filter(user=user).first()
if socialaccount: # If it exists, then update social tables.
# updating account.
socialaccount.extra_data = extra_data
socialaccount.save()
# updating token.
SocialToken.objects.filter(account=socialaccount) \
.update(token=token)
else: # else create.
# saving Social EmailAddress.
EmailAddress.objects.create(email=email, user=user)
# saving social account.
provider = 'facebook'
kwargs = {
'uid': extra_data.get('id'),
'provider': provider,
'extra_data': extra_data,
'user': user
}
socialaccount = SocialAccount.objects.create(**kwargs)
# saving social token.
app = SocialApp.objects.get(provider=provider)
kwargs = {
'token': token,
'account': socialaccount,
'app': app
}
SocialToken.objects.create(**kwargs)
# finally login.
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, user)
raise ImmediateHttpResponse(redirect(reverse('index')))

Related

Django Rest Framework - Register user with email verification

I'm working on DRF project. I use email as a unique username in my own user model and using jwt as authentication. I made everything but cannot implement email verification when create user model.
I was thinking of making token of user and use it to make user activate after first logged in. So I tried to override every single methods in generics.createAPIView and django.contrib.auth.tokens.PasswordResetTokenGenerator. And now it seems like impossible. I coulnd't find any information who made it with DRF.
I want to do email verification before user model is actually written in database, and if it succeed, then write in database. I'm using vue as front-end so what I want is
[ vue(register page) --> drf(check if it's validate) --> send mail to request.data['email'] -->
click the link in email and finish registration --> drf(finish register and write in database) ]
Is there any possible way to make it with override on methods of CreateAPIView?
I used django all-auth, django rest and django jwt
Objective of this code snippet:
as per django simple jwt docs, the url is "token/", whenever you submit the login form. It will take email and password from frontend, and assign those values in json field like this -
{
"username" : "xyz#gmail.com",
"password" : "password"
}
after that it will check in the email column of AbstractUser Model and if it exists then it will check in the all-auth EmailAddress Model table if it's verified. if it's verified then it will return tokens. Others it will show error messages.
override TokenObtainPairSerializer from Django Rest Simple JWT in serilizer.py
class CustomJWTSerializer(TokenObtainPairSerializer):
def update(self, instance, validated_data):
pass
def create(self, validated_data):
pass
def validate(self, attrs):
credentials = {
'username': '',
'password': attrs.get("password")
}
user = User.objects.filter(email=attrs.get("username")).first()
email_address = EmailAddress.objects.filter(user=user, verified=True).exists()
if email_address and user:
credentials['username'] = user.username
return super().validate(credentials)
elif user and not email_address:
return {'message': 'Email not verified'}
else:
return {'message': 'This email does not exist, please create a new account'}
router.py -
path(API_VERSION + 'token/', TokenObtainPairView.as_view(serializer_class=CustomJWTSerializer), name='token_obtain_pair'),
Then it will only return jwt tokens if the mail is validate.
to send mail use all-auth and in settings.py -
OLD_PASSWORD_FIELD_ENABLED = True
LOGOUT_ON_PASSWORD_CHANGE = True
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_CONFIRM_EMAIL_ON_GET = True
ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = FRONTEND_URL

Django allauth with email as username and multiple sites

Is it possible to use Django allauth with the authentication method set to 'email' when using it on multiple sites?
I'm aiming to allow a user with the email address bob#example.com to create an account at site1.com and a separate account at site2.com.
In order to use email authentication, I need to leave UNIQUE_EMAIL set to True in the settings but this prevents users who already have accounts in one site from creating accounts in the other site.
I am assuming you'd like to allow the same email to be registered separately for each of the sites in your Django setup.
Looking at the allauth code; it appears that it is infeasible to do so at the moment, likely because allauth does not take into account site ID as part of the User signup process.
class AppSettings(object):
class AuthenticationMethod:
USERNAME = 'username'
EMAIL = 'email'
USERNAME_EMAIL = 'username_email'
class EmailVerificationMethod:
# After signing up, keep the user account inactive until the email
# address is verified
MANDATORY = 'mandatory'
# Allow login with unverified e-mail (e-mail verification is
# still sent)
OPTIONAL = 'optional'
# Don't send e-mail verification mails during signup
NONE = 'none'
def __init__(self, prefix):
self.prefix = prefix
# If login is by email, email must be required
assert (not self.AUTHENTICATION_METHOD ==
self.AuthenticationMethod.EMAIL) or self.EMAIL_REQUIRED
# If login includes email, login must be unique
assert (self.AUTHENTICATION_METHOD ==
self.AuthenticationMethod.USERNAME) or self.UNIQUE_EMAIL
One way to do this would be as follows:
- Keep allauth AUTHENTICATION_METHOD as Username
- Store the site alongside the User information, perhaps in a UserProfile or by overriding the User Model.
- Make the combination of Email and Site unique.
- Override the LoginView such that the user enters email; you can translate the combination of Email, Site to a Unique User account and username; which you can pass on to allauth to perform login.
Assuming you use the Sites framework; your code would look something like this:
from allauth.account.views import LoginView
from django.core.exceptions import ObjectDoesNotExist
class CustomLoginView(LoginView):
def get_user():
email = request.POST.get('email')
current_site = Site.objects.get_current()
try:
user = User.objects.get(email=email, site=current_site)
except ObjectDoesNotExist:
pass # Handle Error: Perhaps redirect to signup
return user
def dispatch(self, request, *args, **kwargs):
user = self.get_user()
request.POST = request.POST.copy()
request.POST['username'] = user.username
return super(CustomLoginView, self).dispatch(request, *args, **kwargs)
Then monkey-patch the LoginView with the custom login view:
allauth.account.views.LoginView = CustomLoginView
Related Reading on setting up a Site FK, and custom auth backends:
How to get unique users across multiple Django sites powered by the "sites" framework?
https://docs.djangoproject.com/en/dev/topics/auth/#writing-an-authentication-backend

Handling duplicate email address in django allauth

What I am trying to do ?
I am trying to avoid duplicate email address by following these steps:
Before user login to my site from a social account I check to see if the email address already exists.
If no then login the user otherwise check the below steps.
Check to see if the provider of registered user match the user trying to login.
If no then don't allow user to login otherwise login the user.
What is the problem ?
I get the following error:
Error:AttributeError at /accounts/twitter/login/callback/
'QuerySet' object has no attribute 'profile'
My Code:
views.py:
#receiver(pre_social_login)
def handleDuplicateEmail(sender, request, sociallogin, **kwargs):
if sociallogin.account.provider == 'facebook' or sociallogin.account.provider == 'twitter':
email_address = sociallogin.account.extra_data['email'] # get email address from fb or twitter social account.
else:
email_address = sociallogin.account.extra_data['email-address'] # Get email from linkedin social account.
users = User.objects.all().filter(email=email_address) # This line is problematic
if users:
if not (users.profile.provider == sociallogin.account.provider): # Different user is trying to login with already existing user's email address.
response = 'Your social account email address is already registered to some account. Please choose a different one.'
raise ImmediateHttpResponse(render(request, 'index.html', {'type': True, 'response': response})) # redirect to index template with error message.
models.py:
class Profile(models.Model):
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE)
provider = models.CharField(max_length=256, blank=True, null=True) # for storing name of provider used to login with.
Edit:
Since Facebook, Twitter and Linkedin give users a choice to login with their phone number or email address and if they choose phone number then in that cause users won't have an email address associated with them to handle this situation I have updated my code like so:
if sociallogin.account.provider == 'facebook' or sociallogin.account.provider == 'twitter':
try:
email_address = sociallogin.account.extra_data['email']
except:
email_address = None # If social account was created on phone number for facebook & twitter
else:
try:
email_address = sociallogin.account.extra_data['email-address']
except:
email_address = None # If social account was created on phone number or linkedin
if email_address:
users = User.objects.all().filter(email=email_address)
if users.exists():
...
else:
response = 'You have not provided an email address in your social account. Please register as local user or use a different social account.'
raise ImmediateHttpResponse(render(request, 'index.html', {'type': True, 'response': response}))
users = User.objects.all().filter(email=email_address) returns a QuerySet so you can't just call .profile on it. In theory this query could return multiple User objects. But it could also contain 0 objects (which is more likely).
So you need to handle these cases:
if users.exists():
user = users.first() # assuming there will always only be one user
if not user.profile.provider == sociallogin.account.provider:
etc...
or
if users.exists():
for user in users:
if not user.profile.provider == sociallogin.account.provider:
etc...
break

How to use another field for logging in with Django Allauth?

I have successfully setup django-allauth along with a custom user model which let's users sign in directly using email and password or through Facebook, in which case email is taken from Facebook and saved in the Email field of the custom user model. I have also created a mobile field which stays empty as of now.
I want to allow users to log in using their Facebook, Email or MOBILE. Unfortunately that field is not unique=True in the model. I have thought of capturing the mobile and fetching the associated email address and then use that along with the password to log in any user.
However, I don't know how to extend the SIGN IN form that comes with django-allauth or the block of code that signs a user in where I can change it to meet my need.
As of now I don't find any of my current code relevant to this problem, but if it helps I am willing to provide it upon mention.
The following solution worked for me. I put the code in the forms.py file.
from allauth.account.forms import LoginForm
from auth_project import settings
class CustomLoginForm(LoginForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
if settings.ACCOUNT_AUTHENTICATION_METHOD == "email":
login_widget = forms.TextInput(attrs={'type': 'text',
'placeholder':
('Mobile Number'),
'autofocus': 'autofocus'})
login_field = forms.CharField(label=("Mobile"),
widget=login_widget)
self.fields["login"] = login_field
set_form_field_order(self, ["login", "password", "remember"])
def user_credentials(self):
credentials = {}
mobile = self.cleaned_data["login"]
login = CustomUser.objects.filter(mobile=mobile).values('email')[0]['email']
if settings.ACCOUNT_AUTHENTICATION_METHOD == "email":
credentials["email"] = login
credentials["password"] = self.cleaned_data["password"]
return credentials
# this is to set the form field in order
# careful with the indentation
def set_form_field_order(form, fields_order):
if hasattr(form.fields, 'keyOrder'):
form.fields.keyOrder = fields_order
else:
# Python 2.7+
from collections import OrderedDict
assert isinstance(form.fields, OrderedDict)
form.fields = OrderedDict((f, form.fields[f])
for f in fields_order)
Thanks.

Using email as username with django

I have run into the following error trying to create a user in django:
>>> email = 'verylongemail#verylongemail.com'
>>> user_object = User.objects.create_user(username=email, email=email, password='password')
Data truncated for column 'username' at row 1
It seems Django has a limit on the number of chars allowed in a username. How would I get around this?
I've had to modify the auth_user table by hand to make the field longer and then convert emails into a username by removing the # symbol and the period (maybe other characters too, it's really not a great solution). Then, you have to write a custom auth backend that authenticates a user based on their email, not the username, since you just need to store the username to appease django.
In other words, don't use the username field for auth anymore, use the email field and just store the username as a version of the email to make Django happy.
Their official response on this topic is that many sites prefer usernames for auth. It really depends if you are making a social site or just a private site for users.
If you override the form for Django users you can actually pull this off pretty gracefully.
class CustomUserCreationForm(UserCreationForm):
"""
The form that handles our custom user creation
Currently this is only used by the admin, but it
would make sense to allow users to register on their own later
"""
email = forms.EmailField(required=True)
first_name = forms.CharField(required=True)
last_name = forms.CharField(required=True)
class Meta:
model = User
fields = ('first_name','last_name','email')
and then in your backends.py you could put
class EmailAsUsernameBackend(ModelBackend):
"""
Try to log the user in treating given username as email.
We do not want superusers here as well
"""
def authenticate(self, username, password):
try:
user = User.objects.get(email=username)
if user.check_password(password):
if user.is_superuser():
pass
else: return user
except User.DoesNotExist: return None
then in your admin.py you could override with
class UserCreationForm(CustomUserCreationForm):
"""
This overrides django's requirements on creating a user
We only need email, first_name, last_name
We're going to email the password
"""
def __init__(self, *args, **kwargs):
super(UserCreationForm, self).__init__(*args, **kwargs)
# let's require these fields
self.fields['email'].required = True
self.fields['first_name'].required = True
self.fields['last_name'].required = True
# let's not require these since we're going to send a reset email to start their account
self.fields['username'].required = False
self.fields['password1'].required = False
self.fields['password2'].required = False
Mine has a few other modifications, but this should get you on the right track.
You have to modify the username length field so that syncdb will create the proper length varchar and you also have to modify the AuthenticationForm to allow greater values as well or else your users won't be able to log in.
from django.contrib.auth.forms import AuthenticationForm
AuthenticationForm.base_fields['username'].max_length = 150
AuthenticationForm.base_fields['username'].widget.attrs['maxlength'] = 150
AuthenticationForm.base_fields['username'].validators[0].limit_value = 150