django-allauth icon indicating copy to clipboard operation
django-allauth copied to clipboard

Improve ease of integration for 3rd party REST/JSON APIs

Open mpetyx opened this issue 11 years ago • 27 comments

Hello guys,

I am using django allauth in a variety of application that i make and its awesome!

Right now, I am trying to develop a login process in two separate steps. I have a client(mobile) social login with some provider(twitter, Facebook) and the client takes the oauth credentials and sends them back to the server.

That means, that i do not need to use the forms and the templates provided, since all that are handled just fine by the client already. What i need, is to know what functions/views i need to call in order to avoid all the functions that need the request to work properly.

I hope that i explained in detail my issue. Thank you in advance, Michael.

mpetyx avatar Aug 17 '13 10:08 mpetyx

For now, this is something that is beyond allauth scope. There is no "create account given an access token"-like function. It would not be too difficult to provide one, but I am not sure the end result would fit all use cases.

pennersr avatar Aug 17 '13 11:08 pennersr

I see. Thank you very much for you answer. I see that in providers/facebook/views there is a function login_by_token that with a little bit of modification it would work. But twitter seems a little bit trickier for some reason. Anyway, thank you very much. Any feedback would be highly appreciated!!

mpetyx avatar Aug 17 '13 11:08 mpetyx

Main problem I see is how to deal with signup conflicts. Consider a local user account, signed up with [email protected]. Then a mobile device comes along, hands over a token. Now, if that token represents a complete new user then all is good. If that token belongs to a user with the same e-mail address, then what do you do? Given that the local user did not have his social account connected, you cannot be 100% certain that this is the same user.

pennersr avatar Aug 17 '13 11:08 pennersr

That is indeed a major problem. In my workflow, I resolve such issues by redirecting the specific user into the website where i apply the full allauth functionality. What I ideally would like to succeed, in the mobile version, is to sign up completely new users and those users returning either with their email, but mainly with their social account. Any conflicts are going to be resolved by the web panel of the application.

mpetyx avatar Aug 17 '13 11:08 mpetyx

I would probably opt for:

  • In case of conflicts that cannot be 100% cleared up -- let the API call fail. Show a message to the user that he needs to hookup his social account on the web site itself before being able to use the app. Rationale: resolving the conflict requires interactivity, which you do not have in the API. Furthermore, this is not expected to be a mainstream scenario.
  • Add a generic method def get_social_login(request, app, token) to the Provider base class. Move the complete_login -like methods of all providers here.
  • Add some form of noninteractive_save to SocialLogin. This should contain the (provider independent) logic of mapping the SocialLogin to an existing account, or setup a new one. In case of conflicts, it should bail out.

The above should provide sufficient primitives to make API integration easy.

All in all, it requires some internal refactoring to make this available in a provider independent fashion. Whipping something project-specific together just for FB/Twitter is easier on the short term.

pennersr avatar Aug 17 '13 11:08 pennersr

Thank you very much! This what I am going to try immediately. One last question. Do you have any mechanism to create test "request" objects? I am trying to fake them in order to test that my workflow works, since I already have the tokens, the app and everything. (when i test the above in some examples i have created the previous days, it always crashes in the request part)

mpetyx avatar Aug 17 '13 11:08 mpetyx

Have a look at Django's request factory:

https://docs.djangoproject.com/en/1.5/topics/testing/advanced/

pennersr avatar Aug 20 '13 10:08 pennersr

thank you very much! Actually, following your advice the previous day i managed to solve the problem. Hopefully when I make my solution more generic I will create a pull request if you want. Anyway, thank you very much for your help! (you can close the issue if you want)

mpetyx avatar Aug 20 '13 12:08 mpetyx

Great that you got something to work. I am keeping the issue open -- this is something that needs to be addressed...

pennersr avatar Aug 20 '13 12:08 pennersr

@mpetyx I am working on this exact same issue. I'd love to see your solution if possible.

jtbthethird avatar Sep 11 '13 20:09 jtbthethird

