Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,20 @@ Please check `cookiecutter-django Docker documentation` for more details how to

With MailHog running, to view messages that are sent by your application, open your browser and go to ``http://127.0.0.1:8025``

### API Authentication (JWT)

The API uses JWT authentication (SimpleJWT).

- Obtain tokens:
- `POST /api/token/` with JSON body `{"username": "<username>", "password": "<password>"}`
- Response includes `access` and `refresh`
- Use the access token on requests:
- `Authorization: Bearer <access>` (legacy clients may also use `Authorization: JWT <access>`)
- Refresh tokens:
- `POST /api/token/refresh/` with `{"refresh": "<refresh>"}`

For backwards compatibility, `POST /api-token-auth/` is still available and returns a `token` field.

## Stargazers ⭐

### Thanks to all of our `Stargazers` ⭐ 🔭 who are supporting CodersHQ project
Expand Down
25 changes: 25 additions & 0 deletions codershq/api/auth_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer


class LegacyTokenObtainView(APIView):
"""Backwards-compatible JWT endpoint.

Historically this project exposed `/api-token-auth/` (drf-jwt) which returned
`{"token": "..."}`. SimpleJWT returns `{access, refresh}`.

This view preserves the legacy response shape while also returning the
modern fields.
"""

permission_classes = (AllowAny,)
Comment thread
kamrankhan78694 marked this conversation as resolved.
Outdated
Comment thread
kamrankhan78694 marked this conversation as resolved.
Outdated

def post(self, request, *args, **kwargs):
serializer = TokenObtainPairSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
tokens = serializer.validated_data
access = tokens.get("access")
refresh = tokens.get("refresh")
return Response({"token": access, "access": access, "refresh": refresh})
Comment thread
kamrankhan78694 marked this conversation as resolved.
71 changes: 71 additions & 0 deletions codershq/api/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import pytest
from rest_framework.test import APIClient


@pytest.mark.django_db
def test_token_obtain_and_admin_endpoint_permissions(django_user_model):
client = APIClient()

admin_password = "admin-pass-123"
admin = django_user_model.objects.create_user(
username="admin_user",
password=admin_password,
is_staff=True,
is_superuser=True,
)

user_password = "user-pass-123"
django_user_model.objects.create_user(
username="normal_user",
password=user_password,
is_staff=False,
is_superuser=False,
)

# Unauthenticated should be rejected (IsAdminUser)
unauth_resp = client.get("/api/users/data/")
assert unauth_resp.status_code in (401, 403)

# Normal user token -> still forbidden
token_resp = client.post(
"/api/token/",
{"username": "normal_user", "password": user_password},
format="json",
)
assert token_resp.status_code == 200
access = token_resp.data["access"]
client.credentials(HTTP_AUTHORIZATION=f"Bearer {access}")
forbidden_resp = client.get("/api/users/data/")
assert forbidden_resp.status_code in (401, 403)

# Admin token -> allowed
token_resp = client.post(
"/api/token/",
{"username": admin.username, "password": admin_password},
format="json",
)
assert token_resp.status_code == 200
admin_access = token_resp.data["access"]
client.credentials(HTTP_AUTHORIZATION=f"Bearer {admin_access}")

ok_resp = client.get("/api/users/data/")
assert ok_resp.status_code == 200
assert "data" in ok_resp.json()


@pytest.mark.django_db
def test_legacy_api_token_auth_returns_token_field(django_user_model):
client = APIClient()

password = "pass-123"
django_user_model.objects.create_user(username="legacy_user", password=password)

resp = client.post(
"/api-token-auth/",
{"username": "legacy_user", "password": password},
format="json",
)
assert resp.status_code == 200
assert "token" in resp.data
assert "access" in resp.data
assert "refresh" in resp.data
Comment thread
kamrankhan78694 marked this conversation as resolved.
13 changes: 7 additions & 6 deletions codershq/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from rest_framework.parsers import JSONParser
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_simplejwt.authentication import JWTAuthentication

