Search code examples
djangocordovaionic-frameworkpython-social-authdjango-rest-framework

Ionic Google social authentication to Django Rest Framework backend


I am trying to get social authentication working for my mobile app (an Ionic app on Android). Django rest framework backend with rest_framework_jwt, social_django, and rest_social_auth.

On my Ionic app I was using satellizer.js, however, I can't use InAppBrowser so now I am trying to do the following with cordova-plugin-googleplus:

Step#1 (On client/app)

if (provider == 'google') {
    // Use Google API Natively to get tokens and user info  
    window.plugins.googleplus.login(
                {
                    // TODO Get the WebClient from App settings
                  'webClientId': '[*.myclientid]', // optional clientId of your Web application from Credentials settings of your project - On Android, this MUST be included to get an idToken. On iOS, it is not required.
                  'offline': true, // optional, but requires the webClientId - if set to true the plugin will also return a serverAuthCode, which can be used to grant offline access to a non-Google server
                }) ................

Result: This gets me a valid response with both a idToken, serverAuthCode, and a userId.

Step#2

I am not sure what the next step is. Originally, I was going to try using Django rest_social_auth to do the following from my client/app:

POST /api/login/social/

with data (json)

provider=google&code=ASLKDJASLDKJASLD

Which was supposed to return a JWT token (from my understanding of the docs), however, it is not passing the JWTAuthMixin as there is no value returned from a call to get_authorization_header(request).split() in that Mixin. These means that nothing is returned to my client/app except a 400 error.

Am I supposed to be adding a header to my Ionic app POST when passing my idToken or serverAuthCode? Or am I on the wrong side of the tracks...

Are there any implementation recommendations for this auth flow?


Solution

  • So far I did the following and it works.

    1. On app/client

    (The client uses satellizer.js and the cordova-plugin-googleplus)

    if (provider == 'google') {
                // Use Google API Natively to get tokens and user info  
                window.plugins.googleplus.login(
                        {
                            // TODO Get the WebClient from App settings
                          'webClientId': '*[googleclientid]*.apps.googleusercontent.com',
                          'offline': true
                        },
                        function (obj) {
                            $http.post(SERVER.url + '[MY BACKEND URL]' + '/google-oauth2/', {code: obj.idToken, servAuthCode: obj.serverAuthCode})
                              .success(function(data){
                                $auth.setToken(data.jwt_token);
                                /.. Do something ../
                              })
                              .error(function(data){
                                 console.log("There was an error" + JSON.stringify(data)); 
                              });                       
                        },
                        function (msg) {
                            // TODO Set Error states
                            console.error('error: ' + msg);
                        }
    
                    );
            }
    

    Summary

    • The app calls the Google plus API googleplus.login method (sending my webClientId)
    • I post the resulting idToken and serverAuthCode obtained from google after login to my Django backend.

    2. My backend methods

    URL

    My app/client hits the url(r'^[MY BACKEND URL]/(?P<backend>[\w-]+)/$', ObtainAuthToken.as_view(), ),

    View

    This calls the following view and functions:

    class ObtainAuthToken(APIView):
        permission_classes = (AllowAny,)
    
        def post(self, request, backend):
    
            data = request.data
            user_tokenID = data['code']
            server_auth_code = data['servAuthCode']
    
            if user_tokenID and server_auth_code and verify_google_user_token_ID(user_tokenID):
    
                # Get Google OAuth credentials for the verified GOOGLE user.
                credentials = settings.GOOGLE_FLOW.step2_exchange(server_auth_code)
    
                # Here we call PSA to authenticate like we would if we used PSA on server side.
                user = register_by_access_token(request, backend, token=credentials.access_token)
    
                # If user is active we get or create the REST token and send it back with user data
                if user and user.is_active:
                    # Generate JWT token for user and pass back to client
                    jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
                    jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
    
                    payload = jwt_payload_handler(user)
                    token = jwt_encode_handler(payload)
    
                    return JsonResponse({'id': user.id, 'name': user.username, 'jwt_token': token})
    
            return JsonResponse({'status':'false','error':'Bad Credentials, check the Access Token and/or the UID'},
                                status=403)
    
    
    def verify_google_user_token_ID(user_tokenID):
        try:
            google_http_request = google.auth.transport.requests.Request()
    
            idinfo = verify_token(user_tokenID, request=google_http_request,
                     audience=settings.SOCIAL_AUTH_GOOGLE_OAUTH2_FULL_KEY)
    
            # Or, if multiple clients access the backend server:
            if idinfo['aud'] not in [settings.GOOGLE_APP_ID_ANDROID, settings.GOOGLE_APP_ID_WEB]:
               raise crypt.AppIdentityError("Unrecognized client.")
    
            if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
                raise crypt.AppIdentityError("Wrong issuer.")
    
            return True
    
        except crypt.AppIdentityError as e:
            # Invalid token
            return False
    
    
    @psa('social:complete')
    def register_by_access_token(request, backend, token):
        backend = social_core.backends.google.GoogleOAuth2()
        user = backend.do_auth(access_token=token, backend=backend)
    
        if user:
            return user
        else:
            return None
    

    3. Back on the client

    My client then looks at the response and takes the returned JWT and loads it to memory with $auth.setToken(data.jwt_token);

    I think this works for now, but I still have to deal with token refresh and revocation etc.