Coder Social home page Coder Social logo

requests-oauth's Introduction

requests-oauth

This plugins adds OAuth v1.0 support to @kennethreitz well-known requests library providing both header and url-encoded authentication.

requests-oauth wants to provide the simplest and easiest way to do OAuth in Python. It was initially based on python-oauth2 (which looks unmaintained), kudos to the authors and contributors for doing a huge effort in providing OAuth to python httplib2. From that point on, the code base has been cleaned, fixing several bugs and heavily refactoring it to eliminate dependencies with python-oauth2, being now a stand-alone plugin.

Installation

You can install requests-oauth by simply doing:

pip install requests-oauth

Usage

Import the hook doing:

from oauth_hook import OAuthHook

You can initialize the hook passing it 5 parameters: access_token, access_token_secret, consumer_key, consumer_secret and header_auth. First two access_token and access_token_secret are optional, in case you want to retrieve those from the API service (see later for an example). There are two ways to do initialize the hook. First one:

oauth_hook = OAuthHook(access_token, access_token_secret, consumer_key, consumer_secret, header_auth)

The header_auth parameter lets you chose the authentication method used. It's a boolean, if you set it to True you will be using an Authorization header. If your API supports this authentication method, it's the one you should be using and the prefered method by the OAuth spec (RFC 5849), an example would be Twitter's API. By default header_auth is set to False, which means url encoded authentication will be used. This is because this the most widely supported authentication system.

If you are using the same consumer_key and consumer_secret all the time, you probably want to setup those fixed, so that you only have to pass the token parameters for setting the hook:

OAuthHook.consumer_key = consumer_key
OAuthHook.consumer_secret = consumer_secret
oauth_hook = OAuthHook(access_token, access_token_secret, header_auth=True)

Now you need to pass the hook to python-requests, you probably want to do it as a session, so you don't have to do this every time:

client = requests.session(hooks={'pre_request': oauth_hook})

What you get is python-requests client which you can use the same way as you use requests API. Let's see a GET example:

response = client.get('http://api.twitter.com/1/account/rate_limit_status.json')
results = json.loads(response.content)

And a POST example:

response = client.post('http://api.twitter.com/1/statuses/update.json', {'status': "Yay! It works!", 'wrap_links': True})

3-legged Authorization

First time authorization and authentication follows a system named three legged OAuth, very well described in Twitter documentation.

Basically it is composed of three steps. Let's see an example based on Imgur's API. All the other APIs work pretty much the same way, only endpoints (urls) change:

Step 1: Obtaining a request token

We start asking for a request token, which will finally turn into an access token, the one we need to operate on behalf of the user.

imgur_oauth_hook = OAuthHook(consumer_key=YOUR_IMGUR_CONSUMER_KEY, consumer_secret=YOUR_IMGUR_CONSUMER_SECRET)
response = requests.post('http://api.imgur.com/oauth/request_token', hooks={'pre_request': imgur_oauth_hook})
qs = parse_qs(response.text)
oauth_token = qs['oauth_token'][0]
oauth_secret = qs['oauth_token_secret'][0]

Step 2: Redirecting the user for getting authorization

In this step we give the user a link or open a web browser redirecting him to an endpoint, passing the oauth_token got in the previous step as a url parameter. The user will get a dialog asking for authorization for our application. In this case we are doing an out of band desktop application, so the user will have to input us a code named verifier. In web apps, we will get this code as a webhook.

print "Go to http://api.imgur.com/oauth/authorize?oauth_token=%s allow the app and copy your PIN" % oauth_token
oauth_verifier = raw_input('Please enter your PIN:')

Step 3: Authenticate

Once we get user's authorization, we request a final access token, to operate on behalf of the user. We build a new hook using previous request token information achieved on step1 and pass the verifier (got in step2) as data using oauth_verifier key:

new_imgur_oauth_hook = OAuthHook(oauth_token, oauth_secret, IMGUR_CONSUMER_KEY, IMGUR_CONSUMER_SECRET)
response = requests.post('http://api.imgur.com/oauth/access_token', {'oauth_verifier': oauth_verifier}, hooks={'pre_request': new_imgur_oauth_hook})
response = parse_qs(response.content)
final_token = response['oauth_token'][0]
final_token_secret = response['oauth_token_secret'][0]

These final_token and final_token_secret are the credentials we need to use for handling user's oauth, so most likely you will want to persist them somehow. These are the ones you should use for building a requests session with a new hook. Beware that not all OAuth APIs provide unlimited time credentials.

Testing

If you want to run the tests, you will need to copy test_settings.py.template into test_settings.py. This file is in the .gitignore index, so it won't be committed:

cp test_settings.py.template test_settings.py

Then fill in the information there. The testing of the library is done in a functional way, doing GETs and POSTs against public OAuth APIs like Twitter, so use a test account and not your personal account:

./tests.py

Contributing

If you'd like to contribute, simply fork the repository, commit your changes to the dev branch (or branch off of it), and send a pull request. Make sure you add yourself to AUTHORS.

requests-oauth's People

Contributors

funkatron avatar istruble avatar jemerick avatar jjmaestro avatar maraujop avatar mart-e avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

requests-oauth's Issues

fix for issue #12 breaks GET request with params

Doing a simple GET request passing in params when header_auth is False breaks. Ie,

session.get("http://www.example.com", params={"foo": "bar"})

This is because get_normalized_parameters() only sets request.data_and_params if the Content-Type is application/x-www-form-urlencoded or request.data is a basestring. For most GET requests neither is true: Content-Type is unset and request.data is None because there is no body. (Things may work if you pass in the values as data because this sends a body, but sending a body with a GET request is considered bad form.)

When header_auth is False, __call__() replaces the request.url with a call to to_url(), which iterates over the values in the now-unset request.data_and_params. As a result, your parameters are never passed through.

Determining if consumer_key/secret exist

Say I set

OAuthHook.consumer_key = 'my_consumer_key'
OAuthHook.consumer_secret = 'my_consumer_secret'

It'd be nice if there was a way to determine if consumer_key and consumer_secret were set.

In my case, I want to use a request.session with a hook-pre_request of OAuthHook() using the key/secret if they are set AND if the oauth_token/secret is set when someone initializes my class, I want to then have request.session use the hook-pre_request also passing the oauth_token/secret

Let me know if you need me to elaborate more?

Something like:

if OAuthHook.consumer_key is not None and OAuthHook.consumer_secret is not None:
    self.client = requests.session(hooks={'pre_request': OAuthHook()})

if oauth_token and oauth_secret:  # These are the ones that are passed in my init from maybe a recored in the db
    oauth_hook = OAuthHook(self.oauth_token, self.oauth_secret)
    self.client = requests.session(hooks={'pre_request': oauth_hook})

Oauth2

Hi everyone,

does requests-oauth will support OAuth 2.0 in the future?

thank you

Signing broke with parameters that have a None value

The following payload correctly signs

ret = sesh.put('/users/me', {'username': 'myusername', 'email': '[email protected]'})

However, if one of your parameters is None it does not correctly sign:

ret = sesh.put('/users/me', {'username': None, 'email': '[email protected]'})

The issue is that it tries to sign with the string "username=None" however it should ignore that parameter (as the requests library does). The requests library does not sign "None" paramters in POST bodies.

Support oAuth calls without provided access token.

Currently, some oAuth calls require a consumer key and secret to be set, but are made without an access token. I refer specifically to the Twitter oAuth web flow login process where one must request an authentication URI to forward a user to before possessing a key and secret. Currently the hook does not support this.

Broken with update of python-requests

An update of requests seems to have broken the package. The most notably is the creation of sessions where it used to work with request.session(hooks=...). The current version of requests does not accept any arguments for the constructor. I don't know exactly when it happened as I haven't used the package for some time.

However I guess there is more problems because moving the hooks parameter outside the constructor does not fix the problem. It seems that the parameters used in the hooks are not sent with the request. The test suite fails for 9 of the 13 Twitter tests (haven't tested for other networks).

Version of python-requests used : 1.0.3-1

OauthHook not passing in oauth_* to Request in auth_header=False

We're writing a nostest framework against our web service API using requests and requests-oauth where our API

In the setUp():

...
        if self.auth == "oauth":
            oauth_hook = OAuthHook(self.access_token, self.access_token_secret, 
                                   self.consumer_key, self.consumer_secret, 
                                   header_auth=False)
            self.client = requests.session(hooks={'pre_request': oauth_hook})
        else:
            self.client = requests
...

and then the method that makes the API webservice call:

...
            # Handle POST vs. GET
            if method == 'POST':
                data = self.append_auth(data=data) # doesn't do anything if 'oauth'
                req = self.client.post(url, data=data, allow_redirects=True)
            else:
                url = self.append_auth(url=url) # doesn't do anything if 'oauth'
                req = self.client.get(url, allow_redirects=True)
...

Our API authenticates the oauth_* parameters through the HTTP request however when I output the API result, it's not showing the oauth_* parameters passed through.

print "req.request=", req.request.__dict__

Note that when I do the header_auth=True, I do see the headers {Authorization...} int the request.

i.e,
I'm testing http://my.webservice.com/get?foo=bar

I would expect your oauthhook to do the following:

http://my.webservice.com/get?foo=bar&oauth_token=xxx&oauth_signature=xxx&oauth_nonce=xxx...etc...

But that's not what I'm seeing. Am I missing something?

Trouble POSTing UTF-8 data

I'm having some trouble POSTing UTF-8 content to Bitbucket's REST API. I see there are no tests covering UTF-8 content, so would like to get this verified.

If I make a POST request with the vanilla Requests library using a hardcoded auth=('un','pw) in my session I can submit content containing UTF-8 characters. If I make the same POST request via an authenticated session using requests-oauth Bitbucket gives me a 500 response. POST requests via requests-oauth that do not include UTF-8 content work just fine.

General approach is:

client = requests.session(hooks={
'pre_request': OAuthHook(access_token = tok,
access_token_secret = sec)
})
client.post(url, data=request.form)

I've verified the exact same content being sent works via curl and via a vanilla Requests library client.post(), so something seems funny with requests-oauth's handling of unicode content. Any thoughts?

POST with utf-8 not correctly catched

The reason #31 does not work seems to be the same problem I have.

I worked on a httpie fork for oauth and httpie is building all requests with:
Content-Type: application/x-www-form-urlencoded; charset=utf-8

The following test in your code will not work correctly as it only test for equality with "Content-Type: application/x-www-form-urlencoded".
Then the POST data will disappear in the signature, here is a simple patch (one line) that's why I'm posting here.

diff --git a/oauth_hook/hook.py b/oauth_hook/hook.py
index b4042fd..3a85a8b 100644
--- a/oauth_hook/hook.py
+++ b/oauth_hook/hook.py
@@ -71,7 +71,7 @@ class OAuthHook(object):
         """
         # See issues #10 and #12
         if ('Content-Type' not in request.headers or \
-            request.headers.get('Content-Type') == 'application/x-www-form-urlencoded') \
+            request.headers.get('Content-Type').startswith('application/x-www-form-urlencoded')) \
             and not isinstance(request.data, basestring):
             data_and_params = dict(request.data.items() + request.params.items())

Invalid signature generation with double encoding

Using these settings, oauth-requests (with header_auth=True) and this oauth signer produce different signatures:

url: http://api.v3.factual.com/multi
parameters: queries=%7B%22query1%22%3A%22%2Ft%2Fplaces%3Ffilters%3D%257B%2522postcode%2522%253A%252290067%2522%257D%22%2C%22query2%22%3A%22%2Ft%2Fplaces%2Ffacets%3Ffilters%3D%257B%2522postcode%2522%253A%252290067%2522%257D%26select%3Dcategory%22%7D
consumer key: key
consumer secret: secret
# token and token secret are blank
timestamp: 1338404204
nonce: 38585129

The signer produces this oauth_signature "+1S2b8kxgOvJAUV90QjbYeB1i7k=" while oauth-requests produces "MWH9o1cwUOMhmrIpybbrsdi/eME=".

However, changing just the following settings seems to work.

parameters: queries=%7B%22query1%22%3A+%22%2Ft%2Fplaces%3Fq%3Dsushi%22%7D
nonce: 2555196

In this case both tools produce this signature: "tvSvg9hjL4ijuMT309h8EAVo44c=".

I'm not certain this is actually caused by the double url encoding, but after running a few tests that seems to be the trigger.

Retrieving a request_token from Twitter fails with Requests-oauth 0.2.2 and Requests 0.8.1

Previously, posting to https://api.twitter.com/oauth/request_token with a consumer key and secret provided would give back a useable request token. It now gives a rather useless http 401 error:

>>> r=t.client.post('https://api.twitter.com/oauth/request_token')
>>> r
<Response [401]>
>>> r.content
u'Failed to validate oauth signature and token'
>>> r.request.headers
{'Content-Type': 'application/x-www-form-urlencoded', 'Accept-Encoding': 'identi
ty, deflate, compress, gzip', 'Accept': '*/*', 'User-Agent': 'python-requests/0.
8.1'}
>>> r.request.params
{'oauth_nonce': '69262010', 'oauth_timestamp': '1321701553', 'oauth_consumer_key
': 'Iay3HXuKW3lYQ1REEKnWg', 'oauth_signature_method': 'HMAC-SHA1', 'oauth_versio
n': '1.0', 'oauth_signature': 'wcPIt0x1G0MpE+eO9NNQUwruRP8='}
>>> r.request.data
{}
>>> r.request.cookies
{'guest_id': 'v1%3A132170155381775185', '_twitter_sess': 'BAh7CDoPY3JlYXRlZF9hdG
wrCJo2jbszAToHaWQiJTU5MWVjNmJjYjRiZTA1%250AMmRhM2UxMDZlNWM4Yjk2MWE1IgpmbGFzaElDO
idBY3Rpb25Db250cm9sbGVy%250AOjpGbGFzaDo6Rmxhc2hIYXNoewAGOgpAdXNlZHsA--1ba7fb8449
bec52a105b50b825a4623c35a2fe07', 'k': '10.35.56.136.1321701553810853'}
>>>

Requests 0.8 breaks requests-oauth

It appears that 0.8 breaks something in requests-oauth.

consider the following code:

# I'm assuming you have CONSUMER_KEY, CONSUMER_SECRET,
# TWITTER_TOKEN, TWITTER_SECRET already.

import requests
from oauth_hook.hook import OAuthHook
OAuthHook.consumer_key = CONSUMER_KEY
OAuthHook.consumer_secret = CONSUMER_SECRET
hook = OAuthHook(TWITTER_TOKEN, TWITTER_SECRET)

client = requests.session(hooks={'pre_request': hook})

r1 = client.get('http://api.twitter.com/1/account/rate_limit_status.json')
print(r1)

r2 = client.get('http://api.twitter.com/1/account/verify_credentials.json')
print(r2)

With requests 0.7.6, both requests return 200. With 0.8.x, the 2nd (and subsequent request) return HTTP/500.

I checked with keep-alive disabled, but it doesn't solve the issue. :(

AttributeError: 'Request' object has no attribute '_enc_params'

Hi,

I use the request-oauth library in a netflix webservice client library that I have written (pyflix2). One of the users in Windows has found that the oauth hook is throwing the following error:

Traceback (most recent call last):
  File "C:\Users\guthrie\AppData\Roaming\Python\Python27\site-packages\requests\hooks.py", line 47, in dispatch_hook
    hook_data = hook(hook_data) or hook_data
  File "C:\Users\guthrie\AppData\Roaming\Python\Python27\site-packages\oauth_hook\hook.py", line 207, in __call__
    if request._enc_params:
AttributeError: 'Request' object has no attribute '_enc_params'

I don't have windows environment to reproduce the bug, hoping that the error message would give you some pointer.

Original comment: amalakar/pyflix2#5 (comment)

Thanks,
Arup

Signature broken when using `params` argument with requests.get

See sample code below

import requests, oauth_hook


hook = oauth_hook.OAuthHook(
    consumer_key='abcde',
    consumer_secret='fegdslaje',
)
c = requests.session(hooks={'pre_request': hook})

url = 'http://localhost:8888/utils/haha'
r =  c.get(url, params={
    'limit': 3,
})
print r, 'using params'
print r.url 
print r.request.url
print r.request.full_url

# prints the following
# <Response [404]> using params
# http://localhost:8888/utils/haha?oauth_nonce=28493927&oauth_timestamp=1349201520&oauth_consumer_key=abcde&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&limit=3&oauth_signature=wA4HFWbSNGBuaafDIAznbe0pI7E%3D&limit=3
# http://localhost:8888/utils/haha?oauth_nonce=28493927&oauth_timestamp=1349201520&oauth_consumer_key=abcde&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&limit=3&oauth_signature=wA4HFWbSNGBuaafDIAznbe0pI7E%3D
# http://localhost:8888/utils/haha?oauth_nonce=28493927&oauth_timestamp=1349201520&oauth_consumer_key=abcde&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&limit=3&oauth_signature=wA4HFWbSNGBuaafDIAznbe0pI7E%3D&limit=3

r =  c.get(url, data={
    'limit': 3,
})
print r, 'using data'
print r.url #, r.request.url
print r.request.url
print r.request.full_url

# prints the following
# <Response [404]> using data
# http://localhost:8888/utils/haha?oauth_nonce=28493927&oauth_timestamp=1349201520&oauth_consumer_key=abcde&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&limit=3&oauth_signature=wA4HFWbSNGBuaafDIAznbe0pI7E%3D
# http://localhost:8888/utils/haha?oauth_nonce=28493927&oauth_timestamp=1349201520&oauth_consumer_key=abcde&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&limit=3&oauth_signature=wA4HFWbSNGBuaafDIAznbe0pI7E%3D
# http://localhost:8888/utils/haha?oauth_nonce=28493927&oauth_timestamp=1349201520&oauth_consumer_key=abcde&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&limit=3&oauth_signature=wA4HFWbSNGBuaafDIAznbe0pI7E%3D

When using params argument together with requests.get, the limit parameter gets duplicated in the final url of the get request, resulting in an invalid signature as well as incorrect argument (limit now = [3,3] rather than just [3]).

Value Error

ValueError: Error trying to decode a non urlencoded string. Found invalid characters: set([u'[', u']'])

Breaks with requests==0.12.1.

Looks like it expects ._enc_params on Request and also the .add_to_header() is missing on the new CookieJar as it is a dict. I believe the former is fixed in dev, but the latter still exists.

Can provide more details, but basically, upgrading to 0.12.1 should result in the same Exceptions.

Does not work with file uploads

I've created the "client" object as described in your documentation.

client = requests.session(hooks={'pre_request': oauth_hook})

However, when I try to upload a file to a server using client.post(URL, files={'filename':open('filename')}), the token and signature are not sent to the server. (I've used WIreshark to inspect the network traffic.)

Hence, the upload fails because of missing authentication.

Python 3 support

After a few tests, it seems it doesn't support python 3, while requests does. Is it planned ?

Can't import modules correctly

There is something inncorrect with the importing of modules,

python3.2
from oauth_hook import OAuthHook

leads to

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.2/lib/python3.2/site-packages/requests_oauth-0.4.1-py3.2.egg/oauth_hook/__init__.py", line 1, in <module>
    from hook import OAuthHook
ImportError: No module named hook

I tried moving the hook code to the init.py but then the same error comes up with auth.py and just creates more issues.

Also note: I tried it in python3.3 and same issue

    from oauth_hook import OAuthHook
  File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages/oauth_hook/__init__.py", line 1, in <module>
    from hook import OAuthHook
ImportError: No module named 'hook'

correct url getting 400 status

I'm actually not sure if this is an issue with requests or requests-oauth, but I'll post here first ;)

I'm creating a requests for which I get a 400 status returned from my API. The odd thing is, if I just print out the URL and paste it into my browser it works fine. My solution is to make another response, but it seems like there must be a bug somewhere :)

response = self.client.request(method, url, data=request_params, allow_redirects=True)
response.raise_for_status()
>> 400

response = self.client.request(method, url, data=request_params, allow_redirects=True)
response = requests.get(response.url)
response.raise_for_status()
print response
>> correct response json

Using only consumer_key and consumer_secret breaks POST requests

I can't really write an example, but if you only specify the consumer_key and consumer_secret in the hook, POST requests don't send data, they send it as part of the query string (even though the data isn't present in request.params either).

I've tried similar code with restkit and it worked fine with that.

OAuth verifier fails signature with header_auth=True

When fetching the access token using header auth, the signature base is incorrectly computed.

Doesn't work:

# create an oauth hook with the consumer and request token values
hook = OAuthHook(..., header_auth=True)
hook.token.verifier = get_verifier_from_redirect(previous_request)
sesh = requests.session(hooks={'pre_request': hook})
sesh.get('http://something.com/oauth/access_token').content == 'Invalid signing base'

Does work:

# create an oauth hook with the consumer and request token values
hook = OAuthHook(...)
sesh = requests.session(hooks={'pre_request': hook})
sesh.get('http://something.com/oauth/access_token', 
          params={'oauth_verifier': get_verifier_from_redirect(previous_request)})

No support for string data

This is what I think gldnspud's pull request was in reference to.

If you provide a string instead of a dict as the data arg to the request, then 'get_normalized_parameters()' will cause an exception when it tries to access 'request.data.items()'; which is a string not a dict.

This behaviour is supported by the requests library: "There are many times that you want to send data that is not form-encoded. If you pass in a string instead of a dict, that data will be posted directly".

I'm not sure on the best solution to this - I'm using a solution similar to gldnspud's, but am checking the whether request.data is a basestring instance rather than checking the "Content-Type" header explicitly.

Thanks, Phil.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.