Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Implementation of Flickr API authentication (desktop auth) and its specs

  • Loading branch information...
commit 2d0dea4bf5e72614c7bc491c7b27f90878aeb620 1 parent 0df7abe
@pietern pietern authored
View
143 lib/contacts/flickr.rb
@@ -0,0 +1,143 @@
+$:.push File.join File.dirname(__FILE__), '..'
+require 'md5'
+require 'contacts'
+require 'cgi'
+require 'time'
+require 'zlib'
+require 'stringio'
+require 'net/http'
+require 'rubygems'
+require 'hpricot'
+
+module Contacts
+
+ class Flickr
+ DOMAIN = 'api.flickr.com'
+ ServicesPath = '/services/rest/'
+
+ def self.frob_url(key, secret)
+ url_for(:api_key => key, :secret => secret, :method => 'flickr.auth.getFrob')
+ end
+
+ def self.frob_from_response(response)
+ doc = Hpricot::XML response.body
+ doc.at('frob').inner_text
+ end
+
+ def self.authentication_url_for_frob(frob, key, secret)
+ params = { :api_key => key, :secret => secret, :perms => 'read', :frob => frob }
+ 'http://www.flickr.com/services/auth/?' + query_string(params)
+ end
+
+ def self.authentication_url(key, secret)
+ response = http_start do |flickr|
+ flickr.get(frob_url(key, secret))
+ end
+ authentication_url_for_frob(frob_from_response(response), key, secret)
+ end
+
+ def self.token_url(key, secret, frob)
+ params = { :api_key => key, :secret => secret, :frob => frob, :method => 'flickr.auth.getToken' }
+ url_for(params)
+ end
+
+ def self.get_token_from_frob(key, secret, frob)
+ response = http_start do |flickr|
+ flickr.get(token_url(key, secret, frob))
+ end
+ doc = Hpricot::XML response.body
+ doc.at('token').inner_text
+ end
+
+
+ private
+ # Use the key-sorted version of the parameters to construct
+ # a string, to which the secret is prepended.
+
+ def self.sort_params(params)
+ params.sort do |a,b|
+ a.to_s <=> b.to_s
+ end
+ end
+
+ def self.string_to_sign(params, secret)
+ string_to_sign = secret + sort_params(params).inject('') do |str, pair|
+ key, value = pair
+ str + key.to_s + value.to_s
+ end
+ end
+
+ # Get the MD5 digest of the string to sign
+ def self.get_signature(params, secret)
+ ::Digest::MD5.hexdigest(string_to_sign(params, secret))
+ end
+
+ def self.query_string(params)
+ secret = params.delete(:secret)
+ params[:api_sig] = get_signature(params, secret)
+
+ params.inject([]) do |arr, pair|
+ key, value = pair
+ arr << "#{key}=#{value}"
+ end.join('&')
+ end
+
+ def self.url_for(params)
+ ServicesPath + '?' + query_string(params)
+ end
+
+ def self.http_start(ssl = false)
+ port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
+ http = Net::HTTP.new(DOMAIN, port)
+ redirects = 0
+ if ssl
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+ http.start
+
+ begin
+ response = yield(http)
+
+ loop do
+ inspect_response(response) if Contacts::verbose?
+
+ case response
+ when Net::HTTPSuccess
+ break response
+ when Net::HTTPRedirection
+ if redirects == TooManyRedirects::MAX_REDIRECTS
+ raise TooManyRedirects.new(response)
+ end
+ location = URI.parse response['Location']
+ puts "Redirected to #{location}"
+ response = http.get(location.path)
+ redirects += 1
+ else
+ response.error!
+ end
+ end
+ ensure
+ http.finish
+ end
+ end
+
+ def self.response_body(response)
+ unless response['Content-Encoding'] == 'gzip'
+ response.body
+ else
+ gzipped = StringIO.new(response.body)
+ Zlib::GzipReader.new(gzipped).read
+ end
+ end
+
+ def self.inspect_response(response, out = $stderr)
+ out.puts response.inspect
+ for name, value in response
+ out.puts "#{name}: #{value}"
+ end
+ out.puts "----\n#{response_body response}\n----" unless response.body.empty?
+ end
+ end
+
+end
View
4 spec/feeds/flickr/auth.getFrob.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<rsp stat="ok">
+ <frob>934-746563215463214621</frob>
+</rsp>
View
5 spec/feeds/flickr/auth.getToken.xml
@@ -0,0 +1,5 @@
+<auth>
+ <token>45-76598454353455</token>
+ <perms>read</perms>
+ <user nsid="12037949754@N01" username="Bees" fullname="Cal H" />
+</auth>
View
80 spec/flickr/auth_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+require 'contacts/flickr'
+require 'uri'
+
+describe Contacts::Flickr, "authentication" do
+
+ before(:each) do
+ @f = Contacts::Flickr
+ end
+
+ describe "authentication for desktop apps" do
+ # Key, secret and signature come from the Flickr API docs.
+ # http://www.flickr.com/services/api/auth.howto.desktop.html
+ it "should generate a correct url to retrieve a frob" do
+ path, query = Contacts::Flickr.frob_url('9a0554259914a86fb9e7eb014e4e5d52', '000005fab4534d05').split('?')
+ path.should == '/services/rest/'
+
+ hsh = hash_from_query(query)
+ hsh[:method].should == 'flickr.auth.getFrob'
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
+ hsh[:api_sig].should == '8ad70cd3888ce493c8dde4931f7d6bd0'
+ hsh[:secret].should be_nil
+ end
+
+ it "should generate an authentication url from a response with a frob" do
+ response = mock_response
+ response.stubs(:body).returns sample_xml('flickr/auth.getFrob')
+ Contacts::Flickr.frob_from_response(response).should == '934-746563215463214621'
+ end
+
+ # The :api_sig parameter is documented wronly in the Flickr API docs. It says the string
+ # to sign ends in 'permswread', but this should be 'permsread' of course. Therefore
+ # the :api_sig here does not correspond to the Flickr docs, but it makes the spec valid.
+ it "should return an url to authenticate to containing a frob" do
+ response = mock_response
+ response.stubs(:body).returns sample_xml('flickr/auth.getFrob')
+ @f.expects(:http_start).returns(response)
+ uri = URI.parse @f.authentication_url('9a0554259914a86fb9e7eb014e4e5d52', '000005fab4534d05')
+
+ uri.host.should == 'www.flickr.com'
+ uri.scheme.should == 'http'
+ uri.path.should == '/services/auth/'
+ hsh = hash_from_query(uri.query)
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
+ hsh[:api_sig].should == '0d08a9522d152d2e43daaa2a932edf67'
+ hsh[:frob].should == '934-746563215463214621'
+ hsh[:perms].should == 'read'
+ hsh[:secret].should be_nil
+ end
+
+ it "should get a token from a frob" do
+ response = mock_response
+ response.stubs(:body).returns sample_xml('flickr/auth.getToken')
+ connection = mock('Connection')
+ connection.expects(:get).with do |value|
+ path, query = value.split('?')
+ path.should == '/services/rest/'
+
+ hsh = hash_from_query(query)
+ hsh[:method].should == 'flickr.auth.getToken'
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
+ hsh[:api_sig].should == 'a5902059792a7976d03be67bdb1e98fd'
+ hsh[:frob].should == '934-746563215463214621'
+ hsh[:secret].should be_nil
+ true
+ end
+ @f.expects(:http_start).returns(response).yields(connection)
+ @f.get_token_from_frob('9a0554259914a86fb9e7eb014e4e5d52', '000005fab4534d05', '934-746563215463214621').should == '45-76598454353455'
+ end
+
+ end
+
+ def hash_from_query(str)
+ str.split('&').inject({}) do |hsh, pair|
+ key, value = pair.split('=')
+ hsh[key.to_sym] = value
+ hsh
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.