Step by Step Guide to Email + Social Logins in Django

Abhik
13 min readJan 2, 2021

--

drf oauth2

As a beginner , logging/ registering a user with basic email password and google as well as facebook using token based authentication can be a amazing stuff to learn. This step by step guide will help any beginner in django to learn a lot of stuff including custom user in django and also a bonus axios interceptors.

Let me explain what we want and how will authentication happen:

What we want:

  1. We will register a user we need to pass username , email , first_name , password (because of our model) .
  2. We will log in that user and that will authenticate user
  3. We will make authenticated requests to api

How will authentication happen:

Before that let me clarify we will send authenticated requests by sending a Bearer token in Authorization headers of the request .This token will tell the server which user sent the request. For that we will do these:

  1. We will send the username and password to a endpoint to get the token and this will serve as our login
  2. In return we will get access token and refresh token, we will set the authorization header of all requests to this access token
  3. However access token will expire after a short time, then we will send the refresh token to the endpoint to get a new access token
  4. thus we will repeat step 2 and 3 internally without letting the end user know when the access token expire

Before starting , let us go through some prerequisites:

  • Basic knowledge of Django
  • Create a django project (inside a virtual environment preferably)
  • Postman installed in your system (you can download it from here or use any alternative of postman)

Step 1: Creating Custom User Model Django

This is a mandatory step for email based authentication. For that we are creating a app Accounts.

python manage.py startapp accounts

Then you can see that a new folder is created with accounts name, now lets add it to the INSTALLED_APPS in settings.py. So it should look something like this:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# add this
'accounts',
]

Inside the models.py lets create the model Account and its Manager & also import these:

from django.db import models
from django.contrib.auth.models import AbstractBaseUser,PermissionsMixin,BaseUserManager
from django.utils.translation import gettext_lazy as _
class Account(AbstractBaseUser,PermissionsMixin):
email=models.EmailField(unique=True)
username= models.CharField(_('User Name'),max_length=150)
first_name = models.CharField(_('First Name'),max_length=150)
last_name = models.CharField(_('last Name'),max_length=150)
is_staff=models.BooleanField(default=False)
is_active=models.BooleanField(default=True)
objects=CustomAccountManager() USERNAME_FIELD='email'
REQUIRED_FIELDS=['username','first_name']
def __str__(self):
return self.email
  • here we are using email as default USERNAME_FIELD and we want username and first_name as required fields (you can also have any other fields but username field should be there
  • is_active should be True by default and is_staff should be False by default
  • for creating objects(users) we need a Custom manager (which I am creating below)
  • write “username” instead of user_name or any other style because facebook or google login returns username
  • using gettextlazy is optional
class CustomAccountManager(BaseUserManager):
def create_user(self,email,username,first_name,password,**other_fields):
if not email:
raise ValueError(_('Please provide an email address'))
email=self.normalize_email(email)
user=self.model(email=email,username=username,first_name=first_name,**other_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self,email,username,first_name,password,**other_fields):
other_fields.setdefault('is_staff',True)
other_fields.setdefault('is_superuser',True)
other_fields.setdefault('is_active',True)
if other_fields.get('is_staff') is not True:
raise ValueError(_('Please assign is_staff=True for superuser'))
if other_fields.get('is_superuser') is not True:
raise ValueError(_('Please assign is_superuser=True for superuser'))
return self.create_user(email,username,first_name,password,**other_fields)

Here create_user will create regular users while create_superuser is create super users (admin).

create_user

  • for creating users we need to pass email, username , first_name and password as well as any other fields
  • if no email then raise error otherwise normailze email (Read here)
  • then create a model Account object with email,username and other fields
  • then set password , set_password actually sets the password as a hashed password in the model’s object so no one can see the actual password
  • then save the user object and return it

create_superuser

  • is_staff , is_superuser, is_active should be set to True by default
  • if not set to True or is passed False then raise errors else create_user with this values as well as other fields

Now add this in settings.py to use our custom user model :

AUTH_USER_MODEL='accounts.Account'

Then we need to migrate this model to database.

python manage.py makemigrations
python manage.py migrate

Step 2: Making REST API Endpoints for Authentication Django

For that we need to install some libraries first which I will explain below why we need them:

pip install djangorestframework
pip install django-cors-headers
pip install drf_social_oauth2
  • djangorestframework is for REST API Endpoints
  • django-cors-headers is required so that our React app can communicate with the django server
  • drf_social_oauth2 — this is the main library which enables us oauth2 token based authentication for email password as well as google and facebook

Now inside the settings.py we should add these to INSTALLED_APPS for our app to work as expected:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# add these
'rest_framework',
'oauth2_provider',
'social_django',
'drf_social_oauth2',
'corsheaders',
# LOCAL
'accounts',
]

Let’s also add these configurations to settings.py which I’ll explain below:

AUTHENTICATION_BACKENDS = (  
'drf_social_oauth2.backends.DjangoOAuth2',
'django.contrib.auth.backends.ModelBackend',
)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
'drf_social_oauth2.authentication.SocialAuthentication',
)
}
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000"
]
  • AUTHENTICATION_BACKENDS — so that we can authenticate with either OAuth2(token based) or basic authentication(without token)
  • REST_FRAMEWORK endpoint requests can be authenticated using tokens only
  • CORS_ALLOWED_ORIGINS will be our frontend’s address (here it’s react website’s address)

Alright now let’s add some configuration to the MIDDLEWARE and TEMPLATES :

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# add these
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
# add these
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
},
},
]

Those are required by social logins and cors

Let’s add some URL to django project’s urls.py as those are required by both rest framework and oauth

from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
# add these
path('api-auth/', include('rest_framework.urls')),
path('api-auth/', include('drf_social_oauth2.urls',namespace='drf')),#???? namespace='drf' is mandatory
]

After adding this and that as required let’s start coding again.

For Registering / Signing Up a user we need to write a serializer and view . Note that after signing up , that user needs to login by himself , that won’t happen automatically. However I have covered that as well! So a user can register and our backend will login that user!!

Let’s create the serializer, I have already created a serializers.py in accounts folder , so now write this code:

from rest_framework import serializers
from .models import Account
class RegistrationSerializer(serializers.ModelSerializer):
class Meta:
model=Account
fields=('email','username','password','first_name')
extra_kwargs={'password':{'write_only':True}}
def create(self,validated_data):
password=validated_data.pop('password',None)
instance=self.Meta.model(**validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance

Inside the RegistrationSerializer :

  • class Meta has what model , fields will be serialized and password cannot be read it will only be write only (for forms )
  • create method will create new instance of Account and will fill that with validated data which we pass from our view
  • we need that password and if that password is not None we can set hashed password so we will pop it out of the validated data
  • then we will save the instance and return it

Now we will write the view for creating users:

from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework import status,generics
from rest_framework.response import Response
from .serializers import RegistrationSerializer
from rest_framework import permissions
from .models import Account
class CreateAccount(APIView):
permission_classes=[permissions.AllowAny]
def post(self,request):
reg_serializer=RegistrationSerializer(data=request.data)
if reg_serializer.is_valid():
new_user=reg_serializer.save()
if new_user:
return Response(status=status.HTTP_201_CREATED)
return Response(reg_serializer.errors,status=status.HTTP_400_BAD_REQUEST)
  • we import the necessary , allow anyone to access this view and only post requests to this view is allowed
  • when post request happens we will initialize RegistrationSerializer with request.data and if the data is valid then save it else return error
  • *Note : we will revisit this view to login the user after saving the serialized data later on

also create a urls.py in accounts directory and write this

from django.urls import path
from .views import CreateAccount
app_name = 'users'urlpatterns = [
path('create/', CreateAccount.as_view(), name="create_user"),]

finally add this url to project’s urls.py

from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')),
path('api-auth/', include('drf_social_oauth2.urls',namespace='drf')),
#add this
path('api-auth/', include('accounts.urls'))
]

Now for login we don’t need to write any extra code as login is nothing but getting access token from server and all these is already taken care of by drf-social-oauth2. We don’t even need to write any endpoints for that !

We need to create a Application and get client_id and client_secret to get access tokens, So let’s visit admin running on http://127.0.0.1:8000/admin/ and you should see something like this:

Here you can see Applications , press the add button of that and create a new application. Don’t touch the Client id and Client secret , select user as your admin superuser and Client type will be confidential , Authorization grant type will be Resource owner password-based , then save it.

That’s it we are ready to check our application now so let’s create a superuser and pass the required values and then run server inside virtual environment

python manage.py runserver

Step 3 : Creating & Logging in user with email password Postman

