Using Google auth in Javascript

This tutorial gives a detailed introduction about integrating the Google auth into a client-side web application

Author's image
Tamás Sallai
5 mins

Motivation

Using a third party login provider yields many benefits. You do not need to worry about all the sensitive user data and secure password storage, and you can integrate with the provider's other services too. You might also don't need any server side for managing users if you want that, as client-side libraries often support this use case too. Although you can implement the full OAuth handshake, for some use cases it is not necessary.

Implementing the login

For a login like this we generally want to implement two methods. The first is to check whether the user is currently logged in or not, and the second to actually do the login. In case of Google auth, we also need to load and initialize the client library.

For demonstration, I will use AngularJS and it's promise library, $q, as most of the scripts are asynchronous.

Loading and initializing the gapi client library

First, we need to include the script in the page. It uses the callback pattern, so we need to pass a function that will be called when the library is loaded. So first, we need to include the script at the bottom of the page:

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

The tricky part is that the client library is loaded asynchronously, we would want to use it before it happens. That means we need to implement a queue that stores the callbacks and makes sure that they will be called after the library is initialized. Google Analytics provides a nice way to work around this issue, and it is detailed in this post. Basically it initializes an array that other components can push their callbacks, and during the initialization it replaces it with an object thats push method is redefined, and lastly calls all existing callbacks. It can be implemented like this:

window.gapiCallbacks=[];
function gapiLoaded(){
	gapi.auth.init(function() {
		var GapiQueue = function () {
			this.push = function (callback) {
				setTimeout(callback, 0);
			};
		};
		var _old_gapiCallbacks = window.gapiCallbacks;
		window.gapiCallbacks = new GapiQueue();
		_old_gapiCallbacks.forEach(function (callback) {
			window.gapiCallbacks.push(callback);
		});
	});
}

Just make sure it is defined before the above script tag (so that the callback function exists). Also you should take care of loading additional client library interfaces you would like to use.

It can then be used like this:

gapiCallbacks.push(function () {
	// .. Do something with gapi, as it is defined and initialized
}

The good thing is that from now on, we don't need to worry about the asynchronicity of the client library, when the callback is run the client library will be up and running.

Checking the login

The next part is to check if the user is already logged in or not. This is a 3 step process:

  • We need to check if a token is already present and valid
  • If there is no valid token, we then need to construct a config object and call authorize
  • Then we need to see into the result whether or not the user is logged in

Checking the token

A token is either missing, in case there were no previous auth attempts, expired or valid. Currently the tokens have a 1 hour expiration time, and it has an expires_at field we can check. For safety, we should treat nearly expired tokens as expired ones and ask for a new one.

Let's define our checker method!

function isTokenNeedsRefresh(token){
	return !token || moment.duration(moment(token.expires_at*1000).valueOf()-moment().valueOf()).minutes()<10;
}

Note: For pain-free date handling I'm using the excellent moment library.

The config object

The gapi.auth.authorize needs a config object (reference) that we need to populate. There are 3 important components we need to provide:

  • client_id: This can be generated in the Google Developers Console for a project
  • scope: These are the required permissions we are asking the user. The full list can be found at the OAuth Playground. Generally you would want to include https://www.googleapis.com/auth/plus.me in order to be able to identify the user
  • immediate: If this is true, then the login popup will not show

Our config method will be like this:

function getConfig(immediate){
    return {
        'client_id': ...,
        'scope': ...,
        immediate: immediate
    }
}

Putting it all together

The last missing part is to actually put it all together and define a method that can be called:

checkLogin:function(){
    var deferred = $q.defer();

    gapiCallbacks.push(function () {
        if (isTokenNeedsRefresh(gapi.auth.getToken())) {
            gapi.auth.authorize(getConfig(true), function (authResult) {
                if (authResult && !authResult.error) {
                    deferred.resolve(gapi.auth.getToken().access_token);
                } else {
                    deferred.reject(authResult);
                }
            })
        } else {
            deferred.resolve(gapi.auth.getToken().access_token);
        }
    });

    return deferred.promise;
}

Calling this method will return a promise that will be resolved if the user is logged in and rejected otherwise. The success handler will also get the access token that we can use from there on.

We can then use the function like this:

gapiAuthService.checkLogin().then(function(){
    $scope.loggedIn=true;
},function(){
    $scope.loggedIn=false;
}).finally(function(){
    $scope.checkingLogin=false;
});

The login method

We can easily construct our login method using the above techniques. First, we do a silent check to see if the user is logged in, and if not, then try to log in with the immediate parameter set to false.

The full method would look like this:

login:function() {
    var deferred = $q.defer();

    this.checkLogin().then(function (accessToken) {
        deferred.resolve(accessToken);
    }, function () {
        gapi.auth.authorize(getConfig(false), function (authResult) {
            if (authResult && !authResult.error) {
                deferred.resolve(gapi.auth.getToken().access_token);
            } else {
                deferred.reject(authResult);
            }
        })
    });

    return deferred.promise;
}

And we can implement a click handler like this:

$scope.login=function(){
    gapiAuthService.login().then(function(){
        $scope.loggedIn=true;
    },function(authResult){
        $scope.loggedIn = false;
        console.err(authResult);
    })
};

Note: The login method should always be called from a click handler, otherwise the browser would block the popup

Conclusion

Integrating Google auth is somewhat tricky in some places, but after one gets the hang of it, it can be easily integrated and requires very little effort compared to a full blown authentication and user handling solution. Hopefully this article gives the necessary insight to help you get started. Also check out the sample project at GitHub.

May 12, 2015