Skip to content

Commit

Permalink
Fix signing the AuthSub request. It's set in the headers, instead of …
Browse files Browse the repository at this point in the history
…in the params.
  • Loading branch information
kamal committed Mar 18, 2009
1 parent 2ceaadb commit b3e9585
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 41 deletions.
62 changes: 40 additions & 22 deletions lib/contacts/google.rb
Expand Up @@ -78,34 +78,34 @@ def self.client_login_options
# (default: false)
def self.authentication_url(target, options = {})
params = authentication_url_options.merge(options)
key = params.delete(:key)
params[:secure] = true if !params[:secure] && key
if key = params.delete(:key)
params[:secure] = true
set_private_key(key)
end
params[:next] = target
query = query_string(params)
url = "https://#{DOMAIN}#{AuthSubPath}Request?#{query}"
sig = generate_sig(url, key) if key
key ? "#{url}&#{query_string(:sig => sig)}" : url
"https://#{DOMAIN}#{AuthSubPath}Request?#{query}"
end

# Generates the signature for a secure AuthSub request. +key+ may be an IO, String or
# Sets the private key for a secure AuthSub request. +key+ may be an IO, String or
# OpenSSL::PKey::RSA.
# Stolen from http://github.com/stuart/google-authsub/lib/googleauthsub.rb
def self.generate_sig(url, key)
pkey = case key
def self.set_private_key(key)
case key
when OpenSSL::PKey::RSA
key
@@pkey = key
when File
OpenSSL::PKey::RSA.new(key.read)
@@pkey = OpenSSL::PKey::RSA.new(key.read)
when String
OpenSSL::PKey::RSA.new(key)
@@pkey = OpenSSL::PKey::RSA.new(key)
else
raise "Private Key in wrong format. Require IO, String or OpenSSL::PKey::RSA, you gave me #{key.class}"
end
timestamp = Time.now.to_i
nonce = OpenSSL::BN.rand_range(2**64)
data = "GET #{url} #{timestamp} #{nonce}"
digest = OpenSSL::Digest::SHA1.new(data).hexdigest
sig = [pkey.private_encrypt(digest)].pack("m") #Base64 encode
end

# Unsets the private key. Only used for test teardowns.
def self.unset_private_key
@@pkey = nil
end

# Makes an HTTPS request to exchange the given token with a session one. Session
Expand All @@ -115,13 +115,14 @@ def self.generate_sig(url, key)
# body.
def self.session_token(token)
response = http_start do |google|
google.get(AuthSubPath + 'SessionToken', authorization_header(token))
uri = AuthSubPath + 'SessionToken'
google.get(uri, authorization_header(token, false, uri))
end

pair = response.body.split(/\n/).detect { |p| p.index('Token=') == 0 }
pair.split('=').last if pair
end

