django-ninja: 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.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 3
  • Comments: 16 (10 by maintainers)

Commits related to this issue

Most upvoted comments

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!"}

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):
    ....

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!"}

@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.

@skokado please check with latest version

pip install django-ninja==1.0rc0

@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 ?

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.

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