saush

Third party user authentication with Ruby in a just few lines of code

Posted in DataMapper, Ruby, Sinatra by sausheong on April 25, 2009

Some time ago (8 years to be exact) when I was still chest-deep in Java and working in elipva, I wrote Tapestry, a centralized user management system and also an article in Javaworld describing it. That particular project is long gone (it got merged into elipva’s Zephyr product and disappeared as a standalone system) but the problems I wrote about are really quite universal.

To summarize, the main problem that Tapestry tried to solve was that software applications that serve more than 1 user at a time usually require its users to sign in. To do that, most of the time, we create a user management module. Within this user management module we need to provide these 2 major services:

As you can imagine, authentication is usually simpler than access control. Tapestry’s approach was to externalize these services into a configurable standalone system that provides both services. While I never really completed Tapestry, there are many third-party providers of these services in the market now. In this blog post I will talk about authentication and explain some ways to easily integrate your Ruby application with these third-party authentication providers.

But before that, some background. In the open era, 2 different open technologies have surfaced to solve the the issues of authentication and access control separately. OpenID is a popular digital identity used for authentication, originally developed for LiveJournal in 2005. It’s supported by many large companies including Yahoo, Google, PayPal, IBM, Microsoft and so on. As of Nov 2008, there are over 500 million OpenIDs on the Internet and about 27,000 sites have integrated it. OAuth is a newer protocol used for access control, first drafted in 2007. It allows a user to grant access to information on one site (provider) to another site (consumer) without sharing his identity. OAuth is strongly supported by Google and Yahoo, and is used also in Twitter.

At the same time, many larger organizations started to tout their proprietary own authentication and access control mechanism. Microsoft started its Passport service in 1999 and received much criticism along the way (later it changed its name to Windows Live ID). Soon after, in September 2001, the Liberty Alliance was started by Sun Microsystems, Oracle, Novell and so on (which many considered a counter to Passport) to do federated identity management. As mentioned, Google is one of the initial and strong supporter of OAuth but it also offers an older AuthSub web authentication and ClientLogin client authentication APIs while being an OpenID provider (it calls its OpenID service Federated Login). Yahoo started supporting OAuth officially in 2008, in addition to its older BBAuth APIs and is also an OpenID provider. Facebook joined the race in 2008 with Facebook Connect, which does authentication as well as share Facebook information. MySpace also came in with their platform in 2008, originally called Data Availability, which quickly became MySpaceID. In April 2009, Twitter started offering ‘Sign In with Twitter‘, which true to its name, allow users to sign in with their Twitter accounts. In addition to that Twitter uses HTTP basic authentication to protect its resources, and also recently announced support for OAuth.

As you can see there’re really lots of players in the market. RPXNow, which I blogged about previously, took advantage of these offerings and came up with a unified and simplified set of APIs to allow authentication with Yahoo, Google, Windows Live ID, Facebook, AOL, MySpace and various OpenID providers.

Looking from another perspective, there are 2 types of third party authentication APIs:

  • Web authentication for web applications
  • Client authentication for desktop or mobile client applications

The main difference between these 2 types of authentication is that web authentication can redirect the user to provider’s site for him to enter his username and password, then returns to the calling web application, usually with a success or failure token. Client authentication (usually) cannot do this, and would require the application to capture the username and password, which is sent by the client application to the provider. Obviously from a security point of view, web authentication is more secure and re-assuring to users since the user never needs to pass the username and password to the application at all.

Let’s jump into some code now. I will give 2 examples of both web authentication and client authentication each. You can find all the code here at git://github.com/sausheong/auth.gi

Let’s start with RPX because that’s the easiest. Using RPX is probably the easiest way to do third party web authentication with a large number of different providers. You can either use its provided interfaces (either embedded or pop-up Javascript) or you can call its simple API to authenticate your web application. Here’s a simple Sinatra example.

There are just 2 files. rpx_auth.html is in the public folder:

<form method="POST" action="https://chirp-saush.rpxnow.com/openid/start?token_url=http://localhost:4567/login">
	<input type="hidden" name="openid_identifier" value="yahoo.com" />
	<input type="image" src='https://a248.e.akamai.net/sec.yimg.com/i/yahoo.gif' />
