Hands-on Assignment: Custom User Model & Login via Facebook
May 17, 2018
Welcome to your first project with Django. One of the most common things you’ll do as you begin to design your own web applications is to manage user authentication. We covered the generic user creation process earlier that Django provides, but now we’ll delve a little deeper by creating our own custom user model to replace the default one that Django provides. We’ll also integrate social login so that you can login via Facebook into your application!
Project Goals
- Create a custom, more comprehensive User model. This can store additional information about the user such as their birth date, their profile photo, etc.
- Learn about several built in Django features such as custom authentication backends and the
sites
framework. - Allow users to log in via social platforms such as Facebook.
- Integrate 3rd party Django packages into your application
1. Set up Packages
We will start off from where the last tutorial left off but without any of the database or migrations (since we will be modifying some of the models). You can find the starter code here. (If you’re using Django 1.11, use the starter code here). Alternatively if you want to use your own code from the previous tutorials, just delete all the files in the myblog/blog/migrations
folder and delete the db.sqlite3
file at myblog/db.sqlite3
.
Python Packages Requirements File
We need to make sure we install all the python packages we need. Remember that earlier we manually installed Django by running pip install django~=1.11.0
in the command prompt inside our virtual environment. When we have a lot of packages to manage, we create a requirements.txt
file that just lists the name and versions of all the packages.
Create this file at the root of your folder, i.e. at proj1-starter/requirements.txt
.
django==2.0.6
django-allauth==0.35.0
If you’re using Django 1.11, you should use the following instead:
django==1.11.0
django-allauth==0.35.0
[django-allauth](https://django-allauth.readthedocs.io/en/latest/index.html)
(allauth
for short) is the package we are doing to use to help us integrate Login via Facebook into our application. There is a huge ecosystem of packages available that you can simply plug into your Django application. When you find a package that matches your needs, it’s often a better idea to use it rather than re-invent the wheel every time.
Let’s go ahead and install these packages. Remember that you may have to use a different command to start the virtual environment depending on whether you’re on Windows, OSX, or Linux, as we did here.
python3 -m venv myvenv
source myvenv/bin/activate
Install the packages. -r
simply denotes the path to the file that has all the package information.
pip install -r requirements.txt
Perfect, on to step 2!
2. Create Custom User Model & UserManager
We will begin by creating a custom user model that will add some fields and remove others from Django’s default user model. We’d like to
- Get rid of the requirement to have a username and replace it with a default email field.
- Add a
profile_img_url
field so that we can store the user’s profile image
To override the default user model, we need to create our own user class that subclasses both AbstractBaseUser
and PermissionsMixin
.
[AbstractBaseUser](https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#django.contrib.auth.models.AbstractBaseUser)
provides the core implementation of a user model, including hashed passwords and tokenized password resets.
To make it easy to include Django’s permission framework into your own user class, Django provides
[PermissionsMixin](https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#django.contrib.auth.models.PermissionsMixin)
. This is an abstract model you can include in the class hierarchy for your user model, giving you all the methods and database fields necessary to support Django’s permission model.
Let’s go ahead and create a new app that will hold our user model.
python manage.py startapp custom_auth
CLUser Model
Now to define our new user model. Go to custom_auth/models.py
and create a class called CLUser
.
from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from .managers import CLUserManager
class CLUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=40, blank=True)
last_name = models.CharField(max_length=40, blank=True)
profile_image_url = models.URLField(null=True, blank=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(auto_now_add=True)
objects = CLUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def get_full_name(self):
'''
Returns the first_name plus the last_name, with a space in between.
'''
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
'''
Returns the short name for the user.
'''
return self.first_name
A few important things to note here:
- We’ve gotten rid of the
username
field and set the USERNAME_FIELD (line 17) to instead be theemail
field. Any field we choose as theUSERNAME_FIELD
must haveunique=True
set on it since it needs to uniquely identify the user. - We’ve added a
profile_image_url
field which is a URLField on line 10. - We’ve kept around some of the default user attributes such as
first_name
,last_name
,is_staff
,is_active
, anddate_joined
. As an aside,is_staff
isTrue
if the user has access to the admin site, andis_active
isTrue
if the user account is currently active. - We added a few helper methods to print out the user’s full and short name. These are generally included by default on Django’s User model so we added them here as well. These should be pretty self explanatory.
- Finally we need to write our own UserManager . You can see on line 15 that we’ve added
objects = CLUserManager()
. TheUserManager
defines helper methods likecreate_user
andcreate_superuser.
Since we created our own user object, we need to write our ownUserManager
and provide implementations for both these methods.mana
CLUserManager
Let’s create a file called managers.py
inside the custom_auth
app and put in the code for our custom CLUserManager
.
# custom_auth/managers.py
from django.contrib.auth.models import BaseUserManager
class CLUserManager(BaseUserManager):
def _create_user(self, email, password,
is_staff, is_superuser, **extra_fields):
"""
Creates and saves a User with the given email and password.
"""
if not email: raise ValueError('The given email must be set')
email = self.normalize_email(email)
user = self.model(email=email,
is_staff=is_staff, is_active=True,
is_superuser=is_superuser,
**extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False,
**extra_fields)
def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True,
**extra_fields)
Here, we provide implementations for create_user
and create_superuser
both of which use a _create_user
helper method.
Our _create_user
helper method takes in email
, password
, is_staff
, and is_superuser
, as well as a dictionary of extra_fields.
**var_name
is simply a shorthand in Python for passing keyword arguments in python.var_name
in this case refers to a dictionary of key-value pairs. So for example, calling_create_user(email, password, is_staff, is_superuser,{'profile_image_url' : 'http://www.example.com'})
would passprofile_image_url
to `createuserand give it a value of
’http://www.example.com’`. Read more here.
You can see that create_user
sets both is_staff
to False
and is_superuser
to False
. create_superuser
sets is_staff
to True
and is_superuser
to True
.
Let’s talk about the _create_user
method in a little more detail. We first make sure that an email
is passed in. Remember that we’re trying to get rid of the username
and make the email
the default! Next we normalize the email by calling the BaseUserManager’s normalize_email
method which just lowercases the domain portion of the email address (i.e. GOOGLE.com
would normalize to google.com
.
We then go ahead and create the user with self.model → this is just a reference to our CLUser
model. Finally, we set the password using set_password
, save the user, and return it.
Whew! That was a lot to digest. Take a moment before continuing on :).
Add the new User to the admin
We now register the CLUser
with the admin. Copy the following into custom_auth/admin.py
:
# custom_auth/admin.py
from django.contrib import admin
from .models import CLUser
# Register your models here.
admin.site.register(CLUser)
Update settings
In our settings.py
file located inside the mysite
folder, we need to add the custom_auth
app after the blog
app in the INSTALLED_APPS
variable. We also set the AUTH_USER_MODEL
to the CLUser
model we just created.
INSTALLED_APPS = [
...
'blog',
'custom_auth'
]
AUTH_USER_MODEL = 'custom_auth.CLUser'
Update the Blog App to point to our new user
Change the author field inside of the Post
class in blog/models.py
to point to our new user model. It’s considered good practice to refer to the model by using the AUTH_USER_MODEL
settings variable we just defined above instead of referring to the User class directly using custom_auth.CLUser
. Import settings by writing from django.conf import settings
(line 3), and then use it to modify line 6 as shown below.
from django.db import models
from django.utils import timezone
from django.conf import settings
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
...
Migrate
Let’s finally run our migrations. Go to the command prompt and type in the following:
python manage.py makemigrations
python manage.py migrate
Go ahead and create a superuser as well. Don’t forget the email and password!
python manage.py createsuperuser
Check that everything is running by starting our server: python manage.py runserver
.
3. Serve your local site to the world using Ngrok
When you try to integrate Facebook login into your app, you will see that Facebook requires your site to be served over https. When we’re running everything locally, our website is being served over normal http.
Ngrok is a free service allows you to expose your website from your localhost to a url that is generated for you. This means that you can now give a link to your local website to your friend and they can see it!
Install Ngrok
Go to Ngrok and download their software. You will have to create an account to get your auth_token to be able to use the software. Follow the instructions to unzip and set up your auth token. Once that is done, start ngrok by running:
./ngrok http 8000
./
just says to run thengrok
program in the directory you’re in. So make sure you’re in the directory wherever you downloaded ngrok! (Remember you can usecd
to change your directory).
After running the above, you should see the following output (with different account name and urls of course):
Session Status online
Account James M (Plan: Free)
Version 2.2.8
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://db4da85e.ngrok.io -> localhost:8000
Forwarding https://db4da85e.ngrok.io -> localhost:8000
Modify settings.py
Add the https forwarding url (https://db4da85e.ngrok.io
) to your ALLOWED_HOSTS
in settings.py
so that Django doesn’t complain about it.
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '.pythonanywhere.com', 'db4da85e.ngrok.io']
Note that anytime you stop the above ngrok process (by closing your command prompt, killing the process with ctrl+c, etc.), your urls will change when you restart it (unless you’ve paid ngrok). So we advise that while you’re testing this and getting it all set up, keep this process running (maybe in a new command prompt or tab).
4. Setting up allauth
Finally! We’re getting to the part where we will start integrating Facebook Login into our application :).
We will make several changes to your settings.py
file located at proj1-starter/settings.py
.
Add Authentication Backends
We need to let Django know to use the the authentication backend provided by django-allauth
. An authentication backend just implements two required methods. These include get_user(user_id)
and authenticate(request,**credentials)
. Add this to your settings.py
file.
AUTHENTICATION_BACKENDS = (
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by e-mail
'allauth.account.auth_backends.AuthenticationBackend',
)
Add allauth
related settings
Next, we add some more apps to INSTALLED_APPS
. We need to add several apps from allauth
as well as django.contrib.sites
.
The sites
app is a built in Django feature that allows your Django installation to power more than one site — it is useful if you need to differentiate between those sites in some way. allauth
uses the sites
framework so that it is easy to switch between development and production setups and to support multi-domain projects. We won’t be delving deeper into this but you’re welcome to check out the documentation if you’d like to know more here.
The allauth
apps we’re adding include management for SocialAccount
(i.e. the actual user social accounts for different providers) as well as the SocialApplication
(contains configuration information like api keys for different providers such as Facebook or Twitter). Add the following to your INSTALLED_APPS
list in your settings.py
file. We’ve denoted the additions with ######
.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
####################
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.facebook',
#####################
'blog',
'custom_auth'
]
We need to also add configuration settings for allauth
. The explanations for each variable are below. Add these to your settings.py
file as well.
## Allauth
# No username field exists
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
# No username field required
ACCOUNT_USERNAME_REQUIRED = False
# We require email
ACCOUNT_EMAIL_REQUIRED = True
# We authenticate via email
ACCOUNT_AUTHENTICATION_METHOD = 'email'
# Do all auth over https (necessary for facebook login)
ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'https'
# No need to verify email for now
ACCOUNT_EMAIL_VERIFICATION = "none"
# This is just the default site that will be created when you
# run migrate below (because we included it in INSTALLED_APPS).
SITE_ID = 1
You can now run migrations. Go to the command prompt and type in the following. You may have to quit the server if it’s already running with Ctrl + C.
python manage.py makemigrations
python manage.py migrate
Modify the urls.py
We need to makes sure the authentication urls for logging in, out, and connecting to social providers like Facebook are properly set up. Open up mysite/urls.py
, and delete the old accounts/login
and accounts/logout
url configurations. We’ve commented them out below so you can see what was there. Instead we replace it with allauth.urls
like below.
If you’re using Django 2.0 and above, use the following:
#mysite/urls.py
...
urlpatterns = [
path('admin/', admin.site.urls),
# path('accounts/login/', views.login, name='login'),
# path('accounts/logout/', views.logout, name='logout', kwargs={'next_page': '/'}),
path('accounts/', include('allauth.urls')),
path('', include('blog.urls')),
]
If you’r using Django 1.11, use the following:
#mysite/urls.py
...
urlpatterns = [
url(r'^admin/', admin.site.urls),
# url(r'^accounts/login/$', views.login, name='login'),
# url(r'^accounts/logout/$', views.logout, name='logout', kwargs={'next_page': '/'}),
url(r'^accounts/', include('allauth.urls')),
url(r'', include('blog.urls')),
]
5. Setting up our Facebook App
Create the Facebook App
- Go to Facebook Developers and log in using your Facebook account.
- Go to My Apps, and click Add New App.
- Choose any Display Name you’d like and enter your Contact Email.
- On the Add a Product page, select Facebook Login, by clicking on Set Up.
- Ignore the Quickstart menu and go directly to Facebook Login → Settings from the left hand menu.
- Add the following Valid OAuth Redirect URIs:
https://db4da85e.ngrok.io/
https://db4da85e.ngrok.io/accounts/facebook/
https://db4da85e.ngrok.io/accounts/facebook/login/callback/
Replace https://db4da85e.ngrok.io/ with whatever your url was from ngrok output above.
Hit Save Changes at the bottom.
- Go to the overall app Settings → Basic on the left-hand menu.
You should see your App Id and App Secret at the very top. We are now going to configure allauth
to use these two values.
Create the Social Account in our Admin
Go to http://localhost:8000/admin/. Sign in using the superuser email and password from earlier. Under the SOCIAL ACCOUNTS heading click on the + Add next to the Social applications row. You should see something that looks like this:
Select:
- Facebook for the provider
- Choose any Name that you like
- For the Client id, use the App Id from the facebook settings page above.
- For the Secret key, use the App Secret from the facebook settings page above.
- Double click the default example.com site from the Sites so that it becomes a Chosen site.
Now hit save.
6. Integrating
Requesting the appropriate information from Facebook
Now we need a way to tell facebook exactly what information we want to request from the user. Some permissions don’t require the user’s explicit approval such as their public profile information (name, profile picture, etc.)
Fortunately allauth
allows us to specify exactly what we need, so add the SOCIALACCOUNT_PROVIDERS
configuration to your settings.py
file (full documentation). Comments are below.
SOCIALACCOUNT_PROVIDERS = {
'facebook': {
# requesting the user's email and public profile
'SCOPE': ['email', 'public_profile'],
# The fields to fetch from Facebook's Graph API. We won't
# use all of these but just wanted to show you a sample
'FIELDS': [
'id',
'email',
'name',
'first_name',
'last_name',
'verified',
'locale',
'timezone',
'link',
'gender',
'updated_time',
'picture'
],
}
}
Modifying the default allauth
flow
Now when the user goes through the facebook flow, allauth
will automatically store all this information and create our CLUser
object for us. We do however want to explicitly add the users profile_image_url
so we need a way to tell allauth
exactly how to modify the user object before it’s saved.
Because of allauth
’s great architecture, this is easy to do! It has adapters that we can override for any operation. One in particular is the populate_user(self, request, sociallogin, data)
function. It is a hook that can be used to further populate the user instance (available via sociallogin.account.user
). Here, data
is a dictionary of common user properties (first_name
, last_name
, email
, name
) that the provider already extracted for you.
Let’s create an adapter.py
file at custom_auth/adapter.py
. This will be pretty simple. We will extend the DefaultSocialAccountAdapter
class that allauth
provides and override the populate_user
method from above.
from django.conf import settings
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def populate_user(self, request, sociallogin, data):
user = super(CustomSocialAccountAdapter, self).populate_user(request, sociallogin, data)
url = sociallogin.account.extra_data.get('picture', {}).get('data', {}).get('url')
if url: user.profile_image_url = url
return user
On line 7, we first let the default method do its modifications to the user object. In Python, we can easily do this by invoking the same function in the parent class by calling super
. Now that we have the modified user
object, we just have to add a profile_image_url
to it.
The passed in sociallogin
object will contain extra_data
available via sociallogin.account.extra_data
. extra_data
will be a dictionary that will look something like:
{
"id": "1703333330834",
"email": "test@test.com",
"name": "James Mat",
"first_name": "James",
"last_name": "Mat",
"verified": True,
"locale": "en_US",
"timezone": -7,
"link": "https://www.facebook.com/app_scoped_user_id/1703333330834/",
"gender": "male",
"updated_time": "2018-04-18T17:12:54+0000",
"picture": {"data": {"height": 50, "is_silhouette": False, "url": "https://lookaside.facebook.com/platform/profilepic/?asid=17027334asdf333&height=50&width=50&ext=15233ad329&hash=A33ag31ar4bD35uJ", "width": 50}}
}
On line 8, we simply extract the url, and if it exists, on line 9, we set it on the user object.
Great! We’ve now added the users profile picture to the CLUser
model.
The final step before we get to integrating all this new stuff into our templates is to let allauth
know where to find this adapter class we wrote.
Just go to settings.py
and add the SOCIALACCOUNT_ADAPTER
variable.
SOCIALACCOUNT_ADAPTER = 'custom_auth.adapter.CustomSocialAccountAdapter'
Modifying Templates
Now let’s actually integrate everything into the template. We need to show the right urls in our template, add the user’s first name, and show their profile picture.
Open up base.html
in blog/templates/blog/base.html
. We’re going to load the allauth
static files right after the {% load staticfiles %}
like below.
{% load staticfiles %}
{% load socialaccount %}
{% load account %}
Next we’re going to change the login and logout urls to point to the ones allauth
generates.
So we’ll make the following changes:
- Replace
{% url 'logout' %}
→{% url 'account_logout' %}
- Replace
{% url 'login' %}
→{% provider_login_url "facebook" method="oauth2" %}
- Replace
Hello {{ user.username }}
withHello {{ user.first_name }}
- Add the user’s profile image right next to the edit icon, by creating an image tag →
<img class="top-menu" src="{{user.profile_image_url}}"></img>
Your final file should look like the one below.
{% load staticfiles %}
{% load socialaccount %}
{% load account %}
<html>
<head>
<title>My Blog</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<link href='//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{% static 'css/blog.css' %}">
</head>
<body>
<div class="page-header">
{% if user.is_authenticated %}
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
<a href="{% url 'post_draft_list' %}" class="top-menu"><span class="glyphicon glyphicon-edit"></span></a>
<img class="top-menu" src="{{user.profile_image_url}}"></img>
<p class="top-menu">Hello {{ user.username }} <small>(<a href="{% url 'account_logout' %}">Log out</a>)</small></p>
{% else %}
<a href="{% provider_login_url "facebook" method="oauth2" %}" class="top-menu"><span class="glyphicon glyphicon-lock"></span></a>
{% endif %}
<h1><a href="/">My Blog</a></h1>
</div>
<div class="content container">
<div class="row">
<div class="col-md-8">
{% block content %}
{% endblock %}
</div>
</div>
</div>
</body>
</html>
7. Testing
To test, we need to load the site using our https ngrok link. For example, you may navigate to something like https://dbxee81e.ngrok.io. Now click the lock icon, to be redirected to facebook, where you’ll see a page like the one below.
Click the big blue “Continue” button and you should be all logged in! You should see both your first name and profile picture displayed in the top bar.
Congratulations 🎉 🎉 !
The final code is also available here for your reference. If you’re using Django 1.11, use the final code here.
Note that, you’ll have to run the following commands after downloading the above project.
python3 -m venv myvenv
source myvenv/bin/activate
pip install -r requirements.txt
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
You’ll also have to create the social account in the admin again!