Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API token authentication. #9

Closed
tomchristie opened this issue May 13, 2011 · 7 comments
Closed

Add API token authentication. #9

tomchristie opened this issue May 13, 2011 · 7 comments

Comments

@tomchristie
Copy link
Member

At the moment Basic authentication only support username/password.

It'd be great if it could also be used with token objects, stored in the database, where each token has a key, a secret, and is tied to a user.

The right way to do this would be to write a standard django auth backend that validates (username, password) arguments against an APIToken table rather than the standard backend which validates it against the User table.

After that I'd modify the BasicAuthentication class, adding a 'backend' attribute, which would be unset by default. If the attribute is not set then the class would simply call 'authenticate(username, password)', otherwise it would call backend.authenticate(username, password).

TokenBasicAuthentication would then simply extend BasicAuthentication, setting 'backend=APITokenBackend'.

It would be nice if the token table didn't get installed by default on syncdb unless it's actually being used.

@alanjds
Copy link
Contributor

alanjds commented Dec 21, 2011

Maybe this is what we need: https://github.com/jpulgarin/django-tokenapi

I'll just post a request at that repo to allow to set timeout per-token instead of using a django setting

@machineghost
Copy link

First off thanks for the link alanjds; my workplace is now happily using django-tokenapi together with django rest framework!

For anyone who wants to follow in our footsteps, you can basically just:

  1. Install both frameworks
  2. Add the token URLs to your urls.py:
(r'^token/', include('tokenapi.urls')),

(NOTE: This is the URL your users will use to acquire a token).

  1. Instead of using the django token api decorator, use this one:
def token_required(view_func):
    """Django-TokenAPI provides a decorator (similarly named "token_required"), which this decorator
       is based on.  However, Django-TokenAPI's decorator isn't designed to handle
       Django-Rest-Framework's views, so we had to make our own version that can."""

    @csrf_exempt
    @wraps(view_func)
    def _wrapped_view(self, request,  *args, **kwargs):
        user_id = request.REQUEST.get('user')
        token = request.REQUEST.get('token')
        if user_id and token:
            # This is an API-based request; "login" the user specified with the provided token
            user = authenticate(pk=user_id, token=token)
            login(request, user)
        return view_func(self, request, *args, **kwargs)
    return _wrapped_view
  1. Add that decorator to all of your get/post/put/whatever methods ... or just to the dispatch method (which is what I did)

With that all of your API calls will now accept normal (cookie/session-based) Django authentication OR authentication tokens. If you want to not accept normal Django authentication it should be pretty easy to tweak the decorator.

Hope this helps someone :-)

@machineghost
Copy link

Oops, I lied; don't use that decorator, use this one (which has a few extra lines to actually return a ResponseForbidden if the user fails to authenticate):

from django.http import HttpResponseForbidden
def token_required(view_func):
    """Django-TokenAPI provides a decorator (similarly named "token_required"), which this decorator
       is based on.  However, Django-TokenAPI's decorator isn't designed to handle
       Django-Rest-Framework's views, so we had to make our own version that can."""

    @csrf_exempt
    @wraps(view_func)
    def _wrapped_view(self, request,  *args, **kwargs):
        user_id = request.REQUEST.get('user')
        token = request.REQUEST.get('token')
        if user_id and token:
            # This is an API-based request; "login" the user specified with the provided token
            user = authenticate(pk=user_id, token=token)
            login(request, user)
        if request.user.is_authenticated():
            return view_func(self, request, *args, **kwargs)
        return HttpResponseForbidden('Unable to authenticate')
    return _wrapped_view

@tomchristie
Copy link
Member Author

Fixed in restframework2 branch. See source and docs.

@machineghost
Copy link

Awesome! I have found one other bug in my code through (sorry, should have tested better before posting). Can you please change:

     def _wrapped_view(self, request,  *args, **kwargs):
+        user = request.user
         user_id = request.REQUEST.get('user')

             login(request, user)
-        if request.user.is_authenticated():
+        if user.is_authenticated():
             return view_func(self, request, *args, **kwargs)

(without that fix the normal authentication flow fails).

@jpulgarin
Copy link

Hey @machineghost I'm trying to understand the changes you made to token_required. As far as I can tell, the only difference is that you're not checking if you get a valid user object after calling authenticate. Any reason for that?

@machineghost
Copy link

It's been nine months, I've since stopped working on Python, and I have an absolutely terrible memory even for code I wrote yesterday, so ... you've been warned.

That being said, I think the issue I had was just that request.user wasn't authenticated, but the user that came back from the login was, so I wanted to be sure to check that user. Unfortunately I have no recollection of any details beyond that (eg. what the circumstances were when I observed that problem); sorry :-(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants