Commits

David Larlet committed a31148e

Switching to python-oauth2 thanks to the work done by Thomas Johansson on django-piston. Most of the work is done (tests pass) but it requires some cleanup, try it at your own risk!
I tried to keep previous views' logic to ease the migration from django-oauth but it'll probably require some extra work from your side.
Dependencies are now handled by pip, see requirements.txt

  • Participants
  • Parent commits 549a34c

Comments (0)

Files changed (12)

 Ariel Nunez
 Jesper Noerh
 Toby White
+Thomas Johansson

File oauth/__init__.py

Empty file removed.

File oauth/oauth.py

-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
-import binascii
-
-
-VERSION = '1.0' # Hi Blaine!
-HTTP_METHOD = 'GET'
-SIGNATURE_METHOD = 'PLAINTEXT'
-
-
-class OAuthError(RuntimeError):
-    """Generic exception class."""
-    def __init__(self, message='OAuth error occured.'):
-        self.message = message
-
-def build_authenticate_header(realm=''):
-    """Optional WWW-Authenticate header (401 error)"""
-    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-def escape(s):
-    """Escape a URL including any /."""
-    return urllib.quote(s, safe='~')
-
-def _utf8_str(s):
-    """Convert unicode to utf-8."""
-    if isinstance(s, unicode):
-        return s.encode("utf-8")
-    else:
-        return str(s)
-
-def generate_timestamp():
-    """Get seconds since epoch (UTC)."""
-    return int(time.time())
-
-def generate_nonce(length=8):
-    """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-def generate_verifier(length=8):
-    """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-
-class OAuthConsumer(object):
-    """Consumer of OAuth authentication.
-
-    OAuthConsumer is a data type that represents the identity of the Consumer
-    via its shared secret with the Service Provider.
-
-    """
-    key = None
-    secret = None
-
-    def __init__(self, key, secret):
-        self.key = key
-        self.secret = secret
-
-
-class OAuthToken(object):
-    """OAuthToken is a data type that represents an End User via either an access
-    or request token.
-    
-    key -- the token
-    secret -- the token secret
-
-    """
-    key = None
-    secret = None
-    callback = None
-    callback_confirmed = None
-    verifier = None
-
-    def __init__(self, key, secret):
-        self.key = key
-        self.secret = secret
-
-    def set_callback(self, callback):
-        self.callback = callback
-        self.callback_confirmed = 'true'
-
-    def set_verifier(self, verifier=None):
-        if verifier is not None:
-            self.verifier = verifier
-        else:
-            self.verifier = generate_verifier()
-
-    def get_callback_url(self):
-        if self.callback and self.verifier:
-            # Append the oauth_verifier.
-            parts = urlparse.urlparse(self.callback)
-            scheme, netloc, path, params, query, fragment = parts[:6]
-            if query:
-                query = '%s&oauth_verifier=%s' % (query, self.verifier)
-            else:
-                query = 'oauth_verifier=%s' % self.verifier
-            return urlparse.urlunparse((scheme, netloc, path, params,
-                query, fragment))
-        return self.callback
-
-    def to_string(self):
-        data = {
-            'oauth_token': self.key,
-            'oauth_token_secret': self.secret,
-        }
-        if self.callback_confirmed is not None:
-            data['oauth_callback_confirmed'] = self.callback_confirmed
-        return urllib.urlencode(data)
- 
-    def from_string(s):
-        """ Returns a token from something like:
-        oauth_token_secret=xxx&oauth_token=xxx
-        """
-        params = cgi.parse_qs(s, keep_blank_values=False)
-        key = params['oauth_token'][0]
-        secret = params['oauth_token_secret'][0]
-        token = OAuthToken(key, secret)
-        try:
-            token.callback_confirmed = params['oauth_callback_confirmed'][0]
-        except KeyError:
-            pass # 1.0, no callback confirmed.
-        return token
-    from_string = staticmethod(from_string)
-
-    def __str__(self):
-        return self.to_string()
-
-
-class OAuthRequest(object):
-    """OAuthRequest represents the request and can be serialized.
-
-    OAuth parameters:
-        - oauth_consumer_key 
-        - oauth_token
-        - oauth_signature_method
-        - oauth_signature 
-        - oauth_timestamp 
-        - oauth_nonce
-        - oauth_version
-        - oauth_verifier
-        ... any additional parameters, as defined by the Service Provider.
-    """
-    parameters = None # OAuth parameters.
-    http_method = HTTP_METHOD
-    http_url = None
-    version = VERSION
-
-    def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
-        self.http_method = http_method
-        self.http_url = http_url
-        self.parameters = parameters or {}
-
-    def set_parameter(self, parameter, value):
-        self.parameters[parameter] = value
-
-    def get_parameter(self, parameter):
-        try:
-            return self.parameters[parameter]
-        except:
-            raise OAuthError('Parameter not found: %s' % parameter)
-
-    def _get_timestamp_nonce(self):
-        return self.get_parameter('oauth_timestamp'), self.get_parameter(
-            'oauth_nonce')
-
-    def get_nonoauth_parameters(self):
-        """Get any non-OAuth parameters."""
-        parameters = {}
-        for k, v in self.parameters.iteritems():
-            # Ignore oauth parameters.
-            if k.find('oauth_') < 0:
-                parameters[k] = v
-        return parameters
-
-    def to_header(self, realm=''):
-        """Serialize as a header for an HTTPAuth request."""
-        auth_header = 'OAuth realm="%s"' % realm
-        # Add the oauth parameters.
-        if self.parameters:
-            for k, v in self.parameters.iteritems():
-                if k[:6] == 'oauth_':
-                    auth_header += ', %s="%s"' % (k, escape(str(v)))
-        return {'Authorization': auth_header}
-
-    def to_postdata(self):
-        """Serialize as post data for a POST request."""
-        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
-            for k, v in self.parameters.iteritems()])
-
-    def to_url(self):
-        """Serialize as a URL for a GET request."""
-        return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
-
-    def get_normalized_parameters(self):
-        """Return a string that contains the parameters that must be signed."""
-        params = self.parameters
-        try:
-            # Exclude the signature if it exists.
-            del params['oauth_signature']
-        except:
-            pass
-        # Escape key values before sorting.
-        key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
-            for k,v in params.items()]
-        # Sort lexicographically, first after key, then after value.
-        key_values.sort()
-        # Combine key value pairs into a string.
-        return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
-
-    def get_normalized_http_method(self):
-        """Uppercases the http method."""
-        return self.http_method.upper()
-
-    def get_normalized_http_url(self):
-        """Parses the URL and rebuilds it to be scheme://host/path."""
-        parts = urlparse.urlparse(self.http_url)
-        scheme, netloc, path = parts[:3]
-        # Exclude default port numbers.
-        if scheme == 'http' and netloc[-3:] == ':80':
-            netloc = netloc[:-3]
-        elif scheme == 'https' and netloc[-4:] == ':443':
-            netloc = netloc[:-4]
-        return '%s://%s%s' % (scheme, netloc, path)
-
-    def sign_request(self, signature_method, consumer, token):
-        """Set the signature parameter to the result of build_signature."""
-        # Set the signature method.
-        self.set_parameter('oauth_signature_method',
-            signature_method.get_name())
-        # Set the signature.
-        self.set_parameter('oauth_signature',
-            self.build_signature(signature_method, consumer, token))
-
-    def build_signature(self, signature_method, consumer, token):
-        """Calls the build signature method within the signature method."""
-        return signature_method.build_signature(self, consumer, token)
-
-    def from_request(http_method, http_url, headers=None, parameters=None,
-            query_string=None):
-        """Combines multiple parameter sources."""
-        if parameters is None:
-            parameters = {}
-
-        # Headers
-        if headers and 'Authorization' in headers:
-            auth_header = headers['Authorization']
-            # Check that the authorization header is OAuth.
-            if auth_header[:6] == 'OAuth ':
-                auth_header = auth_header[6:]
-                try:
-                    # Get the parameters from the header.
-                    header_params = OAuthRequest._split_header(auth_header)
-                    parameters.update(header_params)
-                except:
-                    raise OAuthError('Unable to parse OAuth parameters from '
-                        'Authorization header.')
-
-        # GET or POST query string.
-        if query_string:
-            query_params = OAuthRequest._split_url_string(query_string)
-            parameters.update(query_params)
-
-        # URL parameters.
-        param_str = urlparse.urlparse(http_url)[4] # query
-        url_params = OAuthRequest._split_url_string(param_str)
-        parameters.update(url_params)
-
-        if parameters:
-            return OAuthRequest(http_method, http_url, parameters)
-
-        return None
-    from_request = staticmethod(from_request)
-
-    def from_consumer_and_token(oauth_consumer, token=None,
-            callback=None, verifier=None, http_method=HTTP_METHOD,
-            http_url=None, parameters=None):
-        if not parameters:
-            parameters = {}
-
-        defaults = {
-            'oauth_consumer_key': oauth_consumer.key,
-            'oauth_timestamp': generate_timestamp(),
-            'oauth_nonce': generate_nonce(),
-            'oauth_version': OAuthRequest.version,
-        }
-
-        defaults.update(parameters)
-        parameters = defaults
-
-        if token:
-            parameters['oauth_token'] = token.key
-            if token.callback:
-                parameters['oauth_callback'] = token.callback
-            # 1.0a support for verifier.
-            if verifier:
-                parameters['oauth_verifier'] = verifier
-        elif callback:
-            # 1.0a support for callback in the request token request.
-            parameters['oauth_callback'] = callback
-
-        return OAuthRequest(http_method, http_url, parameters)
-    from_consumer_and_token = staticmethod(from_consumer_and_token)
-
-    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
-            http_url=None, parameters=None):
-        if not parameters:
-            parameters = {}
-
-        parameters['oauth_token'] = token.key
-
-        if callback:
-            parameters['oauth_callback'] = callback
-
-        return OAuthRequest(http_method, http_url, parameters)
-    from_token_and_callback = staticmethod(from_token_and_callback)
-
-    def _split_header(header):
-        """Turn Authorization: header into parameters."""
-        params = {}
-        parts = header.split(',')
-        for param in parts:
-            # Ignore realm parameter.
-            if param.find('realm') > -1:
-                continue
-            # Remove whitespace.
-            param = param.strip()
-            # Split key-value.
-            param_parts = param.split('=', 1)
-            # Remove quotes and unescape the value.
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
-        return params
-    _split_header = staticmethod(_split_header)
-
-    def _split_url_string(param_str):
-        """Turn URL string into parameters."""
-        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
-        return parameters
-    _split_url_string = staticmethod(_split_url_string)
-
-class OAuthServer(object):
-    """A worker to check the validity of a request against a data store."""
-    timestamp_threshold = 300 # In seconds, five minutes.
-    version = VERSION
-    signature_methods = None
-    data_store = None
-
-    def __init__(self, data_store=None, signature_methods=None):
-        self.data_store = data_store
-        self.signature_methods = signature_methods or {}
-
-    def set_data_store(self, data_store):
-        self.data_store = data_store
-
-    def get_data_store(self):
-        return self.data_store
-
-    def add_signature_method(self, signature_method):
-        self.signature_methods[signature_method.get_name()] = signature_method
-        return self.signature_methods
-
-    def fetch_request_token(self, oauth_request):
-        """Processes a request_token request and returns the
-        request token on success.
-        """
-        try:
-            # Get the request token for authorization.
-            token = self._get_token(oauth_request, 'request')
-        except OAuthError:
-            # No token required for the initial token request.
-            version = self._get_version(oauth_request)
-            consumer = self._get_consumer(oauth_request)
-            try:
-                callback = self.get_callback(oauth_request)
-            except OAuthError:
-                callback = None # 1.0, no callback specified.
-            self._check_signature(oauth_request, consumer, None)
-            # Fetch a new token.
-            token = self.data_store.fetch_request_token(consumer, callback)
-        return token
-
-    def fetch_access_token(self, oauth_request):
-        """Processes an access_token request and returns the
-        access token on success.
-        """
-        version = self._get_version(oauth_request)
-        consumer = self._get_consumer(oauth_request)
-        try:
-            verifier = self._get_verifier(oauth_request)
-        except OAuthError:
-            verifier = None
-        # Get the request token.
-        token = self._get_token(oauth_request, 'request')
-        self._check_signature(oauth_request, consumer, token)
-        new_token = self.data_store.fetch_access_token(consumer, token, verifier)
-        return new_token
-
-    def verify_request(self, oauth_request):
-        """Verifies an api call and checks all the parameters."""
-        # -> consumer and token
-        version = self._get_version(oauth_request)
-        consumer = self._get_consumer(oauth_request)
-        # Get the access token.
-        token = self._get_token(oauth_request, 'access')
-        self._check_signature(oauth_request, consumer, token)
-        parameters = oauth_request.get_nonoauth_parameters()
-        return consumer, token, parameters
-
-    def authorize_token(self, token, user):
-        """Authorize a request token."""
-        return self.data_store.authorize_request_token(token, user)
-
-    def get_callback(self, oauth_request):
-        """Get the callback URL."""
-        return oauth_request.get_parameter('oauth_callback')
- 
-    def build_authenticate_header(self, realm=''):
-        """Optional support for the authenticate header."""
-        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-    def _get_version(self, oauth_request):
-        """Verify the correct version request for this server."""
-        try:
-            version = oauth_request.get_parameter('oauth_version')
-        except:
-            version = VERSION
-        if version and version != self.version:
-            raise OAuthError('OAuth version %s not supported.' % str(version))
-        return version
-
-    def _get_signature_method(self, oauth_request):
-        """Figure out the signature with some defaults."""
-        try:
-            signature_method = oauth_request.get_parameter(
-                'oauth_signature_method')
-        except:
-            signature_method = SIGNATURE_METHOD
-        try:
-            # Get the signature method object.
-            signature_method = self.signature_methods[signature_method]
-        except:
-            signature_method_names = ', '.join(self.signature_methods.keys())
-            raise OAuthError('Signature method %s not supported try one of the '
-                'following: %s' % (signature_method, signature_method_names))
-
-        return signature_method
-
-    def _get_consumer(self, oauth_request):
-        consumer_key = oauth_request.get_parameter('oauth_consumer_key')
-        consumer = self.data_store.lookup_consumer(consumer_key)
-        if not consumer:
-            raise OAuthError('Invalid consumer.')
-        return consumer
-
-    def _get_token(self, oauth_request, token_type='access'):
-        """Try to find the token for the provided request token key."""
-        token_field = oauth_request.get_parameter('oauth_token')
-        token = self.data_store.lookup_token(token_type, token_field)
-        if not token:
-            raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
-        return token
-    
-    def _get_verifier(self, oauth_request):
-        return oauth_request.get_parameter('oauth_verifier')
-
-    def _check_signature(self, oauth_request, consumer, token):
-        timestamp, nonce = oauth_request._get_timestamp_nonce()
-        self._check_timestamp(timestamp)
-        self._check_nonce(consumer, token, nonce)
-        signature_method = self._get_signature_method(oauth_request)
-        try:
-            signature = oauth_request.get_parameter('oauth_signature')
-        except:
-            raise OAuthError('Missing signature.')
-        # Validate the signature.
-        valid_sig = signature_method.check_signature(oauth_request, consumer,
-            token, signature)
-        if not valid_sig:
-            key, base = signature_method.build_signature_base_string(
-                oauth_request, consumer, token)
-            raise OAuthError('Invalid signature. Expected signature base '
-                'string: %s' % base)
-        built = signature_method.build_signature(oauth_request, consumer, token)
-
-    def _check_timestamp(self, timestamp):
-        """Verify that timestamp is recentish."""
-        timestamp = int(timestamp)
-        now = int(time.time())
-        lapsed = now - timestamp
-        if lapsed > self.timestamp_threshold:
-            raise OAuthError('Expired timestamp: given %d and now %s has a '
-                'greater difference than threshold %d' %
-                (timestamp, now, self.timestamp_threshold))
-
-    def _check_nonce(self, consumer, token, nonce):
-        """Verify that the nonce is uniqueish."""
-        nonce = self.data_store.lookup_nonce(consumer, token, nonce)
-        if nonce:
-            raise OAuthError('Nonce already used: %s' % str(nonce))
-
-
-class OAuthClient(object):
-    """OAuthClient is a worker to attempt to execute a request."""
-    consumer = None
-    token = None
-
-    def __init__(self, oauth_consumer, oauth_token):
-        self.consumer = oauth_consumer
-        self.token = oauth_token
-
-    def get_consumer(self):
-        return self.consumer
-
-    def get_token(self):
-        return self.token
-
-    def fetch_request_token(self, oauth_request):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_access_token(self, oauth_request):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def access_resource(self, oauth_request):
-        """-> Some protected resource."""
-        raise NotImplementedError
-
-
-class OAuthDataStore(object):
-    """A database abstraction used to lookup consumers and tokens."""
-
-    def lookup_consumer(self, key):
-        """-> OAuthConsumer."""
-        raise NotImplementedError
-
-    def lookup_token(self, oauth_consumer, token_type, token_token):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_request_token(self, oauth_consumer, oauth_callback):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def authorize_request_token(self, oauth_token, user):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-
-class OAuthSignatureMethod(object):
-    """A strategy class that implements a signature method."""
-    def get_name(self):
-        """-> str."""
-        raise NotImplementedError
-
-    def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
-        """-> str key, str raw."""
-        raise NotImplementedError
-
-    def build_signature(self, oauth_request, oauth_consumer, oauth_token):
-        """-> str."""
-        raise NotImplementedError
-
-    def check_signature(self, oauth_request, consumer, token, signature):
-        built = self.build_signature(oauth_request, consumer, token)
-        return built == signature
-
-
-class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
-
-    def get_name(self):
-        return 'HMAC-SHA1'
-        
-    def build_signature_base_string(self, oauth_request, consumer, token):
-        sig = (
-            escape(oauth_request.get_normalized_http_method()),
-            escape(oauth_request.get_normalized_http_url()),
-            escape(oauth_request.get_normalized_parameters()),
-        )
-
-        key = '%s&' % escape(consumer.secret)
-        if token:
-            key += escape(token.secret)
-        raw = '&'.join(sig)
-        return key, raw
-
-    def build_signature(self, oauth_request, consumer, token):
-        """Builds the base signature string."""
-        key, raw = self.build_signature_base_string(oauth_request, consumer,
-            token)
-
-        # HMAC object.
-        try:
-            import hashlib # 2.5
-            hashed = hmac.new(key, raw, hashlib.sha1)
-        except:
-            import sha # Deprecated
-            hashed = hmac.new(key, raw, sha)
-
-        # Calculate the digest base 64.
-        return binascii.b2a_base64(hashed.digest())[:-1]
-
-
-class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
-
-    def get_name(self):
-        return 'PLAINTEXT'
-
-    def build_signature_base_string(self, oauth_request, consumer, token):
-        """Concatenates the consumer key and secret."""
-        sig = '%s&' % escape(consumer.secret)
-        if token:
-            sig = sig + escape(token.secret)
-        return sig, sig
-
-    def build_signature(self, oauth_request, consumer, token):
-        key, raw = self.build_signature_base_string(oauth_request, consumer,
-            token)
-        return key

