Commits

David Larlet committed 5d75cdd

Update to 1.0a doc

Comments (0)

Files changed (1)

 Consumers. More generally, OAuth creates a freely-implementable and generic 
 methodology for API authentication.
 
-.. _`OAuth protocol`: http://oauth.net/core/1.0/
+.. _`OAuth protocol`: http://oauth.net/core/1.0a
 
-.. warning::
-    An OAuth security issue `has been identified`_. Please read the `advisory`_, issued on April 23, 2009. This issue hasn't been (yet!) fixed in this implementation of OAuth in Django, patches welcome :)
-
-.. _`has been identified`: http://blog.oauth.net/2009/04/22/acknowledgement-of-the-oauth-security-issue/
-.. _`advisory`: http://oauth.net/advisories/2009-1
 
 Authenticating with OAuth
 =========================
 
 .. _`OAuth Python library`: http://oauth.googlecode.com/svn/code/python/oauth/
 
-You can find a custom version of the module at the root level of django-oauth.  You can install both with ``pip install django-oauth`` or ``easy_install django-oauth``
+You can find a custom version of the module at the root level of django-oauth.
 
-You need to add the ``'oauth_provider'`` application in your settings and to 
-sync your database with the ``syncdb`` command. Then add it to your 
+You need to specify the OAuth provider application in your settings and to 
+sync your database thanks to the ``syncdb`` command. Then add it to your 
 URLs::
 
     # urls.py
     ...
 
 
-Protocol Example
-================
+Protocol Example 1.0
+====================
+
+.. warning::
+    DUE TO THE SECURITY ISSUE, THIS EXAMPLE IS NOT THE RECOMMENDED WAY ANYMORE.
+    SEE BELOW FOR A MORE ROBUST EXAMPLE WHICH IS 1.0a COMPLIANT.
 
 In this example, the Service Provider photos.example.net is a photo sharing 
 website, and the Consumer printer.example.com is a photo printing website. 
     >>> response.content
     'Resource videos does not exist.'
 
-If a 'scope' parameter is not provided, oauth_provider will default its value to 'all'.  So you may want to create a Resource named 'all'.
-
 
 Requesting User Authorization
 -----------------------------
     >>> response.status_code
     401
     >>> response.content
-    'Consumer key or token key does not match. Make sure your request token is approved too.'
+    'Consumer key or token key does not match. Make sure your request token is approved. Check your verifier too if you use OAuth 1.0a.'
 
 
 Accessing Protected Resources
 The Consumer is now ready to request the private photo. Since the photo URL is 
 not secure (HTTP), it must use HMAC-SHA1.
 
-Protecting Resources
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-You can require oauth permission to a view, by using a decorator::
-
-    from oauth_provider.decorators import oauth_required
-    @oauth_required
-    def get_photo(request, id):
-    ...
-
-
 Generating Signature Base String
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
     >>> response.content
     'Invalid access token: ...'
 
+
+Clean up
+--------
+
+Remove created models' instances to be able to launch 1.0a tests just below::
+
+    >>> Token.objects.all().delete()
+    >>> Resource.objects.all().delete()
+    >>> Consumer.objects.all().delete()
+    >>> Nonce.objects.all().delete()
+    >>> User.objects.all().delete()
+
+
+
+
+
+
+
+Protocol Example 1.0a
+=====================
+
+.. warning::
+    THIS IS THE RECOMMENDED WAY TO USE THIS APPLICATION.
+
+This example is exactly the same as 1.0 except it uses newly introduced
+arguments to be 1.0a compatible and fix the security issue.
+
+An account for Jane is necessary::
+
+    >>> from django.contrib.auth.models import User
+    >>> jane = User.objects.create_user('jane', 'jane@example.com', 'toto')
+
+
+Documentation and Registration
+------------------------------
+
+The Service Provider documentation explains how to register for a Consumer Key 
+and Consumer Secret, and declares the following URLs:
+
+    * Request Token URL:
+      http://photos.example.net/request_token, using HTTP POST
+    * User Authorization URL:
+      http://photos.example.net/authorize, using HTTP GET
+    * Access Token URL:
+      http://photos.example.net/access_token, using HTTP POST
+    * Photo (Protected Resource) URL:
+      http://photos.example.net/photo with required parameter file and 
+      optional parameter size
+
+The Service Provider declares support for the HMAC-SHA1 signature method for 
+all requests, and PLAINTEXT only for secure (HTTPS) requests.
+
+The Consumer printer.example.com already established a Consumer Key and 
+Consumer Secret with photos.example.net and advertizes its printing services 
+for photos stored on photos.example.net. The Consumer registration is:
+
+    * Consumer Key: dpf43f3p2l4k3l03
+    * Consumer Secret: kd94hf93k423kf44
+
+We need to create the Protected Resource and the Consumer first::
+
+    >>> from oauth_provider.models import Resource, Consumer
+    >>> resource = Resource(name='photos', url='/oauth/photo/')
+    >>> resource.save()
+    >>> CONSUMER_KEY = 'dpf43f3p2l4k3l03'
+    >>> CONSUMER_SECRET = 'kd94hf93k423kf44'
+    >>> consumer = Consumer(key=CONSUMER_KEY, secret=CONSUMER_SECRET, 
+    ...                     name='printer.example.com')
+    >>> consumer.save()
+
+
+Obtaining a Request Token
+-------------------------
+
+After Jane informs printer.example.com that she would like to print her 
+vacation photo stored at photos.example.net, the printer website tries to 
+access the photo and receives HTTP 401 Unauthorized indicating it is private. 
+The Service Provider includes the following header with the response::
+
+    >>> from django.test.client import Client
+    >>> c = Client()
+    >>> response = c.get("/oauth/request_token/")
+    >>> response.status_code
+    401
+    >>> # depends on REALM_KEY_NAME Django setting
+    >>> response._headers['www-authenticate']
+    ('WWW-Authenticate', 'OAuth realm=""')
+    >>> response.content
+    'Invalid request parameters.'
+
+The Consumer sends the following HTTP POST request to the Service Provider::
+
+    >>> import time
+    >>> parameters = {
+    ...     'oauth_consumer_key': CONSUMER_KEY,
+    ...     'oauth_signature_method': 'PLAINTEXT',
+    ...     'oauth_signature': '%s&' % CONSUMER_SECRET,
+    ...     'oauth_timestamp': str(int(time.time())),
+    ...     'oauth_nonce': 'requestnonce',
+    ...     'oauth_version': '1.0',
+    ...     'oauth_callback': 'http://printer.example.com/request_token_ready',
+    ...     'scope': 'photos', # custom argument to specify Protected Resource
+    ... }
+    >>> response = c.get("/oauth/request_token/", parameters)
+
+The Service Provider checks the signature and replies with an unauthorized 
+Request Token in the body of the HTTP response::
+
+    >>> response.status_code
+    200
+    >>> response.content
+    'oauth_token_secret=...&oauth_token=...&oauth_callback_confirmed=true'
+    >>> from oauth_provider.models import Token
+    >>> token = list(Token.objects.all())[-1]
+    >>> token.key in response.content, token.secret in response.content
+    (True, True)
+    >>> token.callback, token.callback_confirmed
+    (u'http://printer.example.com/request_token_ready', True)
+
+If you try to access a resource with a wrong scope, it will return an error::
+
+    >>> parameters['scope'] = 'videos'
+    >>> response = c.get("/oauth/request_token/", parameters)
+    >>> response.status_code
+    401
+    >>> response.content
+    'Resource videos does not exist.'
+    >>> parameters['scope'] = 'photos' # restore
+
+If you try to put a wrong callback, it will return an error::
+
+    >>> parameters['oauth_callback'] = 'wrongcallback'
+    >>> response = c.get("/oauth/request_token/", parameters)
+    >>> response.status_code
+    401
+    >>> response.content
+    'Invalid callback URL.'
+
+
+Requesting User Authorization
+-----------------------------
+
+The Consumer redirects Jane's browser to the Service Provider User 
+Authorization URL to obtain Jane's approval for accessing her private photos.
+
+The Service Provider asks Jane to sign-in using her username and password::
+
+    >>> parameters = {
+    ...     'oauth_token': token.key,
+    ... }
+    >>> response = c.get("/oauth/authorize/", parameters)
+    >>> response.status_code
+    302
+    >>> response['Location']
+    'http://.../accounts/login/?next=/oauth/authorize/%3Foauth_token%3D...'
+    >>> token.key in response['Location']
+    True
+
+If successful, asks her if she approves granting printer.example.com access to 
+her private photos. If Jane approves the request, the Service Provider 
+redirects her back to the Consumer's callback URL::
+
+    >>> c.login(username='jane', password='toto')
+    True
+    >>> token.is_approved
+    0
+    >>> response = c.get("/oauth/authorize/", parameters)
+    >>> response.status_code
+    200
+    >>> response.content
+    'Fake authorize view for printer.example.com.'
+    
+    >>> # fake authorization by the user
+    >>> parameters['authorize_access'] = 1
+    >>> response = c.post("/oauth/authorize/", parameters)
+    >>> response.status_code
+    302
+    >>> response['Location']
+    'http://printer.example.com/request_token_ready?oauth_verifier=...&oauth_token=...'
+    >>> token = list(Token.objects.all())[-1]
+    >>> token.key in response['Location']
+    True
+    >>> token.is_approved
+    1
+
+    >>> # without session parameter (previous POST removed it)
+    >>> response = c.post("/oauth/authorize/", parameters)
+    >>> response.status_code
+    401
+    >>> response.content
+    'Action not allowed.'
+    
+    >>> # fake access not granted by the user (set session parameter again)
+    >>> response = c.get("/oauth/authorize/", parameters)
+    >>> parameters['authorize_access'] = 0
+    >>> response = c.post("/oauth/authorize/", parameters)
+    >>> response.status_code
+    302
+    >>> response['Location']
+    'http://printer.example.com/request_token_ready?error=Access%20not%20granted%20by%20user.'
+    >>> c.logout()
+
+With OAuth 1.0a, the callback argument can be set to "oob" (out-of-band), 
+you can specify your own default callback view with the
+``OAUTH_CALLBACK_VIEW`` setting::
+
+    >>> from oauth_provider.consts import OUT_OF_BAND
+    >>> token.callback = OUT_OF_BAND
+    >>> token.save()
+    >>> parameters = {
+    ...     'oauth_token': token.key,
+    ... }
+    >>> c.login(username='jane', password='toto')
+    True
+    >>> response = c.get("/oauth/authorize/", parameters)
+    >>> parameters['authorize_access'] = 0
+    >>> response = c.post("/oauth/authorize/", parameters)
+    >>> response.status_code
+    200
+    >>> response.content
+    'Fake callback view.'
+    >>> c.logout()
+
+
+Obtaining an Access Token
+-------------------------
+
+Now that the Consumer knows Jane approved the Request Token, it asks the 
+Service Provider to exchange it for an Access Token::
+
+    >>> c = Client()
+    >>> parameters = {
+    ...     'oauth_consumer_key': CONSUMER_KEY,
+    ...     'oauth_token': token.key,
+    ...     'oauth_signature_method': 'PLAINTEXT',
+    ...     'oauth_signature': '%s&%s' % (CONSUMER_SECRET, token.secret),
+    ...     'oauth_timestamp': str(int(time.time())),
+    ...     'oauth_nonce': 'accessnonce',
+    ...     'oauth_version': '1.0',
+    ...     'oauth_verifier': token.verifier,
+    ... }
+    >>> response = c.get("/oauth/access_token/", parameters)
+
+.. note::
+    You can use HTTP Authorization header, if you provide both, header will be
+    checked before parameters. It depends on your needs.
+
+The Service Provider checks the signature and replies with an Access Token in 
+the body of the HTTP response::
+
+    >>> response.status_code
+    200
+    >>> response.content
+    'oauth_token_secret=...&oauth_token=...'
+    >>> access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1]
+    >>> access_token.key in response.content
+    True
+    >>> access_token.secret in response.content
+    True
+    >>> access_token.user.username
+    u'jane'
+
+The Consumer will not be able to request another Access Token with the same
+Nonce::
+
+    >>> from oauth_provider.models import Nonce
+    >>> Nonce.objects.all()
+    [<Nonce: Nonce accessnonce for ...>]
+    >>> response = c.get("/oauth/access_token/", parameters)
+    >>> response.status_code
+    401
+    >>> response.content
+    'Nonce already used: accessnonce'
+
+Nor with a missing/invalid verifier::
+
+    >>> parameters['oauth_nonce'] = 'yetanotheraccessnonce'
+    >>> parameters['oauth_verifier'] = 'invalidverifier'
+    >>> response = c.get("/oauth/access_token/", parameters)
+    >>> response.status_code
+    401
+    >>> response.content
+    'Consumer key or token key does not match. Make sure your request token is approved. Check your verifier too if you use OAuth 1.0a.'
+    >>> parameters['oauth_verifier'] = token.verifier # restore
+
+The Consumer will not be able to request an Access Token if the token is not
+approved::
+
+    >>> parameters['oauth_nonce'] = 'anotheraccessnonce'
+    >>> token.is_approved = False
+    >>> token.save()
+    >>> response = c.get("/oauth/access_token/", parameters)
+    >>> response.status_code
+    401
+    >>> response.content
+    'Consumer key or token key does not match. Make sure your request token is approved. Check your verifier too if you use OAuth 1.0a.'
+
+
+Accessing Protected Resources
+-----------------------------
+
+The Consumer is now ready to request the private photo. Since the photo URL is 
+not secure (HTTP), it must use HMAC-SHA1.
+
+Generating Signature Base String
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To generate the signature, it first needs to generate the Signature Base 
+String. The request contains the following parameters (oauth_signature 
+excluded) which are ordered and concatenated into a normalized string::
+
+    >>> parameters = {
+    ...     'oauth_consumer_key': CONSUMER_KEY,
+    ...     'oauth_token': access_token.key,
+    ...     'oauth_signature_method': 'HMAC-SHA1',
+    ...     'oauth_timestamp': str(int(time.time())),
+    ...     'oauth_nonce': 'accessresourcenonce',
+    ...     'oauth_version': '1.0',
+    ... }
+
+
+Calculating Signature Value
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+HMAC-SHA1 produces the following digest value as a base64-encoded string 
+(using the Signature Base String as text and kd94hf93k423kf44&pfkkdhi9sl3r4s00 
+as key)::
+
+    >>> from oauth.oauth import OAuthRequest, OAuthSignatureMethod_HMAC_SHA1
+    >>> oauth_request = OAuthRequest.from_token_and_callback(access_token,
+    ...     http_url='http://testserver/oauth/photo/', parameters=parameters)
+    >>> signature_method = OAuthSignatureMethod_HMAC_SHA1()
+    >>> signature = signature_method.build_signature(oauth_request, consumer, 
+    ...                                                 access_token)
+
+
+Requesting Protected Resource
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+All together, the Consumer request for the photo is::
+
+    >>> parameters['oauth_signature'] = signature
+    >>> response = c.get("/oauth/photo/", parameters)
+    >>> response.status_code
+    200
+    >>> response.content
+    'Protected Resource access!'
+
+Otherwise, an explicit error will be raised::
+
+    >>> parameters['oauth_signature'] = 'wrongsignature'
+    >>> parameters['oauth_nonce'] = 'anotheraccessresourcenonce'
+    >>> response = c.get("/oauth/photo/", parameters)
+    >>> response.status_code
+    401
+    >>> response.content
+    'Invalid signature. Expected signature base string: GET&http%3A%2F%2F...%2Foauth%2Fphoto%2F&oauth_...'
+
+    >>> response = c.get("/oauth/photo/")
+    >>> response.status_code
+    401
+    >>> response.content
+    'Invalid request parameters.'
+
+
+Revoking Access
+---------------
+
+If Jane deletes the Access Token of printer.example.com, the Consumer will not 
+be able to access the Protected Resource anymore::
+
+    >>> access_token.delete()
+    >>> parameters['oauth_signature'] = signature
+    >>> parameters['oauth_nonce'] = 'yetanotheraccessresourcenonce'
+    >>> response = c.get("/oauth/photo/", parameters)
+    >>> response.status_code
+    401
+    >>> response.content
+    'Invalid access token: ...'
+
 }}}