@mpetyx have you pushed your solution to your fork repos? I've sent a pull request that partially fix this issue for Google accounts. The problem is that I still have to confirm the email address and I am trying to figure out a way of bypass it.

talktojp avatar Sep 13 '13 05:09 talktojp

I'm dealing with this issue and basically duct-taping together an API that gets most of what I want done. Here's a stackoverflow answer that helped me—maybe it'll help others.

http://stackoverflow.com/questions/16857450/how-to-register-users-in-django-rest-framework

Kobold avatar Dec 11 '13 09:12 Kobold

I've also thought about rewriting parts of all auth to allow for rest api calls, but I've found that just using what is already provided is sufficient. For exmaple using the django rest framework and allauth to update passwords:

class PasswordUpdateView(generics.UpdateAPIView): permission_classes = (IsAuthenticated,)

def post(self, request, format=None):
    if not request.user.has_usable_password():
        return Response({}, status=status.HTTP_400_BAD_REQUEST ) 
    f = ChangePasswordForm(data=request.DATA, user=request.user)
    if f.is_valid():
        f.save();
        return Response({'status': 'Password Changed'})
    return Response(dict(f.errors.items()), status=status.HTTP_400_BAD_REQUEST ) 

This is just an example, but I think it illustrates how you can combine the two without changing allauth or django rest framework.

stuross avatar Dec 11 '13 10:12 stuross

Here's a SO answer that I found extremely useful for doing the Facebook component of the registration API with allauth: http://stackoverflow.com/a/16382270/36092

The view I ended up writing:

@api_view(['POST'])
def create_facebook_user(request):
    """Allows REST calls to programmatically create new facebook users.

    This code is very heavily based on
    allauth.socialaccount.providers.facebook.views.login_by_token
    as of allauth 0.15.0.
    """
    form = FacebookConnectForm(request.DATA)
    if form.is_valid():
        try:
            app = providers.registry.by_id(FacebookProvider.id) \
                .get_app(request)
            access_token = form.cleaned_data['access_token']
            token = SocialToken(app=app,
                                token=access_token)
            login = fb_complete_login(request, app, token)
            login.token = token
            login.state = SocialLogin.state_from_request(request)
            complete_social_login(request, login)
            return Response({})

        except requests.RequestException:
            errors = {'access_token': ['Error accessing FB user profile.']}
    else:
        errors = dict(form.errors.items())

    return Response(errors, status=status.HTTP_400_BAD_REQUEST)

Kobold avatar Dec 14 '13 23:12 Kobold

I think a REST interface to Django allauth will be useful to many projects. Django allauth has been a like breath of fresh air for me but lack of REST support is a major stumbling block. I am currently building REST interface for my project that will leverage Django Allauth and I will release it online so that maybe it will be useful for others also.

unitedroad avatar Jun 03 '14 07:06 unitedroad

+1 on this. Does anybody have a good implementation for this?

philippeluickx avatar Jun 06 '14 18:06 philippeluickx

+1 for this. @unitedroad, any news regarding your allauth api wrapper? perhaps others (like me) in need of this could contribute to your work, if you already have some WIP..

n0phx avatar Jun 27 '14 09:06 n0phx

Starting my investigation on this, found a decent-looking and quite recent post: http://bytefilia.com/titanium-mobile-facebook-application-django-allauth-sign-sign/

philippeluickx avatar Jun 27 '14 16:06 philippeluickx

Anyone made progress on this? I am trying to figure out how to deal with anything else than facebook and wondering if it is just easier to simply use the e.g. twitter api (namely: send to a url your token and get the response back). If we could have something similar to fb_complete_login function for all the other providers, that would be awesome!

philippeluickx avatar Dec 06 '14 22:12 philippeluickx

Allright, here is my code that somehow manages to get things working for Facebook and Twitter... Not really elegant at some points, so feel free to suggest improvements! urls.py

    url(
        r'^rest/facebook-login/$',
        csrf_exempt(RestFacebookLogin.as_view()),
        name='rest-facebook-login'
    ),
    url(
        r'^rest/twitter-login/$',
        csrf_exempt(RestTwitterLogin.as_view()),
        name='rest-twitter-login'
    ),