</form>

This is the input form. Notice that it is entirely in HTML (it’s just a simple form submit with one post parameter). The actual work is done by auth.rb in the root folder:

%w(rubygems sinatra rest_client json).each  { |lib| require lib}

get '/login' do
  'You are logged in!' if authenticate(params[:token])
end

def authenticate(token)
  response = JSON.parse(RestClient.post 'https://rpxnow.com/api/v2/auth_info', :token => token, 'apiKey' => <rpx API KEY>, :format => 'json', :extended => 'true')
  return true if response['stat'] == 'ok'
  return false
end

You will need to register an application in RPXNow. In this example, I’m reusing Chirp-Chirp’s application in RPXNow, which is why you see the post action  is to the server chirp-saush.rpxnow.com. The token is the URL that RPX will redirect to after it authenticates the user. The pleasant surprise here is that it allows you to redirect to localhost, which eases the development effort. In this example also, I’m using Yahoo as the OpenID provider. RPXNow provides a host of other popular providers — in Chirp Chirp I actually used Yahoo, Google and Windows Live ID though it provides a lot more others. I used an image button to submit but that’s to prettify the page.

Notice that RPX redirects to the localhost with the URL ‘/login’. I created a get method for this, which in turn passes the returned token to the authenticate method. Inside this method, I used Adam Wiggin’s (of Heroku fame) really convenient rest-client library to send a post request with the token and the API key provided when you register for an application. I indicated that I want to receive the response in JSON. The response is parsed for the status and can also be parsed for data. For simplicity I didn’t return the Yahoo user information (but it’s a really small subset of data only) though I could potentially parse the JSON data for it.

And that’s it! You now have web application authentication by a third party provider, courtesy of RPX.

What if you don’t want to have a middleman like RPX to do web authentication? In this example, I was using RPX Basic, which is free because I’m using an rpxnow.com domain. If I want my own domain for authentication, I need to pay for commercial account with RPX (which is really their business model). An alternative is to use OpenID directly instead, which is very popular and doesn’t add too much complexity.

This is the input form for OpenID authentication, openid_auth.html:

<form method="get" action='/login'>
  OpenID identifier:
  <input type="text" class="openid" name="openid_identifier" size='50'/>
  <input type="submit" value="Verify" /><br />
</form>

The input requires the user to enter an OpenID identifier. If you’re not sure what an OpenID identifier is like you can check it out here. Most likely you’d already have one. Just like in the RPX case, the user is redirected to the OpenID provider, where he is requested to log in to that provider. After logging in, he will be returned to the calling application.

This is the Sinatra app that does this work, in a file called openid_auth.rb:

%w(rubygems sinatra openid openid/store/filesystem).each  { |lib| require lib}

REALM = 'http://localhost:4567'
RETURN_TO = "#{REALM}/complete"

get '/login' do
  checkid_request = openid_consumer.begin(params[:openid_identifier])
  redirect checkid_request.redirect_url(REALM, RETURN_TO) if checkid_request.send_redirect?(REALM, RETURN_TO)
  checkid_request.html_markup(REALM, RETURN_TO)
end

get '/complete' do
  response = openid_consumer.complete(params, RETURN_TO)
  return 'You are logged in!' if response.status == OpenID::Consumer::SUCCESS
  'Could not log on with your OpenID'
end

def openid_consumer
  @consumer = OpenID::Consumer.new(session, OpenID::Store::Filesystem.new('auth/store')) if @consumer.nil?
  return @consumer
end

