Question

So, it seems that I can perform this action just fine from the browser, but I can't seem to replicate it via CURL. Any pointers on how this is supposed to work are greatly, greatly appreciated.

I perform this request to log in a user:

curl -X POST -H "Content-Type: application/json" \ 
-d '{"username":"tester", "password":"password"}' --verbose \
http://localhost:8000/api/user/login/

And the response seems to indicate that the request was successful:

* About to connect() to localhost port 8000 (#0)
*   Trying 127.0.0.1... connected
> POST /api/user/login/ HTTP/1.1
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: localhost:8000
> Accept: */*
> Content-Type: application/json
> Content-Length: 44
> 
* upload completely sent off: 44out of 44 bytes
< HTTP/1.1 200 OK
< Server: nginx/1.1.19
< Date: Wed, 11 Dec 2013 12:31:34 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Vary: Accept, Cookie
< Set-Cookie: csrftoken=h4tjM6o3QyelsAvUhdqNJPinZRdJyrBz; Path=/
< Set-Cookie: sessionid=4tsny8kcl7j9x7icr6vptnq1ims89tzr; expires=Wed, 25-Dec-2013 12:31:34 GMT; httponly; Max-Age=1209600; Path=/
< 
* Connection #0 to host localhost left intact
* Closing connection #0
{"success": true, "username": "tester"}

If I include only the CSRF token in my authenticated request, I get a 401. However, if I include both the CSRF token and the session ID, I get some kind of Python error. For example:

curl -X GET -H "Content-Type: application/json" -H \
"X-CSRFToken: h4tjM6o3QyelsAvUhdqNJPinZRdJyrBz" --cookie \
"sessionid=4tsny8kcl7j9x7icr6vptnq1ims89tzr" --verbose \
http://localhost:8000/api/user/ | python -mjson.tool \

I get back from the server:

{
    "error_message": "getattr(): attribute name must be string", 
    "traceback": "Traceback (most recent call last):
File \"/opt/phaidra/env/local/lib/python2.7/site-packages/tastypie/resources.py\", line 195, in wrapper\n    response = callback(request, *args, **kwargs)\n\n  
File \"/opt/phaidra/env/local/lib/python2.7/site-packages/tastypie/resources.py\", line 426, in dispatch_list\n    return self.dispatch('list', request, **kwargs)\n\n  
File \"/opt/phaidra/env/local/lib/python2.7/site-packages/tastypie/resources.py\", line 454, in dispatch\n    self.throttle_check(request)\n\n  
File \"/opt/phaidra/env/local/lib/python2.7/site-packages/tastypie/resources.py\", line 551, in throttle_check\n    identifier = self._meta.authentication.get_identifier(request)\n\n  
File \"/opt/phaidra/env/local/lib/python2.7/site-packages/tastypie/authentication.py\", line 515, in get_identifier\n    return request._authentication_backend.get_identifier(request)\n\n  
File \"/opt/phaidra/env/local/lib/python2.7/site-packages/tastypie/authentication.py\", line 283, in get_identifier\n    return getattr(request.user, username_field)\n\n
TypeError: getattr(): attribute name must be string\n"
}

Looking at the lines of the errors is not particularly illuminating. Since this error doesn't occur unless --cookie is used, I'm presuming it's trying incorrectly to parse the cookie parameter.

It should also be noted that I am using Neo4django, which I believe precludes me from being able to use API Key Authentication. The code for my user is as such:

class UserResource(ModelResource):
    class Meta:
        queryset = AppUser.objects.all()
        resource_name = 'user'
        fields = ['first_name', 'last_name', 'username', 'email', 'is_staff']
        allowed_methods = ['get', 'post', 'patch']
        always_return_data = True
        authentication = MultiAuthentication(SessionAuthentication(), BasicAuthentication()) 
        authorization = Authorization()

    def prepend_urls(self):
        params = (self._meta.resource_name, trailing_slash())
        return [
            url(r"^(?P<resource_name>%s)/login%s$" % params, self.wrap_view('login'), name="api_login"),
            url(r"^(?P<resource_name>%s)/logout%s$" % params, self.wrap_view('logout'), name="api_logout")
    ]

    def login(self, request, **kwargs):
        """
        Authenticate a user, create a CSRF token for them, and return the user object as JSON.
        """
        self.method_check(request, allowed=['post'])

        data = self.deserialize(request, request.raw_post_data, format=request.META.get('CONTENT_TYPE', 'application/json'))

        username = data.get('username', '')
        password = data.get('password', '')

        if username == '' or password == '':
            return self.create_response(request, {
                'success': False,
                'error_message': 'Missing username or password'
            })

        user = authenticate(username=username, password=password)

        if user:
            if user.is_active:
                login(request, user)
                response = self.create_response(request, {
                    'success': True,
                    'username': user.username
                })
                response.set_cookie("csrftoken", get_new_csrf_key())
                return response
            else:
                return self.create_response(request, {
                    'success': False,
                    'reason': 'disabled',
                }, HttpForbidden)
        else:
            return self.create_response(request, {
                'success': False,
                'error_message': 'Incorrect username or password'
            })

def read_list(self, object_list, bundle):
        """
        Allow the endpoint for the User Resource to display only the logged in user's information
        """
        self.is_authenticated(request)
        return object_list.filter(pk=bundle.request.user.id)

(You can view the entire contents of the file, if you need, at https://github.com/OpenPhilology/phaidra/blob/master/api/api.py)

So, in summary, the main questions/points of confusion for me:

  1. Which data must be sent via the curl request to send an authenticated GET/POST/etc.?
  2. Is the Authentication value correct for the User Resource?
  3. Am I supposed to be able to authenticate with only the CSRF token, or is the session ID also necessary?

Thanks in advance for any insight on this!

EDIT: Here is the custom user model we have.

from django.contrib.auth import authenticate

from django.db import models as django_models 
from neo4django.db import models
from neo4django.graph_auth.models import User, UserManager


class AppUser(User):
    objects = UserManager()

    USERNAME_FIELD = 'username'

    def __unicode__(self):
        return unicode(self.username) or u'' 
Was it helpful?

Solution

The issue here ended up being two fold:

I discovered the Django function get_user_model() was failing -- which is used in several places -- but not because the USERNAME_FIELD was blank. If I hardcoded the values into this file, everything worked fine. The issue instead is that it was failing because Django requires a very specific naming scheme for custom user models. From the Django docs:

This dotted pair describes the name of the Django app (which must be in your INSTALLED_APPS), and the name of the Django model that you wish to use as your User model.

https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model

HOWEVER, this is not the entire story. Django presumes that your AUTH_USER_MODEL can be split by the period in the middle, and this will give it two variables, "app_label" and "model_name". See:

def get_user_model():
    "Return the User model that is active in this project"
    from django.conf import settings
    from django.db.models import get_model

    try:
        app_label, model_name = settings.AUTH_USER_MODEL.split('.')
    except ValueError:
        raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'")
    user_model = get_model(app_label, model_name)
    if user_model is None:
        raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL)
    return user_model

(in file: django/contrib/auth/__init__.py)

However, mine had been accessible via 'from core.models.user import AppUser'. I had to flatten my project structure so I had an app called "app", all my models in a file called "models.py", and then in settings.py I was able to set my AUTH_USER_MODEL to 'app.AppUser'.

The weird part about this: In many other situations, I had been able to log in via the API, even while my APP_USER_MODEL was set to 'core.models.user.AppUser'. It was only when I tried to use SessionAuth that I had issues.

Furthermore, there were recent changes to Neo4Django that also had to be upgraded, as they dealt directly with graph auth. Previously, backends.py hadn't be property importing and trying to use my custom model. Now it does. Specifically, this file:

https://github.com/scholrly/neo4django/blob/9058c0b6f4eb9d23c2a87044f0661f8178b80b12/neo4django/graph_auth/backends.py

OTHER TIPS

Which version of Django are you using? If you're using 1.5 or above, you might be running into an issue where the name of your username field wasn't specified:

https://github.com/toastdriven/django-tastypie/blob/master/tastypie/compat.py

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top