24

I'm trying to override is_authenticated in my custom authentication. I have something simple (to start with) like this:

class MyAuthentication(BasicAuthentication):
    def __init__(self, *args, **kwargs):
        super(MyAuthentication, self).__init__(*args, **kwargs)

    def is_authenticated(self, request, **kwargs):
        return True

then in my ModelResource I have

class LoginUserResource(ModelResource):

    class Meta:
        resource_name = 'login'
        queryset = User.objects.all()
        excludes = ['id', 'email', 'password', 'is_staff', 'is_superuser']
        list_allowed_methods = ['post']

        authentication = MyAuthentication()
        authorization = DjangoAuthorization()

I keep getting a 500 error back with "error_message": "column username is not unique". I only have one username in the db and it's the user I am trying to authenticate.

Any ideas as to why it's returning this error? How would I allow an api client to login?

Thanks for the help.

astevanovic
  • 4,246
  • 25
  • 24
imns
  • 4,728
  • 11
  • 51
  • 77

2 Answers2

69

Your approach will try to create a new user with the username that you are authenticating with. This will bubble up at the DB layer, as you've noticed, that such a user already exists.

What you want is to create a UserResource, add a method on it that users can post to and login with data passing in username/password.

from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from tastypie.http import HttpUnauthorized, HttpForbidden
from django.conf.urls import url
from tastypie.utils import trailing_slash

class UserResource(ModelResource):
    class Meta:
        queryset = User.objects.all()
        fields = ['first_name', 'last_name', 'email']
        allowed_methods = ['get', 'post']
        resource_name = 'user'

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

    def login(self, request, **kwargs):
        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', '')

        user = authenticate(username=username, password=password)
        if user:
            if user.is_active:
                login(request, user)
                return self.create_response(request, {
                    'success': True
                })
            else:
                return self.create_response(request, {
                    'success': False,
                    'reason': 'disabled',
                    }, HttpForbidden )
        else:
            return self.create_response(request, {
                'success': False,
                'reason': 'incorrect',
                }, HttpUnauthorized )

    def logout(self, request, **kwargs):
        self.method_check(request, allowed=['get'])
        if request.user and request.user.is_authenticated():
            logout(request)
            return self.create_response(request, { 'success': True })
        else:
            return self.create_response(request, { 'success': False }, HttpUnauthorized)

Now you can do send a POST to http://hostname/api/user/login with data { 'username' : 'me', 'password' : 'l33t' }.

astevanovic
  • 4,246
  • 25
  • 24
  • Thanks, this looks great. The only thing I am confused about is this line `PublicModelResource.Meta`. What's PublicModelResource? – imns Aug 02 '12 at 13:19
  • Ignore it, just mistyped it from something else. Modified the answer to reflect just regular meta class. – astevanovic Aug 02 '12 at 17:46
  • Won't this approach compromise some security issues, like when i do a GET on /user/ I will be able to see the user's hashed password and with the correct algorithm I will be able to get the plain text password. – Milind Oct 23 '12 at 21:25
  • 2
    No, since the only fields exposed on the resource are `fields = ['first_name', 'last_name', 'email']`. Additionally, login method only allows posts while logout method only allows gets. – astevanovic Oct 23 '12 at 22:08
  • 1
    You need to apply authentication for `logout()` as it doesn't know about `request.user` and will always return `AnonymousUser()`. – Robert Mar 11 '13 at 13:30
  • 1
    How about CSRF protection with this method? – sanfilippopablo Jan 09 '14 at 04:38
  • This won't persist the authentication in a session, though. Will it? – Dor Feb 06 '14 at 05:06
  • The field `request.raw_post_data` has been deprecated since Django 1.4 and is removed in Django 1.6. It has been renamed `request.body`. – Ankit Popli Mar 17 '14 at 09:20
  • is there a way i can use a django form to validate the data within the login view ? – Amyth Mar 24 '14 at 15:25
  • How do you apply authentication for logout()? – John Z Mar 31 '15 at 16:34
1

This update removes security issues on the GET method. Works on Django 1.5.4.

class UserResource(ModelResource):

    class Meta:
        queryset = User.objects.all()
        resource_name = 'user'
        allowed_methods = ['post']


    def prepend_urls(self):
        return [
            url(r"^user/login/$", self.wrap_view('login'), name="api_login"),
            url(r"^user/logout/$", self.wrap_view('logout'), name='api_logout'),
        ]

    def login(self, request, **kwargs):
        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', '')

        user = authenticate(username=username, password=password)
        if user:
            if user.is_active:
                login(request, user)
                return self.create_response(request, {
                    'success': True
                })
            else:
                return self.create_response(request, {
                    'success': False,
                    'reason': 'disabled',
                }, HttpForbidden )
        else:
            return self.create_response(request, {
                'success': False,
                'reason': 'incorrect',
                }, HttpUnauthorized )

    def logout(self, request, **kwargs):
        self.method_check(request, allowed=['post'])
        if request.user and request.user.is_authenticated():
            logout(request)
            return self.create_response(request, { 'success': True })
        else:
            return self.create_response(request, { 'success': False }, HttpUnauthorized)
elena
  • 2,820
  • 4
  • 19
  • 32
ktsw
  • 11
  • 1
  • From raphodn (not enough rep to comment) : *quick note on ktsw's great answer, request.raw_post_data is deprecated, you should put request.body* – Serge Ballesta Jan 12 '15 at 23:04
  • 1
    @ktsw what security issues on GET is this solved? From the comment of the selected answer, it seems no security issue for that answer. """No, since the only fields exposed on the resource are fields = ['first_name', 'last_name', 'email']. Additionally, login method only allows posts while logout method only allows gets.""" – Michael SM Mar 30 '16 at 08:50