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

Nathan/social login2 #160

Closed
wants to merge 11 commits into from
Closed
25 changes: 16 additions & 9 deletions jwtauth/authentication.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from django.contrib.auth import get_user_model
import jwt
import jwt, logging
from django.conf import settings


logger = logging.getLogger(__name__)
User = get_user_model()


Expand All @@ -25,19 +26,25 @@ def authenticate(self, request):
access_token, settings.SECRET_KEY, algorithms=["HS256"]
)

user_id = payload.get("user_id")
user = User.objects.get(id=user_id)
user = User(
id=payload["user_id"],
email=payload.get("email"),
is_staff=payload.get("is_staff"),
is_superuser=payload.get("is_superuser"),
)
user.is_authenticated = True

return (user, None)

except jwt.ExpiredSignatureError:
except jwt.ExpiredSignatureError:
raise AuthenticationFailed("토큰이 만료되었습니다!")
except IndexError:
except IndexError:
raise AuthenticationFailed("토큰이 없습니다!")
except jwt.DecodeError:
raise AuthenticationFailed("토큰이 유효하지 않습니다!")
except jwt.DecodeError:
raise AuthenticationFailed("토큰 디코딩 오류!")
except Exception as e:
raise AuthenticationFailed(f"인증 오류: {str(e)}")
except Exception as e: #
logger.error(f"인증 오류: {str(e)}")
raise AuthenticationFailed(f"인증이 유효하지 않습니다!")

def authenticate_header(self, request):
return "Bearer"
43 changes: 43 additions & 0 deletions jwtauth/migrations/0003_socialaccount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.1.1 on 2024-10-08 02:35

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("jwtauth", "0002_blacklistedtoken_token_type_blacklistedtoken_user_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="SocialAccount",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("uid", models.CharField(max_length=255)),
("provider", models.CharField(max_length=50)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="social_accounts",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("provider", "uid")},
},
),
]
16 changes: 16 additions & 0 deletions jwtauth/migrations/0004_delete_socialaccount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 5.1.1 on 2024-10-10 05:48

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("jwtauth", "0003_socialaccount"),
]

operations = [
migrations.DeleteModel(
name="SocialAccount",
),
]
30 changes: 13 additions & 17 deletions jwtauth/test/test_authentication.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import pytest
from rest_framework.test import APIClient
from rest_framework import status
from django.utils import timezone
from django.urls import reverse
from datetime import timedelta

import jwt
import pytest
from django.conf import settings
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient

from jwtauth.models import BlacklistedToken
from jwtauth.utils.token_generator import generate_access_token, generate_refresh_token

User = get_user_model()
from accounts.models import CustomUser as User


@pytest.fixture
Expand Down Expand Up @@ -57,12 +53,12 @@ def test_로그인_성공(api_client, user):
# Given: 유효한 사용자 정보가 있음
# When: 로그인 API에 POST 요청을 보냄
response = api_client.post(
"/api/login/", {"email": "[email protected]", "password": "testpass123"}
reverse("login"), {"email": "[email protected]", "password": "testpass123"}
)
# Then: 응답 상태 코드가 200이고, 액세스 토큰과 리프레시 토큰이 포함되어 있음
assert response.status_code == status.HTTP_200_OK
assert "access_token" in response.data
assert "refresh_token" in response.data
assert "refresh_token" in response.cookies


@pytest.mark.django_db
Expand All @@ -71,7 +67,7 @@ def test_로그인_실패(api_client):
# Given: 잘못된 사용자 정보가 있음
# When: 로그인 API에 잘못된 정보로 POST 요청을 보냄
response = api_client.post(
"/api/login/", {"email": "[email protected]", "password": "wrongpass"}
reverse("login"), {"email": "[email protected]", "password": "wrongpass"}
)
# Then: 응답 상태 코드가 401 (Unauthorized)임
assert response.status_code == status.HTTP_401_UNAUTHORIZED
Expand All @@ -83,7 +79,7 @@ def test_로그아웃_성공(api_client, user, refresh_token):
# Given: 인증된 사용자와 유효한 리프레시 토큰이 있음
api_client.force_authenticate(user=user)
# When: 로그아웃 API에 리프레시 토큰과 함께 POST 요청을 보냄
response = api_client.post("/api/logout/", {"refresh_token": refresh_token})
response = api_client.post(reverse("logout"), {"refresh_token": refresh_token})
# Then: 응답 상태 코드가 200이고, 리프레시 토큰이 블랙리스트에 추가됨
assert response.status_code == status.HTTP_200_OK
assert BlacklistedToken.objects.filter(token=refresh_token).exists()
Expand All @@ -95,7 +91,7 @@ def test_로그아웃_실패_토큰없음(api_client, user):
# Given: 인증된 사용자가 있지만 리프레시 토큰이 없음
api_client.force_authenticate(user=user)
# When: 로그아웃 API에 리프레시 토큰 없이 POST 요청을 보냄
response = api_client.post("/api/logout/", {})
response = api_client.post(reverse("logout"), {})
# Then: 응답 상태 코드가 400 (Bad Request)임
assert response.status_code == status.HTTP_400_BAD_REQUEST

Expand All @@ -105,7 +101,7 @@ def test_리프레시_토큰_갱신_성공(api_client, user, refresh_token):
"""리프레시 토큰 갱신 API를 테스트합니다."""
# Given: 유효한 리프레시 토큰이 있음
# When: 리프레시 API에 리프레시 토큰과 함께 POST 요청을 보냄
response = api_client.post("/api/refresh/", {"refresh_token": refresh_token})
response = api_client.post(reverse("refresh"), {"refresh_token": refresh_token})
# Then: 응답 상태 코드가 200이고, 새로운 액세스 토큰과 리프레시 토큰이 반환되며, 기존 리프레시 토큰이 블랙리스트에 추가됨
assert response.status_code == status.HTTP_200_OK
assert "access_token" in response.data
Expand All @@ -121,7 +117,7 @@ def test_리프레시_토큰_갱신_실패_블랙리스트(api_client, user, ref
token=refresh_token, user=user, token_type="refresh"
)
# When: 리프레시 API에 블랙리스트에 등록된 리프레시 토큰과 함께 POST 요청을 보냄
response = api_client.post("/api/refresh/", {"refresh_token": refresh_token})
response = api_client.post(reverse("refresh"), {"refresh_token": refresh_token})
# Then: 응답 상태 코드가 400 (Bad Request)임
assert response.status_code == status.HTTP_400_BAD_REQUEST

Expand Down
4 changes: 3 additions & 1 deletion jwtauth/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.urls import path
from .views import LoginView, LogoutView, RefreshTokenView
from .views import LoginView, LogoutView, RefreshTokenView, GoogleLogin


urlpatterns = [
path("login/", LoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
path("refresh/", RefreshTokenView.as_view(), name="refresh"),
path("social-login/google/", GoogleLogin.as_view(), name="google_login"),
]
4 changes: 4 additions & 0 deletions jwtauth/utils/token_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def generate_access_token(user):
"user_id": user.id,
"is_staff": user.is_staff,
"is_superuser": user.is_superuser,
"iat": timezone.now(),
"nickname": user.nickname,
"email": user.email,
# "image": user.image,
"exp": timezone.now() + timedelta(minutes=30),
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
Expand Down
53 changes: 47 additions & 6 deletions jwtauth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework import status
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from django.contrib.auth import authenticate, get_user_model
from django.conf import settings
from .serializers import LoginSerializer, LogoutSerializer, RefreshTokenSerializer
from .utils.token_generator import generate_access_token, generate_refresh_token
from .serializers import (
LoginSerializer,
LogoutSerializer,
RefreshTokenSerializer,
)
from .utils.token_generator import (
generate_access_token,
generate_refresh_token,
)
from .models import BlacklistedToken
import jwt, logging


logger = logging.getLogger(__name__)
User = get_user_model()

Expand Down Expand Up @@ -36,10 +47,15 @@ def post(self, request):
access_token = generate_access_token(user)
refresh_token = generate_refresh_token(user)

return Response(
{"access_token": access_token, "refresh_token": refresh_token}
response = Response({"access_token": access_token})
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=not settings.DEBUG,
samesite="None",
)

return response
else:
return Response(
{"error": "회원 가입하세요"}, status=status.HTTP_401_UNAUTHORIZED
Expand All @@ -64,7 +80,9 @@ def post(self, request):
refresh_token = serializer.validated_data["refresh_token"]

try:
BlacklistedToken.objects.create(token=refresh_token, user=request.user)
BlacklistedToken.objects.create(
token=refresh_token, user=request.user, token_type="refresh"
)
return Response(
{"success": "로그아웃 완료."},
status=status.HTTP_200_OK,
Expand Down Expand Up @@ -136,3 +154,26 @@ def post(self, request):
)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class GoogleLogin(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
callback_url = settings.GOOGLE_CALLBACK_URL
client_class = OAuth2Client

def get_response(self):
response = super().get_response()
user = self.user
access_token = generate_access_token(user)
refresh_token = generate_refresh_token(user)

response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=not settings.DEBUG,
samesite="None",
)
response.data = {"access_token": access_token}

return response
Binary file modified requirements.txt
Binary file not shown.
56 changes: 56 additions & 0 deletions weaverse/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,21 @@
"payments",
"corsheaders",
"django_filters",
# social login
"social_django",
"django.contrib.sites",
"rest_framework.authtoken",
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
"allauth.socialaccount.providers.kakao",
"dj_rest_auth",
"dj_rest_auth.registration",
]

MIDDLEWARE = [
"allauth.account.middleware.AccountMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
Expand Down Expand Up @@ -160,3 +172,47 @@
}

DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"

SITE_ID = 1

AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
"social_core.backends.google.GoogleOAuth2",
]

SOCIALACCOUNT_PROVIDERS = {
"google": {
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {
"access_type": "online",
},
"APP": {
"client_id": os.getenv("SOCIAL_AUTH_GOOGLE_CLIENT_ID"),
"secret": os.getenv("SOCIAL_AUTH_GOOGLE_SECRET"),
"key": "",
},
},
}


ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False


REST_USE_JWT = True
JWT_AUTH_COOKIE = "my-app-auth"
JWT_AUTH_REFRESH_COOKIE = "my-refresh-token"

GOOGLE_CALLBACK_URL = "https://www.weaverse.site/social-login/google/"

SOCIAL_AUTH_KAKAO_KEY = os.getenv("SOCIAL_AUTH_KAKAO_KEY")

REDIRECT_URL = "https://www.weaverse.site"
LOGIN_REDIRECT_URL = "/dashboard/"
LOGOUT_REDIRECT_URL = "/"
Loading