Twitter Authentication in Sinatra

Tweet

sinatra_twitterAt the recent Summit Awesome Hackathon in Manchester, my team were working on a web app that updated a user’s bio on various social networks in one place. This meant getting down and dirty with various social network APIs (not a pretty thing I can tell you). I thought I’d do a quick write up on how you can authenticate users in a Sinatra app by allowing them to sign in with Twitter.

Set Up The Site

To demonstrate this working, first of all, let’s set up a simple app that has two urls – public and private. Save the following code in a file called ‘main.rb':

require 'sinatra'

helpers do
  def admin?
    true
  end
end

get '/public' do
  "This is the public page - everybody is welcome!"
end

get '/private' do
  halt(401,'Not Authorized') unless admin?
  "This is the private page - members only"
end

This sets up a helper method called admin? that we can use to check if a user is logged in. It’s set to true by default to enable us to check that things work. We’ve also set up two route handlers. The first is for the route ‘/public’, which shows a message to everybody who visits.

The second route is a private page that will only show the message if the admin? helper returns true. If not, then we use Sinatra’s halt method to stop the app in it’s tracks and show a ‘Not Authorized’ message. To check that this happens, change the admin? helper method to false, restart the server (ruby main.rb) and go to ‘http://localhost:4567/private’.

Using Sessions to Log in and Out

We can add some route handlers to allow the user to log in and out and use sessions to keep track of whether a user is logged in or not. Change the code in main.rb to the following:

require 'sinatra'

configure do
  enable :sessions
end

helpers do
  def admin?
    session[:admin]
  end
end

get '/public' do
  "This is the public page - everybody is welcome!"
end

get '/private' do
  halt(401,'Not Authorized') unless admin?
  "This is the private page - members only"
end

get '/login' do
  session[:admin] = true
  "You are now logged in"
end

get '/logout' do
  session[:admin] = nil
  "You are now logged out"
end

Now restart the server and try going to ‘http://localhost:4567/login’, then go to ‘http://localhost:4567/private’, and you should be able to see the page. Go to ‘http://localhost:4567/logout’, then try ‘http://localhost:4567/private’ again, and you should get the ‘unauthorized’ message.

Great – we have a working log in and log out system. The next step is to use Twitter to authenticate the user instead of just allowing anybody to log in using the login url.

Register Your App with Twitter

Before you can start using Twitter to sign in, you need to register your app with Twitter. This is easily done – just head over to https://dev.twitter.com/apps and log in using your Twitter credentials. Then click on the ‘create a new application’ button and fill in the form. You can pick any name for your application (as long as it doesn’t contain the word ‘Twitter’) and can use any website as a placeholder until you get an actual domain name for your application. For example, I used http://www.sitepoint.com.

In the callback URL field, you need to add /auth/twitter/callback to whichever URL you used in the website field. For example, I used http://www.sitepoint.com/auth/twitter/callback. You will then be taken to a page that contains the OAuth settings for your application. Make a note of the ‘consumer key’ and ‘consumer secret’ as we will be using them in our app.

Ominauth

The library we are going to use to authenticate with is called ‘omniauth’. As the name suggests, it can actually be used to authenticate with a variety of services such as GitHub and Facebook, amongst others. It is split into different modules based on the service being used, so in this case we need to install the omniauth-twitter gem:

$ gem install omniauth-twitter

Next, you need to set up ominauth which can be done at the top of main.rb:

require 'sinatra'
require 'omniauth-twitter'

use OmniAuth::Builder do
  provider :twitter, 'put your consumer key here', 'put your consumer secret here'
end

Sign in with Twitter

We need to create the sign in with Twitter functionality. First of all, we’ll edit our ‘/login’ route handler so that it redirects to the URL that has automatically been set up by Omniauth:

get '/login' do
  redirect to("/auth/twitter")
end