Note that it is not that much longer than the RPX code. I used the ruby-openid gem from JanRain, which is probably the defacto OpenID Ruby library. The steps are only a bit more complicated:

  1. The input form asks the user to enter an OpenID identifier and sends it to the ‘/login’ get block (it could easily be a post block)
  2. The ‘/login’ block first gets a consumer object. To create the consumer object, you need to pass in the session where you will store the request information (this is usually provided by the web framework) as well as a store. The store is the place where we keep associations, the shared secrets between the relying party (your application) and an OpenID provider as well as nonces, cryptographic one-time alphanumeric strings used to prevent replay attacks. In the example above, I created the store in the file system.
  3. Once we have the consumer, we initiate the start of the authentication process by calling its begin method with the identifier, which returns a CheckIDRequest object. This object contains the state necessary to generate the actual OpenID request
  4. Next we ask the CheckIDRequest object if we should send the HTTP request as a redirect or if we should produce a html post form for the user
  5. Then we either redirect the user to the OpenID provider or create a HTML form that does the same thing. Here we provide a return_to path for the OpenID provider to return the user to our application with a bunch of other data
  6. Once the user authenticates himself, he will be sent back to our application, and I used another get block ‘/complete’ to process the returned data
  7. We don’t need to go through the returned parameters one by one, we can re-use the same consumer object we created earlier and use the complete method to process those parameters
  8. The complete method returns a response object, which we can check for status

The process actually looks more complicated than it really is. The ruby-openid library simplifies the processing a great deal. In the example above, I only wanted to check if the user is valid or not, if I wanted to get more data on the user, there are a bunch of other things we can do.

We’ve just covered the web authentication. Let’s move on to client authentication, which doesn’t involve a browser. Google provides ClientLogin, which is an authentication API specifically desktop or mobile clients. Using it is actually simpler than web authentication. For this example and the next, I will use Shoes, the excellent Ruby GUI toolkit by why the lucky stiff. To run the code, you need to download and install Shoes, then follow the instructions on how to execute Shoes applications. I won’t go through how to create a Shoes application here though, that’s for another day. Here’s the complete code for using Google’s ClientLogin API, in a file called goog-auth-client.rb:

Shoes.setup do
   gem 'rest-client'
   gem 'net-ssh'

end
%w(rest_client openssl.so openssl/bn openssl/cipher openssl/digest openssl/ssl openssl/x509 net/ssh).each  { |lib| require lib}

class GoogleAuthClient < Shoes
  url '/', :login
  url '/list', :list

  def login
    stack do
      title "Please login", :margin => 4
      para "Log in using your Google credentials!"
      flow :margin_left => 4 do para "Email"; @me = edit_line; end
      flow :margin_left => 4 do para "Password"; @password = edit_line :secret => true; end
      button "login" do $me = @me.text; authenticate(@me.text,@password.text) ? visit('/list') : alert("Wrong email or password"); end
    end
  end

  def list
    title "You are logged in!"
  end

  def authenticate(email,password)
    response = RestClient.post 'https://www.google.com/accounts/ClientLogin', 'accountType' => 'HOSTED_OR_GOOGLE', 'Email' => email, 'Passwd' => password, :service => 'xapi', :source => 'Goog-Auth-1.0'
    return true if response.code == 200
    return false
  end
end
Shoes.app :width => 400, :height => 200, :title => 'Google Authenticator'

You might notice that I actually included a large number of libraries. This is because there is a problem in the current Shoes build, which didn’t include the OpenSSL libraries so we’ll need to add them in manually. The method to focus on is really the authenticate method. I used the rest_client library again to send a post request to the ClientLogin URL. the parameter ‘service’ indicates which Google service you want to access. In our case, we just want to authenticate a Google user, so we can use the generic service name ‘xapi’. The ‘source’ parameter is a short string that identifies our application, this is for logging purposes only.

An alternative to ClientLogin and just as easy is Twitter’s basic authentication service. The code is also exactly the same. Here’s the code, in a file named twit-auth_client.rb.

class AuthClient < Shoes
  url '/', :login
  url '/list', :list

  def login
    stack do
      title "Please login", :margin => 4
      para "Log in using your Twitter credentials!"
      flow :margin_left => 4 do para "Username"; @me = edit_line; end
      flow :margin_left => 4 do para "Password"; @password = edit_line :secret => true; end
      button "login" do authenticate(@me.text,@password.text) ? visit('/list') : alert("Wrong user or password"); end
    end
  end

  def list
    title "You are logged in!"
  end

  def authenticate(user,password)
    open("http://twitter.com/account/verify_credentials.xml", :http_basic_authentication=>[user, password]) rescue return false
    return true
  end
end
Shoes.app :width => 400, :height => 200, :title => 'Blab!'