File oauth_provider/decorators.py

-from oauth.oauth import OAuthError
+from oauth2 import Error
 
 try:
     from functools import wraps, update_wrapper
 from django.contrib.auth import REDIRECT_FIELD_NAME
 from django.utils.translation import ugettext as _
 
-from utils import initialize_server_request, send_oauth_error
+from utils import initialize_server_request, send_oauth_error, get_oauth_request
 from consts import OAUTH_PARAMETERS_NAMES
+from store import store, InvalidTokenError
 
 def oauth_required(view_func=None, resource_name=None):
     return CheckOAuth(view_func, resource_name)
     
     def __call__(self, request, *args, **kwargs):
         if self.is_valid_request(request):
+            oauth_request = get_oauth_request(request)
+            consumer = store.get_consumer(request, oauth_request, 
+                            oauth_request.get_parameter('oauth_consumer_key'))
             try:
-                consumer, token, parameters = self.validate_token(request)
-            except OAuthError, e:
+                token = store.get_access_token(request, oauth_request, 
+                                consumer, oauth_request.get_parameter('oauth_token'))
+            except InvalidTokenError:
+                return send_oauth_error(Error(_('Invalid access token: %s') % oauth_request.get_parameter('oauth_token')))
+            try:
+                parameters = self.validate_token(request, consumer, token)
+            except Error, e:
                 return send_oauth_error(e)
             
             if self.resource_name and token.resource.name != self.resource_name:
-                return send_oauth_error(OAuthError(_('You are not allowed to access this resource.')))
+                return send_oauth_error(Error(_('You are not allowed to access this resource.')))
             elif consumer and token:
                 return self.view_func(request, *args, **kwargs)
         
-        return send_oauth_error(OAuthError(_('Invalid request parameters.')))
+        return send_oauth_error(Error(_('Invalid request parameters.')))
 
     @staticmethod
     def is_valid_request(request):
         return is_in(auth_params) or is_in(request.REQUEST)
 
     @staticmethod
-    def validate_token(request):
+    def validate_token(request, consumer, token):
         oauth_server, oauth_request = initialize_server_request(request)
-        return oauth_server.verify_request(oauth_request)
+        return oauth_server.verify_request(oauth_request, consumer, token)

File oauth_provider/forms.py

+from django import forms
+
+
+class AuthorizeRequestTokenForm(forms.Form):
+    oauth_token = forms.CharField(widget=forms.HiddenInput)
+    authorize_access = forms.BooleanField(required=False)

File oauth_provider/models.py

 import urllib
 import urlparse
 from time import time
+import oauth2 as oauth
 
 from django.db import models
 from django.contrib.auth.models import User
 
 from managers import TokenManager, ConsumerManager, ResourceManager
 from consts import KEY_SIZE, SECRET_SIZE, CONSUMER_KEY_SIZE, CONSUMER_STATES,\