serializers.py

class EverybodyCanAuthentication(SessionAuthentication):
    def authenticate(self, request):
        return None


class RestFacebookLogin(APIView):
    """
    Login or register a user based on an authentication token coming
    from Facebook.
    Returns user data including session id.
    """

    # this is a public api!!!
    permission_classes = (AllowAny,)
    authentication_classes = (EverybodyCanAuthentication,)

    def dispatch(self, *args, **kwargs):
        return super(RestFacebookLogin, self).dispatch(*args, **kwargs)

    def get(self, request, *args, **kwargs):
        try:
            original_request = request._request
            auth_token = request.GET.get('auth_token', '')

            # Find the token matching the passed Auth token
            app = SocialApp.objects.get(provider='facebook')
            fb_auth_token = SocialToken(app=app, token=auth_token)

            # check token against facebook
            login = fb_complete_login(original_request, app, fb_auth_token)
            login.token = fb_auth_token
            login.state = SocialLogin.state_from_request(original_request)

            # add or update the user into users table
            complete_social_login(original_request, login)
            # Create or fetch the session id for this user
            token, _ = Token.objects.get_or_create(user=original_request.user)
            # if we get here we've succeeded
            data = {
                'username': original_request.user.username,
                'objectId': original_request.user.pk,
                'firstName': original_request.user.first_name,
                'lastName': original_request.user.last_name,
                'sessionToken': token.key,
                'email': original_request.user.email,
            }
            return Response(
                status=200,
                data=data
            )

        except:
            return Response(status=401, data={
                'detail': 'Bad Access Token',
            })


class RestTwitterLogin(APIView):
    """
    Login or register a user based on an authentication token coming
    from Twitter.
    Returns user data including session id.
    """

    # this is a public api!!!
    permission_classes = (AllowAny,)
    authentication_classes = (EverybodyCanAuthentication,)

    def dispatch(self, *args, **kwargs):
        return super(RestTwitterLogin, self).dispatch(*args, **kwargs)

    def get(self, request, *args, **kwargs):
        try:
            original_request = request._request
            auth_token = request.GET.get('auth_token', '')
            auth_token_secret = request.GET.get('auth_token_secret', '')

            # Find the token matching the passed Auth token
            app = SocialApp.objects.get(provider='twitter')
            twitter_auth_token = {
                'oauth_token': auth_token,
                'oauth_token_secret': auth_token_secret,
            }
            self.request.session['oauth_api.twitter.com_access_token'] = twitter_auth_token

            twitter_auth_socialtoken = SocialToken(app=app, token=auth_token)

            # check token against twitter
            adapter = TwitterOAuthAdapter()
            login = adapter.complete_login(
                original_request,
                app,
                twitter_auth_socialtoken
            )
            login.token = twitter_auth_socialtoken
            login.state = SocialLogin.state_from_request(original_request)

            # add or update the user into users table
            complete_social_login(original_request, login)
            # Create or fetch the session id for this user
            token, _ = Token.objects.get_or_create(user=original_request.user)
            # if we get here we've succeeded
            data = {
                'username': original_request.user.username,
                'objectId': original_request.user.pk,
                'firstName': original_request.user.first_name,
                'lastName': original_request.user.last_name,
                'sessionToken': token.key,
                'email': original_request.user.email,
            }
            return Response(
                status=200,
                data=data
            )

        except:
            return Response(status=401, data={
                'detail': 'Bad Access Token',
            })

And even comes with some basic tests.