Let’s jump into the authenticate method again. I used OpenURI to send a get request to the verify credentials API. Twitter uses basic authentication to protect its resources, so we need to pass in the user and password information as well (notice that OpenURI allows you to use basic authentication). I could parse the returned results, but what happens here is that if the user and password information is invalid, it will throw an error anyway, so I just do a rescue and return false. If all is well, I can just return true.

You might notice that the code that allows me to use both Google and Twitter as a the third-party authentication provider is just a single line! Don’t you just love Ruby?

About these ads

34 Responses

Subscribe to comments with RSS.

  1. Tran Duc Minh said, on April 25, 2009 at 10:54 am

    I used to try acegi for authentication + authorization :)
    It ‘s a good one for service level in Java

  2. Seng Ming said, on April 28, 2009 at 10:10 pm

    Great post, I was actually considering OpenId for an app and was so glad to see that there were alternatives. Call me lazy but doing a post request a-la the google authentication is just so much easier.

    I just tried out

    response = RestClient.post 'https://www.google.com/accounts/ClientLogin', 'accountType' => 'HOSTED_OR_GOOGLE', 'Email' => email, 'Passwd' => password, :service => 'xapi', :source => 'Goog-Auth-1.0'

    and RestClient will throw a RestClient::RequestFailed exception if you submit the wrong email / password combination so you need a little more code to rescue that.

  3. Seng Ming said, on April 28, 2009 at 10:13 pm

    Great post, I was considering OpenId for an app and was so glad to see that there were alternatives. Call me lazy but doing a post request a-la the google authentication is just so much easier.

    I just tried out

    response = RestClient.post 'https://www.google.com/accounts/ClientLogin', 'accountType' => 'HOSTED_OR_GOOGLE', 'Email' => email, 'Passwd' => password, :service => 'xapi', :source => 'Goog-Auth-1.0'

    and RestClient will throw a RestClient::RequestFailed exception if you submit the wrong email / password combination so you need a little more code to rescue that.

  4. sausheong said, on April 30, 2009 at 10:14 am

    @SengMing, absolutely right, it’s just sample code. You can add a rescue at the back of the statement and then catch it somewhere else for exception handling, I guess I was just trying to make it as simple as possible.

    Google ClientLogin etc are easier of course, but you need to capture the user’s id and password and that might make your user uncomfortable, in case where you might store it for your nefarious purposes :D

  5. rubayeet said, on May 31, 2009 at 1:06 am

    Hi sausheong. Nice post! I’m trying to build a contact importer tool for my web application, and my googling returned your post. Do you happen to know if Yahoo! provides client login like Google? Twitter’s invitation tool (http://twitter.com/invitations) imports contacts from Yahoo! without the redirection. How can I achieve this?

  6. sausheong said, on May 31, 2009 at 9:10 am

    Hi Rubayeet, as far as I know Yahoo doesn’t have a client authentication mechanism for external developers. Twitter’s invitations either uses a third party provider (for contacts importing) or has direct partnership with Yahoo.

  7. Soussi said, on June 2, 2009 at 6:29 am

    Thanks for this post.
    I am currently building my GAE application and I was considering building my own authentication mechanism using Django.

    Do you think it is still worth creating one’s own authentication mechanism? May be integrating with existing providers should be enough?

  8. freebird said, on January 11, 2010 at 1:00 am

    Hi,
    Amazing guide indeed…
    I was just keen on knowing how will a similar authentication snippet would look like for Yahoo OpenID. I not able to find the exact request and response formats for the same. Kindly help me out…

  9. anonymous said, on September 27, 2010 at 10:07 pm

    9 return true if response['stat'] == ‘ok’
    10 return false

    should be
    return response['stat'] == ‘ok’

  10. Veena said, on September 26, 2011 at 5:50 pm

    Hi

    is that facebook platform supprts only oath2.0 for authentication.

  11. sledaollie said, on December 11, 2011 at 11:11 pm

    view online shopping for gift

  12. HanaPipers said, on February 8, 2012 at 2:36 pm

    :)

  13. media blog said, on April 9, 2013 at 2:32 am

    The next time I read a blog, I hope that it doesn’t disappoint me just as much as this particular one. After all, I know it was my choice to read through, nonetheless I truly thought you would have something helpful to say. All I hear is a bunch of moaning about something you can fix if you weren’t
    too busy seeking attention.

  14. the north face uk said, on October 17, 2013 at 3:38 pm

    I enjoy what you guys are up too. This sort of clever work and reporting! Keep up the wonderful works guys I’ve you guys to my blogroll.

  15. chelancatering.com said, on December 2, 2013 at 12:34 am

    Greetings from Idaho! I’m bored to death at work so I decided to browse your site on my iphone during lunch break. I love the information you provide here and can’t wait to take a look when I get home. I’m shocked at how quick your blog loaded on my phone .. I’m not even using WIFI, just 3G .. Anyways, good blog!

  16. Vivien said, on December 26, 2013 at 7:38 am

    I just couldn’t depart your web site before suggesting that I extremely enjoyed the standard
    information a person provide for your visitors? Is going to
    be back often in

    order to check up on new posts

  17. I do trust all the ideas you have

    presented in your post. They are really convincing and

    will definitely work. Nonetheless, the posts are very short for

    beginners. May just you please prolong them a little from next time?
    Thanks for the post.

  18. Sewer Reno NV said, on January 26, 2014 at 4:08 pm

    A main line stoppage occurs in the main sewer line that connects your home plumbing to either a septic tank or your city’s municipal sewer system.
    If you are not aware of the number, your local government
    office will be able to direct you to the appropriate office.
    How does a problem like this go completely unsolved.

  19. Car Accident Attorney Baltimore said, on February 16, 2014 at 2:32 pm

    If you are in vocational training or school,
    or if you are at a job at the time of your accident, you
    may be entitled to some compensation to make up for lost time
    and wages. Thus, should anyone find himself or herself in such a
    circumstance it would be prudent to enlist the help of a competent car accident attorney St.
    If a trial becomes necessary, it can take several years to complete
    the case.

  20. Jack Cooper said, on June 21, 2014 at 2:15 pm

    I searched for this title and discovered this, great read

  21. Joshua Carter said, on June 22, 2014 at 2:50 pm

    Hello, important advice and an fascinating post, it will be fascinating if this is still the situation in a
    few years time

  22. Oliver Smith said, on June 22, 2014 at 6:30 pm

    I searched for this title and discovered this, great
    read

  23. Abigail Cook said, on June 22, 2014 at 7:36 pm

    Hello, great information and an interesting article, it’ll be exciting
    if this is still the situation in a few months time

  24. Ava Coleman said, on June 23, 2014 at 12:11 pm

    Hi, great suggestion and an exciting post, it will be fascinating
    if this is still the state of affairs in a few years time

  25. Matthew Murphy said, on June 24, 2014 at 10:37 am

    Hi, fantastic advice and an fascinating post, it is
    going to be exciting if this is still the case in a few years time

  26. Audrey Myers said, on June 24, 2014 at 9:18 pm

    I hardly ever discuss these posts, but I thought this on deserved a thumb up

  27. Noah Ford said, on June 25, 2014 at 3:39 am

    Hi, great suggestion and an fascinating article post, it will be exciting if this is
    still the situation in a few months time

  28. Scarlett Rogers said, on June 25, 2014 at 3:44 am

    Great Piece

  29. Scarlett Woods said, on June 25, 2014 at 5:18 am

    Well I searched for this title and found this,
    great read

  30. Abigail Lopez said, on June 25, 2014 at 8:06 am

    Awfully interesting critique

  31. Sophia Thomas said, on June 25, 2014 at 12:43 pm

    Exceptionally fascinating piece

  32. instagram followers said, on July 3, 2014 at 7:26 pm

    inphyyuctdh
    instagram followers http://gaininstafollowers.blogspot.com

  33. fitness models said, on July 14, 2014 at 6:19 am

    Great delivery. Outstanding arguments. Keep up the
    amazing spirit.

  34. Car Accident Attorney Howard County said, on July 25, 2014 at 6:02 pm

    If you are in vocational training or school, or if you are at a job
    at the time of your accident, you may be entitled to some compensation to make up for lost time and wages.
    Yes, you badly need one to discuss the potential case
    with you and have the whole process thoroughly explained.
    This is because of the professionalism that
    this lawyer posses.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 446 other followers

%d bloggers like this: