Introduction
This is a step-by-step, test-driven tutorial for web developers in creating a Rails application that will allow users to sign-in with their Google Accounts. Here’s what you’ll end up with:
- A Rails application that allows users to login/out using their Google credentials
- Test coverage somewhere between mediocre and good (would love suggestions in the comments section)
- Version control using GIT
- Your application up and running on Heroku
- All your secret keys/passwords in your app kept secret (so you can push to GitHub, if desired)
Motivation
I spent the better part of two days implementing a Sign-in with Google button. I’m not proud it took me this long, but I really wanted to do it well. I’d implemented authentication systems previously with help from gems like Devise, but admittingly there were a few times where I’d be left scratching my head from Devise’s magic and I definitely let the magician do his thing, if you know what I mean. Not this time. I wanted to have a full understanding of the entire process, which for me meant:
- Test-Driven (unit tests and functional tests).
- No Devise. Users will login with their Google Account.
- A great dev environment. I don’t want to be creating users in the console.
- Minimalist. One model, one controller, one view and three routes.
I’ll try to bring together all the tutorials, GitHub docs, RailsCasts and StackOverflow Q&A’s that I used, throw in some TDD and provide explanations where required. You’ll end up with a Rails 4 application that will let users register, login and logout - absoultely nothing else.
Let’s get started!
Make sure you have these
For development, I’ll be using:
- Ruby 2.1.5
- Rails 4.2.0
- Git 2.2.1
You can check your versions in the terminal with:
You’ll also want to make sure you’ve installed and configured the Heroku Toolbelt and PostgreSQL which we’ll use as our database system. You should also have Rubygems, Bundler and RVM installed.
On a side note, I’m developing using with Sublime as my editor and strongly prefer iTerm2 over the Mac OX terminal.
The Plan
Allowing users to register and login to your application using their Google ID requires your application to have a conversation with Google. Greg Baugues at Twilio explains this process from the developers perspective:
- You set up an app within Google’s developer portal and tell it what APIs you’d like to use and Google gives you a Client ID and Client Secret (basically, a username and password).
- When a user signs into your app, your app redirects them to Google and passes along the client id/secret.
- Google asks the user permission to access their GMail data
- Google calls back to your app and sends a code which you can use, along with your credentials, to retrieve an access token with which you can make authenticated API calls.
For a more in-depth explanation on why this process takes place, see my previous post: Goodfellas, and a social explanation of 3rd Party Verification
Luckily, the OmniAuth gem will manage most of this entire procedure for us. We’re going to hit a few more bumps in the road as we go, but we’ll see that they’re pretty easy to get through.
Let’s get started!
Create your app!
Let’s get going with a new Rails application using postgres as our database: replace ‘googlelogin’ with your desired application name
We might as well initialize our repository as well:
We’re going to make a few modifcations here to get things working with OmniAuth, Google and Heroku. Let’s go ahead and do this.
Add the following to your Gemfile
:
Now we should be almost ready to deploy to Heroku. We’ll need to install our gems first, and commit our changes to Git:
Next, let’s push to Heroku:
If we were to open our production application by entering $ heroku open
in the terminal we’ll receive an error since we:
- Have no routes or views defined in our application
- Have not yet migrated a database to Heroku
Let’s fix this by adding a Sessions controller with an index action and afterwards migrate our database up to Heroku:
In your `config/routes.rb’ file, add the following route on line 2:
That should do it. Let’s commit our changes and migrate our database:
Hopefully you see the Sessions#index
message from our app/views/sessions/index.html.erb
file. First step is complete!
Building the User model
Before we build out our User model, what information exactly will we have access to? Looking at the omniauth-google-oauth2
documentation shows exactly the type of JSON response we can expect from Google:
There are a few things we’re most likely going to want to pull from this response. Specifically:
- The user ID (:uid)
- The user’s :first_name, :last_name, :email, :image
- The :token, :refresh_token, :expires_at attributes
We’ll leave the rest for now, let’s go ahead and generate a new User model with these attributes and migrate the database:
You may notice above that for the uid
and email
attributes we’re enforcing a uniqueness constraint in our database by using uid:string:uniq
instead of simply uid:string
. This effectively adds the following to our migration file for us:
Creating uniqueness validations in our model is not enough to ensure uniqueness in production. Derek Prior’s provides an excellent explanation in his article, The Perils of Uniqueness Validations, should you so desire. Unfortunately, this will create a new problem for us. When we created the User model, Rails automatically generated a few fixtures for us to use when we run our tests. You can see them in test/fixtures/user.yml
:
Fixtures can come in very handy and we’ll be using them a little later. However, you may notice that both fixtures are using the value MyString
for both their uid
and email
attributes. Since we just enforced a unique constraint on our database, these fixtures will throw an error on every test. For now, lets comment out these fixtures by changing our test/fixtures/user.yml
to the following:
Now we’re ready to migrate our database. In our terminal:
Validations
After generating our User model, Rails has kindly generated a test file that we’ll now make use of. Open up test/models/user_test.rb
and making use of the Shoulda gem we’ll create some failing tests. Add the following at the top of your UserTest
class:
Running rake test
(or simply rake
) should render many failures. Let’s add some validations to our User model to help these pass.
Run our tests again (rake
) and everything should be passing. Excellent.
User.find_or_create_from_google_callback
Now that we have some validations around our User model, let’s build a class helper method so that we can easily find or create a new User from the response we receive from Google’s servers. But first, our test (I’ll be combining two steps here):
Running these new tests should yield a failure and a few errors since we haven’t yet created the desired method. Let’s do that now. Add the following to your User class:
We should still have those two failures since our method is not yet creating or finding any users. Let’s address that now by updating the User method we created above:
Only one more test needed to pass. We’ll need to add some functionality to return a user if they have previously registered:
That should have all of our tests passing. Quickly taking stock, we now have:
- A User model defined with appropriate validations
- A nice class method for finding or initializing new Users
- Reasonable test coverage
Things should start moving quicly now!
Google and ngrok
Before we get going with Google and ngrok you’re going to need your Heroku URL. You can find ths by entering heroku open
in the terminal and copying the URL from your browser’s address bar. It should look something like: https://guarded-coast-30.herokuapp.com. I’ll refer to this as your Heroku URL, and to the part before .herokuapp.com, guarded-coast-30 in this case, as your heroku sub-domain.
Register with ngrok
Ngrok is an excellent free service that will allow us to simulate a User sign-up while in development mode on our local machine. Effectively, it opens a port on our computer and tunnels traffic through the ngrok URL to our machine. Cool eh?!
Head on over to ngrok and sign-up. Although registering is optional, we’re going to need it when we define a custom sub-domain.
Next, download ngrok and unzip into your Rails application directory. Later, you may wish to create a new directory for ngrok but for now this will do fine.
Register your app with Google
Time to head on over to the Google Developer’s Console. Once you arrive, you’ll want to:
- Click Create Project. Give your app a name of Google Login. You do not need to set the project id.
- Click on your project, click APIs and auth, APIs and enable Contacts API and Google+ API (you can do what you’d like with the others).
- Click Credentials and Create new Client ID. Check Web Application on the dialog box.
- Under Redirect URIs enter the following being sure to replace guarded-coast-30 with your Heroku sub-domain:
https://guarded-coast-30.herokuapp.com/auth/google_oauth2/callback
https://guarded-coast-30.ngrok.com/auth/google_oauth2/callback
- Under Javascript origins, enter the following again using your Heroku sub-domain:
https://guarded-coast-30.herokuapp.com/
https://guarded-coast-30.ngrok.com
- Copy the CLIENT_ID and CLIENT_SECRET attributes that Google will provide. Head back to your Rails project and go ahead and paste these in your
config/application.yml
file:
Note: This is where our Figaro gem works its magic. The Figaro gem will help keep information in this file safe. You’re definitely going to want to ensure that your CLIENT_ID and CLIENT_SECRET remain secret through the lifetime of your application.
Finally, note that Google is going to need upwards of 10 minutes to roll out the new redirect URLS your provided. In the mean time, we’ll get up and running with OmniAuth.
Configuring OmniAuth
We’re going to add a few initializers to our config/initializers
directory to get up and running with OmniAuth (and Figaro). First, let’s create the following file:
This will ensure that we get a build error if we try to deploy our application without our CLIENT_ID
or CLIENT_SECRET
. Super handy since our application won’t work without these. Next, add the following file:
Note that we will need to create the above action (oauth_failure
) to catch failed login attempts
Setup SessionController tests
Again, we’ll take on a few steps at a time. I’m going to assume you’re now somewhat familiar with the OAuth 2.0 process. Here are a few important things to remember:
- We provided Google with the callback URL
/auth/google_oauth2/callback
. After our users sign-in with Google they will be redirected to this path and OmniAuth will takeover and do it’s magic (see the magic in action in this post). - Upon completion, OmniAuth will populate our
request.env['omniauth.auth']
object with the user’s information. In our functional controller tests we can simulate these parameters by populating the@request.env['omniauth.auth']
instance variable - We’re not going to test the entire OmniAuth process. OmniAuth is a well tested project, we’re going to forego the OAuth2.0 handshake and parachute in at the end by populating the
request.env['omniauth.auth']
hash as OmniAuth would upon completion.
Let’s go ahead and test that our application:
- Passes control of the root url (“/”) to the “sessions#index” action
- Throws an exception if the “sessions#create” action is called without a
request.env['omniauth.auth']
object defined - Creates a new user if
request.env['omniauth.auth']
is defined AND that user does not yet exist - Returns an existing user if
request.env['omniauth.auth']
is defined AND that user does exist - Sets a session variable,
session['current_user_id']
if a user signs-in or is created (we’ll later use this to quickly retrieve users) - Destroys the user’s session (by setting
session['current_user_id']
) tonil
and redirects the user to ourroot_path
upon clickingsign-out
- Redirects to root_path after completion of
create
anddestroy
actions
Those are quite a few tests. To help us out we’ll add a few fixtures in our test/fixtures/users.yml
file and then our tests in test/controllers/sessions_controller_test.rb
. Feel free to replace the contents of this file with code below:
Time for some tests:
Running these tests in the terminal should invoke quite a few errors and failures. Lets get these tests passing.
Setup routes
Jump into your config/routes.rb
. Change your routes file to look like this:
Worthy of some explanation, our last get
command above is what will be catching the Google callback; this is the redirect URL we provided Google. As a reminder, here’s our process when a user sign-in occurs:
- User is directed to
/auth/google_oauth2
where OmniAuth will again redirect them to Google (https://accounts.google.com/o/oauth2/auth?...
) for sign-in. - Upon granting access and completing the sign-in process, the user will be internally directed to our
create
action inside ourSessionsController
along with the large Hash of information we tested against eariler.
A few helper methods in ApplicationController
These will be available to all of our controllers in our application. Feel free to replace your ApplicationController with the following:
Setting up our Sessions Controller
Time to add some functionality to our Sessions Controller. Feel free to replace the contents of your SessionController with the following:
Let’s explain a little of what’s happening here:
- The create action is locating or creating a new user, adding a session variable (
current_user_id
) and finally redirecting the user to the root_url. - The destroy action simplly removes the session
current_user_id
session variable - The google_response private method is just a helper to check whether our OmniAuth object is present in the
response
object
Setup a view
Lets add some simple code in your index.html.erb
view:
Here we will display a sign-in or sign-out button based on whether we recognize the session token of the user. Note that the path /auth/google_oauth2
is automatically configured and handled for us by OmniAuth; this is where the sign-in process will begin.
Up and running!
Hopefully your tests are all passing. You should now be able to fire up your local server and take a run through your application. We’ll fire-up ngrok using your Heroku sub-domain:
In another terminal window:
Open your web browser and point it to the URL supplied by ngrok. You should now have a fully-functional Google authentication system!
Touch up the edges
Secure user authentication is largely predicated on having encrypted requests using SSL. Let’s enforce this behaviour in Rails by changing the following settings:
Just above the end
tag:
Uncomment the following line:
Push to Heroku
Now point your browser to your live application and test it out:
Conclusion
I hope you found this tutorial helpful. Please email me with any comments.
Additional Resources
If you’re looking to gain a more rich understanding of this tutorial, take a read through of the following:
- OmniAuth Documentation - Will gives you a better understanding of the process and provides a list of ‘Strategies’ (ie sign-in with Twitter, instead of Google).
- OmniAuth Google gem - Provides details for extending functionality beyond what we’ve done here.
- Google OAuth 2.0 API Documentation - Very useful for adding features involving Gmail, Contacts, Maps, etc.
Refreshing tokens
Although I won’t officially support the following, you may wish to implement some functionality in your SessionsContoller to refresh access tokens. This should do the trick: