Step by Step Guide to Email + Social Logins in Django

Image for post
Image for post
drf oauth2

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. In return we will get access token and refresh token, we will set the authorization header of all requests to this access token
  2. 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
  3. 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
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# add this
'accounts',
]
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
  • 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
AUTH_USER_MODEL='accounts.Account'
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
  • 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
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',
]
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"
]
  • 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)
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',
],
},
},
]
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!!

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
  • 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
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)
  • 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
from django.urls import path
from .views import CreateAccount
app_name = 'users'urlpatterns = [
path('create/', CreateAccount.as_view(), name="create_user"),]
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'))
]
Image for post
Image for post
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!

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
  • This access_token can be set on Authorization headers of request to send Authenticated requests (we will do that with react)
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)

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.

class UsersSerializer(serializers.ModelSerializer):
class Meta:
model=Account
fields=('email','username','first_name')
  • 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)
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)

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

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

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"}
/>
</>
);
}
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);
});
}
  1. backend will be the backend from where you got the access token to convert
  2. grant_type will be convert_token to let server know we want this token to be converted to our server’s access token
  3. 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:

  • 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 🤞

Interested in frontend developement & UI design

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store