As you may remember from my introduction post one of the projects that I'm working on at the moment is SRC:CLRs intranet, dubbed iono.
I haven't built iono from scratch, the base code was hacked out by my fellow engineers in a weekend flat, so as you can imagine there are a few issues I am finding trying to pick up where they left off.
One of the first things that I was tasked to do with iono was to upgrade the login system to only allow users to sign in with @sourceclear.com emails through Google Authentication. In February, 2014 the Open ID Foundation announced the OpenID Connect standard and that very same day, Google announced that they would be embracing the change over the next year and slowly deprecating older sign in methods, including the OpenID2.0 standard, the predecessor to the new standard.
Now, when iono had been built, the functionality I was tasked to upgrade certainly existed however there were just 2 issues with it:
-
The passport-google npm package that iono was using to perform the Google authentication was out of date and only uses OpenID 2.0 which will no longer be supported as of April, 2015.
-
In order to check that the email used to login was an @sourceclear.com email, a call to the userinfo.email endpoint was made. Support for any userinfo calls will be dropped in October, 2015.
This clearly wouldn't do. I had to modify the way these 2 things were happening in order for iono to be used for many years to come.
Firstly I addressed the outdated package.
It was clear from the number of open GitHub issues, pull requests and the lack of activity on the packages GitHub page, that the package wasn't going to be updated. In an open ticket regarding upgrading the package to suit the new authentication GitHub user @ilkkao had posted a link to a blog post about the process he went through to switch to another package passport-google-oauth. Not thinking twice I followed the instructions and switched to the new package. I soon realized, that this package would not solve my problems. Looking into the oauth2.js file on line 72 you'll find:
....
Strategy.prototype.userProfile = function(accessToken, done) {
this._oauth2.get('https://www.googleapis.com/oauth2/v1/userinfo',
accessToken, function (err, body, res) {
if (err) { return done(new InternalOAuthError('failed to fetch user profile', err)); }
....
That request is calling the userinfo endpoint mentioned above in issue 2 so I went hunting yet again for an answer.
The next package that I endeavored to test out was github user @sqrrrl's passport-google-plus package. After putting it into my code, I went to test it on my localhost only to find an error. The error message before me said that Google+ was not enabled for my organization.
In an attempt to avoid needing to have Google+ re-enabled, I switched to yet another npm package. This time it was github user @mstade's passport-google-oauth2 package. I still wasn't able to allow a user to login, I needed Google+ enabled. Once Google+ had been enabled, I was able to have a user login by setting the scope to https://www.googleapis.com/auth/plus.login
and the information of their profile returned, onto issue 2!
Now that I had the user's profile information, you would think the second issue would be a breeze right? Not quite, the information that I was returned was as follows (minus a few pieces that I exchanged for smiley faces for the sake of both privacy and readability)
{
"provider": "google",
"id": ":)",
"displayName": "Vanessa Henderson",
"name": {
"familyName": "Henderson",
"givenName": "Vanessa"
},
"isPerson": true,
"isPlusUser": true,
"language": "en",
"gender": "female",
"photos": [
{
"value": ":)",
"type": "default"
}
],
"_raw": ":)",
"_json": {
"kind": "plus#person",
"etag": "\":)"",
"gender": "female",
"objectType": "person",
"id": ":)",
"displayName": "Vanessa Henderson",
"name": {
"familyName": "Henderson",
"givenName": "Vanessa"
},
"url": ":)",
"image": {
"url": ":)",
"isDefault": true
},
"isPlusUser": true,
"language": "en",
"circledByCount": 0,
"verified": false
}
}
Do you notice anything missing? If you said the email you are correct. Nowhere in the profile given back to me by the package was the email which was going to make the second half of my task near impossible! I found this useful section of the Google+ Migrating documentation, which says among other things to "Replace the deprecated userinfo endpoint with people.get". Naturally I then went to the people.get documentation and could not find the information that I was looking for. No where on this page does it say what I needed to have as my scope or endpoint in order to get the result that I wanted: the users email.
After a while of trying to make a scope of https://www.googleapis.com/plus/v1/people/me
to work (which was to no avail), I once again started hunting in the Google Documentation where I finally found my answer. Under the Google+ Platform documentation > API Reference > HTTP API > Authorizing API Requests I found my answer. Rather than using https://www.googleapis.com/auth/plus.login
or https://www.googleapis.com/plus/v1/people/me
as the scope like I had previously thought, I simply needed to have my scope as https://www.googleapis.com/auth/plus.profile.emails.read
.
Now, when a user logs in, the following is returned:
{
"provider": "google",
"id": ":)",
"displayName": "Vanessa Henderson",
"name": {
"familyName": "Henderson",
"givenName": "Vanessa"
},
"isPerson": true,
"isPlusUser": true,
"emails": [
{
"value": ":)@sourceclear.com",
"type": "account"
}
],
"email": ":)@sourceclear.com",
"gender": "female",
"photos": [
{
"value": ":)",
"type": "default"
}
],
"_raw": ":)",
"_json": {
"kind": "plus#person",
"etag": "\":)"",
"gender": "female",
"emails": [
{
"value": ":)@sourceclear.com",
"type": "account"
}
],
"objectType": "person",
"id": ":)",
"displayName": "Vanessa Henderson",
"name": {
"familyName": "Henderson",
"givenName": "Vanessa"
},
"url": ":)",
"image": {
"url": ":)",
"isDefault": true
},
"isPlusUser": true,
"circledByCount": 0,
"verified": false,
"domain": "sourceclear.com"
}
}
With this information I was then able to ensure the user was logging in with an @sourceclear.com email in 1 of 2 ways, either by using the domain value or getting the emailed stored at emails[0] and checking that it contained 'sourceclear.com' in it. I chose the former method; using the email address stored at emails[0]. Firstly, I pulled out the 'emails' property from the JSON response and added it to my user object as the email value.
if (profile.hasOwnProperty('emails')) {
user.email = profile.emails[0].value;
}
Now that I had this value saved, I used the search method to check if the email returned contained 'sourceclear.com'. One of the end goals for iono is to make the platform open source and available to anyone that wants to use it, for this reason, I created a config file, whitelist.js, where the 'sourceclear.com' is being read from. In case you are curious, this is what my config file looks like:
module.exports.whitelist={
email: 'sourceclear.com'
};
I chose to do it in this way so that when repurposing the product, the new user does not have to hunt through the code for this value and can change it easily from the config file. It also allows this value to be used in other areas of the code if needed. Iono is a Node.js project built using the sails.js framework so in order to call my email value in whitelist.js, stored in the 'config' directory in the main src folder of iono, the sails.config.whitelist.email chain was used. If the search function does not find any results, iono will throw an error, as demonstrated by the following code:
if (user.email.toLowerCase().search(sails.config.whitelist.email) === -1) {
return next(new Error('That email was not associated with SourceClear.com'));
}
So, all in all, the process of upgrading the Google Authentication on iono, our company intranet in the making, was an interesting project to work on. It led to me exploring the code behind some of the more popular passport npm packages, commenting on GitHub tickets where others were having the same issues as me, banging my head against my desk a few times and ultimately it led to having a login system that will be compatible for the foreseeable future!
Keep an eye out for my next blog where we will explore the concept of scopes and endpoints, what is the difference and how we use them!