Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are sessions needed for python-social-auth

I'm building a django app with an API backend(built with DRF) and angularjs client. My goal is to completely decouple the server and client using JWT in place of sessions. I'm attempting to integrate python-social-auth(PSA) with django-rest-framework-jwt(DRFJWT), so my goal is to have an auth flow something to this:

User logs with Email/facebook via angular client -> client posts form to PSA's url -> PSA login/create user ->[!] DRFJWT creates token that it then sends back to client -> client stores token in local storage then uses token each request

[!]: This is currently where I'm struggling. My thinking is that I can modify the do_complete method in PSA like so

from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler


def do_complete(backend, login, user=None, redirect_name='next',
            *args, **kwargs):
  # pop redirect value before the session is trashed on login()
  data = backend.strategy.request_data()
  redirect_value = backend.strategy.session_get(redirect_name, '') or \
                 data.get(redirect_name, '')

  is_authenticated = user_is_authenticated(user)
  user = is_authenticated and user or None

  partial = partial_pipeline_data(backend, user, *args, **kwargs)
  if partial:
      xargs, xkwargs = partial
      user = backend.continue_pipeline(*xargs, **xkwargs)
  else:
      user = backend.complete(user=user, *args, **kwargs)

  if user_is_active(user):
      # catch is_new/social_user in case login() resets the instance
      is_new = getattr(user, 'is_new', False)
      social_user = user.social_user
      login(backend, user, social_user)

  payload = jwt_payload_handler(user)
  return { 'token': jwt_encode_handler(payload) }

Is this the only way of doing what I'm trying to accomplish?

I'm also wondering if its okay from a best-practices standpoint to use sessions to manage the pipeline and JWT for auth?

like image 275
Ethan Blackburn Avatar asked Nov 09 '14 01:11

Ethan Blackburn


People also ask

What is Python Social Auth?

Python Social Auth is an easy-to-setup social authentication/registration mechanism with support for several frameworks and auth providers.

What is Session authentication in django REST framework?

SessionAuthentication. This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website. If successfully authenticated, SessionAuthentication provides the following credentials.

What is Social Auth app django?

Python Social Auth - Django Python Social Auth is an easy to setup social authentication/registration mechanism with support for several frameworks and auth providers.

What is Social Auth pipeline?

The social-auth-django library uses a pipeline that manages the user authentication (registry, login and logout). Each step of the pipeline generates a result that can be used in the next, and so on.


2 Answers

I'm also using python-social-auth and django-rest-framework-jwt for user authentication.

The way I was able to integrate the two authentication systems together was by creating a custom view that takes in the 'access_token' provided by the oAuth provider and attempts to create a new user with it. Once the user is created, instead of returning the authenticated user/session I return the JWT token.

The following code snippets explain the solution.

Back-End

In my views.py file I included the following:

@psa()
def auth_by_token(request, backend):
    """Decorator that creates/authenticates a user with an access_token"""
    token = request.DATA.get('access_token')
    user = request.user
    user = request.backend.do_auth(
            access_token=request.DATA.get('access_token')
        )
    if user:
        return user
    else:
        return None

class FacebookView(views.APIView):
    """View to authenticate users through Facebook."""

    permission_classes = (permissions.AllowAny,)

    def post(self, request, format=None):
        auth_token = request.DATA.get('access_token', None)
        backend = request.DATA.get('backend', None)
        if auth_token and backend:
            try:
                # Try to authenticate the user using python-social-auth
                user = auth_by_token(request, backend)
            except Exception,e:
                return Response({
                        'status': 'Bad request',
                        'message': 'Could not authenticate with the provided token.'
                    }, status=status.HTTP_400_BAD_REQUEST)
            if user:
                if not user.is_active:
                    return Response({
                        'status': 'Unauthorized',
                        'message': 'The user account is disabled.'
                    }, status=status.HTTP_401_UNAUTHORIZED)

                # This is the part that differs from the normal python-social-auth implementation.
                # Return the JWT instead.

                # Get the JWT payload for the user.
                payload = jwt_payload_handler(user)

                # Include original issued at time for a brand new token,
                # to allow token refresh
                if api_settings.JWT_ALLOW_REFRESH:
                    payload['orig_iat'] = timegm(
                        datetime.utcnow().utctimetuple()
                    )

                # Create the response object with the JWT payload.
                response_data = {
                    'token': jwt_encode_handler(payload)
                }

                return Response(response_data)
        else:
            return Response({
                    'status': 'Bad request',
                    'message': 'Authentication could not be performed with received data.'
            }, status=status.HTTP_400_BAD_REQUEST)

In my urls.py I included the following route:

urlpatterns = patterns('',
    ...
    url(r'^api/v1/auth/facebook/', FacebookView.as_view()),
    ...
)

Front-End

Now that the backend authentication is wired up, you can use any frontend library to send the access_token and authenticate the user. In my case I used AngularJS.

In a controller file I call the API like so:

/**
* This function gets called after successfully getting the access_token from Facebook's API.
*/
function successLoginFbFn(response) {
    var deferred = $q.defer();
    $http.post('/api/v1/auth/facebook/', {
        "access_token": response.authResponse.accessToken, 
        "backend": "facebook"
    }).success(function(response, status, headers, config) {
        // Success
        if (response.token) {
            // Save the token to localStorage and redirect the user to the front-page.
            Authentication.setToken(response.token);
            window.location = '/';
        }
        deferred.resolve(response, status, headers, config);
    }).error(function(response, status, headers, config) {
        // Error
        console.error('Authentication error.');
        deferred.reject(response, status, headers, config);
    });
}

With this approach you can mix the two plugins. All sent tokens will be coming from django-rest-framework-jwt even though users can still authenticate themselves with the ones provided by sites such as Facebook, Google, Twitter, etc.

I only showed the approach to authenticate through Facebook, however you can follow a similar approach for other providers.

like image 92
Carlos Perea Avatar answered Oct 04 '22 20:10

Carlos Perea


No, you do not need to use sessions(standard Django login system) with python-social-auth. What you need to make JWT and PSA work together is DRF.

Here's my solution:

I used standard PSA's url for making request too social /login/(?P<backend>[^/]+)/$, changed url in urls.py to match redirect from Facebook/Twitter to my own.

url(r'^complete/(?P<backend>[^/]+)/$', views.SocialAuthViewComplete.as_view()),

The point of using API is to have access to user data in request that PSA is doing. DRF allow you to do it if you have JWT authentication in DEFAULT_AUTHENTICATION_CLASSES

REST_FRAMEWORK = {
          'DEFAULT_AUTHENTICATION_CLASSES': (
              'rest_framework.authentication.SessionAuthentication',
              'rest_framework.authentication.TokenAuthentication',
              'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),}

In views.py

from social.apps.django_app.views import complete

class SocialAuthViewComplete(APIView):
    permission_classes = ()

    def post(self, request, backend, *args, **kwargs):
        try:
            #Wrap up  PSA's `complete` method.    
            authentication = complete(request, backend, *args, **kwargs)
        except Exception, e:
            exc = {
                'error': str(e)
            }
            return Response(exc, status=status.HTTP_400_BAD_REQUEST)
        return Response({'data': authentication}, status=status.HTTP_202_ACCEPTED)

Then I modified the do_complete method in PSA:

def do_complete(backend, login, user=None, redirect_name='next',
                *args, **kwargs):
    # pop redirect value before the session is trashed on login()
    data = backend.strategy.request_data()
    redirect_value = backend.strategy.session_get(redirect_name, '') or \
                     data.get(redirect_name, '')

    is_authenticated = user_is_authenticated(user)
    user = is_authenticated and user or None

    partial = partial_pipeline_data(backend, user, *args, **kwargs)
    if partial:
        xargs, xkwargs = partial
        user = backend.continue_pipeline(*xargs, **xkwargs)
    else:
        user = backend.complete(user=user, *args, **kwargs)

    user_model = backend.strategy.storage.user.user_model()
    if user and not isinstance(user, user_model):
        return user

    if is_authenticated:
        if not user:
            information =  'setting_url(backend, redirect_value, LOGIN_REDIRECT_URL'
        else:
            information =  'setting_url(backend, redirect_value, NEW_ASSOCIATION_REDIRECT_URL,LOGIN_REDIRECT_URL'
    elif user:
        # Get the JWT payload for the user.
        payload = jwt_payload_handler(user)

        if user_is_active(user):
            is_new = getattr(user, 'is_new', False)
            if is_new:
                information = 'setting_url(backend, NEW_USER_REDIRECT_URL, redirect_value, LOGIN_REDIRECT_URL'
            else:
                information = 'setting_url(backend, redirect_value, LOGIN_REDIRECT_URL'
        else:
            return Response({
                        'status': 'Unauthorized',
                        'message': 'The user account is disabled.'
                    }, status=status.HTTP_401_UNAUTHORIZED)
    else:
        information = 'setting_url(backend, LOGIN_ERROR_URL, LOGIN_URL'


    return { 'an information i may use in future': information,
             'token': jwt_encode_handler(payload) # Create the response object with the JWT payload.
    }

I tried pipelines and user association and it works correctly. Also you always can modify another method from PSA, if you need it to works with JWT.

like image 41
Comix Avatar answered Oct 04 '22 18:10

Comix