Now open Postman and write this URL and in the body write this and send the request!

You should get a blank response like this

Thus the user is created so it is working fine!

For sending request for token we will pass client_id, client_secret , email as username , password and grant_type=password to let server know this is for getting fresh new access_token and refresh token

Now we can send requests for token, so back in postman send this request to http://127.0.0.1:8000/api-auth/token :

Note that we pass email to username field in request as passing username is mandatory

  • It should return access token and refresh token as well as status of 200 Ok
  • This access_token can be set on Authorization headers of request to send Authenticated requests (we will do that with react)

Remember I said we will revisit that CreateAccount view to login the user after saving the serialized data , Lets do that

import requests # add thisclass CreateAccount(APIView):
permission_classes=[permissions.AllowAny]
def post(self,request):
reg_serializer=RegistrationSerializer(data=request.data)
if reg_serializer.is_valid():
new_user=reg_serializer.save()
if new_user:
#add these
r=requests.post('http://127.0.0.1:8000/api-auth/token', data = {
'username':new_user.email,
'password':request.data['password'],
'client_id':'Your Client ID',
'client_secret':'Your Client Secret',
'grant_type':'password'
})
return Response(r.json(),status=status.HTTP_201_CREATED)
return Response(reg_serializer.errors,status=status.HTTP_400_BAD_REQUEST)

So after saving the new user we will send a post request to get the token and send it back as Response . This will automatically login the user after signup!

Google and Facebook authentications need a frontend for complete demonstrations so I will show you that with react , however any frontend can do if you know what to do ! (I’ll do that after the necessary steps)

Step 4 : Authenticated request demonstration preparation Django

We will create a different serializer which will return info about users and current user through two different views, one will be authenticated request and another will be non authenticated request.

so the serializer will look like this :

class UsersSerializer(serializers.ModelSerializer):
class Meta:
model=Account
fields=('email','username','first_name')

We will write this two views :

  • AllUsers returns all users and any one can view data
  • CurrentUser which returns only the current user and only authenticated requests allowed
from rest_framework import status,genericsclass AllUsers(generics.ListAPIView):
permission_classes=[permissions.AllowAny]
queryset=Account.objects.all()
serializer_class=UsersSerializer
class CurrentUser(APIView):
permission_classes = (permissions.IsAuthenticated,)
def get(self, request):
serializer = UsersSerializer(self.request.user)
return Response(serializer.data)

urls.py will look like this :

from django.urls import path
from .views import CreateAccount,AllUsers,CurrentUser
app_name = 'users'urlpatterns = [
path('create/', CreateAccount.as_view(), name="create_user"),
path('all/', AllUsers.as_view(), name="all"),
path('currentUser/', CurrentUser.as_view(), name="current"),
]

Step 5 : Authenticated request demonstration Postman

So let’s send non-authenticated request first and it returns this response (Note that Authorization header has nothing in it)

Now let’s get the access token we previously recieved on step 3

It will return the current user now if you have by chance sent the request without Authorization header it would have returned like this

Now what if the access token expires what to do then ??

For that we need to send refresh token to the same endpoint to get new access and refresh token

Let me show you how ! So we send the request to /token with client_id, client_secret, grant_type = refresh_token (so that server understands request has a refresh token which then converts old access and refresh token to new access and refresh token ) and refresh_token will be the token we recieved in third step

It returns new tokens as a response you can use this new access token as Bearer token to Authenticate requests

Step 6 : Facebook and Google Login Django

Add these in settings.py and to get required keys for facebook and google you need to visit here for fb and here for google and perform the necessary steps there.

AUTHENTICATION_BACKENDS = (  
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.facebook.FacebookAppOAuth2',
'social_core.backends.facebook.FacebookOAuth2',

'drf_social_oauth2.backends.DjangoOAuth2',
'django.contrib.auth.backends.ModelBackend',
)
# Facebook configuration
SOCIAL_AUTH_FACEBOOK_KEY = 'your facebook key'
SOCIAL_AUTH_FACEBOOK_SECRET = 'your facebook secret'
# Define SOCIAL_AUTH_FACEBOOK_SCOPE to get extra permissions from Facebook.
# Email is not sent by default, to get it, you must request the email permission.
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
'fields': 'id, name, email'
}
SOCIAL_AUTH_USER_FIELDS=['email','first_name','username','password']
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = "your google oauth2 key"
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = "your google oauth2 secret"
# Define SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE to get extra permissions from Google.
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]

