Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Implement google authentication. #113

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions drf_user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ def update_user_settings() -> dict:
Returns
-------
user_settings: dict

Author: Himanshu Shankar (https://himanshus.com)
"""
custom_settings = getattr(settings, "USER_SETTINGS", None)

Expand Down
4 changes: 0 additions & 4 deletions drf_user/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""
All Admin configuration related to drf_user

Author: Himanshu Shankar (https://himanshus.com)
"""
from django.contrib import admin
from django.contrib.auth.admin import Group
Expand All @@ -19,8 +17,6 @@ class DRFUserAdmin(UserAdmin):
"""
Overrides UserAdmin to show fields name & mobile and remove fields:
first_name, last_name

Author: Himanshu Shankar (https://himanshus.com)
"""

fieldsets = (
Expand Down
4 changes: 0 additions & 4 deletions drf_user/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ def ready(self):
Register signals
Call update_user_settings() to update the user setting as per
django configurations
Returns
-------

Author: Himanshu Shankar (https://himanshus.com)
"""

from . import update_user_settings
Expand Down
12 changes: 8 additions & 4 deletions drf_user/auth.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""
Custom backends to facilitate authorizations

Author: Himanshu Shankar (https://himanshus.com)
"""
import re
from typing import Optional

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.http import HttpRequest

from drf_user.models import User


class MultiFieldModelBackend(ModelBackend):
Expand All @@ -17,7 +19,9 @@ class MultiFieldModelBackend(ModelBackend):

user_model = get_user_model()

def authenticate(self, request, username=None, password=None, **kwargs) -> None:
def authenticate(
self, request: HttpRequest, username: str = None, password: str = None, **kwargs
) -> Optional[User]:
"""
This function is used to authenticate a user. User can send
either of email, mobile or username in request to
Expand Down Expand Up @@ -57,7 +61,7 @@ def authenticate(self, request, username=None, password=None, **kwargs) -> None:
except self.user_model.DoesNotExist:
return None

def get_user(self, username: int) -> None:
def get_user(self, username: int) -> Optional[User]:
"""Returns user object if exists otherwise None

Parameters
Expand Down
52 changes: 52 additions & 0 deletions drf_user/google_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Helper methods related to google authentication"""
from typing import Any
from typing import Dict

import requests
from django.conf import settings
from rest_framework.exceptions import ValidationError

from drf_user.variables import GOOGLE_ACCESS_TOKEN_OBTAIN_URL
from drf_user.variables import GOOGLE_AUTHORIZATION_CODE
from drf_user.variables import GOOGLE_USER_INFO_URL


def google_get_access_token(*, code: str, redirect_uri: str) -> str:
"""This method get access token from google API"""
# Reference: https://developers.google.com/identity/protocols/oauth2/web-server#obtainingaccesstokens # NOQA
google_client_id: str = settings.GOOGLE_OAUTH2_CLIENT_ID
google_client_secret: str = settings.GOOGLE_OAUTH2_CLIENT_SECRET
if not (google_client_id and google_client_secret):
raise ValueError(
"GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET must be set in your settings file." # NOQA
)

data = {
"code": code,
"client_id": google_client_id,
"client_secret": google_client_secret,
"redirect_uri": redirect_uri,
"grant_type": GOOGLE_AUTHORIZATION_CODE,
}

response = requests.post(GOOGLE_ACCESS_TOKEN_OBTAIN_URL, data=data)

if not response.ok:
raise ValidationError(
f"Failed to obtain access token from Google. {response.json()}"
)

return response.json()["access_token"]


def google_get_user_info(*, access_token: str) -> Dict[str, Any]:
"""This method gives us the user info using google's access token."""
# Reference: https://developers.google.com/identity/protocols/oauth2/web-server#callinganapi # NOQA
response = requests.get(GOOGLE_USER_INFO_URL, params={"access_token": access_token})

if not response.ok:
raise ValidationError(
f"Failed to obtain user info from Google. {response.json()}"
)

return response.json()
34 changes: 25 additions & 9 deletions drf_user/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
class UserManager(BaseUserManager):
"""
UserManager class for Custom User Model

Author: Himanshu Shankar (https://himanshus.com)
Source: Can't find link but the following solution is inspired
from a solution provided on internet.
"""
Expand All @@ -21,8 +19,8 @@ def _create_user(
self,
username: str,
email: str,
password: str,
fullname: str,
name: str,
password: Optional[str] = None,
mobile: Optional[str] = None,
**kwargs
):
Expand All @@ -31,18 +29,21 @@ def _create_user(
"""
email = self.normalize_email(email)
user = self.model(
username=username, email=email, name=fullname, mobile=mobile, **kwargs
username=username, email=email, name=name, mobile=mobile, **kwargs
)
user.set_password(password)
if password:
user.set_password(password)
else:
user.set_unusable_password()
user.save(using=self._db)
return user

def create_user(
self,
username: str,
email: str,
password: str,
name: str,
password: Optional[str] = None,
mobile: Optional[str] = None,
**kwargs
):
Expand All @@ -69,7 +70,14 @@ def create_user(
kwargs.setdefault("is_staff", False)
kwargs.setdefault("is_active", vals.get("DEFAULT_ACTIVE_STATE", False))

return self._create_user(username, email, password, name, mobile, **kwargs)
return self._create_user(
username=username,
email=email,
name=name,
password=password,
mobile=mobile,
**kwargs
)

def create_superuser(
self,
Expand All @@ -83,6 +91,7 @@ def create_superuser(
"""
Creates a super user considering the specified user settings
from Django Project's settings.py

Parameters
----------
username: str
Expand All @@ -108,4 +117,11 @@ def create_superuser(
if kwargs.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")

return self._create_user(username, email, password, name, mobile, **kwargs)
return self._create_user(
username=username,
email=email,
name=name,
password=password,
mobile=mobile,
**kwargs
)
16 changes: 16 additions & 0 deletions drf_user/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Helper Mixins"""
from rest_framework.permissions import AllowAny
from rest_framework.permissions import IsAuthenticated


class AuthAPIMixin:
"""Mixin for Authenticated APIs"""

permission_classes = (IsAuthenticated,)


class PublicAPIMixin:
"""Mixin for Public APIs"""

authentication_classes = ()
permission_classes = (AllowAny,)
6 changes: 0 additions & 6 deletions drf_user/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ class User(AbstractBaseUser, PermissionsMixin):
mobile: Mobile Number of the user
name: Name of the user. Replaces last_name & first_name
update_date: DateTime instance when the user was updated

Author: Himanshu Shankar (https://himanshus.com)
"""

username = models.CharField(
Expand Down Expand Up @@ -93,8 +91,6 @@ class AuthTransaction(models.Model):
"""
Represents all authentication in the system that took place via
REST API.

Author: Himanshu Shankar (https://himanshus.com)
"""

ip_address = models.GenericIPAddressField(blank=False, null=False)
Expand Down Expand Up @@ -130,8 +126,6 @@ class Meta:
class OTPValidation(models.Model):
"""
Represents all OTP Validation in the System.

Author: Himanshu Shankar (https://himanshus.com)
"""

otp = models.CharField(verbose_name=_("OTP Code"), max_length=10)
Expand Down
36 changes: 26 additions & 10 deletions drf_user/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Serializers related to drf-user"""
from typing import Dict
from typing import Optional

from django.contrib.auth.password_validation import validate_password
from django.core.validators import EmailValidator
from django.core.validators import ValidationError
Expand Down Expand Up @@ -40,7 +43,7 @@ def validate_email(self, value: str) -> str:
return value
else:
raise serializers.ValidationError(
"The email must be " "pre-validated via OTP."
"The email must be pre-validated via OTP."
)

def validate_mobile(self, value: str) -> str:
Expand Down Expand Up @@ -133,16 +136,14 @@ class OTPSerializer(serializers.Serializer):
>>> OTPSerializer(data={"destination": "88xx6xx5xx",
>>> "email": "[email protected]",
>>> "verify_otp": 2930433, "is_login": True})

Author: Himanshu Shankar (https://himanshus.com)
"""

email = serializers.EmailField(required=False)
is_login = serializers.BooleanField(default=False)
verify_otp = serializers.CharField(required=False)
destination = serializers.CharField(required=True)

def get_user(self, prop: str, destination: str) -> User:
def get_user(self, prop: str, destination: str) -> Optional[User]:
"""
Provides current user on the basis of property and destination
provided.
Expand Down Expand Up @@ -170,7 +171,7 @@ def get_user(self, prop: str, destination: str) -> User:

return user

def validate(self, attrs: dict) -> dict:
def validate(self, attrs: Dict) -> Dict:
"""
Performs custom validation to check if any user exists with
provided details.
Expand Down Expand Up @@ -243,7 +244,7 @@ class OTPLoginRegisterSerializer(serializers.Serializer):
mobile = serializers.CharField(required=True)

@staticmethod
def get_user(email: str, mobile: str):
def get_user(email: str, mobile: str) -> Optional[User]:
"""Fetches user object"""
try:
user = User.objects.get(email=email)
Expand Down Expand Up @@ -272,7 +273,7 @@ def get_user(email: str, mobile: str):
)
return user

def validate(self, attrs: dict) -> dict:
def validate(self, attrs: Dict) -> Dict:
"""Validates the response"""

attrs["user"] = self.get_user(
Expand All @@ -294,7 +295,7 @@ class PasswordResetSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
password = serializers.CharField(required=True)

def get_user(self, destination: str) -> User:
def get_user(self, destination: str) -> Optional[User]:
"""Provides current user on the basis of property and destination
provided.

Expand All @@ -313,7 +314,7 @@ def get_user(self, destination: str) -> User:

return user

def validate(self, attrs: dict) -> dict:
def validate(self, attrs: Dict) -> Dict:
"""Performs custom validation to check if any user exists with
provided email.

Expand Down Expand Up @@ -368,7 +369,7 @@ class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
}

@classmethod
def get_token(cls, user):
def get_token(cls, user: User) -> str:
"""Generate token, then add extra data to the token."""
token = super().get_token(user)

Expand All @@ -383,3 +384,18 @@ def get_token(cls, user):
token["name"] = user.name

return token


class GoogleLoginSerializer(serializers.Serializer):
"""Google Login Serializer

Serializer to handle google oauth2 callback
Params
code: If the Google OAuth2 was successful,
Google will call our callback API with a code GET parameter.
error: If the Google OAuth2 was not successful,
Google will call our API with an error GET parameter.
"""

code = serializers.CharField(required=False)
error = serializers.CharField(required=False)
1 change: 1 addition & 0 deletions drf_user/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@
path(
"refresh-token/", views.CustomTokenRefreshView.as_view(), name="refresh_token"
),
path("google/", views.GoogleLoginView.as_view(), name="login-with-google"),
]
Loading