Note that if you were using GitHub, the URL would be “/auth/github”. This will take the user into Twitter and ask them if it’s okay to allow your app to access Twitter.

If sign in is successful, it triggers a callback to ‘/auth/twitter/callback’. All we need to do now is create a route handler to deal with what we want to happen when log in is successful. This is just the same code that we used earlier in the login route handler:

get '/auth/twitter/callback' do
  env['omniauth.auth'] ? session[:admin] = true : halt(401,'Not Authorized')
  "You are now logged in"
end

There is also a route for handling an unsuccessful login, which we need to create a handler for:

get '/auth/failure' do
  params[:message]
end

This displays the message that is sent back from Twitter as to why the authentication failed.

Now we have a fully functional login system that uses Twitter for authentication. This is one of the things I love about Sinatra – we’ve whipped up a fully functional login system that uses Twitter for authentication in around 40 lines of code!

Using the Twitter Credentials

Now that we have somebody logged in with Twitter, what can we do?

When a user logs in with Twitter, you actually gain access to a lot of information about them. This is stored in a hash called env['omniauth.auth']. To see what this looks like, change the callback route handler to the following:

get '/auth/twitter/callback' do
  session[:admin] = true
  env['omniauth.auth']
end

Now try loggin in using Twitter and you will see all the information from your Twitter account that is held in the hash.

For example, you could welcome a user to your site like so:

get '/auth/twitter/callback' do
  session[:admin] = true
  "<h1>Hi #{env['omniauth.auth']['info']['name']}!</h1><img src='#{env['omniauth.auth']['info']['image']}'>"
end

This will give them a personalized welcome and show their Twitter profile picture. If you want to continue using these details then you could save them in the session hash, like so:

get '/auth/twitter/callback' do
  session[:admin] = true
  session[:username] = env['omniauth.auth']['info']['name']
  "<h1>Hi #{session[:username]}!</h1>"
end

You could also use this as a way of authenticating users that you have stored in a database. Twitter provides all of its users with a unique id that can be a property of the User model. You could then look the user like so (this example uses the Mongoid syntax, but you should be able to make it work for your ORM of choice:

@user = User.where(twitter_id: env['omniauth.auth']['uid']

Updating Twitter

Once you’ve got a user authenticated with Twitter, you can theoretcially go on and start manipulating their Twitter account. What you are allowed to do depends on what you access the user grants to the application (you can select whether you want read only or read and write access for your application in the settings tab on the Twitter Developer site). Omniauth doesn’t actually let you manipulate a twitter account though. To do this you will need to install the twitter gem. There’s not enough room to go into using that now, maybe in a future post … but you might want to have a play around with it yourself in the meantime.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://Inprogress... eskadah

    Hello Darren,

    thanks for the awesome job you are doing explaining Sinatra to newbies like me. I just got your Jump Start book and I will soon start working on it. I just looked through this tutorial and I have a quick question: If Sinatra handles the ‘/auth/twitter/callback’ route like any other route, then in this set-up, it seems possible to by-pass the twitter authentication system completely and just go to the log-in confirmation page since session[:admin] is set to ‘true’ by the handler. Is there any way to protect the route in Sinatra or obfuscate it with a token or have omniauth protect the established route intrinsically?

    P.S: I haven’t actually coded this tutorial to see it for myself just wanted to quickly ask this.

    • http://daz4126.com/ Darren Jones

      Hi eskadah!

      Thanks for your kind words! I’m glad you’ve found my stuff useful.

      You’re absolutely right! I was too busy thinking about how to implement the Twitter Auth, I missed the obvious flaw! The best way to stop this happening would be to check if the env['omniauth.auth'] has been set. You could even query it to check if it is a particular user if you wanted to, but for now I’ll go with this:

      env['omniauth.auth'] ? session[:admin] = true : halt(401,'Not Authorized')

      I’ve updated the post too.

      Thanks for spotting this and I hope you enjoy the book!

      DAZ