-                   PENDING, VERIFIER_SIZE, MAX_URL_LENGTH
+                   PENDING, VERIFIER_SIZE, MAX_URL_LENGTH, OUT_OF_BAND
+from utils import check_valid_callback
 
 generate_random = User.objects.make_random_password
 
             return urlparse.urlunparse((scheme, netloc, path, params,
                 query, fragment))
         return self.callback
+
+    def set_callback(self, callback):
+        if callback != OUT_OF_BAND: # out of band, says "we can't do this!"
+            if check_valid_callback(callback):
+                self.callback = callback
+                self.callback_confirmed = True
+                self.save()
+            else:
+                raise oauth.Error('Invalid callback URL.')
+        

File oauth_provider/store/__init__.py

+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import importlib
+
+
+class Error(Exception):
+    """Base class for Store exceptions."""
+
+
+class InvalidConsumerError(Error):
+    """Invalid consumer."""
+
+
+class InvalidTokenError(Error):
+    """Invalid token."""
+
+
+class Store(object):
+    """
+    The Store class is the backbone of piston's OAuth implementation. It is used
+    by the views and the authentication backend to get consumers and tokens, and
+    to create tokens. The following terms are used in the documentation of the
+    API:
+
+    Consumer:
+        A class defining at minimum `key` and `secret` attributes. Both of these
+        attributes must be either str or unicode.
+
+    Token:
+        A class defining at minimum `key` and `secret` attributes. Both of these
+        attributes must be either str or unicode.
+
+    User:
+        A `django.contrib.auth.models.User` instance.
+
+    Any API that takes a consumer or token will be passed a Consumer or Token
+    instance it itself returned at an earlier stage from one of the methods that
+    take a key and return a Consumer or Token. This means if your store
+    implementation uses tokens that keep a reference to its Consumer on the
+    Token itself, `get_consumer_for_request_token` can simply return
+    `request_token.consumer`.
+    """
+    def get_consumer(self, request, oauth_request, consumer_key):
+        """
+        Return the Consumer for `consumer_key` or raise `InvalidConsumerError`.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer_key`: The consumer key.
+        """
+        raise NotImplementedError
+
+    def get_consumer_for_request_token(self, request, oauth_request, request_token):
+        """
+        Return the Consumer associated with the `request_token` Token.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `request_token`: The request token to get the consumer for.
+        """
+        raise NotImplementedError
+
+    def get_consumer_for_access_token(self, request, oauth_request, access_token):
+        """
+        Return the Consumer associated with the `access_token` Token.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `access_token`: The access Token to get the consumer for.
+        """
+        raise NotImplementedError
+
+    def create_request_token(self, request, oauth_request, consumer, callback):
+        """
+        Generate and return a Token.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The Consumer that made the request.
+        """
+        raise NotImplementedError
+
+    def get_request_token(self, request, oauth_request, request_token_key):
+        """
+        Return the Token for `request_token_key` or raise `InvalidTokenError`.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The Consumer that made the request.
+        `request_token_key`: The request token key.
+        """
+        raise NotImplementedError
+
+    def authorize_request_token(self, request, oauth_request, request_token):
+        """
+        Authorize the `request_token` Token and return it.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `request_token`: The request token to authorize.
+        """
+        raise NotImplementedError
+
+    def create_access_token(self, request, oauth_request, consumer, request_token):
+        """
+        Generate and return a Token.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The Consumer that made the request.
+        `request_token`: The Token used to make the request.
+        """
+        raise NotImplementedError
+
+    def get_access_token(self, request, oauth_request, consumer, access_token_key):
+        """
+        Return the Token for `access_token_key` or raise `InvalidTokenError`.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The Consumer that made the request.
+        `access_token_key`: The token key used to make the request.
+        """
+        raise NotImplementedError
+
+    def get_user_for_access_token(self, request, oauth_request, access_token):
+        """
+        Return the associated User for `access_token`.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The Consumer that made the request.
+        `access_token`: The Token used to make the request.
+        """
+        raise NotImplementedError
+
+    def get_user_for_consumer(self, request, oauth_request, consumer):
+        """
+        Return the associated User for `consumer`.
+        
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The Consumer that made the request.
+        """
+        raise NotImplementedError
+
+    def check_nonce(self, request, oauth_request, nonce):
+        """
+        Return `True` if the nonce has not yet been used, `False` otherwise.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `nonce`: The nonce to check.
+        """
+        raise NotImplementedError
+
+
+def get_store(path='oauth_provider.store.db.ModelStore'):
+    """
+    Load the oauth store. Should not be called directly unless testing.
+    """
+    path = getattr(settings, 'OAUTH_STORE', path)
+
+    try:
+        module, attr = path.rsplit('.', 1)
+        store_class = getattr(importlib.import_module(module), attr)
+    except ValueError:
+        raise ImproperlyConfigured('Invalid oauth store string: "%s"' % path)
+    except ImportError, e:
+        raise ImproperlyConfigured('Error loading oauth store module "%s": "%s"' % (module, e))
+    except AttributeError:
+        raise ImproperlyConfigured('Module "%s" does not define an oauth store named "%s"' % (module, attr))
+
+    return store_class()
+
+
+store = get_store()

File oauth_provider/store/db.py

+import oauth2 as oauth
+
+from oauth_provider.store import InvalidConsumerError, InvalidTokenError, Store
+from oauth_provider.models import Nonce, Token, Consumer, Resource, VERIFIER_SIZE
+
+
+class ModelStore(Store):
+    """
+    Store implementation using the Django models defined in `piston.models`.
+    """
+    def get_consumer(self, request, oauth_request, consumer_key):
+        try:
+            return Consumer.objects.get(key=consumer_key)
+        except Consumer.DoesNotExist:
+            raise InvalidConsumerError()
+
+    def get_consumer_for_request_token(self, request, oauth_request, request_token):
+        return request_token.consumer
+
+    def get_consumer_for_access_token(self, request, oauth_request, access_token):
+        return access_token.consumer
+
+    def create_request_token(self, request, oauth_request, consumer, callback):
+        try:
+            scope = oauth_request.get_parameter('scope')
+        except oauth.Error:
+            scope = 'all'
+        try:
+            resource = Resource.objects.get(name=scope)
+        except Resource.DoesNotExist:
+            raise oauth.Error('Resource %s does not exist.' % oauth.escape(scope))
+        
+        token = Token.objects.create_token(
+            token_type=Token.REQUEST,
+            consumer=Consumer.objects.get(key=oauth_request['oauth_consumer_key']),
+            timestamp=oauth_request['oauth_timestamp'],
+            resource=resource,
+        )
+        token.set_callback(callback)
+        token.save()
+
+        return token
+
+    def get_request_token(self, request, oauth_request, request_token_key):
+        try:
+            return Token.objects.get(key=request_token_key, token_type=Token.REQUEST)
+        except Token.DoesNotExist:
+            raise InvalidTokenError()
+
+    def authorize_request_token(self, request, oauth_request, request_token):
+        request_token.is_approved = True
+        request_token.user = request.user
+        request_token.verifier = oauth.generate_verifier(VERIFIER_SIZE)
+        request_token.save()
+        return request_token
+
+    def create_access_token(self, request, oauth_request, consumer, request_token):
+        try:
+            scope = oauth_request.get_parameter('scope')
+        except oauth.Error:
+            scope = 'all'
+        try:
+            resource = Resource.objects.get(name=scope)
+        except Resource.DoesNotExist:
+            raise oauth.Error('Resource %s does not exist.' % oauth.escape(scope))
+
+        access_token = Token.objects.create_token(
+            token_type=Token.ACCESS,
+            timestamp=oauth_request['oauth_timestamp'],
+            consumer=Consumer.objects.get(key=consumer.key),
+            user=request_token.user,
+            resource=resource,
+        )
+        request_token.delete()
+        return access_token
+
+    def get_access_token(self, request, oauth_request, consumer, access_token_key):
+        try:
+            return Token.objects.get(key=access_token_key, token_type=Token.ACCESS)
+        except Token.DoesNotExist:
+            raise InvalidTokenError()
+
+    def get_user_for_access_token(self, request, oauth_request, access_token):
+        return access_token.user
+
+    def get_user_for_consumer(self, request, oauth_request, consumer):
+        return consumer.user
+
+    def check_nonce(self, request, oauth_request, nonce):
+        nonce, created = Nonce.objects.get_or_create(
+            consumer_key=oauth_request['oauth_consumer_key'],
+            token_key=oauth_request.get('oauth_token', ''),
+            key=nonce
+        )
+        return created

File oauth_provider/tests.py

     ...
 
 
-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. 
-Jane, the User, would like printer.example.com to print the private photo 
-vacation.jpg stored at photos.example.net.
-
-When Jane signs-into photos.example.net using her username and password, she 
-can access the photo by going to the URL 
-http://photos.example.net/photo?file=vacation.jpg. Other Users cannot access 
-that photo, and Jane does not want to share her username and password with 
-printer.example.com.
-
-The requests in this example use the URL query method when sending parameters. 
-This is done to simplify the example and should not be taken as an endorsement 
-of one method over the others.
-
-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', 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',
-    ...     '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=...'
-    >>> from oauth_provider.models import Token
-    >>> token = list(Token.objects.all())[-1]
-    >>> token.key in response.content, token.secret in response.content
-    (True, 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.'
-
-
-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,
-    ...     'oauth_callback': 'http://printer.example.com/request_token_ready',
-    ... }
-    >>> response = c.get("/oauth/authorize/", parameters)
-    >>> response.status_code
-    302
-    >>> response['Location']
-    'http://.../accounts/login/?next=/oauth/authorize/%3Foauth_token%3D...%26oauth_callback%3Dhttp...'
-    >>> 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
-    >>> # TODO: find a proper way to use test Client's content_type arg.
-    >>> response = c.post("/oauth/authorize/", parameters)#, content_type="application/x-www-form-urlencoded")
-    >>> response.status_code
-    302
-    >>> response['Location']
-    'http://printer.example.com/request_token_ready?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()
-
-The callback argument is optional, you can specify your own default callback
-view with ``OAUTH_CALLBACK_VIEW`` setting::
-
-    >>> 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',
-    ... }
-    >>> 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'
-
-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()
-    >>> # 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: ...'
-
-
-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
 =====================
 
 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
 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
     302
     >>> response['Location']
     'http://printer.example.com/request_token_ready?oauth_verifier=...&oauth_token=...'
-    >>> token = list(Token.objects.all())[-1]
+    >>> token = Token.objects.get(key=token.key)
     >>> token.key in response['Location']
     True
     >>> token.is_approved
     >>> response.status_code
     302
     >>> response['Location']
-    'http://printer.example.com/request_token_ready?error=Access%20not%20granted%20by%20user.'
+    '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), 
     ...     'oauth_nonce': 'accessnonce',
     ...     'oauth_version': '1.0',
     ...     'oauth_verifier': token.verifier,
+    ...     'scope': 'photos',
     ... }
     >>> response = c.get("/oauth/access_token/", parameters)
 
     u'jane'
 
 The Consumer will not be able to request another Access Token with the same
-Nonce::
+parameters because the Request Token has been deleted once Access Token is
+created::
 
-    >>> from oauth_provider.models import Nonce
-    >>> Nonce.objects.all()
-    [<Nonce: Nonce accessnonce for ...>]
     >>> response = c.get("/oauth/access_token/", parameters)
     >>> response.status_code
-    401
+    400
     >>> response.content
-    'Nonce already used: accessnonce'
+    'Invalid request token.'
 
-Nor with a missing/invalid verifier::
+The Consumer will not be able to request another Access Token with a missing
+or invalid verifier::
 
-    >>> parameters['oauth_nonce'] = 'yetanotheraccessnonce'
+    >>> 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
-    401
+    400
     >>> 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
+    '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'
-    >>> token.is_approved = False
-    >>> token.save()
     >>> response = c.get("/oauth/access_token/", parameters)
     >>> response.status_code
-    401
+    400
     >>> 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.'
+    'Request Token not approved by the user.'
 
 
 Accessing Protected Resources
 (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,
+    >>> import oauth2 as oauth
+    >>> oauth_request = oauth.Request.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)
+    >>> signature_method = oauth.SignatureMethod_HMAC_SHA1()
+    >>> signature = signature_method.sign(oauth_request, consumer, access_token)
 
 
 Requesting Protected Resource

File oauth_provider/utils.py

-from oauth.oauth import OAuthRequest, OAuthServer, build_authenticate_header,\
-    OAuthSignatureMethod_PLAINTEXT, OAuthSignatureMethod_HMAC_SHA1
+import oauth2 as oauth
+from urlparse import urlparse
 
 from django.conf import settings
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseBadRequest
 
-from stores import DataStore
+from consts import MAX_URL_LENGTH, OUT_OF_BAND
 
 OAUTH_REALM_KEY_NAME = getattr(settings, 'OAUTH_REALM_KEY_NAME', '')
 OAUTH_SIGNATURE_METHODS = getattr(settings, 'OAUTH_SIGNATURE_METHODS', ['plaintext', 'hmac-sha1'])
+OAUTH_BLACKLISTED_HOSTNAMES = getattr(settings, 'OAUTH_BLACKLISTED_HOSTNAMES', [])
 
 def initialize_server_request(request):
     """Shortcut for initialization."""
             or request.META.get('SERVER_NAME') == 'testserver'):
         parameters = dict(request.REQUEST.items())
 
-    oauth_request = OAuthRequest.from_request(request.method, 
+    oauth_request = oauth.Request.from_request(request.method, 
                                               request.build_absolute_uri(), 
                                               headers=auth_header,
                                               parameters=parameters,
                                               query_string=request.META.get('QUERY_STRING', ''))
     if oauth_request:
-        oauth_server = OAuthServer(DataStore(oauth_request))
+        oauth_server = oauth.Server()
         if 'plaintext' in OAUTH_SIGNATURE_METHODS:
-            oauth_server.add_signature_method(OAuthSignatureMethod_PLAINTEXT())
+            oauth_server.add_signature_method(oauth.SignatureMethod_PLAINTEXT())
         if 'hmac-sha1' in OAUTH_SIGNATURE_METHODS:
-            oauth_server.add_signature_method(OAuthSignatureMethod_HMAC_SHA1())
+            oauth_server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
     else:
         oauth_server = None
     return oauth_server, oauth_request
     response = HttpResponse(err.message.encode('utf-8'), mimetype="text/plain")
     response.status_code = 401
     # return the authenticate header
-    header = build_authenticate_header(realm=OAUTH_REALM_KEY_NAME)
+    header = oauth.build_authenticate_header(realm=OAUTH_REALM_KEY_NAME)
     for k, v in header.iteritems():
         response[k] = v
     return response
+
+def get_oauth_request(request):
+    """ Converts a Django request object into an `oauth2.Request` object. """
+    headers = {}
+    if 'HTTP_AUTHORIZATION' in request.META:
+        headers['Authorization'] = request.META['HTTP_AUTHORIZATION']
+    return oauth.Request.from_request(request.method, request.build_absolute_uri(request.path), headers, dict(request.REQUEST))
+
+def verify_oauth_request(request, oauth_request, consumer, token=None):
+    """ Helper function to verify requests. """
+    from store import store
+
+    # Check nonce
+    if not store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']):
+        return False
+
+    # Verify request
+    try:
+        oauth_server = oauth.Server()
+        oauth_server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+        oauth_server.add_signature_method(oauth.SignatureMethod_PLAINTEXT())
+
+        # Ensure the passed keys and secrets are ascii, or HMAC will complain.
+        consumer = oauth.Consumer(consumer.key.encode('ascii', 'ignore'), consumer.secret.encode('ascii', 'ignore'))
+        if token is not None:
+            token = oauth.Token(token.key.encode('ascii', 'ignore'), token.secret.encode('ascii', 'ignore'))
+
+        oauth_server.verify_request(oauth_request, consumer, token)
+    except oauth.Error, err:
+        return False
+
+    return True
+
+
+def require_params(oauth_request, parameters=[]):
+    """ Ensures that the request contains all required parameters. """
+    params = [
+        'oauth_consumer_key',
+        'oauth_nonce',
+        'oauth_signature',
+        'oauth_signature_method',
+        'oauth_timestamp'
+    ]
+    params.extend(parameters)
+
+    missing = list(param for param in params if param not in oauth_request)
+    if missing:
+        return HttpResponseBadRequest('Missing OAuth parameters: %s' % (', '.join(missing)))
+
+    return None
+
+
+def check_valid_callback(callback):
+    """
+    Checks the size and nature of the callback.
+    """
+    callback_url = urlparse(callback)
+    return (callback_url.scheme
+            and callback_url.hostname not in OAUTH_BLACKLISTED_HOSTNAMES
+            and len(callback) < MAX_URL_LENGTH)

File oauth_provider/views.py

-from oauth.oauth import OAuthError
+from urllib import urlencode
 
+import oauth2 as oauth
 from django.conf import settings
-from django.http import (
-    HttpResponse, HttpResponseBadRequest, HttpResponseRedirect)
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.views.decorators.csrf import csrf_exempt
 from django.utils.translation import ugettext as _
-from django.contrib.auth.decorators import login_required
 from django.core.urlresolvers import get_callable
 
-from utils import initialize_server_request, send_oauth_error
 from decorators import oauth_required
-from stores import check_valid_callback
+from forms import AuthorizeRequestTokenForm
+from store import store, InvalidConsumerError, InvalidTokenError
+from utils import verify_oauth_request, get_oauth_request, require_params, send_oauth_error
 from consts import OUT_OF_BAND
 
 OAUTH_AUTHORIZE_VIEW = 'OAUTH_AUTHORIZE_VIEW'
 OAUTH_CALLBACK_VIEW = 'OAUTH_CALLBACK_VIEW'
-INVALID_PARAMS_RESPONSE = send_oauth_error(OAuthError(
+INVALID_PARAMS_RESPONSE = send_oauth_error(oauth.Error(
                                             _('Invalid request parameters.')))
 
+@csrf_exempt
 def request_token(request):
-    """
-    The Consumer obtains an unauthorized Request Token by asking the Service 
-    Provider to issue a Token. The Request Token's sole purpose is to receive 
-    User approval and can only be used to obtain an Access Token.
-    """
-    oauth_server, oauth_request = initialize_server_request(request)
-    if oauth_server is None:
-        return INVALID_PARAMS_RESPONSE
-    try:
-        # create a request token
-        token = oauth_server.fetch_request_token(oauth_request)
-        # return the token
-        response = HttpResponse(token.to_string(), mimetype="text/plain")
-    except OAuthError, err:
-        response = send_oauth_error(err)
-    return response
-    
-@login_required
-def user_authorization(request):
-    """
-    The Consumer cannot use the Request Token until it has been authorized by 
-    the User.
-    """
-    oauth_server, oauth_request = initialize_server_request(request)
+    oauth_request = get_oauth_request(request)
     if oauth_request is None:
         return INVALID_PARAMS_RESPONSE
+
+    missing_params = require_params(oauth_request, ('oauth_callback',))
+    if missing_params is not None:
+        return missing_params
+
     try:
-        # get the request token
-        token = oauth_server.fetch_request_token(oauth_request)
-    except OAuthError, err:
+        consumer = store.get_consumer(request, oauth_request, oauth_request['oauth_consumer_key'])
+    except InvalidConsumerError:
+        return HttpResponseBadRequest('Invalid Consumer.')
+
+    if not verify_oauth_request(request, oauth_request, consumer):
+        return HttpResponseBadRequest('Could not verify OAuth request.')
+
+    try:
+        request_token = store.create_request_token(request, oauth_request, consumer, oauth_request['oauth_callback'])
+    except oauth.Error, err:
         return send_oauth_error(err)
 
+    ret = urlencode({
+        'oauth_token': request_token.key,
+        'oauth_token_secret': request_token.secret,
+        'oauth_callback_confirmed': 'true'
+    })
+    return HttpResponse(ret, content_type='application/x-www-form-urlencoded')
+
+
+@login_required
+def user_authorization(request, form_class=AuthorizeRequestTokenForm):
+    if 'oauth_token' not in request.REQUEST:
+        return HttpResponseBadRequest('No request token specified.')
+
+    oauth_request = get_oauth_request(request)
+
     try:
-        # get the request callback, though there might not be one
-        callback = oauth_server.get_callback(oauth_request)
-        
-        # OAuth 1.0a: this parameter should not be present on this version
-        if token.callback_confirmed:
-            return HttpResponseBadRequest("Cannot specify oauth_callback at authorization step for 1.0a protocol")
-        if not check_valid_callback(callback):
-            return HttpResponseBadRequest("Invalid callback URL")
-    except OAuthError:
-        callback = None
+        request_token = store.get_request_token(request, oauth_request, request.REQUEST['oauth_token'])
+    except InvalidTokenError:
+        return HttpResponseBadRequest('Invalid request token.')
 
-    # OAuth 1.0a: use the token's callback if confirmed
-    if token.callback_confirmed:
-        callback = token.callback
-        if callback == OUT_OF_BAND:
-            callback = None
+    consumer = store.get_consumer_for_request_token(request, oauth_request, request_token)
 
-    # entry point for the user
-    if request.method == 'GET':
-        # try to get custom authorize view
-        authorize_view_str = getattr(settings, OAUTH_AUTHORIZE_VIEW, 
-                                    'oauth_provider.views.fake_authorize_view')
-        try:
-            authorize_view = get_callable(authorize_view_str)
-        except AttributeError:
-            raise Exception, "%s view doesn't exist." % authorize_view_str
-        params = oauth_request.get_normalized_parameters()
-        # set the oauth flag
-        request.session['oauth'] = token.key
-        return authorize_view(request, token, callback, params)
-    
-    # user grant access to the service
-    elif request.method == 'POST':
-        # verify the oauth flag set in previous GET
-        if request.session.get('oauth', '') == token.key:
+    if request.method == 'POST':
+        form = form_class(request.POST)
+        if request.session.get('oauth', '') == request_token.key and form.is_valid():
             request.session['oauth'] = ''
-            try:
-                if int(request.POST.get('authorize_access', 0)):
-                    # authorize the token
-                    token = oauth_server.authorize_token(token, request.user)
-                    # return the token key
-                    args = { 'token': token }
-                else:
-                    args = { 'error': _('Access not granted by user.') }
-            except OAuthError, err:
-                response = send_oauth_error(err)
-            
-            if callback:
-                if "?" in callback:
-                    url_delimiter = "&"
-                else:
-                    url_delimiter = "?"
-                if 'token' in args:
-                    query_args = args['token'].to_string(only_key=True)
-                else: # access is not authorized i.e. error
-                    query_args = 'error=%s' % args['error']
-                response = HttpResponseRedirect('%s%s%s' % (callback, url_delimiter, query_args))
+            if form.cleaned_data['authorize_access']:
+                request_token = store.authorize_request_token(request, oauth_request, request_token)
+                args = { 'oauth_token': request_token.key }
+            else:
+                args = { 'error': _('Access not granted by user.') }
+            if request_token.callback is not None and request_token.callback != OUT_OF_BAND:
+                response = HttpResponseRedirect('%s&%s' % (request_token.get_callback_url(), urlencode(args)))
             else:
                 # try to get custom callback view
                 callback_view_str = getattr(settings, OAUTH_CALLBACK_VIEW, 
                     raise Exception, "%s view doesn't exist." % callback_view_str
                 response = callback_view(request, **args)
         else:
-            response = send_oauth_error(OAuthError(_('Action not allowed.')))
-        return response
-    
+            response = send_oauth_error(oauth.Error(_('Action not allowed.')))
+    else:
+        # try to get custom authorize view
+        authorize_view_str = getattr(settings, OAUTH_AUTHORIZE_VIEW, 
+                                    'oauth_provider.views.fake_authorize_view')
+        try:
+            authorize_view = get_callable(authorize_view_str)
+        except AttributeError:
+            raise Exception, "%s view doesn't exist." % authorize_view_str
+        params = oauth_request.get_normalized_parameters()
+        # set the oauth flag
+        request.session['oauth'] = request_token.key
+        response = authorize_view(request, request_token, request_token.get_callback_url(), params)
+        
+    return response
+
+
+@csrf_exempt
 def access_token(request):
-    """
-    The Consumer exchanges the Request Token for an Access Token capable of 
-    accessing the Protected Resources.
-    """
-    oauth_server, oauth_request = initialize_server_request(request)
-    if oauth_request is None:
-        return INVALID_PARAMS_RESPONSE
+    oauth_request = get_oauth_request(request)
+
+    missing_params = require_params(oauth_request, ('oauth_token', 'oauth_verifier'))
+    if missing_params is not None:
+        return missing_params
+
     try:
-        # get the request token
-        token = oauth_server.fetch_access_token(oauth_request)
-        # return the token
-        response = HttpResponse(token.to_string(), mimetype="text/plain")
-    except OAuthError, err:
-        response = send_oauth_error(err)
-    return response
+        request_token = store.get_request_token(request, oauth_request, oauth_request['oauth_token'])
+    except InvalidTokenError:
+        return HttpResponseBadRequest('Invalid request token.')
+    try:
+        consumer = store.get_consumer(request, oauth_request, oauth_request['oauth_consumer_key'])
+    except InvalidConsumerError:
+        return HttpResponseBadRequest('Invalid consumer.')
+
+    if not verify_oauth_request(request, oauth_request, consumer, request_token):
+        return HttpResponseBadRequest('Could not verify OAuth request.')
+
+    if oauth_request.get('oauth_verifier', None) != request_token.verifier:
+        return HttpResponseBadRequest('Invalid OAuth verifier.')
+
+    if not request_token.is_approved:
+        return HttpResponseBadRequest('Request Token not approved by the user.')
+
+    access_token = store.create_access_token(request, oauth_request, consumer, request_token)
+
+    ret = urlencode({
+        'oauth_token': access_token.key,
+        'oauth_token_secret': access_token.secret
+    })
+    return HttpResponse(ret, content_type='application/x-www-form-urlencoded')
 
 @oauth_required
 def protected_resource_example(request):

File requirements.txt

+Django==1.2.1
+httplib2==0.6.0
+-e git://github.com/simplegeo/python-oauth2@1f9640f8467a90fcc20be6bbb8909d21bad308f6#egg=oauth2-1.2.1-py2.6-dev
+wsgiref==0.1.2