Skip to content
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
File renamed without changes.
9 changes: 9 additions & 0 deletions authentication/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib import admin
from .models import User


class UserAdmin(admin.ModelAdmin):
list_display = ['username', 'email', 'auth_provider', 'created_at']


admin.site.register(User, UserAdmin)
6 changes: 6 additions & 0 deletions authentication/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class AuthenticationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'authentication'
37 changes: 37 additions & 0 deletions authentication/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 5.0.3 on 2024-06-05 18:01

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]

operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(db_index=True, max_length=255, unique=True)),
('email', models.EmailField(db_index=True, max_length=255, unique=True)),
('is_verified', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=True)),
('is_staff', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('auth_provider', models.CharField(default=None, max_length=255)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
]
File renamed without changes.
60 changes: 60 additions & 0 deletions authentication/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin)

from django.db import models
from rest_framework_simplejwt.tokens import RefreshToken


class UserManager(BaseUserManager):

def create_user(self, username, email, password=None):
if username is None:
raise TypeError('Users should have a username')
if email is None:
raise TypeError('Users should have a Email')

user = self.model(username=username, email=self.normalize_email(email))
user.set_password(password)
user.save()
return user

def create_superuser(self, username, email, password=None):
if password is None:
raise TypeError('Password should not be none')

user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.save()
return user


AUTH_PROVIDERS = {'google': 'google','email': 'email'}


class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(max_length=255, unique=True, db_index=True)
email = models.EmailField(max_length=255, unique=True, db_index=True)
is_verified = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
auth_provider = models.CharField(
max_length=255, blank=False,
null=False, default=AUTH_PROVIDERS.get('email'))

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']

objects = UserManager()

def __str__(self):
return self.email

def tokens(self):
refresh = RefreshToken.for_user(self)
return {
'refresh': str(refresh),
'access': str(refresh.access_token)
}
File renamed without changes.
File renamed without changes.
38 changes: 36 additions & 2 deletions clearpath_home/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""

from pathlib import Path

import datetime
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

Expand All @@ -26,11 +26,17 @@

# Use the environment variables
SECRET_KEY = str(os.getenv('SECRET_KEY'))
SOCIAL_AUTH_GITHUB_KEY = str(os.getenv('GITHUB_KEY'))
SOCIAL_SECRET = str(os.getenv('SOCIAL_SECRET'))
SOCIAL_AUTH_GITHUB_SECRET = str(os.getenv('GITHUB_SECRET'))
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = str(os.getenv('GOOGLE_KEY'))
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = str(os.getenv('GOOGLE_SECRET'))
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')



AUTH_USER_MODEL = 'authentication.User'

CORS_ALLOW_ALL_ORIGINS = True
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

Expand All @@ -46,6 +52,10 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_simplejwt.token_blacklist',
'authentication',
'corsheaders',
]

MIDDLEWARE = [
Expand All @@ -60,6 +70,30 @@

ROOT_URLCONF = 'clearpath_home.urls'

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',

)
}

# JWT Settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=1),
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),
}

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
Expand Down
3 changes: 2 additions & 1 deletion clearpath_home/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('google_auth/', include(('google_auth.urls', 'google_auth'),namespace="google_auth")),
]
Empty file added google_auth/__init__.py
Empty file.
File renamed without changes.
4 changes: 2 additions & 2 deletions users/apps.py → google_auth/apps.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.apps import AppConfig


class UsersConfig(AppConfig):
class GoogleAuthConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'
name = 'google_auth'
35 changes: 35 additions & 0 deletions google_auth/google.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from google.auth.transport import requests
from google.oauth2 import id_token
from rest_framework import serializers, status

class Google:
"""Google class to fetch the user info and return it"""

@staticmethod
def validate(auth_token):
"""
Validate method queries the Google OAuth2 API to fetch the user info
"""
try:
idinfo = id_token.verify_oauth2_token(auth_token, requests.Request(), clock_skew_in_seconds=5)

