Authenticating via curl with Tasty Pie, using Session Authentication
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'' 
    


Answer: 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>