Thats it we are done with django🙌

Step 7 : Google and Facebook Login on React

Its very easy to authenticate once you have the keys , now we need to install axios , react-facebook-login , react-google-login

Then create a login component where we render these fb and google login button which will take required keys and every user when tries to login with these buttons , will return a accesstoken as well as info about user

import ReactFacebookLogin from "react-facebook-login";
import ReactGoogleLogin from "react-google-login";
import { facebookLogin, googleLogin } from "../axios";# I'll create this later
export default function LogIn() {

function responseFb(response) {
console.log(response);
facebookLogin(response.accessToken);
}
function responseGoogle(response) {
console.log(response);
googleLogin(response.accessToken);
}

return (
<>
<ReactFacebookLogin
appId="Your App Id"
fields="name,email"
callback={responseFb}
/>
<ReactGoogleLogin
clientId="your google client id"
buttonText="Login"
onSuccess={responseGoogle}
onFailure={responseGoogle}
cookiePolicy={"single_host_origin"}
/>
</>
);
}

However our server has no clue that we have logged in a user as this access token is returned from google , facebook and not from our server.

So how to let our server know ? Send this token to our server or set this token as our Authorized Bearer token ? No in that case we cannot create a account object with our Accounts model or access any account object as current user thus our user will not be technically registered or connected.

So we need to convert this token to get access token, refresh token from our server, (registering our user) and for that I created a axios.js file and there I put this code:

export function facebookLogin(accessToken) {
axios
.post(`http://127.0.0.1:8000/api-auth/convert-token`, {
token: accessToken,
backend: "facebook",
grant_type: "convert_token",
client_id: "your client id",
client_secret:"your client secret ",
})
.then((res) => {
// Save somewhere these access and refresh tokens
console.log(res.data);
});
}
export function googleLogin(accessToken) {
axios
.post(`http://127.0.0.1:8000/api-auth/convert-token`, {
token: accessToken,
backend: "google-oauth2",
grant_type: "convert_token",
client_id: "your client id",
client_secret: "your client secret",
})
.then((res) => {
// Save somewhere these access and refresh tokens
console.log(res.data);
});
}

what we are doing here is:

  1. make a post request to http://127.0.0.1:8000/api-auth/convert-token to convert the token
  2. backend will be the backend from where you got the access token to convert
  3. grant_type will be convert_token to let server know we want this token to be converted to our server’s access token
  4. client_id and client_secret will be like previous steps

That’s It !!! We are done with Authentication

Also a bonus for axios in react:

So how to refresh a token if access tokens expire ?? We want that to happen automatically without letting the user bothered.

  • For that we will use axios interceptors which will return responses but if there is error it will handle those error
  • if error status is 401 (Unauthorized request) then if there is no refresh token in local storage then it will ask the user to login , and if refresh token exists then it will send that to http://127.0.0.1:8000/api-auth/token to get new access and refresh tokens
  • Please note I am using local storage to store access tokens as an example but you should use a secure method probably Web Cookies (Secure, HttpOnly, Same Site)
  • window needs to be reloaded to let react fetch the website authenticate with latest access token
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error;
console.log(originalRequest);
if (typeof error.response === "undefined") {
alert("a server error happNeD, we will fix it shortly");
return Promise.reject(error);
}
if (
error.response.status === 401 &&
!localStorage.getItem("refresh_token")
) {
window.location.href = "/login/";
return Promise.reject(error);
}
if (
error.response.status === 401 &&
error.response.statusText === "Unauthorized" &&
localStorage.getItem("refresh_token") !== undefined
) {
const refreshToken = localStorage.getItem("refresh_token");
return axios
.post("http://127.0.0.1:8000/api-auth/token", {
client_id: "Your client id ",
client_secret:
"Your client secret",
grant_type: "refresh_token",
refresh_token: refreshToken,
})
.then((response) => {
localStorage.setItem("access_token", response.data.access_token);
localStorage.setItem("refresh_token", response.data.refresh_token);
window.location.reload();
axiosInstance.defaults.headers["Authorization"] =
"Bearer " + response.data.access_token;
})
.catch((err) => console.log(err));
}
}
);

Hope this helps 🤞

--

--

Abhik
Abhik

Written by Abhik

Interested in frontend developement & UI design

Responses (4)