# Alternative to AuthSub: using email and password.
def self.client_login(email, password)
response = http_start do |google|
Expand All @@ -142,6 +143,7 @@ def self.client_login(email, password)
def initialize(token, user_id = 'default', client = false)
@user = user_id.to_s
@token = token.to_s
@client = client
@headers = {
'Accept-Encoding' => 'gzip',
'User-Agent' => Identifier + ' (gzip)'
Expand All @@ -154,7 +156,9 @@ def get(params) # :nodoc:
path = FeedsPath + CGI.escape(@user)
google_params = translate_parameters(params)
query = self.class.query_string(google_params)
google.get("#{path}/#{@projection}?#{query}", @headers)
uri = "#{path}/#{@projection}?#{query}"
headers = @headers.update(self.class.authorization_header(@token, @client, uri))
google.get(uri, headers)
end
end

Expand Down Expand Up @@ -284,9 +288,23 @@ def translate_parameters(params)
end
end

def self.authorization_header(token, client = false)
type = client ? 'GoogleLogin auth' : 'AuthSub token'
{ 'Authorization' => %(#{type}="#{token}") }
def self.secure?
defined?(@@pkey) && !@@pkey.nil?
end

def self.authorization_header(token, client = false, uri = nil)
if client
{ 'Authorization' => %(GoogleLogin auth="#{token}") }
elsif secure?
timestamp = Time.now.to_i
nonce = OpenSSL::BN.rand_range(2**64)
data = "GET http://#{DOMAIN}#{uri} #{timestamp} #{nonce}"
sig = @@pkey.sign(OpenSSL::Digest::SHA1.new, data)
sig = [sig].pack("m").gsub(/\n/, "") #Base64 encode
{ 'Authorization' => %(AuthSub token="#{token}" sigalg="rsa-sha1" data="#{data}" sig="#{sig}") }
else
{ 'Authorization' => %(AuthSub token="#{token}") }
end
end

def self.http_start(ssl = true)
Expand Down
27 changes: 8 additions & 19 deletions spec/gmail/auth_spec.rb
Expand Up @@ -29,40 +29,29 @@
end

it 'should imply secure=true when the key parameter is set' do
rsa = mock('OpenSSL::PKey::RSA', :private_encrypt => 'signature')
digest = mock('OpenSSL::Digest::SHA1', :hexdigest => 'digest')
OpenSSL::Digest::SHA1.expects(:new).returns(digest).at_least_once
OpenSSL::PKey::RSA.expects(:new).with('secret-key').returns(rsa).at_least_once
pairs = parse_authentication_url(nil, :key => File.open(File.join(File.dirname(__FILE__), 'myrsakey.pem'))).query.split('&')

pairs.should include('secure=1')
end

pairs = parse_authentication_url(nil, :key => 'secret-key').query.split('&')
it 'should accept String key parameter' do
key = File.open(File.join(File.dirname(__FILE__), 'myrsakey.pem')).read
pairs = parse_authentication_url(nil, :key => key).query.split('&')

pairs.should include('secure=1')
pairs.should include('sig=c2lnbmF0dXJl%0A') # ['signature'].pack('m')
pairs.should_not include('key=secret-key')
pairs.should_not include("key=#{key}")
end

it 'should accept File or IO key parameter' do
rsa = mock('OpenSSL::PKey::RSA', :private_encrypt => 'signature')
digest = mock('OpenSSL::Digest::SHA1', :hexdigest => 'digest')
OpenSSL::Digest::SHA1.expects(:new).returns(digest).at_least_once
OpenSSL::PKey::RSA.expects(:new).with(File.open(File.join(File.dirname(__FILE__), 'myrsakey.pem')).read).returns(rsa).at_least_once

pairs = parse_authentication_url(nil, :key => File.open(File.join(File.dirname(__FILE__), 'myrsakey.pem'))).query.split('&')

pairs.should include('secure=1')
pairs.should include('sig=c2lnbmF0dXJl%0A') # ['signature'].pack('m')
pairs.should_not include('key=secret-key')
end

it 'should accept OpenSSL::Pkey::RSA key parameter' do
digest = mock('OpenSSL::Digest::SHA1', :hexdigest => 'digest')
OpenSSL::Digest::SHA1.expects(:new).returns(digest).at_least_once

pairs = parse_authentication_url(nil, :key => OpenSSL::PKey::RSA.new(File.open(File.join(File.dirname(__FILE__), 'myrsakey.pem')).read)).query.split('&')

pairs.should include('secure=1')
pairs.should include('sig=nhzzbfqHUOhN6iE%2BkyHQabRvtfc3pbxKQt4hHqlNtBZVliswTFfVIISFPo5Z%0Ads6YVeAKdqAAZuVFUwMDihA83ihIf8spWN%2BrKpeLxAhrUCM69oihD7csdedG%0AbN9TCToIp4q9tJj2o4SsAgxs3dK55Nc1vhCOlVB7mIbxM%2B8YGL4%3D%0A') # based on 'digest' data
pairs.should_not include('key=secret-key')
end

it 'skips parameters that have nil value' do
Expand Down
24 changes: 24 additions & 0 deletions spec/gmail/fetching_spec.rb
Expand Up @@ -13,6 +13,7 @@ def create

after :each do
FakeWeb.clean_registry
Contacts::Google.unset_private_key
end

describe 'fetches contacts feed via HTTP GET' do
Expand Down Expand Up @@ -41,6 +42,29 @@ def create
response = @gmail.get({})
response.body.should == 'full results'
end

it 'with signed requests' do
# freeze time and nonce for consistent results
time = mock('Time.now')
time.expects(:to_i).with().returns(1237384927).at_least_once
Time.expects(:now).returns(time).at_least_once
OpenSSL::BN.expects(:rand_range).with(2**64).returns(11138977546881557915).at_least_once

Contacts::Google.set_private_key(File.open(File.join(File.dirname(__FILE__), 'myrsakey.pem')))
@gmail = Contacts::Google.new('dummytoken')

FakeWeb::register_uri(:get, 'www.google.com/m8/feeds/contacts/default/thin',
:string => 'thin results',
:verify => lambda { |req|
req['Authorization'].should == %(AuthSub token="dummytoken" sigalg="rsa-sha1" data="GET http://www.google.com/m8/feeds/contacts/default/thin? 1237384927 11138977546881557915" sig="EAO7okp0W+0kAOOFxaAfexym0tWEgKogHpjSs7AX8GZYBWJvyJg6+SFHMAeGUEpAXIyAxCoF+RYzvEt2ONVXEJKaMw4zo/qAFc/RC4dLkFbzwe4r5srFMAGrgbWgqQEeUBeY2koc0vfs2dfOP19rJgACtksm3HLMKpl8M9sUF8c=")
req['Accept-Encoding'].should == 'gzip'
req['User-Agent'].should == "Ruby Contacts v#{Contacts::VERSION::STRING} (gzip)"
}
)

response = @gmail.get({})
response.body.should == 'thin results'
end
end

it 'handles a normal response body' do
Expand Down

0 comments on commit b3e9585

Please sign in to comment.