Skip to content

Commit 1d447ba

Browse files
authored
Merge pull request #6 from eadwinCode/feat/Async
Async support
2 parents 3a2bab4 + 9228f8f commit 1d447ba

File tree

13 files changed

+297
-34
lines changed

13 files changed

+297
-34
lines changed

.github/workflows/test_full.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Install core
2424
run: pip install "Django${{ matrix.django-version }}"
2525
- name: Install tests
26-
run: pip install pytest ninja-schema pytest-django django-ninja-extra python-jose==3.0.0 pyjwt[crypto]
26+
run: pip install pytest pytest-asyncio ninja-schema pytest-django django-ninja-extra python-jose==3.0.0 pyjwt[crypto]
2727
- name: Test
2828
run: pytest
2929
codestyle:

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ For full documentation, [visit](https://eadwincode.github.io/django-ninja-jwt/).
1818
- Python >= 3.6
1919
- Django >= 2.1
2020
- Django-Ninja >= 0.16.1
21-
- Ninja-Schema >= 0.12.2
22-
- Django-Ninja-Extra >= 0.11.0
21+
- Ninja-Schema >= 0.12.8
22+
- Django-Ninja-Extra >= 0.14.2
2323

2424

2525
Installation

docs/docs/auth_integration.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,22 @@ router = router('')
5353
def apikey(request):
5454
return f"Token = {request.auth}"
5555

56-
```
56+
```
57+
58+
### Asynchronous Route Authentication
59+
If you are interested in the Asynchronous Route Authentication, there is `AsyncJWTAuth` class
60+
61+
```python
62+
from ninja_extra import api_controller, route
63+
from ninja_jwt.authentication import AsyncJWTAuth
64+
65+
@api_controller
66+
class MyController:
67+
@route.get('/some-endpoint', auth=AsyncJWTAuth())
68+
async def some_endpoint(self):
69+
...
70+
```
71+
N:B `some_endpoint` must be asynchronous. Any endpoint function marked with `AsyncJWTAuth` must be asynchronous.
72+
73+
!!! warning
74+
Asynchronous feature is only available for django version > 3.0

docs/docs/customizing_token_claims.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ generated by the `NinjaJWTDefaultController` and `NinjaJWTSlidingController`
44
views, create a subclass for the desired controller as well as a subclass for
55
its corresponding serializer. Here\'s an example :
66

7+
!!! info
8+
if you are interested in Asynchronous version of the class, checkout `AsyncNinjaJWTDefaultController` and `AsyncNinjaJWTSlidingController`
9+
710
```python
811
from ninja_jwt.schema import TokenObtainPairSerializer
912
from ninja_jwt.controller import TokenObtainPairController
@@ -63,4 +66,4 @@ from ninja import NinjaAPI
6366

6467
api = NinjaAPI()
6568
api.add_router('', tags=['Auth'], router=router)
66-
```
69+
```

ninja_jwt/authentication.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from typing import Any, Type
22

3+
import django
34
from django.contrib.auth import get_user_model
45
from django.contrib.auth.models import AbstractUser, AnonymousUser
56
from django.http import HttpRequest
67
from django.utils.translation import gettext_lazy as _
7-
from ninja.security import HttpBearer
8+
from ninja_extra.security import AsyncHttpBearer, HttpBearer
89

910
from .exceptions import AuthenticationFailed, InvalidToken, TokenError
1011
from .settings import api_settings
@@ -105,3 +106,29 @@ def default_user_authentication_rule(user) -> bool:
105106
# users from authenticating to enforce a reasonable policy and provide
106107
# sensible backwards compatibility with older Django versions.
107108
return user is not None and user.is_active
109+
110+
111+
if not django.VERSION < (3, 1):
112+
from asgiref.sync import sync_to_async
113+
114+
class AsyncJWTBaseAuthentication(JWTBaseAuthentication):
115+
async def async_jwt_authenticate(
116+
self, request: HttpRequest, token: str
117+
) -> Type[AbstractUser]:
118+
request.user = AnonymousUser()
119+
get_validated_token = sync_to_async(self.get_validated_token)
120+
validated_token = await get_validated_token(token)
121+
get_user = sync_to_async(self.get_user)
122+
user = await get_user(validated_token)
123+
request.user = user
124+
return user
125+
126+
class AsyncJWTAuth(AsyncJWTBaseAuthentication, JWTAuth, AsyncHttpBearer):
127+
async def authenticate(self, request: HttpRequest, token: str) -> Any:
128+
return await self.async_jwt_authenticate(request, token)
129+
130+
class AsyncJWTTokenUserAuth(
131+
AsyncJWTBaseAuthentication, JWTTokenUserAuth, AsyncHttpBearer
132+
):
133+
async def authenticate(self, request: HttpRequest, token: str) -> Any:
134+
return await self.async_jwt_authenticate(request, token)

ninja_jwt/controller.py

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,51 @@
1-
from ninja_extra import api_controller, http_post
1+
import django
2+
from ninja_extra import ControllerBase, api_controller, http_post
23
from ninja_extra.permissions import AllowAny
34
from ninja_schema import Schema
45

56
from ninja_jwt import schema
67

7-
8-
class TokenVerificationController:
8+
exports = [
9+
"TokenVerificationController",
10+
"TokenBlackListController",
11+
"TokenObtainPairController",
12+
"TokenObtainSlidingController",
13+
"TokenObtainSlidingController",
14+
"NinjaJWTDefaultController",
15+
"NinjaJWTSlidingController",
16+
]
17+
18+
if not django.VERSION < (3, 1):
19+
exports += [
20+
"AsyncTokenVerificationController",
21+
"AsyncTokenBlackListController",
22+
"AsyncTokenObtainPairController",
23+
"AsyncTokenObtainSlidingController",
24+
"AsyncTokenObtainSlidingController",
25+
"AsyncNinjaJWTDefaultController",
26+
"AsyncNinjaJWTSlidingController",
27+
]
28+
29+
__all__ = exports
30+
31+
32+
class TokenVerificationController(ControllerBase):
933
auto_import = False
1034

1135
@http_post("/verify", response={200: Schema}, url_name="token_verify")
1236
def verify_token(self, token: schema.TokenVerifySerializer):
1337
return {}
1438

1539

16-
class TokenBlackListController:
40+
class TokenBlackListController(ControllerBase):
1741
auto_import = False
1842

1943
@http_post("/blacklist", response={200: Schema}, url_name="token_blacklist")
2044
def blacklist_token(self, refresh: schema.TokenBlacklistSerializer):
2145
return {}
2246

2347

24-
class TokenObtainPairController:
48+
class TokenObtainPairController(ControllerBase):
2549
auto_import = False
2650

2751
@http_post(
@@ -76,3 +100,69 @@ class NinjaJWTSlidingController(
76100
"""
77101

78102
auto_import = False
103+
104+
105+
if not django.VERSION < (3, 1):
106+
107+
class AsyncTokenVerificationController(TokenVerificationController):
108+
@http_post("/verify", response={200: Schema}, url_name="token_verify")
109+
async def verify_token(self, token: schema.TokenVerifySerializer):
110+
return {}
111+
112+
class AsyncTokenBlackListController(TokenBlackListController):
113+
auto_import = False
114+
115+
@http_post("/blacklist", response={200: Schema}, url_name="token_blacklist")
116+
async def blacklist_token(self, refresh: schema.TokenBlacklistSerializer):
117+
return {}
118+
119+
class AsyncTokenObtainPairController(TokenObtainPairController):
120+
@http_post(
121+
"/pair", response=schema.TokenObtainPairOutput, url_name="token_obtain_pair"
122+
)
123+
async def obtain_token(self, user_token: schema.TokenObtainPairSerializer):
124+
return user_token.output_schema()
125+
126+
@http_post(
127+
"/refresh", response=schema.TokenRefreshSerializer, url_name="token_refresh"
128+
)
129+
async def refresh_token(self, refresh_token: schema.TokenRefreshSchema):
130+
refresh = schema.TokenRefreshSerializer(**refresh_token.dict())
131+
return refresh
132+
133+
class AsyncTokenObtainSlidingController(TokenObtainSlidingController):
134+
@http_post(
135+
"/sliding",
136+
response=schema.TokenObtainSlidingOutput,
137+
url_name="token_obtain_sliding",
138+
)
139+
async def obtain_token(self, user_token: schema.TokenObtainSlidingSerializer):
140+
return user_token.output_schema()
141+
142+
@http_post(
143+
"/sliding/refresh",
144+
response=schema.TokenRefreshSlidingSerializer,
145+
url_name="token_refresh_sliding",
146+
)
147+
async def refresh_token(self, refresh_token: schema.TokenRefreshSlidingSchema):
148+
refresh = schema.TokenRefreshSlidingSerializer(**refresh_token.dict())
149+
return refresh
150+
151+
@api_controller("/token", permissions=[AllowAny], tags=["token"])
152+
class AsyncNinjaJWTDefaultController(
153+
AsyncTokenVerificationController, AsyncTokenObtainPairController
154+
):
155+
"""NinjaJWT Async Default controller for obtaining and refreshing tokens"""
156+
157+
auto_import = False
158+
159+
@api_controller("/token", permissions=[AllowAny], tags=["token"])
160+
class AsyncNinjaJWTSlidingController(
161+
AsyncTokenVerificationController, AsyncTokenObtainSlidingController
162+
):
163+
"""
164+
NinjaJWT Async Sliding controller for obtaining and refreshing tokens
165+
Add 'ninja_jwt.tokens.SlidingToken' in AUTH_TOKEN_CLASSES in Settings
166+
"""
167+
168+
auto_import = False

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ dependencies = [
5050
"Django >= 2.1",
5151
"pyjwt>=2,<3",
5252
"pyjwt[crypto]",
53-
"ninja-schema",
54-
"django-ninja-extra",
53+
"ninja-schema >= 0.12.8",
54+
"django-ninja-extra >= 0.14.2",
5555
]
5656

5757

tests/test_authentication.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import timedelta
22

3+
import django
34
import pytest
45
from django.contrib.auth import get_user_model
56

@@ -16,6 +17,9 @@
1617
class TestJWTAuth:
1718
@pytest.fixture(autouse=True)
1819
def setUp(self):
20+
self.init_backend()
21+
22+
def init_backend(self):
1923
self.backend = authentication.JWTAuth()
2024

2125
@pytest.mark.django_db
@@ -96,6 +100,9 @@ def test_get_user(self):
96100
class TestJWTTokenUserAuth:
97101
@pytest.fixture(autouse=True)
98102
def setUp(self):
103+
self.init_backend()
104+
105+
def init_backend(self):
99106
self.backend = authentication.JWTTokenUserAuth()
100107

101108
@pytest.mark.django_db
@@ -133,3 +140,41 @@ def username(self):
133140
assert isinstance(user, api_settings.TOKEN_USER_CLASS)
134141
assert user.id == 42
135142
assert user.username == "bsaget"
143+
144+
145+
if not django.VERSION < (3, 1):
146+
from asgiref.sync import sync_to_async
147+
148+
class TestAsyncJWTAuth(TestJWTAuth):
149+
def init_backend(self):
150+
self.backend = authentication.AsyncJWTAuth()
151+
152+
@pytest.mark.asyncio
153+
@pytest.mark.django_db
154+
async def test_get_validated_token(self, monkeypatch):
155+
_test_get_validated_token = sync_to_async(
156+
super(TestAsyncJWTAuth, self).test_get_validated_token
157+
)
158+
await _test_get_validated_token(monkeypatch=monkeypatch)
159+
160+
@pytest.mark.asyncio
161+
@pytest.mark.django_db
162+
async def test_get_user(self):
163+
_test_get_user = sync_to_async(super(TestAsyncJWTAuth, self).test_get_user)
164+
await _test_get_user()
165+
166+
class TestAsyncJWTTokenUserAuth(TestJWTTokenUserAuth):
167+
def init_backend(self):
168+
self.backend = authentication.AsyncJWTTokenUserAuth()
169+
170+
@pytest.mark.asyncio
171+
@pytest.mark.django_db
172+
async def test_get_user(self):
173+
_test_get_user = sync_to_async(super().test_get_user)
174+
await _test_get_user()
175+
176+
@pytest.mark.asyncio
177+
@pytest.mark.django_db
178+
async def test_custom_tokenuser(self, monkeypatch):
179+
_test_custom_tokenuser = sync_to_async(super().test_custom_tokenuser)
180+
await _test_custom_tokenuser(monkeypatch=monkeypatch)

0 commit comments

Comments
 (0)