1. David Larlet
  2. django-oauth-plus

Wiki

Clone wiki

django-oauth-plus / protocol_example

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:

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', user=jane)
>>> 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'
>>> parameters['oauth_nonce'] = 'requestnoncevideos'
>>> 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'
>>> parameters['oauth_nonce'] = 'requestnoncewrongcallback'
>>> response = c.get("/oauth/request_token/", parameters)
>>> response.status_code
401
>>> response.content
'Invalid callback URL.'

If you do not provide any callback (i.e. oob), the Service Provider SHOULD display the value of the verification code:

>>> parameters['oauth_callback'] = 'oob'
>>> parameters['oauth_nonce'] = 'requestnonceoob'
>>> response = c.get("/oauth/request_token/", parameters)
>>> response.status_code
200
>>> response.content
'oauth_token_secret=...&oauth_token=...&oauth_callback_confirmed=true'
>>> oobtoken = list(Token.objects.all())[-1]
>>> oobtoken.key in response.content, oobtoken.secret in response.content
(True, True)
>>> oobtoken.callback, oobtoken.callback_confirmed
(None, False)

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 with params: oauth_token=...'

>>> # 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 = Token.objects.get(key=token.key)
>>> 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?oauth_verifier=...&error=Access+not+granted+by+user.'
>>> 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,
...     'scope': 'photos',
... }
>>> 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 parameters because the Request Token has been deleted once Access Token is created:

>>> response = c.get("/oauth/access_token/", parameters)
>>> response.status_code
400
>>> response.content
'Invalid request token.'

The Consumer will not be able to request another Access Token with a missing or invalid verifier:

>>> new_request_token = Token.objects.create_token(
...     token_type=Token.REQUEST,
...     timestamp=str(int(time.time())),
...     consumer=Consumer.objects.get(key=CONSUMER_KEY),
...     user=jane,
...     resource=Resource.objects.get(name='photos'))
>>> new_request_token.is_approved = True
>>> new_request_token.save()
>>> parameters['oauth_token'] = new_request_token.key
>>> parameters['oauth_signature'] = '%s&%s' % (CONSUMER_SECRET, new_request_token.secret)
>>> parameters['oauth_verifier'] = 'invalidverifier'
>>> response = c.get("/oauth/access_token/", parameters)
>>> response.status_code
400
>>> response.content
'Invalid OAuth verifier.'
>>> parameters['oauth_verifier'] = new_request_token.verifier # restore

The Consumer will not be able to request an Access Token if the token is not approved:

>>> new_request_token.is_approved = False
>>> new_request_token.save()
>>> parameters['oauth_nonce'] = 'anotheraccessnonce'
>>> response = c.get("/oauth/access_token/", parameters)
>>> response.status_code
400
>>> response.content
'Request Token not approved by the user.'

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):

>>> import oauth2 as oauth
>>> oauth_request = oauth.Request.from_token_and_callback(access_token,
...     http_url='http://testserver/oauth/photo/', parameters=parameters)
>>> signature_method = oauth.SignatureMethod_HMAC_SHA1()
>>> signature = signature_method.sign(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()
>>> # Note that an "Invalid signature" error will be raised here if the
>>> # token is not revoked by Jane because we reuse a previously used one.
>>> parameters['oauth_signature'] = signature
>>> parameters['oauth_nonce'] = 'yetanotheraccessresourcenonce'
>>> response = c.get("/oauth/photo/", parameters)
>>> response.status_code
401
>>> response.content
'Invalid access token: ...'

Updated