# Acceptable issuer URLs
valid_issuers = [
'https://accounts.google.com',
'accounts.google.com'
]

# matching value issuers
if idinfo['iss'] not in valid_issuers:
raise serializers.ValidationError('Invalid token issuer.')

print(f"Token validated successfully: {idinfo}")
return idinfo

except ValueError as ve:
raise serializers.ValidationError('The token is either invalid or has expired.')

except Exception as e:
# error handling
print(f"Token validation error: {e}")
raise serializers.ValidationError('An error occurred during token validation.')
Empty file.
File renamed without changes.
40 changes: 40 additions & 0 deletions google_auth/register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django.contrib.auth import authenticate
from authentication.models import User
import os
from rest_framework.exceptions import AuthenticationFailed


def register_social_user(provider, user_id, email, name):
filtered_user_by_email = User.objects.filter(email=email)

if filtered_user_by_email.exists():
print(f"User with email {email} exists.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's ensure we remove prints in production but aside that I think we are good

if provider == filtered_user_by_email[0].auth_provider:
registered_user = authenticate(email=email, password=os.environ.get('SOCIAL_SECRET'))
print(f"Authenticated existing user: {registered_user.username}")
return {
'username': registered_user.username,
'email': registered_user.email,
'tokens': registered_user.tokens()
}
else:
print(f"Authentication failed: Existing user uses a different provider.")
raise AuthenticationFailed(detail=f'Please continue your login using {filtered_user_by_email[0].auth_provider}')
else:
print(f"Creating new user with email {email}")
user = {
'username': email,
'email': email,
'password': os.environ.get('SOCIAL_SECRET')
}
user = User.objects.create_user(**user)
user.is_verified = True
user.auth_provider = provider
user.save()
new_user = authenticate(email=email, password=os.environ.get('SOCIAL_SECRET'))
print(f"Created and authenticated new user: {new_user.username}")
return {
'email': new_user.email,
'username': new_user.username,
'tokens': new_user.tokens()
}
29 changes: 29 additions & 0 deletions google_auth/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from rest_framework import serializers
from .google import Google
from .register import register_social_user
import os
from rest_framework.exceptions import AuthenticationFailed

class GoogleSocialAuthSerializer(serializers.Serializer):
auth_token = serializers.CharField()

def validate_auth_token(self, auth_token):
user_data = Google.validate(auth_token)
try:
user_data['sub']
except KeyError:
print('KeyError: sub not found in user_data')
raise serializers.ValidationError('The token is invalid or expired. Please login again.')

if user_data['aud'] != os.environ.get('GOOGLE_CLIENT_ID'):
print(f"Invalid client ID: {user_data['aud']} != {os.environ.get('GOOGLE_CLIENT_ID')}")
raise AuthenticationFailed('Invalid client ID.')

user_id = user_data['sub']
email = user_data['email']
name = user_data['name']
provider = 'google'

print(f"User data: user_id={user_id}, email={email}, name={name}, provider={provider}")

return register_social_user(provider=provider, user_id=user_id, email=email, name=name)
3 changes: 3 additions & 0 deletions google_auth/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
7 changes: 7 additions & 0 deletions google_auth/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path
from .views import GoogleSocialAuthView

urlpatterns = [
path('google/', GoogleSocialAuthView.as_view()),

]
21 changes: 21 additions & 0 deletions google_auth/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from rest_framework import status
from rest_framework.response import Response
from rest_framework.generics import GenericAPIView
from .serializers import GoogleSocialAuthSerializer

class GoogleSocialAuthView(GenericAPIView):
serializer_class = GoogleSocialAuthSerializer

def post(self, request):
"""
POST with "auth_token"
Send an idtoken as from google to get user information
"""
print('Request data:', request.data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also let take this out as well for production

serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data['auth_token']
print('Validated auth_token:', data)
return Response(data, status=status.HTTP_200_OK)