Integrating authentication is the important part for any product, and being a backend developer myself I know this is the first thing which is to be implemented, So I am going to share my knowledge of implementing authentication which is generic and can be implemented anywhere, Let suppose that your users can signup by three ways but all the three authentications must access the same profile.
1) Facebook (provides a uniqueID when the user gets authenticated.)
2) Google (provides a uniqueID when the user gets authenticated.)
3) Manual (User sets a password in-app functionality.)
I will be creating the endpoints for signUp, signIn, testAPI (to check if authentication works) and signOut. I have used Java and GoLang earlier for most of the backend related work, and while learning Django I learned that Django provides us with a User model and its authentication mechanism prebuilt, Wow!! something which was missing in the languages which I have already used, but I want to have my own User model with different features and different authentication mechanism then what Django provides. So I will be going to discuss more in detail of my custom user authentication. So let’s create a Django project with the command
django-admin startproject custom-auth
python manage.py startapp auth-app
the above command will create a new project with its settings.py file where the database credentials will be saved. Let’s create a conf file in your project’s directory and save all the database credentials in this file, then add path of this file in your settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'OPTIONS': {
'read_default_file': 'path-to-your/database-credentials.cnf',
},
}
}
Now our project is connected to a database. We will be using serializers package from the rest-framework library for rendering the JSON response. Now let’s create our models in our auth-app folder, Our first model will be UserAuth which is defined as:
class UserAuth(models.Model):
ACCOUNT_TYPES = (
(1, 'FACEBOOK'),
(2, 'GOOGLE'),
(3, 'MANUAL')
)
user=models.ForeignKey(CustomUser,
related_name='userAuths',
on_delete=models.CASCADE,
null=False)
uniqueId = models.CharField(max_length=255,
null=False,
blank=False)
type = models.IntegerField(choices=ACCOUNT_TYPES,default=3)
def from_dict(self,j):
self.__dict__.update(j)
Where User is a foreign key referencing to our User model and uniqueId is the secret key provided by Google/Facebook or it is a password in case of manual authentication. Let’s create our User model now
from django.db import models
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import UserManager
class CustomUser(AbstractBaseUser):
ACTIVE = 1
INACTIVE = 2
REGISTERED = 3
DEACTIVATED = 4
WAITING_FOR_APPROVAL = 5
STATUS_CHOICES = (
(ACTIVE, 'ACTIVE'),
(INACTIVE, 'INACTIVE'),
(REGISTERED, 'REGISTERED'),
(DEACTIVATED,'DEACTIVATED'),
(WAITING_FOR_APPROVAL,'WAITING_FOR_APPROVAL')
)
name = models.CharField(max_length=255, blank=False)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
status = models.IntegerField(choices=STATUS_CHOICES, default=ACTIVE)
email = models.EmailField(max_length=70,
null=False,
blank=False,
unique=True)
mobile = models.CharField(max_length=255, blank=False)
imageUrl = models.CharField(max_length=255, blank=False)
address = models.CharField(max_length=255, blank=True)
lat = models.DecimalField(decimal_places=10, max_digits=20)
lng = models.DecimalField(decimal_places=10, max_digits=20)
follows = models.ManyToManyField('self',
blank=True,
symmetrical=False,
related_name='followed_by')
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [name,mobile,lat,lng]
objects = UserManager()
active = True
staff = True
admin = True
def followers_count(self):
return self.followed_by.all().count()
def following_count(self):
return self.follows.all().count()
isFollowing = 0
def get_full_name(self):
return self.name
def get_short_name(self):
return self.name
def __str__(self): # __unicode__ on Python 2
return self.name
def has_perm(self, perm, obj=None):
"Does the user have a specific permission?"
# Simplest possible answer: Yes, always
return True
def has_module_perms(self, app_label):
"Does the user have permissions to
view the app`app_label`?"
# Simplest possible answer: Yes, always
return True
@property
def is_staff(self):
"Is the user a member of staff?"
return self.staff
@property
def is_admin(self):
"Is the user a admin member?"
return self.admin
@property
def is_active(self):
"Is the user active?"
return self.active
def from_dict(self,j):
self.__dict__.update(j)
So above we have extended Django’s User model with our own properties yet we can use Django’s authentication now, after adding the line in settings.py
AUTH_USER_MODEL = 'auth-app.CustomUser'
After the line above when we run our project, our custom model will be used instead of Django’s user model. Let’s define our serializer for our custom-user.
class UserSerializer(serializers.ModelSerializer):
followersCount = serializers.ReadOnlyField(source='followers_count')
followingCount = serializers.ReadOnlyField(source='following_count')
class Meta:
model = CustomUser
fields = ('id','name','date_created','date_modified',
'status','email','mobile','imageUrl',
'oneLiner','address','lat','lng','followersCount',
'followingCount')
read_only_fields = ('id','date_created', 'date_modified',
'email','followersCount','followingCount')
where followingCount and followersCount is non-database fields. Ok so till now we have defined our models and serializer to render JSON. Let’s define some paths and handlers now. In our urls.py of auth-app folder add these URLs.
from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns
from .views import SignUp,TestApi,SignOut,SignIn
urlpatterns = {
url(r'^api/signUp', SignUp.as_view(),name="signUp"),
url(r'^api/testApi', TestApi.as_view(),name="testApi"),
url(r'^api/signOut', SignOut.as_view(), name="signOut"),
url(r'^api/signIn', SignIn.as_view(), name="signIn"),
}
urlpatterns = format_suffix_patterns(urlpatterns)
Now let’s define some handlers for our paths. In our views.py
from __future__ import unicode_literals
import json
from rest_framework import status
from rest_framework.views import APIView
from .serializers import UserSerializer
from .models import CustomUser,UserAuth
from rest_framework.response import Response
import urllib.request
from django.db import transaction
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth import login, authenticate, logout
import logging
from django.contrib.auth.decorators import login_required
# Get an instance of a logger
logger = logging.getLogger(__name__)
# Create your views here.
def getAddress(lat,long):
base = "http://maps.googleapis.com/maps/api/geocode/json?"
params = "latlng={lat},{lon}&sensor={sen}".format(
lat=lat,
lon=long,
sen='false'
)
url = "{base}{params}".format(base=base, params=params)
request = urllib.request.Request(url)
response = urllib.request.urlopen(request)
tmp = json.loads(response.read())
if (tmp['status'] != 'OK'):
return ''
return tmp['results'][0]['formatted_address']
class SignUp(APIView):
@transaction.atomic
def post(self, request, format=None):
try:
with transaction.atomic():
user = CustomUser()
user.from_dict(request.data.get('user'))
try:
CustomUser.objects.get(email=user.email)
except ObjectDoesNotExist:
if user.address == "":
user.address = getAddress(user.lat, user.lng)
user.save()
userFromDB = CustomUser.objects.get(email=user.email)
userAuth = UserAuth()
userAuth.from_dict(request.data.get('userAuth'))
userAuth.user_id= userFromDB.id
try:
userAuthFromDB = UserAuth.objects.get(user_id=userFromDB.id,type=userAuth.type)
userAuthFromDB.uniqueId = userAuth.uniqueId
userAuthFromDB.save()
except ObjectDoesNotExist:
userAuth.save()
user = authenticate(username=user.email,
type=userAuth.type,
password=userAuth.uniqueId)
logging.debug("user with login %s does exists " % user)
login(request, user)
return Response(UserSerializer(user).data)
except Exception as e:
return Response({"message": repr(e)}, status.HTTP_400_BAD_REQUEST, content_type="application/json")
class TestApi(APIView):
def get(self, request, format=None):
try:
user = request.user
return Response(UserSerializer(user).data)
except Exception as e:
return Response({"message": "Not logged in cannot access current user"},
content_type="application/json")
class SignOut(APIView):
def get(self, request, format=None):
try:
user = request.user
logout(request)
return Response({"message": "{} was sign-out successfully".format(user)},
content_type="application/json")
except Exception as e:
return Response({"message": "sign-out failed"},
content_type="application/json")
class SignIn(APIView):
@transaction.atomic
def post(self, request, format=None):
try:
with transaction.atomic():
user = CustomUser()
user.from_dict(request.data.get('user'))
userAuth = UserAuth()
userAuth.from_dict(request.data.get('userAuth'))
user = authenticate(username=user.email,
type=userAuth.type,
password=userAuth.uniqueId)
logging.debug("user with login %s does exists " % user)
login(request, user)
return Response(UserSerializer(user).data)
except Exception as e:
return Response({"message": "invalid credentials"}, status.HTTP_400_BAD_REQUEST, content_type="application/json")
So We have defined our endpoints and handlers but it will not work as expected because we have not defined our custom authentication method yet. Django has many authentication functions defined for its own User model like token authentication, username-password authentication, etc, but our custom user has no password attribute nor any token, we want to write our own authentication function, so let’s create one, create a file authentication.py in the auth-app folder.
from .models import CustomUser,UserAuth
import logging
# Get an instance of a logger
logger = logging.getLogger(__name__)
class MyClassBackEnd:
def authenticate(self, username=None, password=None,type=None,):
try:
# Try to find a user matching your username
currentUser = CustomUser.objects.get(email=username)
if type == None:
type = 3
userAuth = UserAuth.objects.get(type=type,user=currentUser)
# Check the password is the reverse of the username
if password == userAuth.uniqueId:
# Yes? return the Django user object
return currentUser
else:
# No? return None - triggers default login failed
return None
except CustomUser.DoesNotExist or UserAuth.DoesNotExist:
# No user was found, return None - triggers default login failed
return None
# Required for your backend to work properly - unchanged in most scenarios
def get_user(self, user_id):
try:
return CustomUser.objects.get(pk=user_id)
except CustomUser.DoesNotExist:
return None
Ok, so what we are doing above is authenticating our user based on user-auth which was received from client side and matching the uniqueID in our database. In case of not found we just return None, Now add our custom authentication in settings.py file.
AUTHENTICATION_BACKENDS = (
'auth-app.authentication.MyClassBackEnd',
)
We have now implemented our authentication system successfully, where a user session will be saved in django-sessions when a user sign-up or sign-in and request.user will be saved in the request context which we can access anytime to fetch the logged-in user, and entry from django-sessions will be deleted.
python manage.py makemigrations
python manage.py migrate
python manage.py runserver 8080
The above commands will create tables for our models and run a server instance at 8080 port
Now let’s test our end-points:
1) http://localhost:8080/api/mobile/signUp (POST)
Request:
{
"user": {
"name": "rahul",
"status": 1,
"email": "rahul.yadav@hotcocoasoftware.com",
"mobile": "80810800102",
"imageUrl": "s3.rahul.bucket.1.jog",
"oneLiner": "i am awsome",
"lat": 28.537278,
"lng": 77.2080403
},
"userAuth": {
"uniqueId": "iamrahul",
"type": 3
}
}
Response:
{
"id": 5,
"name": "rahul",
"date_created": "2017-12-21T08:21:59.043936Z",
"date_modified": "2017-12-21T08:21:59.043990Z",
"status": 1,
"email": "rahul.yadav@hotcocoasoftware.com",
"mobile": "80810800102",
"imageUrl": "s3.rahul.bucket.1.jog",
"oneLiner": "i am awsome",
"address": "46, Guru Govind Singh Rd, Begumpur, New Delhi, Delhi 110017, India",
"lat": "28.5372780000",
"lng": "77.2080403000",
"followersCount": 0,
"followingCount": 0,
}
2) http://localhost:8080/api/mobile/testApi (GET)
Response:
{
"id": 5,
"name": "rahul",
"date_created": "2017-12-21T08:21:59.043936Z",
"date_modified": "2017-12-21T08:21:59.043990Z",
"status": 1,
"email": "rahul.yadav@hotcocoasoftware.com",
"mobile": "80810800102",
"imageUrl": "s3.rahul.bucket.1.jog",
"oneLiner": "i am awsome",
"address": "46, Guru Govind Singh Rd, Begumpur, New Delhi, Delhi 110017, India",
"lat": "28.5372780000",
"lng": "77.2080403000",
"followersCount": 0,
"followingCount": 0,
}
3) http://localhost:8080/api/mobile/signOut (GET)
Response:
{
"message": "rahul was sign-out successfully"
}
Now If you again hit the http://localhost:8080/api/mobile/testApi(GET), you will get response
{
"message": "Not logged in cannot access current user"
}
4) At last lets test our signIn request http://localhost:8080/api/mobile/signIn(POST)
Request:
{
"user": {
"email": "rahul.yadav@hotcocoasoftware.com"
},
"userAuth": {
"uniqueId": "iamrahul",
"type": 3
}
}
Response:
{
"id": 5,
"name": "rahul",
"date_created": "2017-12-21T08:21:59.043936Z",
"date_modified": "2017-12-21T08:21:59.043990Z",
"status": 1,
"email": "rahul.yadav@hotcocoasoftware.com",
"mobile": "80810800102",
"imageUrl": "s3.rahul.bucket.1.jog",
"oneLiner": "i am awsome",
"address": "46, Guru Govind Singh Rd, Begumpur, New Delhi, Delhi 110017, India",
"lat": "28.5372780000",
"lng": "77.2080403000",
"followersCount": 0,
"followingCount": 0,
}
In case of wrong type-uniqueId being sent response will be
{
"message": "invalid credentials"
}
After success response from our sign-in request, we can again access our testApi endpoint with the response as the currently logged-in user.
Thanks for reading please suggest any edits.
NOTE: uniqueId can be saved as hash if you want, In this blog, I just want to show how we can use Django authentication on our own models.