How can I use Django OAuth Toolkit with Python Social Auth?
Asked Answered
D

4

67

I'm building an API using Django Rest Framework. Later this API is supposed to be consumed by iOS and Android devices. I want to allow my users to sign-up with oauth2-providers like Facebook and Google. In this case, they shouldn't have to create an account with my platform at all. But users should also be able to sign-up when not having a Facebook/Google account, for which I'm using django-oauth-toolkit, so I have my own oauth2-provider.

For external providers I'm using python-social-auth, which works fine and automatically creates the user objects.

I want the clients to authenticate by using bearer tokens, which works fine for users that signed up with my provider (django-oauth-toolkit provides authentication scheme and permission classes for Django REST Framework).
However, python-social-auth only implements session based authentication, so there is no straightforward way to make authenticated API requests on behalf of users that registered by an external oauth2 provider.

If I use an access_token that has been generated by django-oauth-toolkit, doing a request like this works:

curl -v -H "Authorization: Bearer <token_generated_by_django-oauth-toolkit>" http://localhost:8000/api/

However, the following doesn't work since there is no corresponding authentication scheme for Django REST Framework and the AUTHENTICATION_BACKENDS provided by python-social-auth only work for session-based authentication:

curl -v -H "Authorization: Bearer <token_stored_by_python-social-auth>" http://localhost:8000/api/

Using the browseable API provided by Django REST Framework after authenticating with python-social-auth works just fine, only API calls without a session cookie don't work.

I'm wondering what the best approach is for this problem. The way I see it, I have basically two options:

A: When a user signs up with an external oauth2 provider (handled by python-social-auth), hook into the process to create an oauth2_provider.models.AccessToken and continue to use 'oauth2_provider.ext.rest_framework.OAuth2Authentication', now authenticating also users that registered with an external provider. This approach is suggested here: https://groups.google.com/d/msg/django-rest-framework/ACKx1kY7kZM/YPWFA2DP9LwJ

B: Use python-social-auth for API request authentication. I could get my own users into python-social-auth by writing a custom backend and using register_by_access_token. However, since API calls cannot utilize Django sessions this would mean I would have to write an authentication scheme for Django Rest Framework that utilizes the data stored by python-social-auth. Some pointers on how to do this can be found here:
http://psa.matiasaguirre.net/docs/use_cases.html#signup-by-oauth-access-token
http://blog.wizer.fr/2013/11/angularjs-facebook-with-a-django-rest-api/
http://cbdev.blogspot.it/2014/02/facebook-login-with-angularjs-django.html
However, the way I understand it python-social-auth only verifies the token when doing a login and relies on the Django session afterwards. This would mean I would have to find a way to prevent python-social-auth from doing the whole oauth2-flow for each stateless API request and rather check against the data stored in the DB, which isn't really optimized for querying since it's stored as JSON (I could use UserSocialAuth.objects.get(extra_data__contains=) though).
I would also have to take care of verifying the scopes of an access token and use them to check permissions, something django-oauth-toolkit already does (TokenHasScope, required_scopes etc).

At the moment, I'm leaning towards using option A, since django-oauth-toolkit provides good integration with Django Rest Framework and I get everything I need out of the box. The only drawback is that I have to "inject" the access_tokens retrieved by python-social-auth into the AccessToken model of django-oauth-toolkit, which feels wrong somehow, but would probably be by far the easiest approach.

Does anybody have any objections on doing that or has maybe tackled the same problem in a different way? Am I missing something obvious and making my life harder than necessary? If anybody has already integrated django-oauth-toolkit with python-social-auth and external oauth2 providers I would be very thankful for some pointers or opinions.

Dextrality answered 20/11, 2014 at 22:58 Comment(0)
M
112

A lot of the difficulty in implementing OAuth comes down to understanding how the authorization flow is supposed to work. This is mostly because this is the "starting point" for logging in, and when working with a third-party backend (using something like Python Social Auth) you are actually doing this twice: once for your API and once for the third-party API.

Authorizing requests using your API and a third-party backend

The authentication process that you need is go through is:

Sequence diagram for option A

Mobile App -> Your API : Authorization redirect
Your API -> Django Login : Displays login page
Django Login -> Facebook : User signs in
Facebook -> Django Login : User authorizes your API
Django Login -> Your API : User signs in
Your API -> Mobile App : User authorizes mobile app

I'm using "Facebook" as the third-party backend here, but the process is the same for any backend.

From the perspective of your mobile app, you are only redirecting to the /authorize url provided by Django OAuth Toolkit. From there, the mobile app waits until the callback url is reached, just like in the standard OAuth authorization flow. Almost everything else (Django login, social login, etc.) is handled by either Django OAuth Toolkit or Python Social Auth in the background.

