Bug 745290 - 2 legged oauth requests from python-oauth2 get 401 response.
Summary: 2 legged oauth requests from python-oauth2 get 401 response.
Keywords:
Status: CLOSED DEFERRED
Alias: None
Product: CloudForms Cloud Engine
Classification: Retired
Component: iwhd
Version: 1.0.0
Hardware: Unspecified
OS: Unspecified
unspecified
high
Target Milestone: rc
Assignee: Pete Zaitcev
QA Contact: wes hayutin
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2011-10-11 22:49 UTC by Steve Loranz
Modified: 2015-08-03 00:02 UTC (History)
6 users (show)

Fixed In Version:
Doc Type: Bug Fix
Doc Text:
Clone Of:
Environment:
Last Closed: 2011-10-12 23:07:51 UTC


Attachments (Terms of Use)

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.


Note You need to log in before you can comment on or make changes to this bug.