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/소셜 로그인 구현 수정 #162

Merged
merged 16 commits into from
Oct 10, 2024
Merged
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
4 changes: 2 additions & 2 deletions accounts/test/test_accounts_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def test_password_reset_unauthenticated(self, api_client):
"confirm_new_password": "confirmnewpassword",
}
response = api_client.post(url, data)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.status_code == status.HTTP_403_FORBIDDEN


@pytest.mark.django_db
Expand All @@ -113,7 +113,7 @@ def test_student_list_view(self, api_client, create_user):
def test_student_list_view_unauthenticated(self, api_client):
url = reverse("accounts:student-list")
response = api_client.get(url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.status_code == status.HTTP_403_FORBIDDEN


@pytest.mark.django_db
Expand Down
12 changes: 6 additions & 6 deletions courses/test/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def test_course_수정_실패_로그인하지않은경우(self, api_client):
response = api_client.put(url, data, format="json")

# Then
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.data == {
"detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
}
Expand Down Expand Up @@ -224,7 +224,7 @@ def test_course_삭제_실패_로그인하지않은경우(self, api_client):
response = api_client.delete(url)

# Then
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.data == {
"detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
}
Expand Down Expand Up @@ -340,7 +340,7 @@ def test_course_생성_요청_실패_로그인하지않은경우(self, api_clien
response = api_client.post(url, data, format="json")

# Then
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.data == {
"detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
}
Expand Down Expand Up @@ -429,7 +429,7 @@ def test_curriculum_생성_요청_실패_로그인하지않은경우(
response = api_client.post(url, data, format="json")

# Then
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.data == {
"detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
}
Expand Down Expand Up @@ -558,7 +558,7 @@ def test_curriculum_수정_실패_로그인하지않은경우(
response = api_client.put(url, data, format="json")

# Then
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.data == {
"detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
}
Expand Down Expand Up @@ -612,7 +612,7 @@ def test_curriculum_삭제_실패_로그인하지않은경우(self, api_client):
response = api_client.delete(url)

# Then
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.data == {
"detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
}
Expand Down
44 changes: 30 additions & 14 deletions jwtauth/authentication.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
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.core.cache import cache
from django.conf import settings


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


class JWTAuthentication(BaseAuthentication):
"""
해당 클래스는 JWT 토큰을 사용하여 사용자를 인증하는 데 사용됩니다.
- 토큰이 유효하지 않으면 해당하는 메시지를 반환합니다.
"""

def authenticate(self, request):
auth_header = request.headers.get("Authorization")
if not auth_header:
Expand All @@ -25,19 +22,38 @@ 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_id = payload["user_id"]

cache_key = f"user_{user_id}"
user_data = cache.get(cache_key)

if user_data is None:
user = User.objects.get(id=user_id)
user_data = {
"id": user.id,
"email": user.email,
"is_staff": user.is_staff,
"is_superuser": user.is_superuser,
}
cache.set(cache_key, user_data, timeout=18000)

user = User(
id=user_data["id"],
email=user_data["email"],
is_staff=user_data["is_staff"],
is_superuser=user_data["is_superuser"],
)

return (user, None)

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

def authenticate_header(self, request):
return "Bearer"
logger.error(f"인증 오류: {str(e)}")
raise AuthenticationFailed("인증이 유효하지 않습니다!")
38 changes: 17 additions & 21 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 All @@ -146,8 +142,8 @@ def test_JWT_인증_실패_만료된_토큰(api_client, user):
url = reverse("refresh")
# When: 리프레시 API에 만료된 토큰과 함께 POST 요청을 보냄
response = api_client.post(url)
# Then: 응답 상태 코드가 401 (Unauthorized)임
assert response.status_code == status.HTTP_401_UNAUTHORIZED
# Then: 응답 상태 코드가 403 (Forbidden)임
assert response.status_code == status.HTTP_403_FORBIDDEN


@pytest.mark.django_db
Expand All @@ -158,8 +154,8 @@ def test_JWT_인증_실패_유효하지_않은_토큰(api_client):
url = reverse("refresh")
# When: 리프레시 API에 유효하지 않은 토큰과 함께 POST 요청을 보냄
response = api_client.post(url)
# Then: 응답 상태 코드가 401 (Unauthorized)임
assert response.status_code == status.HTTP_401_UNAUTHORIZED
# Then: 응답 상태 코드가 403 (Forbidden)임
assert response.status_code == status.HTTP_403_FORBIDDEN


@pytest.mark.django_db
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
10 changes: 9 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ cffi==1.17.1
charset-normalizer==3.3.2
colorama==0.4.6
cryptography==43.0.1
defusedxml==0.8.0rc2
dj-rest-auth==6.0.0
Django==5.1.1
django-allauth==65.0.2
django-appconf==1.0.6
django-cors-headers==4.4.0
django-filter==24.3
Expand All @@ -26,6 +29,7 @@ jsonschema-specifications==2023.12.1
model-bakery==1.19.5
mypy==1.11.2
mypy-extensions==1.0.0
oauthlib==3.2.2
opencv-python==4.10.0.84
packaging==24.1
pillow==10.4.0
Expand All @@ -38,17 +42,21 @@ pytest==8.3.3
pytest-django==4.9.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python3-openid==3.2.0
pytz==2024.2
PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3
requests-oauthlib==2.0.0
rpds-py==0.20.0
s3transfer==0.10.2
setuptools==75.1.0
six==1.16.0
social-auth-app-django==5.4.2
social-auth-core==4.5.4
sqlparse==0.5.1
toposort==1.10
typing_extensions==4.12.2
tzdata==2024.2
uritemplate==4.1.1
urllib3==2.2.3
urllib3==2.2.3
Loading
Loading