This will also be compatible with pretty much any OAuth libraries that you use, and the authorization flow will work the same no matter what third party backend is used. It will even handle the (common) case where you need to be able to support Django's authentication backend (email/username and password) as well as a third-party login.

Option A without a third-party backend

Mobile App -> Your API : Authorization redirect
Your API -> Django Login : Displays login page
Django Login -> Your API : User signs in
Your API -> Mobile App : User authorizes mobile app

What's also important to note here is that the mobile app (which could be any OAuth client) never receives the Facebook/third-party OAuth tokens. This is incredibly important, as it makes sure your API acts as an intermediary between the OAuth client and you user's social accounts.

Sequence diagram with your API as the gatekeeper

Mobile App -> Your API : Authorization redirect
Your API -> Mobile App : Receives OAuth token
Mobile App -> Your API : Requests the display name
Your API -> Facebook : Requests the full name
Facebook -> Your API : Sends back the full name
Your API -> Mobile App : Send back a display name

Otherwise, the OAuth client would be able to bypass your API and make requests on your behalf to the third-party APIs.

Sequence diagram for bypassing your API

Mobile App -> Your API : Authorization redirect
Your API -> Mobile App : Receives Facebook token
Mobile App -> Facebook : Requests all of the followers
Facebook -> Mobile App : Sends any requested data

You'll notice that at this point you would have lost all control over the third-party tokens. This is especially dangerous because most tokens can access a wide range of data, which opens the door to abuse and eventually goes down under your name. Most likely, those logging into your API/website did not intend on sharing their social information with the OAuth client, and were instead expecting you to keep that information private (as much as possible), but instead you are exposing that information to everyone.

Authenticating requests to your API

When the mobile application then uses your OAuth token to make requests to your API, all of the authentication happens through Django OAuth Toolkit (or your OAuth provider) in the background. All you see is that there is a User associated with your request.

How OAuth tokens are validated

Mobile App -> Your API : Sends request with OAuth token
Your API -> Django OAuth Toolkit : Verifies the token
Django OAuth Toolkit -> Your API : Returns the user who is authenticated
Your API -> Mobile App : Sends requested data back

This is important, because after the authorization stage it shouldn't make a difference if the user is coming from Facebook or Django's authentication system. Your API just needs a User to work with, and your OAuth provider should be able to handle the authentication and verification of the token.

This isn't much different from how Django REST framework authenticates the user when using session-backed authentication.

Sequence diagram for authenticating using sessions

Web Browser -> Your API : Sends session cookie
Your API -> Django : Verifies session token
Django -> Your API : Returns session data
Your API -> Django : Verifies the user session
Django -> Your API : Returns the logged in user
Your API -> Web Browser : Returns the requested data

Again, all of this is handled by Django OAuth Toolkit and does not require extra work to implement.

Working with a native SDK

In most cases, you are going to be authenticating the user through your own website and using Python Social Auth to handle everything. But the one notable exception is when using a native SDK, as authentication and authorization is handled through the native system, which means you are bypassing your API entirely. This is great for applications which need to sign in with a third party, or applications which don't use your API at all, but it's a nightmare when both come together.

This is because your server can't validate the login and is forced to assume that the login is valid and genuine, which means it bypasses any and all security that Python Social Auth gives you.

Using a native SDK can cause issues

Mobile App -> Facebook SDK : Opens the authorization prompt
Facebook SDK -> Mobile App : Gets the Facebook token
Mobile App -> Your API : Sends the Facebook token for authorization
Your API -> Django Login : Tries to validate the token
Django Login -> Your API : Returns a matching user
Your API -> Mobile App : Sends back an OAuth token for the user

You'll notice that this skips over your API during the authentication phase, and then forces your API to make assumptions about the token that is passed in. But there are definitely cases where this risk may be worth it, so you should evaluate that before throwing it out. It's a trade off between quick and native logins for your user and potentially handling bad or malicious tokens.

