django-ninja
django-ninja copied to clipboard
Authentication is never awaited in AsyncOperation
Example
from asgiref.sync import sync_to_async
from ninja import NinjaAPI, Schema
from ninja.security import APIKeyQuery
from .models import Client
api = NinjaAPI()
class ApiKey(APIKeyQuery):
param_name = "key"
async def authenticate(self, request, key):
try:
return await sync_to_async(Client.objects.get)(key=key)
except Client.DoesNotExist:
pass
class Test(Schema):
msg: str
@api.get("", response=Test, auth=ApiKey())
async def test(request):
return 200, {"msg": "hello!"}
Scenario
The API is called with a non-existing API key. What should happen: Ninja should return a 401 response. What actually happens: Returns 200.
Possible solution
Implement _run_checks and _run_authentication as async methods in AsyncOperation.
class AsyncOperation(Operation):
def __init__(self, *args, **kwargs):
if django.VERSION < (3, 1): # pragma: no cover
raise Exception("Async operations are supported only with Django 3.1+")
super().__init__(*args, **kwargs)
self.is_async = True
async def _run_checks(self, request):
"Runs security checks for each operation"
# auth:
if self.auth_callbacks:
error = await self._run_authentication(request)
if error:
return error
# csrf:
if self.api.csrf:
error = check_csrf(request, self.view_func)
if error:
return error
async def _run_authentication(self, request):
for callback in self.auth_callbacks:
result = await callback(request)
if result is not None:
request.auth = result
return
return Response({"detail": "Unauthorized"}, status=401)
async def run(self, request, **kw):
error = await self._run_checks(request)
if error:
return error
values, errors = self._get_values(request, kw)
if errors:
return Response({"detail": errors}, status=422)
result = await self.view_func(request, **values)
return self._create_response(result)
Above example will then return 401 on an invalid key as expected.
Yes, in nutshell this is the code I'm targeting for... but there are few things to deal with - like if you have async auth, but not-async operation or vice-versa
@vitalik I'd like to help with this. I don't understand what's wrong with #202 though. It seems that it's very similar to the code above plus taking care of async auth on not-async operation and vice versa. If I understand what needs to be changed compared to #202, I'd be happy to prepare PR with code, tests and docs changes.
One of the main reasons I chose django-ninja
was it's support for async views and I'd like to broaden the async experience even further. Right now, I can't use built-in auth
at all and I'm doomed to calling my async auth function in each view, which is repetitive and prone to errors.
Is there any chance for this issue to be resolved soon? I'm quite fond of django-ninja, it's just this issue that I find a bit unsettling.
any news on this ?
Not sure if this helps, but.. try adding an await statement
@api.get("", response=Test, auth=ApiKey())
async def test(request):
await request.auth <<<
return 200, {"msg": "hello!"}
Not sure if this helps, but.. try adding an await statement
@api.get("", response=Test, auth=ApiKey()) async def test(request): await request.auth <<< return 200, {"msg": "hello!"}
ah, that's the magic of the event loop. Thank you, it really works.
For the full copypasta
@api.get("", response=Test, auth=ApiKey())
async def test(request):
api_key = await request.auth
if not api_key:
raise AuthenticationError()
return 200, {"msg": "hello!"}
Thanks @changhyun-an and @maxmorlocke! I build a decorator from your code :) This should reduce some boilerplate in case of many endpoints
def await_auth(f):
@functools.wraps(f)
async def decorator(*args, **kwargs):
auth = await args[0].auth # args[0] is always the request instance injected from ninja
if not auth :
raise AuthenticationError()
args[0].auth = auth
return await f(*args, **kwargs)
return decorator
Usage:
api = NinjaAPI(auth=ApiKey(), ...)
@api.post(...)
@await_auth
async def create(request, data: Input):
....
While @changhyun-an 's answer works with a single authentication mechanism, I am having issues with multiple authentication classes.
I have a JWTAuth
and an APITokenAuth
. Using the await request.auth
technqiue, if the first auth class returns None
, the second is not tried and auth fails.
I could combine both authentication classes in one, but I prefer to keep them separate.
Does anyone have an idea about this ?
I tried async-auth with v1.0 beta2 but it didn't work, and I finally came across this issue...
I saw "async authentication fully supported on all layers" in the Release Note, but is the async-auth roadmap still in progress? https://github.com/vitalik/django-ninja/releases/tag/v1.0b2
If so, I'd be very happy to mention that in the documentation, also link to this PR :) Thank you
@skokado
I tried async-auth with v1.0 beta2 but it didn't work, and I finally came across this issue...
could you provide your example code ?
@vitalik Sorry 🙇 I was using 0.22.2, I broke my local environment without realizing. It works as I expected with 1.0b2.
But one more, when failed Authentication caused by no token provided, then occurs 500 error Here's my example code.
from ninja import NinjaAPI
from ninja.security import HttpBearer
api = NinjaAPI()
class MyAuth(HttpBearer):
async def authenticate(self, request, token):
if token == "secret":
request.user = MyUser(id=1, name="Foo")
return token
return None
@api.get("/me", auth=MyAuth())
async def me_view(request):
return {"hello": "world"}
curl -X 'GET' \
'http://localhost:8000/me' \
-H 'accept: */*'
Traceback:
Traceback (most recent call last):
File "/home/skokado/workspace/django-ninja-tutorial/venv/lib/python3.11/site-packages/ninja/operation.py", line 304, in _run_authentication
result = await callback(request)
^^^^^^^^^^^^^^^^^^^^^^^
TypeError: object NoneType can't be used in 'await' expression
It does not solve by registering @api.exception_handler
By incorrect token then can get 401 response
curl -X 'GET' \
'http://localhost:8000/api/me' \
-H 'accept: */*' \
-H 'Authorization: Bearer dummy'
# => {"detail": "Unauthorized"}
That does not occur when using sync-auth
@skokado please check with latest version
pip install django-ninja==1.0rc0
@vitalik Works as expected, thank you for fixing.
@vitalik Sorry for repetitive, but I found new Warning in case non-empty token request at only first time. It does not matter the authenticate attempt would be succeede or not.
Sample code is same as above https://github.com/vitalik/django-ninja/issues/44#issuecomment-1794583279
$ curl -X 'GET' \
'http://localhost:8000/api/me' \
-H 'accept: */*' \
-H 'Authorization: Bearer secret'
/home/skokado/workspace/django-ninja/ninja/operation.py:311: RuntimeWarning: coroutine 'MyAuth.authenticate' was never awaited
result = await callback(request)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
{"hello": "world"}
At here, callback
is called twice
https://github.com/vitalik/django-ninja/blob/cd66ac8c25162f6723768eae613e7f6ca4c2e2ca/ninja/operation.py#L308-L310
Created a PR for this https://github.com/vitalik/django-ninja/issues/44#issuecomment-1805733011
https://github.com/vitalik/django-ninja/pull/916