django-ninja-jwt icon indicating copy to clipboard operation
django-ninja-jwt copied to clipboard

[BUG] Django Ninja JWT Token Validation Issue

Open subham1099 opened this issue 11 months ago • 9 comments

Description

When using Django Ninja JWT with a custom token obtain pair schema, the validation is being bypassed due to input type mismatch, leading to authentication errors.

Environment

  • Python: 3.12
  • Django: 5.1.6
  • django-ninja: 1.3.0
  • django-ninja-jwt: 5.3.5
  • pydantic: 2.9.2

Issue

The TokenObtainInputSchemaBase.validate_inputs method expects the input to be a dictionary, but in the current version of Django Ninja, the input is wrapped in a DjangoGetter object. This causes the validation to be bypassed, leading to a NoneType error when trying to authenticate.

Code

class TokenObtainPairInputSchema(TokenObtainInputSchemaBase):
      """Custom schema for token obtain pair."""
      @pyd.model_validator(mode="before")
      def validate_inputs(cls, values: DjangoGetter) -> DjangoGetter:
          input_values = values.obj
          request = values.context.get("request")
          # This condition is never true because input_values is a DjangoGetter
          if isinstance(input_values, dict):  # <--
              values.obj.update(
                  cls.validate_values(request=request, values=input_values)
              )
              return values
          return values

    @classmethod
    def get_response_schema(cls) -> type[Schema]:
        return TokenObtainPairOutputSchema

    @classmethod
    def get_token(cls, user: AbstractUser) -> dict[str, t.Any]:
        values = {}
        refresh = RefreshToken.for_user(user)
        values["refresh"] = str(refresh)
        values["access"] = str(refresh.access_token)
        values.update(
            user=UserSchema.from_orm(user)
        )  # this will be needed when creating output schema
        return values


Request

curl -X 'POST' \
'http://localhost:8001/api/v1/auth/token/pair' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"username": "string",
"password": "string"
}'

Error Log

[debug ] Input validation - values type: <class 'ninja.schema.DjangoGetter'> [debug ] Input validation - input_values type: <class 'ninja.schema.DjangoGetter'> [debug ] Input validation - input_values: <DjangoGetter: {'password': 'string', 'username': 'string'}> [error ] 'NoneType' object has no attribute 'id'

Traceback (most recent call last):
  File "/home/ubuntu/.cache/pypoetry/virtualenvs/app-VA82Wl8V-py3.12/lib/python3.12/site-packages/ninja/operation.py", line 119, in run
    values = self._get_values(request, kw, temporal_response)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/.cache/pypoetry/virtualenvs/app-VA82Wl8V-py3.12/lib/python3.12/site-packages/ninja/operation.py", line 288, in _get_values
    data = model.resolve(request, self.api, path_params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/.cache/pypoetry/virtualenvs/app-VA82Wl8V-py3.12/lib/python3.12/site-packages/ninja/params/models.py", line 57, in resolve
    return cls.model_validate(data, context={"request": request})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/.cache/pypoetry/virtualenvs/app-VA82Wl8V-py3.12/lib/python3.12/site-packages/pydantic/main.py", line 641, in model_validate
    return cls.__pydantic_validator__.validate_python(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/.cache/pypoetry/virtualenvs/app-VA82Wl8V-py3.12/lib/python3.12/site-packages/ninja/schema.py", line 228, in _run_root_validator
    return handler(values)
           ^^^^^^^^^^^^^^^
  File "/home/ubuntu/.cache/pypoetry/virtualenvs/app-VA82Wl8V-py3.12/lib/python3.12/site-packages/ninja_jwt/schema.py", line 117, in post_validate
    return cls.post_validate_schema(values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/.cache/pypoetry/virtualenvs/app-VA82Wl8V-py3.12/lib/python3.12/site-packages/ninja_jwt/schema.py", line 128, in post_validate_schema
    data = cls.get_token(cls._user)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/app/app/api/auth/token_obtain.py", line 93, in get_token
    refresh = RefreshToken.for_user(user)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/.cache/pypoetry/virtualenvs/app-VA82Wl8V-py3.12/lib/python3.12/site-packages/ninja_jwt/tokens.py", line 189, in for_user
    user_id = getattr(user, api_settings.USER_ID_FIELD)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'id'

Expected Behavior

The validation should handle both dictionary and DjangoGetter inputs, ensuring proper validation before authentication attempts.

Current Workaround

We've implemented a workaround by explicitly handling the DjangoGetter case:

@pyd.model_validator(mode="before")
def validate_inputs(cls, values: DjangoGetter) -> DjangoGetter:
    input_values = values.obj
    request = values.context.get("request")
    # Handle both dict and DjangoGetter inputs
    if isinstance(input_values, dict):
        values_dict = input_values
    else:
        # Convert DjangoGetter to dict
        values_dict = input_values._obj

    validated_values = cls.validate_values(request=request, values=values_dict)
    values.obj = validated_values
    return values

Questions

  1. Is this the intended behavior of the input validation?
  2. Should the base implementation be updated to handle DjangoGetter inputs?
  3. Is there a better way to handle this validation in custom schemas?

subham1099 avatar Feb 14 '25 06:02 subham1099