Multinuclear answered 20/11, 2014 at 23:49 Comment(7)
Hi Kevin, thanks a lot for your answer. I probably didn't make it clear enough, but what I want to do is to provide my users with the ability to sign-up using providers like Facebook or Google, much like stackoverflow does. I don't want to get any other information from these providers, only allow users to login with them. If they sign-up with e.g. Google, they shouldn't need to register with my provider at all. Your suggestion to use django-oauth-toolkit as kind of a proxy makes a lot of sense, I need to think it through and will get back to you later.Dextrality
There is only one API and django-oauth-toolkit is supposed to be only one among different options to sign-up, yes. I thought about only offering external providers for sign-up, but would like users that don't have an account with Google or Facebook or don't want to associate it with my platform to sign-up as well.Dextrality
That should work, but in this case I still have the problem how to do API calls with e.g. an iOS app without using something like a cookie-jar. With django-oauth-toolkit I can just use the Authorization header with a bearer token, which doesn't seem possible right now with python-social-auth.Dextrality
That's where having a token-based authentication scheme is useful, so you can use it as a proxy for the different authentication methods. That way you only need to store the token instead of the cookies.Multinuclear
Yeah, I will definitely try to do it the way you suggested, it makes sense even if it wouldn't be problematic to pass the access_tokens from Google/Facebook on to the clients in this case.Dextrality
@Dextrality Did this end up working out? I'm looking for more details on how this is best done.Pressley
this answer doesn't seem to cover authorization code grants -- whilst being a little more complicated to set up, it seems to address all the concerns, and makes the same flow possible from a web-client or a native SDK, no?Spendable
B
10

I solved it by using your A. option.

What I do is registering users that use a third party to sign up by their third party access token.

url(r'^register-by-token/(?P<backend>[^/]+)/$',
    views.register_by_access_token),

This way, I can issue a GET request like this one:

GET http://localhost:8000/register-by-token/facebook/?access_token=123456

And register_by_access_token gets called. request.backend.do_auth will query the provider for the user info from the token and magically register a user account with the info or sign in the user if he's already registered.

Then, I create a token manually and return it as JSON for letting the client query my API.

from oauthlib.common import generate_token
...
@psa('social:complete')
def register_by_access_token(request, backend):
    # This view expects an access_token GET parameter, if it's needed,
    # request.backend and request.strategy will be loaded with the current
    # backend and strategy.
    third_party_token = request.GET.get('access_token')
    user = request.backend.do_auth(third_party_token)

    if user:
        login(request, user)

        # We get our app!   
        app = Application.objects.get(name="myapp")

        # We delete the old token
        try:
            old = AccessToken.objects.get(user=user, application=app)
        except:
            pass
        else:
            old.delete()

        # We create a new one
        my_token = generate_token()

        # We create the access token 
        # (we could create a refresh token too the same way) 
        AccessToken.objects.create(user=user,
                                   application=app,
                                   expires=now() + timedelta(days=365),
                                   token=my_token)

        return "OK" # you can return your token as JSON here

    else:
        return "ERROR"

I'm just not sure about the way I generate the token, is this good practice? Well, in the mean time, it works!!

Boyett answered 5/4, 2015 at 18:31 Comment(7)
Where, or better: how do you get the access_token? Who sends it? I am using satellizer and I am struggling so much.Catty
You get the access token from your front end. for an ios app and facebook, that would be Facebook iOS SDKBoyett
Can you post the complete code? Also, are you using this django-oauth-toolkit. to store token or something build by yourself? Thanks.Queri
Use this library for a key in hand solution.Boyett
@FelixD. You wrote "magically register a user account with the info or sign in the user if he's already registered." Which details from the "user info" are you storing in your application database when registering the user(like email address, etc), to identify the user on next login? To me email address seems to make more sense, because, like in Stackoverflow, if there are multiple oAuth providers(google, and fb), a user like me can login using either, but, Stackoverflow recognizes me in both cases to be the same user. Between my 2 social accounts, email address is the only common entity.Otoole
In other words, it is inevitable that the application that relies on oAuth provider for authentication, and then associating such user with what the user did on that application(like in stackoverflow, the questions, comments etc) should depend on the email address, particularly when the application supports multiple oAuth providers, as any other entity between two oAuth provides is not guaranteed to be same, perhaps except for the email address. Right?Otoole
You are right, it depends on what the OAuth provider can provide you as user info. You may get an email, a username, or a burrito recipe. You should look into the library I mentioned for a more tested solution than doing it yourself.Boyett
Y
6

Maybe django-rest-framework-social-oauth2 is what you're looking for. This package depends on python-social-auth and django-oauth-toolkit, which you already use. I quickly scanned through the documentation, and it seems to implement just what you are trying to do.

Yoakum answered 13/4, 2015 at 15:22 Comment(0)
N
1

I was doing React Native with expo and Django with Django REST framework. This blogpost ended being the way I solved registration (signup) with facebook https://medium.com/@gabriel_gamil/react-native-expo-django-facebook-authentication-sign-in-83625c49da7

tldr; use django-rest-auth https://django-rest-auth.readthedocs.io/en/latest/index.html

use Django-allauth https://django-allauth.readthedocs.io/en/latest/

Nitaniter answered 9/1, 2019 at 1:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.