class AccountTests(APITestCase):
    def test_connect_facebook_account(self):
        """
        Ensure we can connect to a facebook account.
        """

        # Create the social app first for Facebook
        test_site = mommy.make(
            'Site',
        )
        mommy.make(
            'SocialApp',
            provider='facebook',
            name='facebook allauth app',
            client_id=settings.FACEBOOK_CLIENT_ID,
            secret=settings.FACEBOOK_SECRET_KEY,
            sites=[test_site, ],
        )

        data = {'auth_token': settings.FACEBOOK_ACCESS_TOKEN_TEST}
        response = self.client.get(
            reverse('rest-facebook-login'),
            data,
        )

        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_connect_twitter_account(self):
        """
        Ensure we can connect to a twitter account.
        """

        # Create the social app first for Twitter
        test_site = mommy.make(
            'Site',
        )
        mommy.make(
            'SocialApp',
            provider='twitter',
            name='twitter allauth app',
            client_id=settings.TWITTER_CLIENT_ID,
            secret=settings.TWITTER_SECRET_KEY,
            sites=[test_site, ],
        )

        data = {
            'auth_token': settings.TWITTER_ACCESS_TOKEN_TEST,
            'auth_token_secret': settings.TWITTER_ACCESS_TOKEN_SECRET_TEST,
        }
        response = self.client.get(
            reverse('rest-twitter-login'),
            data,
        )

        self.assertEqual(response.status_code, status.HTTP_200_OK)

philippeluickx avatar Dec 07 '14 15:12 philippeluickx

Hi all, Could someone provide a solution for simple login. Since using my own login devoid of allauth login might lead tpo conflict with user accounts

acquayefrank avatar Jan 26 '15 23:01 acquayefrank

Just found this library, partially based on allauth. Testing it out now.

https://github.com/Tivix/django-rest-auth

philippeluickx avatar Apr 09 '15 14:04 philippeluickx

I think a REST interface to Django allauth will be useful to many projects. Django allauth has been a like breath of fresh air for me but lack of REST support is a major stumbling block. I am currently building REST interface for my project that will leverage Django Allauth and I will release it online so that maybe it will be useful for others also.

Did you succeed in your endeavor?

pyronlaboratory avatar Aug 15 '19 22:08 pyronlaboratory

This is a doooosy of an issue that is probably worth your attention. It seems like there is work already done towards this end that can be leveraged. Quite frankly it's above my head so that's all I can say, other than... I'd use it.

derek-adair avatar Aug 30 '23 16:08 derek-adair

The following project offers exactly that: https://github.com/iMerica/dj-rest-auth I would like to hear your opinion of dj-rest-auth as I am intending on using it!

oussjarrousse avatar Jan 07 '24 23:01 oussjarrousse

The following project offers exactly that: https://github.com/iMerica/dj-rest-auth I would like to hear your opinion of dj-rest-auth as I am intending on using it!

https://github.com/iMerica/dj-rest-auth#a-note-on-django-allauth-from-imerica

pennersr avatar Jan 08 '24 18:01 pennersr

I've added a PoC React example, currently showing basic login, signup with just vanilla django-allauth: https://github.com/pennersr/django-allauth/tree/main/examples/react-spa

As mentioned in the README, this is an incomplete work in progress. The goal is to research what we can do to make sure all of allauth is properly accessible in a SPA based setup.

pennersr avatar Jan 20 '24 18:01 pennersr

Work is in progress over at the feat-headless branch to add an official out-of-the-box API covering all the scenario's, from regular login to 2FA and reauthentication. Although things are still in flux, this work is progressing rapidly and most flows are becoming operational. Once there is sufficient confidence in the overall setup an OpenAPI spec will be published.

For more background information, read up here: https://allauth.org/news/2024/03/ngi-zero-grant-plan/

pennersr avatar Mar 20 '24 09:03 pennersr

Status update: draft API and React app now available

See: https://allauth.org/news/2024/04/api-feedback/

pennersr avatar Mar 31 '24 22:03 pennersr

@pennersr the API looks great! Do you have a set plan for when this might be included in a release? We (the OSS project InvenTree) currently use 2-3 libraries that are more or less unneeded after this release and I would like to strip those out once I know a release is somewhat predictable.

matmair avatar Apr 08 '24 16:04 matmair