Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…ackend into yujeong/materials
  • Loading branch information
devnproyj22 committed Oct 10, 2024
2 parents ec7c794 + 0305ac5 commit 34a15b9
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 53 deletions.
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

0 comments on commit 34a15b9

Please sign in to comment.