Bug 745290

Summary: 2 legged oauth requests from python-oauth2 get 401 response.
Product: [Retired] CloudForms Cloud Engine Reporter: Steve Loranz <sloranz>
Component: iwhdAssignee: Pete Zaitcev <zaitcev>
Status: CLOSED DEFERRED QA Contact: wes hayutin <whayutin>
Severity: high Docs Contact:
Priority: unspecified    
Version: 1.0.0CC: akarol, dajohnso, deltacloud-maint, dgao, nobody, ssachdev
Target Milestone: rc   
Target Release: ---   
Hardware: Unspecified   
OS: Unspecified   
Whiteboard:
Fixed In Version: Doc Type: Bug Fix
Doc Text:
Story Points: ---
Clone Of: Environment:
Last Closed: 2011-10-12 23:07:51 UTC Type: ---
Regression: --- Mount Type: ---
Documentation: --- CRM:
Verified Versions: Category: ---
oVirt Team: --- RHEL 7.3 requirements from Atomic Host:
Cloudforms Team: --- Target Upstream Version:

Description Steve Loranz 2011-10-11 22:49:30 UTC
Description of problem:
Doing a simple test of python-oauth2 based client fails with 401.

Version-Release number of selected component (if applicable):
iwhd 0.99

How reproducible:
Always

Steps to Reproduce:
1. start warehouse:

iwhd -v -o -U key:secret -p 9090 -c /etc/iwhd/conf.js -d localhost:27017

2. run the following in python

import oauth2 as oauth
consumer = oauth.Consumer('key', 'secret')
client = oauth.Client(consumer, None)
r, c = client.request('http://localhost:9090')
print 'Response headers: %s\nContent: %s' % (r,c)

  
Actual results:

Response headers: {'date': 'Thu, 06 Oct 2011 13:02:56 GMT', 'status': '401', 'content-length': '0', 'www-authenticate': 'OAuth realm="chifac2"'}
Content: 

Expected results:

status header should be 200 and content should be:
"<api service="image_warehouse" version="0.99">\n\t<link rel="bucket_factory" href="http://localhost:9090/_new"/>\n\t<link rel="provider_list" href="http://localhost:9090/_providers"/>\n</api>\n"


Additional info:

I was able to successfully get a request token from a third party test server as described at http://term.ie/oauth/example/

I am using HMAC-SHA1 as the signature method.

Debugging what is expected by iwhd that I am failing to provide is tough when very little information is logged by iwhd even with verbose logging on:
--- sample iwhd log ---
primary store type is fs
db is at localhost:27017
will listen on port 9090
my location is "primary"
autheticated with OAuth
>> HTTP
user-agent: Python-httplib2/0.7.0 (gzip)
accept-encoding: gzip, deflate
Host: chifac2.usersys.redhat.com:9090
>> headers: 3
Oct 11 16:18:21 iwhd[30921]: parse_url: 6: _providers  
>> HTTP
user-agent: Python-httplib2/0.7.0 (gzip)
accept-encoding: gzip, deflate
Host: chifac2.usersys.redhat.com:9090
>> headers: 3
Oct 11 16:41:34 iwhd[30921]: parse_url: 6: _providers

Comment 2 Pete Zaitcev 2011-10-12 17:27:02 UTC
> user-agent: Python-httplib2/0.7.0 (gzip)
> accept-encoding: gzip, deflate
> Host: chifac2.usersys.redhat.com:9090

For authentication to work, an "Authorization:" header must be set,
and have type "OAuth" with parameters. The client did not transmit it.

Steve on the call promised tcpdump dumps, which is good, but I think
we know that Python code needs some kind of tweaking in the client.

OAuth needs Host: header in order to reconstruct the URL that is
included into HMAC string. The header above looks good.

Comment 3 Steve Loranz 2011-10-12 19:14:04 UTC
Here's what's getting sent over by python-oauth2:

--- begin
2011-10-12 13:56:56.398409 IP (tos 0x0, ttl 59, id 8296, offset 0, flags [DF], proto TCP (6), length 413)
    redacted.50463 > redacted.websm: Flags [P.], cksum 0x9f0c (correct), seq 1:362, ack 1, win 32854, options [nop,nop,TS val 156687778 ecr 2421563146], length 361
E... h@.;.x.
.q0
. ...#..T..x......V.......
	V...V.
GET /_providers?oauth_body_hash=2jmj7l5rSw0yVb%2FvlWAYkK%2FYBwk%3D&oauth_nonce=39138052&oauth_timestamp=1318446499&oauth_consumer_key=key&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_signature=juADSgCmcKy5P9uRm8Ugw2HMA9c%3D HTTP/1.1
Host: redacted:9090
accept-encoding: gzip, deflate
user-agent: Python-httplib2/0.7.0 (gzip)
--- end

So, you are correct that there is not an 'Authorization' header being sent here.  Is that header actually part of a purely two legged flow or is it added for the first part of a three legged flow?

Comment 4 Steve Loranz 2011-10-12 19:23:51 UTC
...and here is what the ruby lib that Matt is using is sending.  The difference is obvious, just a question of how we want to deal with it.

--- begin

2011-10-12 14:08:38.185909 IP (tos 0x0, ttl 61, id 17209, offset 0, flags [DF], proto TCP (6), length 412)
    redacted.55128 > redacted.websm: Flags [P.], cksum 0x1ba9 (correct), seq 1:361, ack 1, win 115, options [nop,nop,TS val 3014657576 ecr 2422264845], length 360
E...C9@.=..N
...
. ..X#...Fn.......s.......
GET / HTTP/1.1
Accept: */*
Connection: close
User-Agent: OAuth gem v0.4.4
Authorization: OAuth oauth_consumer_key="key", oauth_nonce="m14FD3Jxb16auOtdGPxmIIcIQQ6Z4EJ7uwcSN9VxA", oauth_signature="VHKpHSFelGkkXhdzLuEjArp1Va0%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1318447201", oauth_version="1.0"
Host: redacted:9090

--- end

Comment 5 Pete Zaitcev 2011-10-12 21:40:48 UTC
Well, this is unpleasant. We do not use query strings anywhere else in iwhd,
on the account of them being non-RESTful, so processing them would require
a certain amount of surgery. Is there an easy way to make the library to
supply the header instead?

Comment 6 Steve Loranz 2011-10-12 22:04:30 UTC
I've been able to force the headers, but now I'm getting a signature mismatch. 
*sigh* 

>> HTTP
user-agent: Python-httplib2/0.7.0 (gzip)
authorization: OAuth realm="",
oauth_body_hash="2jmj7l5rSw0yVb%2FvlWAYkK%2FYBwk%3D", oauth_nonce="11996549",
oauth_timestamp="1318456557", oauth_consumer_key="key",
oauth_signature_method="HMAC-SHA1", oauth_version="1.0",
oauth_signature="0UCfXKdFAllfdRY8YWDX3R8Q4so%3D"
accept-encoding: gzip, deflate
Host: redacted:9090
>> headers: 4
Oct 12 16:44:32 iwhd[17795]: parse_url: 0:   
OAuth supplied [8]
 realm=
 oauth_body_hash=2jmj7l5rSw0yVb/vlWAYkK/YBwk=
 oauth_nonce=11996549
 oauth_timestamp=1318456557
 oauth_consumer_key=key
 oauth_signature_method=HMAC-SHA1
 oauth_version=1.0
 oauth_signature=0UCfXKdFAllfdRY8YWDX3R8Q4so=
OAuth calculated
 http://redacted:9090/
 oauth_consumer_key=key
 oauth_nonce=11996549
 oauth_signature_method=HMAC-SHA1
 oauth_timestamp=1318456557
 oauth_version=1.0
 oauth_signature=wq63fBvPRG9f2z31I/gfFXmwbqQ=
Oct 12 16:44:32 iwhd[17795]: OAuth signature mismatch

Comment 7 Pete Zaitcev 2011-10-12 22:07:32 UTC
Are you sure you did not supply both by accident? Query is included into
the string to be signed.

BTW, how did you "force" the headers? The source says:
https://github.com/simplegeo/python-oauth2/blob/master/oauth2/__init__.py:

    def to_header(self, realm=''):
        ................
        auth_header = 'OAuth realm="%s"' % realm
        if params_header:
            auth_header = "%s, %s" % (auth_header, params_header)
        return {'Authorization': auth_header}

    def to_url(self):
        base_url = urlparse.urlparse(self.url)
        query = base_url.query
        query = parse_qs(query)
        for k, v in self.items():
            query.setdefault(k, []).append(v)
        ................
        url = (scheme, netloc, path, params,
               urllib.urlencode(query, True), fragment)
        return urlparse.urlunparse(url)

    def request(self, uri, method="GET", body='', headers=None,
        redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
        DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded'
        ................
        if is_form_encoded:
            body = req.to_postdata()
        elif method == "GET":
            uri = req.to_url()
        else:
            headers.update(req.to_header(realm=realm))

        return httplib2.Http.request(self, uri, method=method, body=body,
            headers=headers, redirections=redirections,
            connection_type=connection_type)

Looks like it always does query if method is GET.

Comment 8 Steve Loranz 2011-10-12 22:20:11 UTC
'Forced' with the following...

import oauth2 as oauth
url = "http://redacted:9090/"
consumer = oauth.Consumer(key='key', secret='secret')
sig_method = oauth.SignatureMethod_HMAC_SHA1()
params = {'oauth_version':oauth.OAUTH_VERSION,
          'oauth_nonce':oauth.generate_nonce(),
          'oauth_timestamp':oauth.generate_timestamp(),
          'oauth_signature_method':sig_method.name,
          'oauth_consumer_key':consumer.key}
req = oauth.Request(method='GET', url=url, parameters=params)
req.sign_request(sig_method, consumer, None)
client = oauth.Client(consumer, None)
client.set_signature_method(sig_method)
r, c = client.request(url, headers=req.to_header())
print 'Response headers: %s\nContent: %s' % (r,c)

Comment 9 Steve Loranz 2011-10-12 23:07:51 UTC
Okay... success, finally.

What worked on the python side for future reference:

--- example begin
import oauth2 as oauth
url = "http://redacted:9090/"
consumer = oauth.Consumer(key='key', secret='secret')
sig_method = oauth.SignatureMethod_HMAC_SHA1()
params = {'oauth_version':oauth.OAUTH_VERSION,
          'oauth_nonce':oauth.generate_nonce(),
          'oauth_timestamp':oauth.generate_timestamp(),
          'oauth_signature_method':sig_method.name,
          'oauth_consumer_key':consumer.key}
req = oauth.Request(method='GET', url=url, parameters=params)
sig = sig_method.sign(req, consumer, None)
req['oauth_signature'] = sig
client = oauth.Client(consumer, None)
r, c = client.request(url, headers=req.to_header())
print 'Response headers: %s\nContent: %s' % (r,c)
--- example end

I'll close this out, but it's probably worth noting that the OAuth 1.0 core spec does state that having this information as request GET parameters is one of three valid options (section 9.1.1 - http://oauth.net/core/1.0/#anchor14).  You may run into this problem with other clients in the future.

Comment 10 Pete Zaitcev 2011-10-13 01:35:41 UTC
Quite likely it was specified, maybe I was inattentive. I've only read
the RFC anyway. This should be DEFERRER rather than NOTABUG, I think.