from codershq.api.utils.analytics import Analytics
from codershq.api.utils.pluralsight import PluralSight
Expand All @@ -28,8 +28,9 @@
@api_view(['GET'])
def getRoutes(request):
routes = [
'/api/token',
'/api/token/refresh',
'/api/token/',
'/api/token/refresh/',
'/api-token-auth/',
"users/all/",
"assessment/skills/all/",
"users/skills/<int:id>/",
Expand All @@ -45,7 +46,7 @@ class RegisterView(generics.CreateAPIView):


@api_view(['GET'])
@authentication_classes([SessionAuthentication, BasicAuthentication, JSONWebTokenAuthentication])
@authentication_classes([SessionAuthentication, BasicAuthentication, JWTAuthentication])
@permission_classes([IsAdminUser])
def users_all(request):
"""
Expand All @@ -58,7 +59,7 @@ def users_all(request):
return JsonResponse(data, safe=True)

@api_view(['GET'])
@authentication_classes([SessionAuthentication, BasicAuthentication, JSONWebTokenAuthentication])
@authentication_classes([SessionAuthentication, BasicAuthentication, JWTAuthentication])
@permission_classes([IsAdminUser])
def users_data(request):
"""
Expand All @@ -72,7 +73,7 @@ def users_data(request):


@api_view(['GET','POST'])
@authentication_classes([SessionAuthentication, BasicAuthentication, JSONWebTokenAuthentication, ])
@authentication_classes([SessionAuthentication, BasicAuthentication, JWTAuthentication])
@permission_classes([IsAuthenticated])
def user(request, username):
"""
Expand Down
20 changes: 13 additions & 7 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,12 @@
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.github",
'rest_auth.registration',
"django_celery_beat",
"ckeditor",
"djangosaml2idp",
'rest_framework',
'rest_framework_jwt',
'rest_framework_jwt.blacklist',
'rest_auth',
'dj_rest_auth',
'dj_rest_auth.registration',
"corsheaders",
]

Expand Down Expand Up @@ -344,17 +342,25 @@

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated'
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
)
),
}

REST_USE_JWT = True

# This project uses JWT (SimpleJWT) rather than DRF token authentication.
REST_AUTH_TOKEN_MODEL = None

SIMPLE_JWT = {
# Allow legacy clients that send `Authorization: JWT <token>`.
"AUTH_HEADER_TYPES": ("Bearer", "JWT"),
}

# AUTH_USER_MODEL = "user.User"

#https://github.com/adamchainz/django-cors-headers
Expand Down
17 changes: 12 additions & 5 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
from django.views import defaults as default_views
from django.views.generic import TemplateView
from django.views.generic.base import RedirectView
from rest_framework_jwt.views import obtain_jwt_token
from rest_auth.views import PasswordResetConfirmView, PasswordResetView
from dj_rest_auth.views import PasswordResetConfirmView, PasswordResetView
Comment thread
kamrankhan78694 marked this conversation as resolved.
Outdated
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

from codershq.api.auth_views import LegacyTokenObtainView

urlpatterns = [
path(
Expand Down Expand Up @@ -58,12 +60,17 @@
# API
path('api-auth/', include('rest_framework.urls')),
path('rest-auth/registration/account-confirm-email/<str:key>/', confirm_email, name='account_confirm_email'),
path('rest-auth/', include('rest_auth.urls')),
path('rest-auth/registration/', include('rest_auth.registration.urls')),
path('rest-auth/', include('dj_rest_auth.urls')),
path('rest-auth/registration/', include('dj_rest_auth.registration.urls')),
# path('rest-auth/password/reset/', PasswordResetView.as_view(), name='rest_password_reset',),
path('rest-auth/password/reset/confirm/<uidb64>/<token>/', PasswordResetConfirmView.as_view(), name='password_reset_confirm',),

path('api-token-auth/', obtain_jwt_token),
# Modern JWT auth (SimpleJWT)
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),

# Legacy JWT auth endpoint (drf-jwt compatible response)
path('api-token-auth/', LegacyTokenObtainView.as_view(), name='legacy_api_token_auth'),
path("api/", include("codershq.api.urls", namespace="api")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Expand Down
Loading