CommonLounge Archive

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 the email field. Any field we choose as the USERNAME_FIELD must have unique=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, and date_joined. As an aside, is_staff is True if the user has access to the admin site, and is_active is True 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(). The UserManager defines helper methods like create_userand create_superuser. Since we created our own user object, we need to write our own UserManager 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 pass profile_image_url to `createuserand give it a value ofhttp://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.pyfile 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 the ngrok program in the directory you’re in. So make sure you’re in the directory wherever you downloaded ngrok! (Remember you can use cdto 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

  1. Go to Facebook Developers and log in using your Facebook account.
  2. Go to My Apps, and click Add New App.
  3. Choose any Display Name you’d like and enter your Contact Email.
  4. On the Add a Product page, select Facebook Login, by clicking on Set Up.
  5. Ignore the Quickstart menu and go directly to Facebook LoginSettings from the left hand menu.
  6. Add the following Valid OAuth Redirect URIs:
  7. https://db4da85e.ngrok.io/
  8. https://db4da85e.ngrok.io/accounts/facebook/
  9. 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.

  1. Go to the overall app SettingsBasic 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, allauthwill 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 allauthgenerates.

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 }} with Hello {{ 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!


© 2016-2022. All rights reserved.