Social Sign-in: No, You Can’t Just Stick a Facebook Button on It
Recently, as part of our Nexus release, we launched “Social Sign-in”; a feature that allows anyone to sign up for or log in to an import·io account using one of four external social services (LinkedIn, Facebook, Google+, Github).
When we first proposed the feature, one of our team – now rather infamously – said, “Why can’t you just put a Facebook button on it?” In this day and age, with social accounts being used for authentication all over the web, it can seem trivial to implement. And actually, in some cases, it can be trivial – but to do it properly takes some work. So, I wanted to take a few minutes to outline how we went about the implementation, and why we think this provides a superior user experience.
The net result of all this work is you can now create import·io accounts using LinkedIn, Github, Facebook and Google+ accounts, and log in to existing accounts by linking them from their My Account page.
All four of the external services we use provide OAuth 2 endpoints for authentication. This means we can make use of the OAuth 2 standard (more on this later) to help us reduce the amount of code we write, while still making sure that we support multiple services.
OAuth works on a pretty simple premise. Instead of asking users for their credentials to an external service (such as username and password or their API key), you send the user to the service to confirm that they allow the access to the account, and then that service provides authentication details back to you. (Note here that I’m describing the OAuth server flow, some providers also give you a client flow, which I won’t delve in to).
The first step of OAuth is to set up an “app” on the external service (such as a Facebook App, a Google project, and the equivalents on Linkedin, Github etc). This yields a credentials pair consisting of an application ID and an application secret key. The application ID can (and will) be public, but you must guard your application secret (or just “secret”) carefully so as to prevent anyone else masquerading as your application.
The next step is to redirect the user to the external service’s website when they ask to authenticate with that external service. This redirect sends the user to a URL in their browser, which goes to the service you are connecting them with. This URL contains important information, primarily your application ID but also other parameters such as a redirect URL to lead the user back to your application, a state string to prevent cross-site forgery attacks, any requested OAuth scopes (which allow you to ask for only specific permissions on the external service, such as read-only profile access) and more.
This URL on the external service then typically shows the user some UI so they can confirm their choice and check the permissions. The implementation of this varies somewhat between providers. For example, most providers – if the user has remained logged in on the specific machine they are going through OAuth on and have previously authorised the connection – will not show the authorisation screen again. LinkedIn is the exception to this rule presumably following some high-profile security breaches.
Once the user has accepted the permissions, they are redirected back to a URL that you configure for your application. This redirect, in addition to the URL you specify, contains additional data that your app requires from the external service. These include, as an example, an access code (or “code”), a timestamp for when that code expires, and the state string sent to them in the initial request.
The final step of the OAuth procedure is to exchange the code returned in the aforementioned redirect for an actual OAuth token. This is a server-to-server API call where you send your application ID, secret, and the code; the external service then returns back an access token. This access token acts as the “passphrase” (so to speak) that you can now use to make API calls to the external service on the user’s behalf.
Now that we have a grip on how OAuth 2 works, it’s time to talk about the standard. This is the OAuth 2 standard specification: RFC 6749. What follows is a list of how the four providers we worked with and mentioned above deviate from the above standard (and each other). Note that some of the deviations are only options, but they are mentioned because the service’s documentation highlights the non-standard route rather than the standard one.
For example, Github and Facebook require scopes in the initial redirect to the external service to be separated by commas, where everyone else (and the standard) specifies spaces.
For the final API call to get the access token, Facebook requires an HTTP GET request rather than an HTTP POST. LinkedIn goes a step further by requiring HTTP POST (as the spec says) but requiring the parameters to be specified as part of the URL rather than the request body, contrary to not only the standard, but also conventional logic.
Additionally, every service returns the time to expiry of access tokens from the above request as an “expires_in” parameter, except Facebook which lists it as “expiry”.
And finally, when requesting access with Google+, you must specify an additional “offline” flag if you actually want to use the credentials you obtain once the user has logged out of Google on their current machine.
In addition to all of this, once you have the OAuth 2 access token, all of the services offer completely different APIs for getting hold of data about the user. For this, in the end, we resorted to writing custom implementations for each provider. Github was especially bad in that we couldn’t actually get all the data we wanted in one request, so we had to do 2 in parallel and wait for them both to return with data.
Implementing OAuth 2 (or “social sign-in” as we like to call it) also required some reasonably important changes to our websites and apps.
First of all, we have pivoted away from asking from usernames to log people in, preferring e-mail addresses. This is for a number of reasons, but primarily we found it was far less confusing for new users and made more sense in the context of the whole login process.
We also have to be able to handle the myriad of error conditions that have been introduced with the arrival of social sign-in. For example, how do we handle the situation that we can’t log someone in because the account they chose is not linked to an import·io account already, but we can’t sign them up because the e-mail address (which we require to be unique among accounts) is already owned by another account? One does not simply link two accounts together, and we had to provide a useful and supportive UI for this condition (QA had a lot of fun with this one).
Conversely, what do we do when a user tries to link a social account to an existing account, but it has already been linked with another import·io account? What do we do if the external service returns an error? How can we plan for them being down (this actually happened during the product review when Github was having issues)? There were a shocking number of UI edge cases that need to be catered for so that we didn’t leave users stuck during one of the most important interactions they have with our website.
So, was it worth it?
Implementing social s
ign-in was many things – challenging, hugely entertaining, and complex. We also hope that all the effort we put into making the APIs bulletproof and UX complete and well-thought-out will make it easier for people to get on board with (and come back to) import·io with the minimum amount of friction. We have virtually removed the barrier of forgetting your import·io details if you haven’t been around in a while, meaning that users should be able to leverage our technology without credentials getting in the way.
However, the next version of OAuth will have to do a lot of work to make sure it doesn’t end up with either of the mistakes that OAuth 1 and OAuth 2 made. Version 1 was far too complicated to realistically implement at a large scale, and OAuth 2 has become so sparsely defined that people can get away with implementations which require a ton of special cases on the developer’s part (all of the special cases in our social sign-in code come with a “Blame” note for which service is responsible for the special condition and many extra unit tests).
Additionally, adoption is an issue: we do not offer Twitter login or signup because they currently do not support the OAuth 2 interface, which is something we would like to see before we can consider supporting it.
Turn the web into data for free
Create your own datasets in minutes, no coding required
Powerful data extraction platform
Point and click interface
Export your data in any format
Unlimited queries and APIs