django-ninja-jwt: Bug Report: Inconsistent Response Format for User Authentication in Django


Description

I am implementing a user authentication system in Django, utilizing a standard response format for all API responses. This standard format includes message, success, data, and trace_id. While this format is correctly applied for successful authentication, it fails to be consistent for authentication errors.

Expected Behavior

For both successful and failed authentication attempts, the response should adhere to the following format:

{
  "message": "string or null",
  "success": true or false,
  "data": { /* relevant data or empty */ },
  "trace_id": "string"
}

Current Behavior

  • get token request
curl -X 'POST' \
  'http://127.0.0.1:8000/api/token/pair' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "password": "12345",
  "username": "testuser"
}'
  • Successful Authentication Response:

    {
      "message": null,
      "success": true,
      "data": {
        "refresh": "token",
        "access": "token",
        "user": {
          "first_name": "name",
          "email": "email"
        }
      },
      "trace_id": "string"
    }
    

    This response is as per the expected format.

  • Failed Authentication Response:

    {
      "detail": "{'detail': ErrorDetail(string='No active account found with the given credentials', code=''), 'code': ErrorDetail(string='', code='')}",
      "trace_id": "string"
    }
    

    This response does not match the standard format, particularly lacking the message and success fields.

  • refresh token request

curl -X 'POST' \
  'http://127.0.0.1:8000/api/token/pair' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "password": "12345",
  "username": "testuser"
}'
  • refresh token response
{
  "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwMDE1MzQ4OCwiaWF0IjoxNzAwMDY3MDg4LCJqdGkiOiIzMjU4YjNiYTkyNGE0MjJjOGJiYWRkOTViOTM0MzU1MCIsInVzZXJfaWQiOjF9.tBbo_coSHRo96XmSmdZQjl-Gf2VH5QXb0ZN1AFd_CeA",
  "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzAwMDY3NDcwLCJpYXQiOjE3MDAwNjcwODgsImp0aSI6ImQ0YWY0MzcyYWMzYjQzNzliZGNhYzg4ZjBiZmU0ZGYyIiwidXNlcl9pZCI6MX0.u6ryxuI_kn9RJXK9QJAVcqJSLqPlJeJRqgmQYgdu_9Y",
  "trace_id": "680dc33ef9c149919b6a9ec15d653d16"
}

Code Snippets

  • Custom Schema for Responses:
# core.schemas.py
from pydantic.generics import GenericModel

class ErrorMsg(BaseModel):
    message: Optional[str] = None
    success: bool = True

GenericResultsType = TypeVar("GenericResultsType")


class StandResponse(ErrorMsg, GenericModel, Generic[GenericResultsType]):
    data: GenericResultsType

# custom_token_out.py
from core.schemas import StandResponse

class MyTokenObtainPairOutSchema(Schema):
    refresh: str
    access: str
    user: UserSchema

class MyTokenObtainPairInputSchema(TokenObtainInputSchemaBase):
    @classmethod
    def get_response_schema(cls) -> Type[Schema]:
        return StandResponse[MyTokenObtainPairOutSchema]

  @classmethod
  def get_token(cls, user) -> Dict:
      values = {}
      refresh = RefreshToken.for_user(user)
      values["refresh"] = str(refresh)
      values["access"] = str(refresh.access_token)
      values.update(
          user=UserSchema.from_orm(user)
      )
      return {'data': values}

Steps to Reproduce

  1. Configure the Django application with the mentioned response format.
  2. Implement custom user authentication handling with the provided schemas.
  3. Authenticate with valid credentials to observe the correct format.
  4. Authenticate with invalid credentials to observe the deviation.

Possible Solution

I need guidance on customizing the response format for failed authentication attempts to conform to our standard response format. This might involve modifying the exception handling process within the Django authentication framework or adjusting our custom schema.


About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Comments: 20 (9 by maintainers)

Most upvoted comments

@lihuacai168 you good? I want to close this issue

Sorry, I’m so busy in past several days. Pls close it.

@lihuacai168 if there is no bearer token provided, Django Ninja raises AuthenticationError exception before the Django-Ninja-JWT authentication handler is called.

# Django Ninja 

class Operation:
    def _run_authentication(self, request: HttpRequest) -> Optional[HttpResponse]:
        for callback in self.auth_callbacks:
            try:
                if is_async_callable(callback) or getattr(callback, "is_async", False):
                    result = async_to_sync(callback)(request)
                else:
                    result = callback(request)
            except Exception as exc:
                return self.api.on_exception(request, exc)

            if result:
                request.auth = result  # type: ignore
                return None
        return self.api.on_exception(request, AuthenticationError())

So the solution for this is to add another custom exception handler for AuthenticationError just like you did for APIException

thx, it works.

from ninja.errors import AuthenticationError

def auth_error_exception_handler(request, exc):
    data = {
        "message": "AuthenticationError",
        "success": False,
        "data": None,
    }
    response = api_v1.create_response(request, data, status=401)
    return response

api_v1.exception_handler(AuthenticationError)(auth_error_exception_handler)