A No Bullshit Twitter OAuth Example
So I wanted/needed to really understand Twitter’s OAuth. I tried the docs and while at first they looked thorough; they also seem to have left out a few things. All I wanted was a pseudo-code example of the steps involved in the auth process. After much digging I never found one that really worked. Maybe these examples worked for other OAuth implementations but definitely not Twitters.
I dug through OAuth as well as some other gems to see how they want about this process. The OAuth gem works perfectly as far as I can tell but it’s crazy abstracted codebase wasn’t helping me with what I needed to know. I gained some insight from the more light weight gems like SOAuth and ROAuth but mostly this was a game of trial and error.
With lots and lots of errors.
So here it is. Exactly what I had set out looking for. Step-by-step unoptimized and ugly code, walking you through Twitter’s OAuth madness. Enjoy.
edit Thanks to shadytrees rounding this out with the last couple steps.
%w{rubygems hmac-sha1 base64 cgi net/https uri openssl}.each{ |f| require f } KEY = ''; SECRET = ''; # encodes strings that make twitter oauth happy def encode( string ) URI.escape( string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]") ).gsub('*', '%2A') end # i'll let you guess what this does def generate_nonce(size=7) Base64.encode64(OpenSSL::Random.random_bytes(size)).gsub(/\W/, '') end # used in forming the request signature as well as making the actual request method = 'POST' # this is the base for the token request only. for all other requests # you're on your own for now. base_uri = 'https://api.twitter.com/oauth/request_token' # params that are needed for the token request params = { 'oauth_consumer_key' => KEY, 'oauth_nonce' => generate_nonce, 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_timestamp' => Time.new.to_i.to_s, 'oauth_version' => "1.0" } # format the signature. notice what is encoded and what is not. it fucking matters! trust me. request_signature = (method << '&' << encode(base_uri) << '&' << params.sort.collect{ |h| "#{(h.first)}%3D#{(h.last)}" }.join("%26")) # this is where the actual hashing takes place. be sure to notice that ampersand # that is tagged on to the SECRET. it's important too. digest = OpenSSL::Digest::Digest.new( 'sha1' ) encoded = encode Base64.encode64( OpenSSL::HMAC.digest(digest, SECRET + '&', request_signature)).chomp.gsub(/\n/, "") # formatting the OAuth header for the request oauth_header = <<-HEADER OAuth realm="",oauth_consumer_key="#{KEY}",oauth_version="1.0",oauth_timestamp="#{params['oauth_timestamp']}",oauth_nonce="#{params['oauth_nonce']}",oauth_signature_method="HMAC-SHA1",oauth_signature="#{encoded}" HEADER # send the request with Net::HTTPs and WIN! url = URI.parse(base_uri) http = Net::HTTP.new(url.host, 443) http.use_ssl = true resp, data = http.post(url.path, nil, { 'Authorization' => oauth_header }) p resp.body # send the user to twitter params = CGI::parse(resp.body) token, token_secret = params['oauth_token'][0], params['oauth_token_secret'][0] puts "Your PIN URL:\nhttp://api.twitter.com/oauth/authorize?oauth_token=" << token # exchange pin for access token print 'Enter your PIN: ' base_uri = 'https://api.twitter.com/oauth/access_token' params = { 'oauth_consumer_key' => KEY, 'oauth_nonce' => generate_nonce, 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_token' => token, 'oauth_timestamp' => Time.new.to_i.to_s, 'oauth_verifier' => gets.strip } request_signature = (method << '&' << encode(base_uri) << '&' << params.sort.collect{ |h| "#{(h.first)}%3D#{(h.last)}" }.join("%26")) digest = OpenSSL::Digest::Digest.new( 'sha1' ) # our composite signing key now has the token secret after the ampersand encoded = encode Base64.encode64( OpenSSL::HMAC.digest(digest, SECRET + '&' + token_secret, request_signature)).chomp.gsub(/\n/, "") # our header now has oauth_token and oauth_verifier as key-value pairs oauth_header = <<-HEADER OAuth oauth_consumer_key="#{KEY}",oauth_version="1.0",oauth_timestamp="#{params['oauth_timestamp']}",oauth_nonce="#{params['oauth_nonce']}",oauth_signature_method="HMAC-SHA1",oauth_signature="#{encoded}",oauth_token="#{token}",oauth_verifier="#{params['oauth_verifier']}" HEADER url = URI.parse(base_uri) http = Net::HTTP.new(url.host, 443) http.use_ssl = true # pretty-print the screen name, as if by MAGIC require 'pp' resp, data = http.post(url.path, nil, { 'Authorization' => oauth_header }) pp CGI::parse(resp.body)