Friday, July 5, 2013

Initializing An AngularJS App with Oauth2 and Google Endpoints (Hackish)

So - I originally wrote this back around July 4th but wasn't really happy with it (I ended up writing it down in Florida, away from my actual work computer).  Recently, our last Android app (CoinPrice) got shut down over a cease and desist (which I was stressing about and took some time to figure that one out) and Exposure101 as a whole kind of fell apart.  That being said, I still love technology and plan to update this blog occasionally whenever I get to play with something new.

So - back when I wrote this I had to do something I considered to be kind of kludge to get an AngularJS app initialized with your Google account.  The snag I hit in doing this is as follows: Google provides a demo of bootstrap your app using the endpoints and oauth2 JavaScript libraries - but it doesn't show how to integrate your login status into the AngularJS framework (e.g. putting it in a variable on your controller's scope such that you can hide or display certain things on your page).  Now, I'm a server side Java person, I've been really digging being able to write client stuff recently with AngularJS & Bootstrap, but I am by no means a JavaScript expert.  I started reading Effective JavaScript and have been brushing up on it for work, but there may be better ways out there of doing this.

So - here's what I ended up doing:

Firstly, my goal here was to have your data show up immediately if you were already logged into your account - otherwise your data should show up after you login.

In order to bootstrap Google's Oauth library you have to have the following line in your index.html - when the script has finished loading it will run the init function you give it in the onload parameter.

<script src="https://apis.google.com/js/client.js?onload=init"></script>

Now you actually need to implement the init method somewhere in your JavaScript.  I put mine in a script tag in my index.html as shown below:

<!-- google user authentication -->
<script>
var init = function() {
  var $injector = angular.injector(['ng', 'exposure101.services', 'exposure101.lifelogger']);
  var InitializationService = $injector.get('InitializationService');
  var AuthenticationService = $injector.get('AuthenticationService');
  var EventService = $injector.get('EventService');
  InitializationService.initialize().then(function() {
    AuthenticationService.login(true).then(function(authenticationModel) {
      var $scope = angular.element('body').scope();
      $scope.$apply(function() {
        $scope.authenticationModel = authenticationModel;
      });
      EventService.loadEvents().then(function(events) {
        $scope.$apply(function() {
          $scope.events = events;
        });
      });
    });
  });
};
</script>
<script src="https://apis.google.com/js/client.js?onload=init"></script>

Now there's some stuff here you need to understand.

  • Apparently on every DOM element in Angular there exists an ng-scope that you have access to - you can get access to this scope by using Angular's element selector and calling scope() on it (When you would do this outside of this example, I'm not really sure, but you do have the option available to you).  
  • Services/Factories are normally singleton instances - you can persist state across Controllers by keeping the state in the Service/Factory objects and injecting them into your Controllers.   Normally, using Angular's Dependency Injection system, you only have to worry about single instances of your Services/Factories.  Now that this is drilled in, when you use angular.injector ($injector.get(...) in our case) it will create a new instance of the Service/Factory, this instance is not the one that will be injected into your controller (by default) through Angular's Dependency Injection system.  So - be careful with these Services/Factories and don't persist anything in them you plan to use in your controller (think of them almost like Stateless Session beans)
Now - all that being said, here's the rest of the code starting with the Authentication Model:

angular.module('exposure101.models')
.factory('AuthenticationModel', function() {
  'use strict';

  var authenticationModel = {
    isLoggedIn: false,
    authenticationToken: {}
  };

  return authenticationModel;
});


Here's The Authentication Service:

angular.module('exposure101.services')
.service('AuthenticationService', function($q, $rootScope, AuthenticationModel) {
  'use strict';

  var authenticationToken = {};
  var deferred = {}; // hackity hack hack

  this.login = function(initialLogin) {
    deferred = $q.defer();
    doLogin(initialLogin);
    return deferred.promise;
  };

  var doLogin = function(mode) {
    var opts = {
      // localhost client id (make sure this is set to port 9000 in google api console)
      client_id: 'your_client_id.apps.googleusercontent.com',
      scope: 'https://www.googleapis.com/auth/userinfo.email',
      immediate: mode,
      response_type: 'token id_token'
    };
    gapi.auth.authorize(opts, handleLogin);
  };

  var handleLogin = function() {
    gapi.client.oauth2.userinfo.get().execute(function(response) {
      if (!response.code) {
        authenticationToken = gapi.auth.getToken();
        authenticationToken.access_token = authenticationToken.id_token;
        AuthenticationModel.isLoggedIn = true;
        AuthenticationModel.authenticationToken = authenticationToken;
        $rootScope.$apply(function() {
          deferred.resolve(AuthenticationModel);
        });
      }
    });
  };
});


Here's The Initialization Service:

angular.module('exposure101.services')
.service('InitializationService', function($q, $rootScope) {
  'use strict';

  this.initialize = function() {
    var deferred = $q.defer();
    var apisToLoad = 2;

    var loginCallback = function() {
      if (--apisToLoad === 0) {
        $rootScope.$apply(function() {
          // console.log('finished loading up client libraries - should be resolving');
          deferred.resolve();
        });
      }
    };

    gapi.client.load('events', 'v1', loginCallback, 'http://localhost:8888/_ah/api');
    gapi.client.load('oauth2', 'v2', loginCallback);

    return deferred.promise;
  };
});

And lastly here's the controller:

angular.module('exposure101.lifelogger')
.controller('LifeLoggerController', function($scope, AuthenticationModel, AuthenticationService, EventService) {
  'use strict';

  $scope.authenticationModel = AuthenticationModel;

  $scope.categories = [];
  $scope.events = [];
  $scope.labels = [];

  ...

  $scope.login = function() {
    AuthenticationService.login(false).then(function(authenticationModel) {
      $scope.authenticationModel = authenticationModel;
      EventService.loadEvents().then(function(events) {
        $scope.events = events;
      });
    });
  };
});


Now - recently I got a comment asking how this was different than the directive on this site: https://github.com/sirkitree/angular-directive.g-signin/blob/master/google-plus-signin.js.  This directive is actually a solid implementation of what I was trying to do - instead of going through all the setup described in this blog this actually works very well (and with a nicer interface including a more standardized Google login button).  I'm going to modify this directive a bit to make it more to my liking and I'll post the code in a new article on this blog.

4 comments:

  1. Hi Sean,
    I'm learning AngularJS from a non-web background. Thought I'd give your example here a go but I was not able to do anything constructive.

    Please compare your approach here to the following by sirkitree/Bitner.

    https://github.com/sirkitree/angular-directive.g-signin#example

    As a newbie to the AngularJS world, I felt it interesting enough to comment on.

    ReplyDelete
  2. Hi Dallas,

    Thanks for the feedback - I'm surprised it wasn't working for you (I'm going to update this since I figured out how to use the $q with it and not the callbacks) - What I was trying to do was have it so that as soon as your page loaded it would use your Google Account information without having to have you manually log in - I'll pull down the directive today or tomorrow and give it a try, it may be a better way of doing it - I'm also going to clean up my example here.

    ReplyDelete
  3. Doesn't work for me either.. Is response_type: 'token id_token' supposed to be there? What you can response type is the Secret? Thanks Sean!

    ReplyDelete