diff --git a/README.rst b/README.rst index 60ce8735..12a75976 100644 --- a/README.rst +++ b/README.rst @@ -3,176 +3,49 @@ Requests-OAuthlib This project provides first-class OAuth library support for `Requests `_. -The OAuth workflow ------------------- +The OAuth 1 workflow +-------------------- -OAuth can seem overly complicated and it sure has its quirks. Luckily, +OAuth 1 can seem overly complicated and it sure has its quirks. Luckily, requests_oauthlib hides most of these and let you focus at the task at hand. -You will be forced to go through a few steps when you are using OAuth. Below is an -example of the most common OAuth workflow using HMAC-SHA1 signed requests where -the signature is supplied in the Authorization header. - -The example assumes an interactive prompt which is good for demonstration but in -practice you will likely be using a web application (which makes authorizing much -less awkward since you can simply redirect). - -0. Manual client signup with the OAuth provider (i.e. Google, Twitter) to get - a set of client credentials. Usually a client key and secret. Client might sometimes - be referred to as consumer. For example: - -.. code-block:: pycon - - >>> from __future__ import unicode_literals - >>> import requests - >>> from requests_oauthlib import OAuth1 - - >>> client_key = '...' - >>> client_secret = '...' - -1. Obtain a request token which will identify you (the client) in the next step. - At this stage you will only need your client key and secret. - -.. code-block:: pycon - - >>> oauth = OAuth1(client_key, client_secret=client_secret) - >>> request_token_url = 'https://api.twitter.com/oauth/request_token' - >>> r = requests.post(url=request_token_url, auth=oauth) - >>> r.content - "oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik&oauth_token_secret=Kd75W4OQfb2oJTV0vzGzeXftVAwgMnEK9MumzYcM" - >>> from urlparse import parse_qs - >>> credentials = parse_qs(r.content) - >>> resource_owner_key = credentials.get('oauth_token')[0] - >>> resource_owner_secret = credentials.get('oauth_token_secret')[0] - -2. Obtain authorization from the user (resource owner) to access their protected - resources (images, tweets, etc.). This is commonly done by redirecting the - user to a specific url to which you add the request token as a query parameter. - Note that not all services will give you a verifier even if they should. Also - the oauth_token given here will be the same as the one in the previous step. - -.. code-block:: pycon - - >>> authorize_url = 'https://api.twitter.com/oauth/authorize?oauth_token=' - >>> authorize_url = authorize_url + resource_owner_key - >>> print 'Please go here and authorize,', authorize_url - >>> verifier = raw_input('Please input the verifier') - -3. Obtain an access token from the OAuth provider. Save this token as it can be - re-used later. In this step we will re-use most of the credentials obtained - uptil this point. - -.. code-block:: pycon - - >>> oauth = OAuth1(client_key, - client_secret=client_secret, - resource_owner_key=resource_owner_key, - resource_owner_secret=resource_owner_secret, - verifier=verifier) - >>> access_token_url = 'https://api.twitter.com/oauth/access_token' - >>> r = requests.post(url=access_token_url, auth=oauth) - >>> r.content - "oauth_token=6253282-eWudHldSbIaelX7swmsiHImEL4KinwaGloHANdrY&oauth_token_secret=2EEfA6BG3ly3sR3RjE0IBSnlQu4ZrUzPiYKmrkVU" - >>> credentials = parse_qs(r.content) - >>> resource_owner_key = credentials.get('oauth_token')[0] - >>> resource_owner_secret = credentials.get('oauth_token_secret')[0] - -4. Access protected resources. OAuth1 access tokens typically do not expire - and may be re-used until revoked by the user or yourself. +Accessing protected resources using requests_oauthlib is as simple as: .. code-block:: pycon - >>> oauth = OAuth1(client_key, - client_secret=client_secret, - resource_owner_key=resource_owner_key, - resource_owner_secret=resource_owner_secret) + >>> from requests_oauthlib import OAuth1Session + >>> twitter = OAuth1Session('client_key', + client_secret='client_secret', + resource_owner_key='resource_owner_key', + resource_owner_secret='resource_owner_secret') >>> url = 'https://api.twitter.com/1/account/settings.json' - >>> r = requests.get(url=url, auth=oauth) - >>> # Enjoy =) - - -Signature placement - header, query or body? --------------------------------------------- - -OAuth takes many forms, so let's take a look at a few different forms: - -.. code-block:: python - - import requests - from requests_oauthlib import OAuth1 + >>> r = twitter.get(url) - url = u'https://api.twitter.com/1/account/settings.json' +Before accessing resources you will need to obtain a few credentials from your +provider (i.e. Twitter) and authorization from the user for whom you wish to +retrieve resources for. You can read all about this in the full +`OAuth 1 workflow guide on RTD `_. - client_key = u'...' - client_secret = u'...' - resource_owner_key = u'...' - resource_owner_secret = u'...' +The OAuth 2 workflow +-------------------- +OAuth 2 is generally simpler than OAuth 1 but comes in more flavours. The most +common being the Authorization Code Grant, also known as the WebApplication +flow. -Header signing (recommended): +Fetching a protected resource after obtaining an access token can be as simple as: -.. code-block:: python - - headeroauth = OAuth1(client_key, client_secret, - resource_owner_key, resource_owner_secret, - signature_type='auth_header') - r = requests.get(url, auth=headeroauth) - - - -Query signing: - -.. code-block:: python - - queryoauth = OAuth1(client_key, client_secret, - resource_owner_key, resource_owner_secret, - signature_type='query') - r = requests.get(url, auth=queryoauth) - - -Body signing: - -.. code-block:: python - - bodyoauth = OAuth1(client_key, client_secret, - resource_owner_key, resource_owner_secret, - signature_type='body') - - r = requests.post(url, auth=bodyoauth) - - -Signature types - HMAC (most common), RSA, Plaintext ----------------------------------------------------- - -OAuth1 defaults to using HMAC and examples can be found in the previous -sections. - -Plaintext work on the same credentials as HMAC and the only change you will -need to make when using it is to add signature_type='PLAINTEXT' -to the OAuth1 constructor: - -.. code-block:: python - - headeroauth = OAuth1(client_key, client_secret, - resource_owner_key, resource_owner_secret, - signature_method='PLAINTEXT') - -RSA is different in that it does not use client_secret nor -resource_owner_secret. Instead it uses public and private keys. The public key -is provided to the OAuth provider during client registration. The private key -is used to sign requests. The previous section can be summarized as: - -.. code-block:: python - - key = open("your_rsa_key.pem").read() +.. code-block:: pycon - queryoauth = OAuth1(client_key, signature_method=SIGNATURE_RSA, - rsa_key=key, signature_type='query') - headeroauth = OAuth1(client_key, signature_method=SIGNATURE_RSA, - rsa_key=key, signature_type='auth_header') - bodyoauth = OAuth1(client_key, signature_method=SIGNATURE_RSA, - rsa_key=key, signature_type='body') + >>> from requests_oauthlib import OAuth2Session + >>> google = OAuth2Session('client_id', token='token') + >>> url = 'https://www.googleapis.com/oauth2/v1/userinfo' + >>> r = google.get(url) +Before accessing resources you will need to obtain a few credentials from your +provider (i.e. Google) and authorization from the user for whom you wish to +retrieve resources for. You can read all about this in the full +`OAuth 2 workflow guide on RTD `_. Installation ------------- @@ -182,4 +55,3 @@ To install requests and requests_oauthlib you can use pip: .. code-block:: bash $ pip install requests requests_oauthlib - diff --git a/docs/index.rst b/docs/index.rst index 8a6b9dbb..363876cf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,8 @@ Contents: .. toctree:: :maxdepth: 2 + oauth1_workflow + oauth2_workflow api diff --git a/docs/oauth1_workflow.rst b/docs/oauth1_workflow.rst new file mode 100644 index 00000000..f529a05d --- /dev/null +++ b/docs/oauth1_workflow.rst @@ -0,0 +1,221 @@ +OAuth 1 Workflow +================ + +You will be forced to go through a few steps when you are using OAuth. Below is an +example of the most common OAuth workflow using HMAC-SHA1 signed requests where +the signature is supplied in the Authorization header. + +The example assumes an interactive prompt which is good for demonstration but in +practice you will likely be using a web application (which makes authorizing much +less awkward since you can simply redirect). + +The guide will show two ways of carrying out the OAuth1 workflow. One using the +authentication helper OAuth1 and the alternative using OAuth1Session. The latter +is usually more convenient and requires less code. + +Workflow example showing use of both OAuth1 and OAuth1Session +------------------------------------------------------------- + +0. Manual client signup with the OAuth provider (i.e. Google, Twitter) to get + a set of client credentials. Usually a client key and secret. Client might sometimes + be referred to as consumer. For example: + +.. code-block:: pycon + + >>> # Using OAuth1Session + >>> from requests_oauthlib import OAuth1Session + + >>> # Using OAuth1 auth helper + >>> import requests + >>> from requests_oauthlib import OAuth1 + + >>> client_key = '...' + >>> client_secret = '...' + +1. Obtain a request token which will identify you (the client) in the next step. + At this stage you will only need your client key and secret. + +.. code-block:: pycon + + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + + >>> # Using OAuth1Session + >>> oauth = OAuth1Session(client_key, client_secret=client_secret) + >>> oauth.fetch_request_token(request_token_url) + { + "oauth_token": "Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik", + "oauth_token_secret": "Kd75W4OQfb2oJTV0vzGzeXftVAwgMnEK9MumzYcM" + } + + >>> # Using OAuth1 auth helper + >>> oauth = OAuth1(client_key, client_secret=client_secret) + >>> r = requests.post(url=request_token_url, auth=oauth) + >>> r.content + "oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik&oauth_token_secret=Kd75W4OQfb2oJTV0vzGzeXftVAwgMnEK9MumzYcM" + >>> from urlparse import parse_qs + >>> credentials = parse_qs(r.content) + >>> resource_owner_key = credentials.get('oauth_token')[0] + >>> resource_owner_secret = credentials.get('oauth_token_secret')[0] + +2. Obtain authorization from the user (resource owner) to access their protected + resources (images, tweets, etc.). This is commonly done by redirecting the + user to a specific url to which you add the request token as a query parameter. + Note that not all services will give you a verifier even if they should. Also + the oauth_token given here will be the same as the one in the previous step. + +.. code-block:: pycon + + >>> base_authorization_url = 'https://api.twitter.com/oauth/authorize' + + >>> # Using OAuth1Session + >>> authorization_url = oauth.authorization_url(base_authorization_url) + >>> print 'Please go here and authorize,', authoriation_url + >>> redirect_response = raw_input('Paste the full redirect URL here: ') + >>> oauth_session.parse_authorization_response(redirect_response) + { + "oauth_token": "Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik", + "oauth_verifier": "sdflk3450FASDLJasd2349dfs" + } + + >>> # Using OAuth1 auth helper + >>> authorize_url = base_authorization_url + '?oauth_token=' + >>> authorize_url = authorize_url + resource_owner_key + >>> print 'Please go here and authorize,', authorize_url + >>> verifier = raw_input('Please input the verifier') + +3. Obtain an access token from the OAuth provider. Save this token as it can be + re-used later. In this step we will re-use most of the credentials obtained + uptil this point. + +.. code-block:: pycon + + >>> access_token_url = 'https://api.twitter.com/oauth/access_token' + + >>> # Using OAuth1Session + >>> oauth = OAuth1Session(client_key, + client_secret=client_secret, + resource_owner_key=resource_owner_key, + resource_owner_secret=resource_owner_secret, + verifier=verifier) + >>> oauth.fetch_access_token(access_token_url) + { + "oauth_token": "6253282-eWudHldSbIaelX7swmsiHImEL4KinwaGloHANdrY", + "oauth_token_secret": "2EEfA6BG3ly3sR3RjE0IBSnlQu4ZrUzPiYKmrkVU" + } + + >>> # Using OAuth1 auth helper + >>> oauth = OAuth1(client_key, + client_secret=client_secret, + resource_owner_key=resource_owner_key, + resource_owner_secret=resource_owner_secret, + verifier=verifier) + >>> r = requests.post(url=access_token_url, auth=oauth) + >>> r.content + "oauth_token=6253282-eWudHldSbIaelX7swmsiHImEL4KinwaGloHANdrY&oauth_token_secret=2EEfA6BG3ly3sR3RjE0IBSnlQu4ZrUzPiYKmrkVU" + >>> credentials = parse_qs(r.content) + >>> resource_owner_key = credentials.get('oauth_token')[0] + >>> resource_owner_secret = credentials.get('oauth_token_secret')[0] + +4. Access protected resources. OAuth1 access tokens typically do not expire + and may be re-used until revoked by the user or yourself. + +.. code-block:: pycon + + >>> protected_url = 'https://api.twitter.com/1/account/settings.json' + + >>> # Using OAuth1Session + >>> oauth = OAuth1Session(client_key, + client_secret=client_secret, + resource_owner_key=resource_owner_key, + resource_owner_secret=resource_owner_secret) + >>> r = oauth.get(protected_url) + + >>> # Using OAuth1 auth helper + >>> oauth = OAuth1(client_key, + client_secret=client_secret, + resource_owner_key=resource_owner_key, + resource_owner_secret=resource_owner_secret) + >>> r = requests.get(url=protected_url, auth=oauth) + + +Signature placement - header, query or body? +-------------------------------------------- + +OAuth takes many forms, so let's take a look at a few different forms: + +.. code-block:: python + + import requests + from requests_oauthlib import OAuth1 + + url = u'https://api.twitter.com/1/account/settings.json' + + client_key = u'...' + client_secret = u'...' + resource_owner_key = u'...' + resource_owner_secret = u'...' + + +Header signing (recommended): + +.. code-block:: python + + headeroauth = OAuth1(client_key, client_secret, + resource_owner_key, resource_owner_secret, + signature_type='auth_header') + r = requests.get(url, auth=headeroauth) + + + +Query signing: + +.. code-block:: python + + queryoauth = OAuth1(client_key, client_secret, + resource_owner_key, resource_owner_secret, + signature_type='query') + r = requests.get(url, auth=queryoauth) + + +Body signing: + +.. code-block:: python + + bodyoauth = OAuth1(client_key, client_secret, + resource_owner_key, resource_owner_secret, + signature_type='body') + + r = requests.post(url, auth=bodyoauth) + + +Signature types - HMAC (most common), RSA, Plaintext +---------------------------------------------------- + +OAuth1 defaults to using HMAC and examples can be found in the previous +sections. + +Plaintext work on the same credentials as HMAC and the only change you will +need to make when using it is to add signature_type='PLAINTEXT' +to the OAuth1 constructor: + +.. code-block:: python + + headeroauth = OAuth1(client_key, client_secret, + resource_owner_key, resource_owner_secret, + signature_method='PLAINTEXT') + +RSA is different in that it does not use client_secret nor +resource_owner_secret. Instead it uses public and private keys. The public key +is provided to the OAuth provider during client registration. The private key +is used to sign requests. The previous section can be summarized as: + +.. code-block:: python + + key = open("your_rsa_key.pem").read() + + queryoauth = OAuth1(client_key, signature_method=SIGNATURE_RSA, + rsa_key=key, signature_type='query') + headeroauth = OAuth1(client_key, signature_method=SIGNATURE_RSA, + rsa_key=key, signature_type='auth_header') + bodyoauth = OAuth1(client_key, signature_method=SIGNATURE_RSA, + rsa_key=key, signature_type='body') diff --git a/docs/oauth2_workflow.rst b/docs/oauth2_workflow.rst new file mode 100644 index 00000000..0090ffc5 --- /dev/null +++ b/docs/oauth2_workflow.rst @@ -0,0 +1,173 @@ +OAuth 2 Workflow +================ + +.. contents:: + :depth: 3 + :local: + +Web Application Flow +-------------------- + +The steps below outline how to use the default Authorization Grant Type flow to +obtain an access token and fetch a protected resource. In this example +the provider is Google and the protected resource the user profile. + +0. Obtaining credentials from your OAuth provider manually. You will need + at minimum a ``client_id`` but likely also a ``client_secret``. During + this process you might also be required to register a default redirect + URI to be used by your application: + +.. code-block:: pycon + + >>> client_id = 'your_client_id' + >>> client_secret = 'your_client_secret' + >>> redirect_uri = 'https://your.callback/uri' + +1. User authorization through redirection. First we will create an + authorization url from the base URL given by the provider and + the credentials previously obtained. In addition most providers will + request that you ask for access to a certain scope, in this example + we will ask Google for access to the email address of the user and the + users profile. + +.. code-block:: pycon + + # Note that these are Google specific scopes + >>> scope = ['https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile'] + >>> oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, + scope=scope) + >>> authorization_url, state = oauth.authorization_url( + 'https://accounts.google.com/o/oauth2/auth', + # access_type and approval_prompt are Google specific extra + # parameters. + access_type="offline", approval_prompt="force") + + >>> print 'Please go to %s and authorize access.' % authorization_url + >>> authorization_response = raw_input('Enter the full callback URL') + +2. Fetch an access token from the provider using the authorization code + obtained during user authorization. + +.. code-block:: pycon + + >>> token = oauth.fetch_token( + 'https://accounts.google.com/o/oauth2/token', + authorization_response=authorization_response, + # Google specific extra parameter used for client + # authentication + client_secret=secret) + +3. Access protected resources using the access token you just obtained. + For example, get the users profile info. + +.. code-block:: pycon + + >>> r = oauth.get('https://www.googleapis.com/oauth2/v1/userinfo') + >>> # Enjoy =) + + +Available workflows +------------------- + +There are four core work flows: + +1. Authorization Code Grant (WebApplication flow) - see section above. +2. Implicit Grant (MobileApplication flow) - docs coming soon. +3. Resource Owner Password Credentials Grant (LegacyApplication flow) - docs coming soon. +4. Client Credentials Grant (BackendApplication flow) - docs coming soon. + +Refreshing tokens +----------------- + +Certain providers will give you a ``refresh_token`` along with the +``access_token``. These can be used to directly fetch new access tokens without +going through the normal OAuth workflow. ``requests-oauthlib`` provides three +methods of obtaining refresh tokens. All of these are dependant on you +specifying an accurate ``expires_in`` in the token. + +``expires_in`` is a credential given with the access and refresh token +indiciating in how many seconds from now the access token expires. Commonly, +access tokens expire after an hour an the ``expires_in`` would be ``3600``. +Without this it is impossible for ``requests-oauthlib`` to know when a token +is expired as the status code of a request failing due to token expiration is +not defined. + +If you are not interested in token refreshing, always pass in a positive value +for ``expires_in`` or omit it entirely. + +(ALL) Define the token, token saver and needed credentials +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: pycon + + >>> token = { + ... 'access_token': 'eswfld123kjhn1v5423', + ... 'refresh_token': 'asdfkljh23490sdf', + ... 'token_type': 'Bearer', + ... 'expires_in': '-30', # initially 3600, need to be updated by you + ... } + >>> client_id = 'foo' + >>> refresh_url = 'https://provider.com/token' + >>> protected_url = 'https://provider.com/secret' + + >>> # most providers will ask you for extra credentials to be passed along + >>> # when refreshing tokens, usually for authentication purposes. + >>> extra = { + ... 'client_id': client_id, + ... 'client_secret': 'potato', + ... } + + >>> # After updating the token you will most likely want to save it. + >>> def token_saver(token): + ... # save token in database / session + +(First) Define Try-Catch TokenExpiredError on each request +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the most basic version in which an error is raised when refresh +is necessary but refreshing is done manually. + +.. code-block:: pycon + + >>> from requests_oauthlib import OAuth2Session + >>> from oauthlib.oauth2 import TokenExpiredError + >>> try: + ... client = OAuth2Session(client_id, token=token) + ... r = client.get(protected_url) + >>> except TokenExpiredError as e: + ... token = client.refresh_token(refresh_url, **extra) + ... token_saver(token) + >>> client = OAuth2Session(client_id, token=token) + >>> r = client.get(protected_url) + +(Second) Define automatic token refresh automatic but update manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the, arguably awkward, middle between the basic and convenient refresh +methods in which a token is automatically refreshed, but saving the new token +is done manually. + +.. code-block:: pycon + + >>> from requests_oauthlib import OAuth2Session, TokenUpdated + >>> try: + ... client = OAuth2Session(client_id, token=token, + ... auto_refresh_kwargs=extra, auto_refresh_url=refresh_url) + ... r = client.get(protected_url) + >>> except TokenUpdated as e: + ... token_saver(e.token) + +(Third, Recommended) Define automatic token refresh and update +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The third and recommended method will automatically fetch refresh tokens and +save them. It requires no exception catching and results in clean code. Remember +however that you still need to update ``expires_in`` to trigger the refresh. + +.. code-block:: pycon + + >>> from requests_oauthlib import OAuth2Session + >>> client = OAuth2Session(client_id, token=token, auto_refresh_url=refresh_url, + ... auto_refresh_kwargs=extra, token_updater=token_saver) + >>> r = client.get(protected_url)