diff -Nru python-oauthlib-0.3.4/PKG-INFO python-oauthlib-0.3.7/PKG-INFO --- python-oauthlib-0.3.4/PKG-INFO 2012-11-19 18:28:55.000000000 +0000 +++ python-oauthlib-0.3.7/PKG-INFO 2013-02-11 23:35:19.000000000 +0000 @@ -1,37 +1,11 @@ Metadata-Version: 1.0 Name: oauthlib -Version: 0.3.4 +Version: 0.3.7 Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic Home-page: https://github.com/idan/oauthlib Author: Idan Gazit Author-email: idan@gazit.me -License: Copyright (c) 2011 Idan Gazit and contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - 3. Neither the name of this project nor the names of its contributors may - be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +License: BSD Description: OAuthLib ======== @@ -55,10 +29,11 @@ veneer on top of OAuthLib and get OAuth support for very little effort. Documentation - ------------- + -------------- - Full documentation is available on `Read the Docs`_. + Full documentation is available on `Read the Docs`_. All contributions are very welcome! The documentation is still quite sparse, please open an issue for what you'd like to know, or discuss it in our `G+ community`_, or even better, send a pull request! + .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 .. _`Read the Docs`: https://oauthlib.readthedocs.org/en/latest/index.html Interested in making OAuth requests? @@ -71,6 +46,16 @@ .. _`requests`: https://github.com/kennethreitz/requests .. _`requests OAuth examples`: http://docs.python-requests.org/en/latest/user/quickstart/#oauth-authentication + Using OAuthLib? Please get in touch! + ------------------------------------ + Patching OAuth support onto an http request framework? Creating an OAuth provider extension for a web framework? Simply using OAuthLib to Get Things Done or to learn? + + No matter which we'd love to hear from you in our `G+ community`_ or if you have anything in particular you would like to have, change or comment on don't hesitate for a second to send a pull request or open an issue. We might be quite busy and therefore slow to reply but we love feedback! + + Chances are you have run into something annoying that you wish there was documentation for, if you wish to gain eternal fame and glory, and a drink if we have the pleasure to run into eachother, please send a docs pull request =) + + .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 + License ------- @@ -85,6 +70,12 @@ *OAuthLib is in active development, with most of OAuth1 complete and OAuth2 already in the works.* + 0.3.7: OAuth 1 optional encoding of Client.sign return values + + 0.3.6: Revert default urlencoding. + + 0.3.5: Default unicode conversion (utf-8) and urlencoding of input. + 0.3.4: A number of small features and bug fixes. 0.3.3: OAuth 1 Provider verify now return useful params diff -Nru python-oauthlib-0.3.4/README.rst python-oauthlib-0.3.7/README.rst --- python-oauthlib-0.3.4/README.rst 2012-11-19 18:27:05.000000000 +0000 +++ python-oauthlib-0.3.7/README.rst 2013-02-11 23:34:04.000000000 +0000 @@ -21,10 +21,11 @@ veneer on top of OAuthLib and get OAuth support for very little effort. Documentation -------------- +-------------- -Full documentation is available on `Read the Docs`_. +Full documentation is available on `Read the Docs`_. All contributions are very welcome! The documentation is still quite sparse, please open an issue for what you'd like to know, or discuss it in our `G+ community`_, or even better, send a pull request! +.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 .. _`Read the Docs`: https://oauthlib.readthedocs.org/en/latest/index.html Interested in making OAuth requests? @@ -37,6 +38,16 @@ .. _`requests`: https://github.com/kennethreitz/requests .. _`requests OAuth examples`: http://docs.python-requests.org/en/latest/user/quickstart/#oauth-authentication +Using OAuthLib? Please get in touch! +------------------------------------ +Patching OAuth support onto an http request framework? Creating an OAuth provider extension for a web framework? Simply using OAuthLib to Get Things Done or to learn? + +No matter which we'd love to hear from you in our `G+ community`_ or if you have anything in particular you would like to have, change or comment on don't hesitate for a second to send a pull request or open an issue. We might be quite busy and therefore slow to reply but we love feedback! + +Chances are you have run into something annoying that you wish there was documentation for, if you wish to gain eternal fame and glory, and a drink if we have the pleasure to run into eachother, please send a docs pull request =) + +.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 + License ------- @@ -51,6 +62,12 @@ *OAuthLib is in active development, with most of OAuth1 complete and OAuth2 already in the works.* +0.3.7: OAuth 1 optional encoding of Client.sign return values + +0.3.6: Revert default urlencoding. + +0.3.5: Default unicode conversion (utf-8) and urlencoding of input. + 0.3.4: A number of small features and bug fixes. 0.3.3: OAuth 1 Provider verify now return useful params diff -Nru python-oauthlib-0.3.4/debian/changelog python-oauthlib-0.3.7/debian/changelog --- python-oauthlib-0.3.4/debian/changelog 2012-12-06 12:38:13.000000000 +0000 +++ python-oauthlib-0.3.7/debian/changelog 2013-03-21 14:53:47.000000000 +0000 @@ -1,8 +1,23 @@ -python-oauthlib (0.3.4-0ubuntu1~ubuntu12.04.1~grizzly0) precise; urgency=low +python-oauthlib (0.3.7-0ubuntu1~ubuntu12.04.1~grizzly0) precise; urgency=low * No-change backport to precise - -- Openstack Ubuntu Testing Bot Thu, 06 Dec 2012 07:38:13 -0500 + -- Openstack Ubuntu Testing Bot Thu, 21 Mar 2013 10:53:47 -0400 + +python-oauthlib (0.3.7-0ubuntu1) raring; urgency=low + + * New upstream release. + + -- Barry Warsaw Thu, 21 Feb 2013 16:44:53 -0500 + +python-oauthlib (0.3.5-0ubuntu1) raring; urgency=low + + * New upstream release. + * debian/patches/issue97.patch: Restrict nose from looking outside the + tests directory for tests. This prevents a test failure (and build + dependency) on Django. + + -- Barry Warsaw Tue, 29 Jan 2013 19:21:51 -0500 python-oauthlib (0.3.4-0ubuntu1) raring; urgency=low diff -Nru python-oauthlib-0.3.4/debian/patches/issue97.patch python-oauthlib-0.3.7/debian/patches/issue97.patch --- python-oauthlib-0.3.4/debian/patches/issue97.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-oauthlib-0.3.7/debian/patches/issue97.patch 2013-02-21 21:44:14.000000000 +0000 @@ -0,0 +1,14 @@ +Description: Restrict nose from looking outside the tests directory for tests. + This prevents a test failure (and build dependency) on Django. +Author: Barry Warsaw +Bug: https://github.com/idan/oauthlib/issues/97 +Forwarded: yes + +--- a/setup.cfg ++++ b/setup.cfg +@@ -3,3 +3,5 @@ + tag_date = 0 + tag_svn_revision = 0 + ++[nosetests] ++where=tests diff -Nru python-oauthlib-0.3.4/debian/patches/series python-oauthlib-0.3.7/debian/patches/series --- python-oauthlib-0.3.4/debian/patches/series 2012-11-19 19:39:30.000000000 +0000 +++ python-oauthlib-0.3.7/debian/patches/series 2013-02-21 21:44:14.000000000 +0000 @@ -0,0 +1 @@ +issue97.patch diff -Nru python-oauthlib-0.3.4/oauthlib/common.py python-oauthlib-0.3.7/oauthlib/common.py --- python-oauthlib-0.3.4/oauthlib/common.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/common.py 2013-02-04 23:08:24.000000000 +0000 @@ -47,7 +47,7 @@ # 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either) def quote(s, safe=b'/'): - s = _quote(s, safe) + s = _quote(s.encode('utf-8'), safe) # PY3 always returns unicode. PY2 may return either, depending on whether # it had to modify the string. if isinstance(s, bytes_type): @@ -112,7 +112,7 @@ """ # Check if query contains invalid characters if query and not set(query) <= urlencoded: - raise ValueError('Invalid characters in query string.') + raise ValueError('Not a valid urlencoded string.') # Check for correctly hex encoded values using a regular expression # All encoded values begin with % followed by two hex characters @@ -236,6 +236,32 @@ result |= ord(x) ^ ord(y) return result == 0 + +def to_unicode(data, encoding): + """Convert a number of different types of objects to unicode.""" + if isinstance(data, unicode_type): + return data + + if isinstance(data, bytes_type): + return unicode_type(data, encoding=encoding) + + if hasattr(data, '__iter__'): + try: + dict(data) + except TypeError: + pass + except ValueError: + # Assume it's a one dimensional data structure + return (to_unicode(i, encoding) for i in data) + else: + # We support 2.6 which lacks dict comprehensions + if isinstance(data, dict): + data = data.items() + return dict(((to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data)) + + return data + + class Request(object): """A malleable representation of a signable HTTP request. @@ -250,14 +276,26 @@ unmolested. """ - def __init__(self, uri, http_method='GET', body=None, headers=None): - self.uri = uri - self.http_method = http_method - self.headers = headers or {} - self.body = body - self.decoded_body = extract_params(body) + def __init__(self, uri, http_method='GET', body=None, headers=None, + encoding='utf-8'): + # Convert to unicode using encoding if given, else assume unicode + encode = lambda x: to_unicode(x, encoding) if encoding else x + + self.uri = encode(uri) + self.http_method = encode(http_method) + self.headers = encode(headers or {}) + self.body = encode(body) + self.decoded_body = extract_params(encode(body)) self.oauth_params = [] + self._params = {} + self._params.update(dict(urldecode(self.uri_query))) + self._params.update(dict(self.decoded_body or [])) + self._params.update(self.headers) + + def __getattr__(self, name): + return self._params.get(name, None) + @property def uri_query(self): return urlparse.urlparse(self.uri).query diff -Nru python-oauthlib-0.3.4/oauthlib/oauth1/rfc5849/__init__.py python-oauthlib-0.3.7/oauthlib/oauth1/rfc5849/__init__.py --- python-oauthlib-0.3.4/oauthlib/oauth1/rfc5849/__init__.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth1/rfc5849/__init__.py 2013-02-11 23:23:23.000000000 +0000 @@ -10,14 +10,20 @@ """ import logging +import sys import time try: import urlparse except ImportError: import urllib.parse as urlparse +if sys.version_info[0] == 3: + bytes_type = bytes +else: + bytes_type = str + from oauthlib.common import Request, urlencode, generate_nonce -from oauthlib.common import generate_timestamp +from oauthlib.common import generate_timestamp, to_unicode from . import parameters, signature, utils SIGNATURE_HMAC = "HMAC-SHA1" @@ -41,17 +47,48 @@ callback_uri=None, signature_method=SIGNATURE_HMAC, signature_type=SIGNATURE_TYPE_AUTH_HEADER, - rsa_key=None, verifier=None, realm=None): - self.client_key = client_key - self.client_secret = client_secret - self.resource_owner_key = resource_owner_key - self.resource_owner_secret = resource_owner_secret - self.signature_method = signature_method - self.signature_type = signature_type - self.callback_uri = callback_uri - self.rsa_key = rsa_key - self.verifier = verifier - self.realm = realm + rsa_key=None, verifier=None, realm=None, + encoding='utf-8', decoding=None, + nonce=None, timestamp=None): + """Create an OAuth 1 client. + + :param client_key: Client key (consumer key), mandatory. + :param resource_owner_key: Resource owner key (oauth token). + :param resource_owner_secret: Resource owner secret (oauth token secret). + :param callback_uri: Callback used when obtaining request token. + :param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT. + :param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default), + SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY + depending on where you want to embed the oauth + credentials. + :param rsa_key: RSA key used with SIGNATURE_RSA. + :param verifier: Verifier used when obtaining an access token. + :param realm: Realm (scope) to which access is being requested. + :param encoding: If you provide non-unicode input you may use this + to have oauthlib automatically convert. + :param decoding: If you wish that the returned uri, headers and body + from sign be encoded back from unicode, then set + decoding to your preferred encoding, i.e. utf-8. + :param nonce: Use this nonce instead of generating one. (Mainly for testing) + :param timestamp: Use this timestamp instead of using current. (Mainly for testing) + """ + # Convert to unicode using encoding if given, else assume unicode + encode = lambda x: to_unicode(x, encoding) if encoding else x + + self.client_key = encode(client_key) + self.client_secret = encode(client_secret) + self.resource_owner_key = encode(resource_owner_key) + self.resource_owner_secret = encode(resource_owner_secret) + self.signature_method = encode(signature_method) + self.signature_type = encode(signature_type) + self.callback_uri = encode(callback_uri) + self.rsa_key = encode(rsa_key) + self.verifier = encode(verifier) + self.realm = encode(realm) + self.encoding = encode(encoding) + self.decoding = encode(decoding) + self.nonce = encode(nonce) + self.timestamp = encode(timestamp) if self.signature_method == SIGNATURE_RSA and self.rsa_key is None: raise ValueError('rsa_key is required when using RSA signature method.') @@ -97,9 +134,13 @@ def get_oauth_params(self): """Get the basic OAuth parameters to be used in generating a signature. """ + nonce = (generate_nonce() + if self.nonce is None else self.nonce) + timestamp = (generate_timestamp() + if self.timestamp is None else self.timestamp) params = [ - ('oauth_nonce', generate_nonce()), - ('oauth_timestamp', generate_timestamp()), + ('oauth_nonce', nonce), + ('oauth_timestamp', timestamp), ('oauth_version', '1.0'), ('oauth_signature_method', self.signature_method), ('oauth_consumer_key', self.client_key), @@ -172,7 +213,8 @@ dicts, for example. """ # normalize request data - request = Request(uri, http_method, body, headers) + request = Request(uri, http_method, body, headers, + encoding=self.encoding) # sanity check content_type = request.headers.get('Content-Type', None) @@ -215,8 +257,18 @@ request.oauth_params.append(('oauth_signature', self.get_oauth_signature(request))) # render the signed request and return it - return self._render(request, formencode=True, - realm=(realm or self.realm)) + uri, headers, body = self._render(request, formencode=True, + realm=(realm or self.realm)) + + if self.decoding: + logging.debug('Encoding URI, headers and body to %s.', self.decoding) + uri = uri.encode(self.decoding) + body = body.encode(self.decoding) if body else body + new_headers = {} + for k, v in headers.items(): + new_headers[k.encode(self.decoding)] = v.encode(self.decoding) + headers = new_headers + return uri, headers, body class Server(object): @@ -257,10 +309,10 @@ each methods documentation for detailed usage. The following methods must be implemented: - - validate_client + - validate_client_key - validate_request_token - validate_access_token - - validate_nonce_and_timestamp + - validate_timestamp_and_nonce - validate_redirect_uri - validate_requested_realm - validate_realm @@ -615,9 +667,30 @@ """ raise NotImplementedError("Subclasses must implement this function.") + def verify_request_token_request(self, uri, http_method='GET', body=None, + headers=None): + """Verify the initial request in the OAuth workflow. + + During this step the client obtains a request token for use during + resource owner authorization (which is outside the scope of oauthlib). + """ + return self.verify_request(uri, http_method=http_method, body=body, + headers=headers, require_resource_owner=False, + require_realm=True) + + def verify_access_token_request(self, uri, http_method='GET', body=None, + headers=None): + """Verify the second request in the OAuth workflow. + + During this step the client obtains the access token for use when + accessing protected resources. + """ + return self.verify_request(uri, http_method=http_method, body=body, + headers=headers, require_verifier=True) + def verify_request(self, uri, http_method='GET', body=None, headers=None, require_resource_owner=True, require_verifier=False, - require_realm=False, required_realm=None): + require_realm=False, required_realm=None, require_callback=False): """Verifies a request ensuring that the following is true: Per `section 3.2`_ of the spec. @@ -774,11 +847,16 @@ # Note that early exit would enable client enumeration valid_client = self.validate_client_key(request.client_key) if not valid_client: - client_key = self.dummy_client + request.client_key = self.dummy_client - # Ensure a valid redirection uri is used - valid_redirect = self.validate_redirect_uri(request.client_key, - request.callback_uri) + # Callback is normally never required, except for requests for + # a Temporary Credential as described in `Section 2.1`_ + # .._`Section 2.1`: http://tools.ietf.org/html/rfc5849#section-2.1 + if require_callback: + valid_redirect = self.validate_redirect_uri(request.client_key, + request.callback_uri) + else: + valid_redirect = True # The server SHOULD return a 401 (Unauthorized) status code when # receiving a request with invalid or expired token. @@ -791,11 +869,13 @@ if require_verifier: valid_resource_owner = self.validate_request_token( request.client_key, request.resource_owner_key) + if not valid_resource_owner: + request.resource_owner_key = self.dummy_request_token else: valid_resource_owner = self.validate_access_token( request.client_key, request.resource_owner_key) - if not valid_resource_owner: - resource_owner_key = self.dummy_resource_owner + if not valid_resource_owner: + request.resource_owner_key = self.dummy_access_token else: valid_resource_owner = True diff -Nru python-oauthlib-0.3.4/oauthlib/oauth1/rfc5849/signature.py python-oauthlib-0.3.7/oauthlib/oauth1/rfc5849/signature.py --- python-oauthlib-0.3.4/oauthlib/oauth1/rfc5849/signature.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth1/rfc5849/signature.py 2013-02-11 23:13:00.000000000 +0000 @@ -30,7 +30,8 @@ except ImportError: import urllib.parse as urlparse from . import utils -from oauthlib.common import bytes_type, extract_params, safe_string_equals, unicode_type +from oauthlib.common import urldecode, extract_params, safe_string_equals +from oauthlib.common import bytes_type, unicode_type def construct_base_string(http_method, base_string_uri, @@ -155,7 +156,7 @@ if (scheme, port) in default_ports: netloc = host - return urlparse.urlunparse((scheme, netloc, path, '', '', '')) + return urlparse.urlunparse((scheme, netloc, path, params, '', '')) # ** Request Parameters ** @@ -245,7 +246,7 @@ # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4 # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 if uri_query: - params.extend(urlparse.parse_qsl(uri_query, keep_blank_values=True)) + params.extend(urldecode(uri_query)) # * The OAuth HTTP "Authorization" header field (`Section 3.5.1`_) if # present. The header's content is parsed into a list of name/value diff -Nru python-oauthlib-0.3.4/oauthlib/oauth2/__init__.py python-oauthlib-0.3.7/oauthlib/oauth2/__init__.py --- python-oauthlib-0.3.4/oauthlib/oauth2/__init__.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth2/__init__.py 2013-02-05 21:18:06.000000000 +0000 @@ -11,4 +11,6 @@ from .draft25 import Client, Server from .draft25 import AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint - +from .draft25 import WebApplicationServer, MobileApplicationServer +from .draft25 import LegacyApplicationServer, BackendApplicationServer +from .draft25.grant_types import RequestValidator diff -Nru python-oauthlib-0.3.4/oauthlib/oauth2/draft25/__init__.py python-oauthlib-0.3.7/oauthlib/oauth2/draft25/__init__.py --- python-oauthlib-0.3.4/oauthlib/oauth2/draft25/__init__.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth2/draft25/__init__.py 2013-02-07 22:24:03.000000000 +0000 @@ -8,21 +8,32 @@ This module is an implementation of various logic needed for signing and checking OAuth 2.0 draft 25 requests. """ +import logging + from oauthlib.common import Request -from oauthlib.oauth2.draft25 import errors -from .tokens import prepare_bearer_uri, prepare_bearer_headers -from .tokens import prepare_bearer_body, prepare_mac_header -from .tokens import BearerToken +from oauthlib.oauth2.draft25 import tokens, grant_types from .parameters import prepare_grant_uri, prepare_token_request from .parameters import parse_authorization_code_response from .parameters import parse_implicit_response, parse_token_response -from .utils import params_from_uri AUTH_HEADER = 'auth_header' URI_QUERY = 'query' BODY = 'body' +log = logging.getLogger('oauthlib') + +# Add a NullHandler to prevent warnings for users who don't wish +# to configure logging. +try: + log.addHandler(logging.NullHandler()) +# NullHandler gracefully backported to 2.6 +except AttributeError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + log.addHandler(NullHandler()) + class Client(object): """Base OAuth2 client responsible for access tokens. @@ -134,13 +145,13 @@ headers=None, token_placement=None): """Add a bearer token to the request uri, body or authorization header.""" if token_placement == AUTH_HEADER: - headers = prepare_bearer_headers(self.access_token, headers) + headers = tokens.prepare_bearer_headers(self.access_token, headers) elif token_placement == URI_QUERY: - uri = prepare_bearer_uri(self.access_token, uri) + uri = tokens.prepare_bearer_uri(self.access_token, uri) elif token_placement == BODY: - body = prepare_bearer_body(self.access_token, body) + body = tokens.prepare_bearer_body(self.access_token, body) else: raise ValueError("Invalid token placement.") @@ -152,9 +163,9 @@ Warning: MAC token support is experimental as the spec is not yet stable. """ - headers = prepare_mac_header(self.access_token, uri, self.mac_key, http_method, - headers=headers, body=body, ext=ext, - hash_algorithm=self.mac_algorithm, **kwargs) + headers = tokens.prepare_mac_header(self.access_token, uri, + self.mac_key, http_method, headers=headers, body=body, ext=ext, + hash_algorithm=self.mac_algorithm, **kwargs) return uri, headers, body def _populate_attributes(self, response): @@ -567,109 +578,116 @@ MUST NOT be included more than once. """ - def __init__(self, default_token=None, response_types=None): - self._response_types = response_types or {} - self._default_token = default_token or BearerToken() + def __init__(self, default_response_type, default_token_type, + response_types): + self._response_types = response_types + self._default_response_type = default_response_type + self._default_token_type = default_token_type @property def response_types(self): return self._response_types @property - def default_token(self): - return self._default_token + def default_response_type(self): + return self._default_response_type + + @property + def default_response_type_handler(self): + return self.response_types.get(self.default_response_type) + + @property + def default_token_type(self): + return self._default_token_type def create_authorization_response(self, uri, http_method='GET', body=None, - headers=None): + headers=None, scopes=None, credentials=None): """Extract response_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) - query_params = params_from_uri(self.request.uri) - body_params = self.request.decoded_body - - # Prioritize response_type defined as query param over those in body. - # Chosen because the two core grant types utilizing the response type - # parameter both supply it in the uri. However it is not specified - # explicitely in RFC 6748. - if 'response_type' in query_params: - request.response_type = query_params.get('response_type') - elif 'response_type' in body_params: - request.response_type = body_params.get('response_type') - else: - raise errors.InvalidRequestError( - description='The response_type parameter is missing.') + request.authorized_scopes = scopes + for k, v in (credentials or {}).items(): + setattr(request, k, v) + response_type_handler = self.response_types.get( + request.response_type, self.default_response_type_handler) + log.debug('Dispatching response_type %s request to %r.', + request.response_type, response_type_handler) + return response_type_handler.create_authorization_response( + request, self.default_token_type) - if not request.response_type in self.response_types: - raise errors.UnsupportedResponseTypeError( - description='Invalid response type') - - return self.response_types.get( - request.response_type).create_authorization_response( - request, self.default_token) + def validate_authorization_request(self, uri, http_method='GET', body=None, + headers=None): + """Extract response_type and route to the designated handler.""" + request = Request(uri, http_method=http_method, body=body, headers=headers) + response_type_handler = self.response_types.get( + request.response_type, self.default_response_type_handler) + return response_type_handler.validate_authorization_request(request) class TokenEndpoint(object): - def __init__(self, default_token=None, grant_types=None): - self._grant_types = grant_types or {} - self._default_token = default_token or BearerToken() + def __init__(self, default_grant_type, default_token_type, grant_types): + self._grant_types = grant_types + self._default_token_type = default_token_type + self._default_grant_type = default_grant_type @property def grant_types(self): return self._grant_types @property - def default_token(self): - return self._default_token + def default_grant_type(self): + return self._default_grant_type + + @property + def default_grant_type_handler(self): + return self.grant_types.get(self.default_grant_type) + + @property + def default_token_type(self): + return self._default_token_type - def create_token_response(self, uri, http_method='GET', body=None, headers=None): + def create_token_response(self, uri, http_method='GET', body=None, + headers=None, credentials=None): """Extract grant_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) - query_params = params_from_uri(self.request.uri) - body_params = self.request.decoded_body + request.extra_credentials = credentials + grant_type_handler = self.grant_types.get(request.grant_type, + self.default_grant_type_handler) + log.debug('Dispatching grant_type %s request to %r.', + request.grant_type, grant_type_handler) + return grant_type_handler.create_token_response( + request, self.default_token_type) - # Prioritize grant_type defined as body param over those in uri. - # Chosen because all three core grant types supply this parameter - # in the body. However it is not specified explicitely in RFC 6748. - if 'grant_type' in body_params: - request.grant_type = query_params.get('grant_type') - elif 'grant_type' in query_params: - request.grant_type = body_params.get('grant_type') - else: - raise errors.InvalidRequestError( - description='The grant_type parameter is missing.') - if not request.grant_type in self.grant_types: - raise errors.UnsupportedGrantTypeError( - description='Invalid response type') - - return self.grant_types.get( - request.grant_type).create_token_response( - request, self.default_token) +class ResourceEndpoint(object): + def __init__(self, default_token, token_types): + self._tokens = token_types + self._default_token = default_token -class ResourceEndpoint(object): + @property + def default_token(self): + return self._default_token - def __init__(self, tokens=None): - self._tokens = tokens or {'Bearer': BearerToken()} + @property + def default_token_type_handler(self): + return self.tokens.get(self.default_token) @property def tokens(self): return self._tokens - def verify_request(self, uri, http_method='GET', body=None, headers=None): + def verify_request(self, uri, http_method='GET', body=None, headers=None, + scopes=None): """Validate client, code etc, return body + headers""" request = Request(uri, http_method, body, headers) request.token_type = self.find_token_type(request) - - # TODO(ib-lundgren): How to return errors is not strictly defined and - # should allow for customization. - if not request.token_type: - raise ValueError('Could not determine the token type.') - - if not request.token_type in self.tokens: - raise ValueError('Unsupported token type.') - - return self.tokens.get(request.token_type).validate_request(request) + request.scopes = scopes + token_type_handler = self.tokens.get(request.token_type, + self.default_token_type_handler) + log.debug('Dispatching token_type %s request to %r.', + request.token_type, token_type_handler) + return token_type_handler.validate_request(request), request def find_token_type(self, request): """Token type identification. @@ -679,9 +697,96 @@ the most likely token type (if any) by asking each known token type to give an estimation based on the request. """ - estimates = sorted((t.estimate_type(request) for t in self.tokens)) - return estimates[0] if len(estimates) else None + estimates = sorted(((t.estimate_type(request), n) for n, t in self.tokens.items())) + return estimates[0][1] if len(estimates) else None class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint): - pass + """An all-in-one endpoint featuring all four major grant types.""" + + def __init__(self, request_validator, *args, **kwargs): + auth_grant = grant_types.AuthorizationCodeGrant(request_validator) + implicit_grant = grant_types.ImplicitGrant(request_validator) + password_grant = grant_types.ResourceOwnerPasswordCredentialsGrant(request_validator) + credentials_grant = grant_types.ClientCredentialsGrant(request_validator) + refresh_grant = grant_types.RefreshTokenGrant(request_validator) + bearer = tokens.BearerToken(request_validator) + AuthorizationEndpoint.__init__(self, default_response_type='code', + response_types={ + 'code': auth_grant, + 'token': implicit_grant, + }, + default_token_type=bearer) + TokenEndpoint.__init__(self, default_grant_type='authorization_code', + grant_types={ + 'authorization_code': auth_grant, + 'password': password_grant, + 'client_credentials': credentials_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + + +class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" + + def __init__(self, request_validator, *args, **kwargs): + auth_grant = grant_types.AuthorizationCodeGrant(request_validator) + refresh_grant = grant_types.RefreshTokenGrant(request_validator) + bearer = tokens.BearerToken(request_validator) + AuthorizationEndpoint.__init__(self, default_response_type='code', + response_types={'code': auth_grant}, + default_token_type=bearer) + TokenEndpoint.__init__(self, default_grant_type='authorization_code', + grant_types={ + 'authorization_code': auth_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + + +class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" + + def __init__(self, request_validator, *args, **kwargs): + implicit_grant = grant_types.ImplicitGrant(request_validator) + bearer = tokens.BearerToken(request_validator) + AuthorizationEndpoint.__init__(self, default_response_type='token', + response_types={'token': implicit_grant}, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + + +class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" + + def __init__(self, request_validator, *args, **kwargs): + password_grant = grant_types.ResourceOwnerPasswordCredentialsGrant(request_validator) + refresh_grant = grant_types.RefreshTokenGrant(request_validator) + bearer = tokens.BearerToken(request_validator) + TokenEndpoint.__init__(self, default_grant_type='password', + grant_types={ + 'password': password_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + + +class BackendApplicationServer(TokenEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" + + def __init__(self, request_validator, *args, **kwargs): + credentials_grant = grant_types.ClientCredentialsGrant(request_validator) + bearer = tokens.BearerToken(request_validator) + TokenEndpoint.__init__(self, default_grant_type='client_credentials', + grant_types={'client_credentials': credentials_grant}, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) diff -Nru python-oauthlib-0.3.4/oauthlib/oauth2/draft25/errors.py python-oauthlib-0.3.7/oauthlib/oauth2/draft25/errors.py --- python-oauthlib-0.3.4/oauthlib/oauth2/draft25/errors.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth2/draft25/errors.py 2013-02-07 22:24:03.000000000 +0000 @@ -1,15 +1,17 @@ +# coding=utf-8 """ oauthlib.oauth2.draft_25.errors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ from __future__ import unicode_literals import json -from oauthlib.common import urlencode +from oauthlib.common import urlencode, add_params_to_uri class OAuth2Error(Exception): + error = None - def __init__(self, description=None, uri=None, state=None): + def __init__(self, description=None, uri=None, state=None, status_code=400): """ description: A human-readable ASCII [USASCII] text providing additional information, used to assist the client @@ -30,6 +32,10 @@ self.description = description self.uri = uri self.state = state + self.status_code = status_code + + def in_uri(self, uri): + return add_params_to_uri(uri, self.twotuples) @property def twotuples(self): @@ -48,7 +54,31 @@ @property def json(self): - return json.dumps(self.twotuples) + return json.dumps(dict(self.twotuples)) + + +class FatalClientError(OAuth2Error): + pass + + +class InvalidRedirectURIError(FatalClientError): + error = 'invalid_redirect_uri' + + +class MissingRedirectURIError(FatalClientError): + error = 'missing_redirect_uri' + + +class MismatchingRedirectURIError(FatalClientError): + error = 'mismatching_redirect_uri' + + +class MissingClientIdError(FatalClientError): + error = 'invalid_client_id' + + +class InvalidClientIdError(FatalClientError): + error = 'invalid_client_id' class InvalidRequestError(OAuth2Error): @@ -63,7 +93,7 @@ """The client is not authorized to request an authorization code using this method. """ -error = 'unauthorized_client' + error = 'unauthorized_client' class AccessDeniedError(OAuth2Error): @@ -92,7 +122,7 @@ error = 'server_error' -class TemporarilyUnvailableError(OAuth2Error): +class TemporarilyUnavailableError(OAuth2Error): """The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server. (This error code is needed because a 503 Service Unavailable HTTP diff -Nru python-oauthlib-0.3.4/oauthlib/oauth2/draft25/grant_types.py python-oauthlib-0.3.7/oauthlib/oauth2/draft25/grant_types.py --- python-oauthlib-0.3.4/oauthlib/oauth2/draft25/grant_types.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth2/draft25/grant_types.py 2013-02-07 22:24:03.000000000 +0000 @@ -1,126 +1,397 @@ +# -*- coding: utf-8 -*- """ oauthlib.oauth2.draft_25.grant_types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ from __future__ import unicode_literals import json -from oauthlib.common import generate_token, add_params_to_uri -from oauthlib.oauth2.draft25 import errors +import logging +from oauthlib import common +from oauthlib.oauth2.draft25 import errors, utils from oauthlib.uri_validate import is_absolute_uri +log = logging.getLogger('oauthlib') + class RequestValidator(object): - @property - def response_types(self): - return ('code', 'token') + def authenticate_client(self, request, *args, **kwargs): + """Authenticate client through means outside the OAuth 2 spec. - def validate_request(self, request, response_types=None): - request.state = getattr(request, 'state', None) - response_types = response_types or self.response_types or [] + Means of authentication is negotiated beforehand and may for example + be `HTTP Basic Authentication Scheme`_ which utilizes the Authorization + header. + + Headers may be accesses through request.headers and parameters found in + both body and query can be obtained by direct attribute access, i.e. + request.client_id for client_id in the URL query. + + OBS! Certain grant types rely on this authentication, possibly with + other fallbacks, and for them to recognize this authorization please + set the client attribute on the request (request.client). Note that + preferably this client object should have a client_id attribute of + unicode type (request.client.client_id). + + :param request: oauthlib.common.Request + :rtype: True or False + + Method is used by: + - Authorization Code Grant + - Resource Owner Password Credentials Grant (may be disabled) + - Client Credentials Grant + - Refresh Token Grant - if not request.client_id: - raise errors.InvalidRequestError(state=request.state, - description='Missing client_id parameter.') + .. _`HTTP Basic Authentication Scheme`: http://tools.ietf.org/html/rfc1945#section-11.1 + """ + raise NotImplementedError('Subclasses must implement this method.') - if not request.response_type: - raise errors.InvalidRequestError(state=request.state, - description='Missing response_type parameter.') + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): + """Ensure client is authorized to redirect to the redirect_uri requested. - if not self.validate_client(request.client_id): - raise errors.UnauthorizedClientError(state=request.state) + All clients should register the absolute URIs of all URIs they intend + to redirect to. The registration is outside of the scope of oauthlib. - if not request.response_type in response_types: - raise errors.UnsupportedResponseTypeError(state=request.state) + :param client_id: Unicode client identifier + :param code: Unicode authorization_code. + :param redirect_uri: Unicode absolute URI + :param client: Client object set by you, see authenticate_client. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - Authorization Code Grant (during token request) + """ + raise NotImplementedError('Subclasses must implement this method.') - self.validate_request_scopes(request) + def confirm_scopes(self, refresh_token, scopes, request, *args, **kwargs): + """Ensure the refresh token is authorized access to requested scopes. - if getattr(request, 'redirect_uri', None): - if not is_absolute_uri(request.redirect_uri): - raise errors.InvalidRequestError(state=request.state, - description='Non absolute redirect URI. See RFC3986') + :param refresh_token: Unicode refresh token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False - if not self.validate_redirect_uri(request.client_id, request.redirect_uri): - raise errors.AccessDeniedError(state=request.state) - else: - request.redirect_uri = self.get_default_redirect_uri(request.client_id) - if not request.redirect_uri: - raise errors.AccessDeniedError(state=request.state) + Method is used by: + - Refresh token grant + """ + raise NotImplementedError('Subclasses must implement this method.') - return True + def get_default_redirect_uri(self, client_id, request, *args, **kwargs): + """Get the default redirect URI for the client. - def validate_request_scopes(self, request): - request.state = getattr(request, 'state', None) - if request.scopes: - if not self.validate_scopes(request.client_id, request.scopes): - raise errors.InvalidScopeError(state=request.state) - else: - request.scopes = self.get_default_scopes(request.client_id) + :param client_id: Unicode client identifier + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: The default redirect URI for the client + + Method is used by: + - Authorization Code Grant + - Implicit Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def get_default_scopes(self, client_id, request, *args, **kwargs): + """Get the default scopes for the client. + + :param client_id: Unicode client identifier + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: List of default scopes + + Method is used by all core grant types: + - Authorization Code Grant + - Implicit Grant + - Resource Owner Password Credentials Grant + - Client Credentials grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def save_authorization_code(self, client_id, code, request, *args, **kwargs): + """Persist the authorization_code. + + The code should at minimum be associated with: + - a client and it's client_id + - the redirect URI used (request.redirect_uri) + - whether the redirect URI used is the client default or not + - a resource owner / user (request.user) + - authorized scopes (request.scopes) + + The authorization code grant dict (code) holds at least the key 'code', + {'code': 'sdf345jsdf0934f'}. + + :param client_id: Unicode client identifier + :param code: A dict of the authorization code grant. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: The default redirect URI for the client + + Method is used by: + - Authorization Code Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def save_bearer_token(self, token, request, *args, **kwargs): + """Persist the Bearer token. + + The Bearer token should at minimum be associated with: + - a client and it's client_id, if available + - a resource owner / user (request.user) + - authorized scopes (request.scopes) + - an expiration time + - a refresh token, if issued + + The Bearer token dict may hold a number of items, + { + 'token_type': 'Bearer', + 'token': 'askfjh234as9sd8', + 'expires_in': 3600, + 'scope': ['list', 'of', 'authorized', 'scopes'], + 'refresh_token': '23sdf876234', # if issued + 'state': 'given_by_client', # if supplied by client + } + + :param client_id: Unicode client identifier + :param token: A Bearer token dict + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: The default redirect URI for the client + + Method is used by all core grant types issuing Bearer tokens: + - Authorization Code Grant + - Implicit Grant + - Resource Owner Password Credentials Grant (might not associate a client) + - Client Credentials grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_bearer_token(self, token, scopes, request): + """Ensure the Bearer token is valid and authorized access to scopes. + + Note, the request.user attribute can be set to the resource owner + associated with this token. Similarly the request.client and + request.scopes attribute can be set to associated client object + and authorized scopes. If you then use a decorator such as the + one provided for django these attributes will be made available + in all protected views as keyword arguments. + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core Bearer token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Resource Owner Password Credentials Grant + - Client Credentials Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_client_id(self, client_id, request, *args, **kwargs): + """Ensure client_id belong to a valid and active client. - def validate_client(self, client, *args, **kwargs): + Note, while not strictly necessary it can often be very convenient + to set request.client to the client object associated with the + given client_id. + + :param request: oauthlib.common.Request + :rtype: True or False + + Method is used by: + - Authorization Code Grant + - Implicit Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_code(self, client_id, code, client, *args, **kwargs): + """Ensure the authorization_code is valid and assigned to client. + + OBS! The request.user attribute should be set to the resource owner + associated with this authorization code. + + :param client_id: Unicode client identifier + :param code: Unicode authorization code + :param client: Client object set by you, see authenticate_client. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - Authorization Code Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs): + """Ensure client is authorized to use the grant_type requested. + + :param client_id: Unicode client identifier + :param grant_type: Unicode grant type, i.e. authorization_code, password. + :param client: Client object set by you, see authenticate_client. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - Authorization Code Grant + - Resource Owner Password Credentials Grant + - Client Credentials Grant + - Refresh Token Grant + """ raise NotImplementedError('Subclasses must implement this method.') - def validate_scopes(self, client, scopes): + def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): + """Ensure client is authorized to redirect to the redirect_uri requested. + + All clients should register the absolute URIs of all URIs they intend + to redirect to. The registration is outside of the scope of oauthlib. + + :param client_id: Unicode client identifier + :param redirect_uri: Unicode absolute URI + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - Authorization Code Grant + - Implicit Grant + """ raise NotImplementedError('Subclasses must implement this method.') - def validate_user(self, username, password, client=None): + def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs): + """Ensure the Bearer token is valid and authorized access to scopes. + + OBS! The request.user attribute should be set to the resource owner + associated with this refresh token. + + :param refresh_token: Unicode refresh token + :param client: Client object set by you, see authenticate_client. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - Authorization Code Grant (indirectly by issuing refresh tokens) + - Resource Owner Password Credentials Grant (also indirectly) + - Refresh Token Grant + """ raise NotImplementedError('Subclasses must implement this method.') - def validate_redirect_uri(self, client, redirect_uri): + def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs): + """Ensure client is authorized to use the grant_type requested. + + :param client_id: Unicode client identifier + :param response_type: Unicode response type, i.e. code, token. + :param client: Client object set by you, see authenticate_client. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - Authorization Code Grant + - Implicit Grant + """ raise NotImplementedError('Subclasses must implement this method.') - def get_default_redirect_uri(self, client): + def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): + """Ensure the client is authorized access to requested scopes. + + :param client_id: Unicode client identifier + :param scopes: List of scopes (defined by you) + :param client: Client object set by you, see authenticate_client. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by all core grant types: + - Authorization Code Grant + - Implicit Grant + - Resource Owner Password Credentials Grant + - Client Credentials Grant + """ raise NotImplementedError('Subclasses must implement this method.') - def get_default_scopes(self, client): + def validate_user(self, username, password, client, request, *args, **kwargs): + """Ensure the username and password is valid. + + OBS! The validation should also set the user attribute of the request + to a valid resource owner, i.e. request.user = username or similar. If + not set you will be unable to associate a token with a user in the + persistance method used (commonly, save_bearer_token). + + :param username: Unicode username + :param password: Unicode password + :param client: Client object set by you, see authenticate_client. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - Resource Owner Password Credentials Grant + """ raise NotImplementedError('Subclasses must implement this method.') class GrantTypeBase(object): + error_uri = None + request_validator = None - def create_authorization_response(self, request): + def create_authorization_response(self, request, token_handler): raise NotImplementedError('Subclasses must implement this method.') def create_token_response(self, request, token_handler): raise NotImplementedError('Subclasses must implement this method.') + def validate_grant_type(self, request): + if not self.request_validator.validate_grant_type(request.client_id, + request.grant_type, request.client, request): + log.debug('Unauthorized from %r (%r) access to grant type %s.', + request.client_id, request.client, request.grant_type) + raise errors.UnauthorizedClientError() -class AuthorizationCodeGrant(GrantTypeBase): + def validate_scopes(self, request): + request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list( + self.request_validator.get_default_scopes(request.client_id, request)) + log.debug('Validating access to scopes %r for client %r (%r).', + request.scopes, request.client_id, request.client) + if not self.request_validator.validate_scopes(request.client_id, + request.scopes, request.client, request): + raise errors.InvalidScopeError(state=request.state) - @property - def scopes(self): - return ('default',) - @property - def error_uri(self): - return '/oauth_error' +class AuthorizationCodeGrant(GrantTypeBase): def __init__(self, request_validator=None): self.request_validator = request_validator or RequestValidator() def create_authorization_code(self, request): """Generates an authorization grant represented as a dictionary.""" - grant = {'code': generate_token()} + grant = {'code': common.generate_token()} if hasattr(request, 'state') and request.state: grant['state'] = request.state + log.debug('Created authorization code grant %r for request %r.', + grant, request) return grant - def save_authorization_code(self, client_id, grant): - """Saves authorization codes for later use by the token endpoint.""" - raise NotImplementedError('Subclasses must implement this method.') - def create_authorization_response(self, request, token_handler): try: - self.request_validator.validate_request(request) - + self.validate_authorization_request(request) + log.debug('Pre resource owner authorization validation ok for %r.', + request) + + # If the request fails due to a missing, invalid, or mismatching + # redirection URI, or if the client identifier is missing or invalid, + # the authorization server SHOULD inform the resource owner of the + # error and MUST NOT automatically redirect the user-agent to the + # invalid redirection URI. + except errors.FatalClientError as e: + log.debug('Fatal client error during validation of %r. %r.', + request, e) + raise + + # If the resource owner denies the access request or if the request + # fails for reasons other than a missing or invalid redirection URI, + # the authorization server informs the client by adding the following + # parameters to the query component of the redirection URI using the + # "application/x-www-form-urlencoded" format, per Appendix B: + # http://tools.ietf.org/html/rfc6749#appendix-B except errors.OAuth2Error as e: - request.redirect_uri = getattr(request, 'redirect_uri', - self.error_uri) - return add_params_to_uri(request.redirect_uri, e.twotuples) + log.debug('Client error during validation of %r. %r.', request, e) + request.redirect_uri = request.redirect_uri or self.error_uri + return common.add_params_to_uri(request.redirect_uri, e.twotuples), None, None, e.status_code grant = self.create_authorization_code(request) - self.save_authorization_code(request.client_id, grant) - return add_params_to_uri(request.redirect_uri, grant.items()) + logging.debug('Saving grant %r for %r.', grant, request) + self.request_validator.save_authorization_code(request, grant) + return common.add_params_to_uri(request.redirect_uri, grant.items()), None, None, 200 def create_token_response(self, request, token_handler): """Validate the authorization code. @@ -133,29 +404,146 @@ """ try: self.validate_token_request(request) + log.debug('Token request validation ok for %r.', request) except errors.OAuth2Error as e: - return e.json - return json.dumps(token_handler(request, refresh_token=True)) + log.debug('Client error during validation of %r. %r.', request, e) + return None, {}, e.json, e.status_code - def validate_token_request(self, request): + return None, {}, json.dumps(token_handler.create_token(request, refresh_token=True)), 200 + + def validate_authorization_request(self, request): + """Check the authorization request for normal and fatal errors. - if getattr(request, 'grant_type', '') != 'authorization_code': + A normal error could be a missing response_type parameter or the client + attempting to access scope it is not allowed to ask authorization for. + Normal errors can safely be included in the redirection URI and + sent back to the client. + + Fatal errors occur when the client_id or redirect_uri is invalid or + missing. These must be caught by the provider and handled, how this + is done is outside of the scope of OAuthLib but showing an error + page describing the issue is a good idea. + """ + + # First check for fatal errors + + # If the request fails due to a missing, invalid, or mismatching + # redirection URI, or if the client identifier is missing or invalid, + # the authorization server SHOULD inform the resource owner of the + # error and MUST NOT automatically redirect the user-agent to the + # invalid redirection URI. + + # REQUIRED. The client identifier as described in Section 2.2. + # http://tools.ietf.org/html/rfc6749#section-2.2 + if not request.client_id: + raise errors.MissingClientIdError(state=request.state) + + if not self.request_validator.validate_client_id(request.client_id, request): + raise errors.InvalidClientIdError(state=request.state) + + # OPTIONAL. As described in Section 3.1.2. + # http://tools.ietf.org/html/rfc6749#section-3.1.2 + log.debug('Validating redirection uri %s for client %s.', + request.redirect_uri, request.client_id) + if request.redirect_uri is not None: + request.using_default_redirect_uri = False + log.debug('Using provided redirect_uri %s', request.redirect_uri) + if not is_absolute_uri(request.redirect_uri): + raise errors.InvalidRedirectURIError(state=request.state) + + if not self.request_validator.validate_redirect_uri( + request.client_id, request.redirect_uri): + raise errors.MismatchingRedirectURIError(state=request.state) + else: + request.redirect_uri = self.request_validator.get_default_redirect_uri( + request.client_id, request) + request.using_default_redirect_uri = True + log.debug('Using default redirect_uri %s.', request.redirect_uri) + if not request.redirect_uri: + raise errors.MissingRedirectURIError(state=request.state) + + # Then check for normal errors. + + # If the resource owner denies the access request or if the request + # fails for reasons other than a missing or invalid redirection URI, + # the authorization server informs the client by adding the following + # parameters to the query component of the redirection URI using the + # "application/x-www-form-urlencoded" format, per Appendix B. + # http://tools.ietf.org/html/rfc6749#appendix-B + + # Note that the correct parameters to be added are automatically + # populated through the use of specific exceptions. + if request.response_type is None: + raise errors.InvalidRequestError(state=request.state, + description='Missing response_type parameter.') + + if not self.request_validator.validate_response_type(request.client_id, + request.response_type, request): + log.debug('Client %s is not authorized to use response_type %s.', + request.client_id, request.response_type) + raise errors.UnauthorizedClientError() + + # REQUIRED. Value MUST be set to "code". + if request.response_type != 'code': + raise errors.UnsupportedResponseTypeError(state=request.state) + + # OPTIONAL. The scope of the access request as described by Section 3.3 + # http://tools.ietf.org/html/rfc6749#section-3.3 + self.validate_scopes(request) + + return request.scopes, { + 'client_id': request.client_id, + 'redirect_uri': request.redirect_uri, + 'response_type': request.response_type, + } + + def validate_token_request(self, request): + # REQUIRED. Value MUST be set to "authorization_code". + if request.grant_type != 'authorization_code': raise errors.UnsupportedGrantTypeError() - if not getattr(request, 'code', None): + if request.code is None: raise errors.InvalidRequestError( description='Missing code parameter.') - # TODO: document diff client & client_id, former is authenticated - # outside spec, i.e. http basic - if (not hasattr(request, 'client') or - not self.request_validator.validate_client(request.client, request.grant_type)): + # If the client type is confidential or the client was issued client + # credentials (or assigned other authentication requirements), the + # client MUST authenticate with the authorization server as described + # in Section 3.2.1. + # http://tools.ietf.org/html/rfc6749#section-3.2.1 + if not self.request_validator.authenticate_client(request): + log.debug('Could not authenticate client, %r.', request) + raise errors.AccessDeniedError() + + # REQUIRED, if the client is not authenticating with the + # authorization server as described in Section 3.2.1. + # http://tools.ietf.org/html/rfc6749#section-3.2.1 + if (not request.client and not + self.request_validator.validate_client_id( + request.client_id, request)): + log.debug('Client_id not provided for unauthenticated client, %r.', + request) raise errors.UnauthorizedClientError() - if not self.request_validator.validate_code(request.client, request.code): + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + # REQUIRED. The authorization code received from the + # authorization server. + if not self.request_validator.validate_code(request.client_id, + request.code, request.client, request): + log.debug('Client, %r (%r), is not allowed access to scopes %r.', + request.client_id, request.client, request.scopes) raise errors.InvalidGrantError() - # TODO: validate scopes + # REQUIRED, if the "redirect_uri" parameter was included in the + # authorization request as described in Section 4.1.1, and their + # values MUST be identical. + if not self.request_validator.confirm_redirect_uri(request.client_id, + request.code, request.redirect_uri, request.client): + log.debug('Redirect_uri (%r) invalid for client %r (%r).', + request.redirect_uri, request.client_id, request.client) + raise errors.AccessDeniedError() class ImplicitGrant(GrantTypeBase): @@ -223,6 +611,9 @@ def __init__(self, request_validator=None): self.request_validator = request_validator or RequestValidator() + def create_authorization_response(self, request, token_handler): + return self.create_token_response(request, token_handler) + def create_token_response(self, request, token_handler): """Return token or error embedded in the URI fragment. @@ -263,13 +654,129 @@ .. _`Section 7.2`: http://tools.ietf.org/html/rfc6749#section-7.2 """ try: - self.request_validator.validate_request(request) + self.validate_token_request(request) + + # If the request fails due to a missing, invalid, or mismatching + # redirection URI, or if the client identifier is missing or invalid, + # the authorization server SHOULD inform the resource owner of the + # error and MUST NOT automatically redirect the user-agent to the + # invalid redirection URI. + except errors.FatalClientError as e: + log.debug('Fatal client error during validation of %r. %r.', + request, e) + raise + + # If the resource owner denies the access request or if the request + # fails for reasons other than a missing or invalid redirection URI, + # the authorization server informs the client by adding the following + # parameters to the fragment component of the redirection URI using the + # "application/x-www-form-urlencoded" format, per Appendix B: + # http://tools.ietf.org/html/rfc6749#appendix-B except errors.OAuth2Error as e: - return add_params_to_uri(request.redirect_uri, e.twotuples, - fragment=True) - token = token_handler(request, refresh_token=False) - return add_params_to_uri(request.redirect_uri, token.items(), - fragment=True), {}, None + log.debug('Client error during validation of %r. %r.', request, e) + return common.add_params_to_uri(request.redirect_uri, e.twotuples, + fragment=True), {}, None, e.status_code + + token = token_handler.create_token(request, refresh_token=False) + return common.add_params_to_uri(request.redirect_uri, token.items(), + fragment=True), {}, None, 200 + + def validate_authorization_request(self, request): + return self.validate_token_request(request) + + def validate_token_request(self, request): + """Check the token request for normal and fatal errors. + + This method is very similar to validate_authorization_request in + the AuthorizationCodeGrant but differ in a few subtle areas. + + A normal error could be a missing response_type parameter or the client + attempting to access scope it is not allowed to ask authorization for. + Normal errors can safely be included in the redirection URI and + sent back to the client. + + Fatal errors occur when the client_id or redirect_uri is invalid or + missing. These must be caught by the provider and handled, how this + is done is outside of the scope of OAuthLib but showing an error + page describing the issue is a good idea. + """ + + # First check for fatal errors + + # If the request fails due to a missing, invalid, or mismatching + # redirection URI, or if the client identifier is missing or invalid, + # the authorization server SHOULD inform the resource owner of the + # error and MUST NOT automatically redirect the user-agent to the + # invalid redirection URI. + + # REQUIRED. The client identifier as described in Section 2.2. + # http://tools.ietf.org/html/rfc6749#section-2.2 + if not request.client_id: + raise errors.MissingClientIdError(state=request.state) + + if not self.request_validator.validate_client_id(request.client_id, request): + raise errors.InvalidClientIdError(state=request.state) + + # OPTIONAL. As described in Section 3.1.2. + # http://tools.ietf.org/html/rfc6749#section-3.1.2 + if request.redirect_uri is not None: + request.using_default_redirect_uri = False + log.debug('Using provided redirect_uri %s', request.redirect_uri) + if not is_absolute_uri(request.redirect_uri): + raise errors.InvalidRedirectURIError(state=request.state) + + # The authorization server MUST verify that the redirection URI + # to which it will redirect the access token matches a + # redirection URI registered by the client as described in + # Section 3.1.2. + # http://tools.ietf.org/html/rfc6749#section-3.1.2 + if not self.request_validator.validate_redirect_uri( + request.client_id, request.redirect_uri): + raise errors.MismatchingRedirectURIError(state=request.state) + else: + request.redirect_uri = self.request_validator.get_default_redirect_uri( + request.client_id, request) + request.using_default_redirect_uri = True + log.debug('Using default redirect_uri %s.', request.redirect_uri) + if not request.redirect_uri: + raise errors.MissingRedirectURIError(state=request.state) + + # Then check for normal errors. + + # If the resource owner denies the access request or if the request + # fails for reasons other than a missing or invalid redirection URI, + # the authorization server informs the client by adding the following + # parameters to the fragment component of the redirection URI using the + # "application/x-www-form-urlencoded" format, per Appendix B. + # http://tools.ietf.org/html/rfc6749#appendix-B + + # Note that the correct parameters to be added are automatically + # populated through the use of specific exceptions. + if request.response_type is None: + raise errors.InvalidRequestError(state=request.state, + description='Missing response_type parameter.') + + # REQUIRED. Value MUST be set to "token". + if request.response_type != 'token': + raise errors.UnsupportedResponseTypeError(state=request.state) + + log.debug('Validating use of response_type token for client %r (%r).', + request.client_id, request.client) + if not self.request_validator.validate_response_type(request.client_id, + request.response_type, request): + log.debug('Client %s is not authorized to use response_type %s.', + request.client_id, request.response_type) + raise errors.UnauthorizedClientError() + + # OPTIONAL. The scope of the access request as described by Section 3.3 + # http://tools.ietf.org/html/rfc6749#section-3.3 + self.validate_scopes(request) + + return request.scopes, { + 'client_id': request.client_id, + 'redirect_uri': request.redirect_uri, + 'response_type': request.response_type, + } class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): @@ -348,11 +855,20 @@ """ try: if require_authentication: + log.debug('Authenticating client, %r.', request) self.request_validator.authenticate_client(request) + else: + log.debug('Client authentication disabled, %r.', request) + log.debug('Validating access token request, %r.', request) self.validate_token_request(request) except errors.OAuth2Error as e: - return None, {}, e.json - return None, {}, json.dumps(token_handler(request, refresh_token=True)) + log.debug('Client error in token request, %s.', e) + return None, {}, e.json, e.status_code + + token = token_handler.create_token(request, refresh_token=True) + log.debug('Issuing token %r to client id %r (%r) and username %s.', + token, request.client_id, request.client, request.username) + return None, {}, json.dumps(token), 200 def validate_token_request(self, request): for param in ('grant_type', 'username', 'password'): @@ -365,13 +881,19 @@ if not request.grant_type == 'password': raise errors.UnsupportedGrantTypeError() - # request.client is populated during client authentication - client = request.client if getattr(request, 'client') else None + log.debug('Validating username %s and password %s.', + request.username, request.password) if not self.request_validator.validate_user(request.username, - request.password, client=client): + request.password, request.client, request): raise errors.InvalidGrantError('Invalid credentials given.') + log.debug('Authorizing access to user %r.', request.user) - self.request_validator.validate_request_scopes(request) + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + if request.client: + request.client_id = request.client_id or request.client.client_id + self.validate_scopes(request) class ClientCredentialsGrant(GrantTypeBase): @@ -424,17 +946,124 @@ .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 """ try: + log.debug('Authenticating client, %r.', request) self.request_validator.authenticate_client(request) + log.debug('Validating access token request, %r.', request) self.validate_token_request(request) except errors.OAuth2Error as e: - return None, {}, e.json - return None, {}, json.dumps(token_handler(request, refresh_token=True)) + log.debug('Client error in token request. %s.', e) + return None, {}, e.json, e.status_code + + token = token_handler.create_token(request, refresh_token=False) + log.debug('Issuing token to client id %r (%r), %r.', + request.client_id, request.client, token) + return None, {}, json.dumps(token), 200 def validate_token_request(self, request): if not getattr(request, 'grant_type'): - raise errors.InvalidRequestError('Request is issing grant type.') + raise errors.InvalidRequestError('Request is missing grant type.') if not request.grant_type == 'client_credentials': raise errors.UnsupportedGrantTypeError() - self.request_validator.validate_request_scopes(request) + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + request.user = request.user or request.client.user + log.debug('Authorizing access to user %r.', request.user) + request.client_id = request.client_id or request.client.client_id + self.validate_scopes(request) + + +class RefreshTokenGrant(GrantTypeBase): + """`Refresh token grant`_ + + .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6 + """ + + @property + def issue_new_refresh_tokens(self): + return True + + def __init__(self, request_validator=None, issue_new_refresh_tokens=True): + self.request_validator = request_validator or RequestValidator() + + def create_token_response(self, request, token_handler): + """Create a new access token from a refresh_token. + + If valid and authorized, the authorization server issues an access + token as described in `Section 5.1`_. If the request failed + verification or is invalid, the authorization server returns an error + response as described in `Section 5.2`_. + + The authorization server MAY issue a new refresh token, in which case + the client MUST discard the old refresh token and replace it with the + new refresh token. The authorization server MAY revoke the old + refresh token after issuing a new refresh token to the client. If a + new refresh token is issued, the refresh token scope MUST be + identical to that of the refresh token included by the client in the + request. + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + """ + try: + log.debug('Validating refresh token request, %r.', request) + self.validate_token_request(request) + except errors.OAuth2Error as e: + return None, {}, e.json, 400 + + token = token_handler.create_token(request, + refresh_token=self.issue_new_refresh_tokens) + log.debug('Issuing new token to client id %r (%r), %r.', + request.client_id, request.client, token) + return None, {}, json.dumps(token), 200 + + def validate_token_request(self, request): + # REQUIRED. Value MUST be set to "refresh_token". + if request.grant_type != 'refresh_token': + raise errors.UnsupportedGrantTypeError() + + if request.refresh_token is None: + raise errors.InvalidRequestError( + description='Missing refresh token parameter.') + + # Because refresh tokens are typically long-lasting credentials used to + # request additional access tokens, the refresh token is bound to the + # client to which it was issued. If the client type is confidential or + # the client was issued client credentials (or assigned other + # authentication requirements), the client MUST authenticate with the + # authorization server as described in Section 3.2.1. + # http://tools.ietf.org/html/rfc6749#section-3.2.1 + log.debug('Authenticating client, %r.', request) + if not self.request_validator.authenticate_client(request): + log.debug('Invalid client (%r), denying access.', request) + raise errors.AccessDeniedError() + + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + # OPTIONAL. The scope of the access request as described by + # Section 3.3. The requested scope MUST NOT include any scope + # not originally granted by the resource owner, and if omitted is + # treated as equal to the scope originally granted by the + # resource owner. + if request.scopes: + log.debug('Ensuring refresh token %s has access to scopes %r.', + request.refresh_token, request.scopes) + else: + log.debug('Reusing scopes from previous access token.') + if not self.request_validator.confirm_scopes(request.refresh_token, + request.scopes, request): + log.debug('Refresh token %s lack requested scopes, %r.', + request.refresh_token, request.scopes) + raise errors.InvalidScopeError(state=request.state) + + # REQUIRED. The refresh token issued to the client. + log.debug('Validating refresh token %s for client %r.', + request.refresh_token, request.client) + if not self.request_validator.validate_refresh_token( + request.refresh_token, request.client, request): + log.debug('Invalid refresh token, %s, for client %r.', + request.refresh_token, request.client) + raise errors.InvalidRequestError() diff -Nru python-oauthlib-0.3.4/oauthlib/oauth2/draft25/parameters.py python-oauthlib-0.3.7/oauthlib/oauth2/draft25/parameters.py --- python-oauthlib-0.3.4/oauthlib/oauth2/draft25/parameters.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth2/draft25/parameters.py 2013-02-02 13:08:44.000000000 +0000 @@ -16,7 +16,7 @@ except ImportError: import urllib.parse as urlparse from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type -from oauthlib.oauth2.draft25.utils import scope_to_string, scope_to_list +from oauthlib.oauth2.draft25.utils import list_to_scope, scope_to_list def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, @@ -60,7 +60,7 @@ if redirect_uri: params.append(('redirect_uri', redirect_uri)) if scope: - params.append(('scope', scope_to_string(scope))) + params.append(('scope', list_to_scope(scope))) if state: params.append(('state', state)) @@ -96,7 +96,7 @@ params = [('grant_type', grant_type)] if 'scope' in kwargs: - kwargs['scope'] = scope_to_string(kwargs['scope']) + kwargs['scope'] = list_to_scope(kwargs['scope']) for k in kwargs: if kwargs[k]: diff -Nru python-oauthlib-0.3.4/oauthlib/oauth2/draft25/tokens.py python-oauthlib-0.3.7/oauthlib/oauth2/draft25/tokens.py --- python-oauthlib-0.3.4/oauthlib/oauth2/draft25/tokens.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth2/draft25/tokens.py 2013-02-07 22:24:03.000000000 +0000 @@ -10,7 +10,6 @@ """ from binascii import b2a_base64 -import datetime import hashlib import hmac try: @@ -48,7 +47,7 @@ :param headers: Request headers as a dictionary. :param http_method: HTTP Request method. :param key: MAC given provided by token endpoint. - :param algorithm: HMAC algorithm provided by token endpoint. + :param hash_algorithm: HMAC algorithm provided by token endpoint. :param issue_time: Time when the MAC credentials were issues as a datetime object. :param draft: MAC authentication specification version. :return: headers dictionary with the authorization field added. @@ -169,32 +168,46 @@ class BearerToken(TokenBase): + def __init__(self, request_validator=None): + self.request_validator = request_validator + @property def expires_in(self): return 3600 - def save_token(self, request, token): - """Saves authorization codes for later use by the token endpoint.""" - raise NotImplementedError('Subclasses must implement this method.') - - def __call__(self, request, refresh_token=False): + def create_token(self, request, refresh_token=False): + """Create a BearerToken, by default without refresh token.""" token = { 'access_token': common.generate_token(), 'expires_in': self.expires_in, - 'scope': ' '.join(request.scopes), 'token_type': 'Bearer', } - if getattr(request, 'state', None): + if request.scopes is not None: + token['scope'] = ' '.join(request.scopes) + + if request.state is not None: token['state'] = request.state if refresh_token: token['refresh_token'] = common.generate_token() - self.save_token(request, token) + token.update(request.extra_credentials or {}) + + self.request_validator.save_bearer_token(token, request) return token def validate_request(self, request): - pass + token = None + if 'Authorization' in request.headers: + token = request.headers.get('Authorization')[7:] + else: + token = request.access_token + return self.request_validator.validate_bearer_token(token, request.scopes, request) def estimate_type(self, request): - pass + if request.headers.get('Authorization', '').startswith('Bearer'): + return 9 + elif request.access_token is not None: + return 5 + else: + return 0 diff -Nru python-oauthlib-0.3.4/oauthlib/oauth2/draft25/utils.py python-oauthlib-0.3.7/oauthlib/oauth2/draft25/utils.py --- python-oauthlib-0.3.4/oauthlib/oauth2/draft25/utils.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth2/draft25/utils.py 2013-02-02 13:05:50.000000000 +0000 @@ -20,7 +20,7 @@ from oauthlib.common import unicode_type, urldecode -def scope_to_string(scope): +def list_to_scope(scope): """Convert a list of scopes to a space separated string.""" if isinstance(scope, unicode_type) or scope is None: return scope diff -Nru python-oauthlib-0.3.4/oauthlib/oauth2/ext/django.py python-oauthlib-0.3.7/oauthlib/oauth2/ext/django.py --- python-oauthlib-0.3.4/oauthlib/oauth2/ext/django.py 1970-01-01 00:00:00.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib/oauth2/ext/django.py 2013-02-07 22:28:59.000000000 +0000 @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import +from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden +from django.views.decorators.csrf import csrf_exempt +import functools +import logging +from oauthlib.common import urlencode +from oauthlib.oauth2.draft25 import errors + +log = logging.getLogger('oauthlib') + + +class OAuth2ProviderDecorator(object): + + def __init__(self, error_uri, server=None, authorization_endpoint=None, + token_endpoint=None, resource_endpoint=None): + self._authorization_endpoint = authorization_endpoint or server + self._token_endpoint = token_endpoint or server + self._resource_endpoint = resource_endpoint or server + self._error_uri = error_uri + + def _extract_params(self, request): + log.debug('Extracting parameters from request.') + uri = request.build_absolute_uri() + http_method = request.method + headers = request.META + del headers['wsgi.input'] + del headers['wsgi.errors'] + if 'HTTP_AUTHORIZATION' in headers: + headers['Authorization'] = headers['HTTP_AUTHORIZATION'] + body = urlencode(request.POST.items()) + return uri, http_method, body, headers + + def pre_authorization_view(self, f): + @functools.wraps(f) + def wrapper(request, *args, **kwargs): + uri, http_method, body, headers = self._extract_params(request) + redirect_uri = request.GET.get('redirect_uri', None) + log.debug('Found redirect uri %s.', redirect_uri) + try: + scopes, credentials = self._authorization_endpoint.validate_authorization_request( + uri, http_method, body, headers) + log.debug('Saving credentials to session, %r.', credentials) + request.session['oauth2_credentials'] = credentials + kwargs['scopes'] = scopes + log.debug('Invoking view method, %r.', f) + return f(request, *args, **kwargs) + + except errors.FatalClientError as e: + log.debug('Fatal client error, redirecting to error page.') + return HttpResponseRedirect(e.in_uri(self._error_uri)) + except errors.OAuth2Error as e: + log.debug('Client error, redirecting back to client.') + return HttpResponseRedirect(e.in_uri(redirect_uri)) + return wrapper + + def post_authorization_view(self, f): + @functools.wraps(f) + def wrapper(request, *args, **kwargs): + uri, http_method, body, headers = self._extract_params(request) + scopes, credentials = f(request, *args, **kwargs) + log.debug('Fetched credentials view, %r.', credentials) + credentials.update(request.session.get('oauth2_credentials', {})) + log.debug('Fetched credentials from session, %r.', credentials) + redirect_uri = credentials.get('redirect_uri') + log.debug('Found redirect uri %s.', redirect_uri) + try: + url, headers, body, status = self._authorization_endpoint.create_authorization_response( + uri, http_method, body, headers, scopes, credentials) + log.debug('Authorization successful, redirecting to client.') + return HttpResponseRedirect(url) + except errors.FatalClientError as e: + log.debug('Fatal client error, redirecting to error page.') + return HttpResponseRedirect(e.in_uri(self._error_uri)) + except errors.OAuth2Error as e: + log.debug('Client error, redirecting back to client.') + return HttpResponseRedirect(e.in_uri(redirect_uri)) + + return wrapper + + def access_token_view(self, f): + @csrf_exempt + @functools.wraps(f) + def wrapper(request, *args, **kwargs): + uri, http_method, body, headers = self._extract_params(request) + credentials = f(request, *args, **kwargs) + log.debug('Fetched credentials view, %r.', credentials) + url, headers, body, status = self._token_endpoint.create_token_response( + uri, http_method, body, headers, credentials) + response = HttpResponse(content=body, status=status) + for k, v in headers: + response[k] = v + response['Content-Type'] = 'application/json;charset=UTF-8' + response['Cache-Control'] = 'no-store' + response['Pragma'] = 'no-cache' + return response + return wrapper + + def protected_resource_view(self, scopes=None): + def decorator(f): + @csrf_exempt + @functools.wraps(f) + def wrapper(request, *args, **kwargs): + uri, http_method, body, headers = self._extract_params(request) + valid, r = self._resource_endpoint.verify_request( + uri, http_method, body, headers, scopes) + kwargs.update({ + 'client': r.client, + 'resource_owner': r.resource_owner, + 'scopes': r.scopes + }) + if valid: + return f(request, *args, **kwargs) + else: + return HttpResponseForbidden() + return wrapper + return decorator diff -Nru python-oauthlib-0.3.4/oauthlib.egg-info/PKG-INFO python-oauthlib-0.3.7/oauthlib.egg-info/PKG-INFO --- python-oauthlib-0.3.4/oauthlib.egg-info/PKG-INFO 2012-11-19 18:28:55.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib.egg-info/PKG-INFO 2013-02-11 23:35:19.000000000 +0000 @@ -1,37 +1,11 @@ Metadata-Version: 1.0 Name: oauthlib -Version: 0.3.4 +Version: 0.3.7 Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic Home-page: https://github.com/idan/oauthlib Author: Idan Gazit Author-email: idan@gazit.me -License: Copyright (c) 2011 Idan Gazit and contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - 3. Neither the name of this project nor the names of its contributors may - be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +License: BSD Description: OAuthLib ======== @@ -55,10 +29,11 @@ veneer on top of OAuthLib and get OAuth support for very little effort. Documentation - ------------- + -------------- - Full documentation is available on `Read the Docs`_. + Full documentation is available on `Read the Docs`_. All contributions are very welcome! The documentation is still quite sparse, please open an issue for what you'd like to know, or discuss it in our `G+ community`_, or even better, send a pull request! + .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 .. _`Read the Docs`: https://oauthlib.readthedocs.org/en/latest/index.html Interested in making OAuth requests? @@ -71,6 +46,16 @@ .. _`requests`: https://github.com/kennethreitz/requests .. _`requests OAuth examples`: http://docs.python-requests.org/en/latest/user/quickstart/#oauth-authentication + Using OAuthLib? Please get in touch! + ------------------------------------ + Patching OAuth support onto an http request framework? Creating an OAuth provider extension for a web framework? Simply using OAuthLib to Get Things Done or to learn? + + No matter which we'd love to hear from you in our `G+ community`_ or if you have anything in particular you would like to have, change or comment on don't hesitate for a second to send a pull request or open an issue. We might be quite busy and therefore slow to reply but we love feedback! + + Chances are you have run into something annoying that you wish there was documentation for, if you wish to gain eternal fame and glory, and a drink if we have the pleasure to run into eachother, please send a docs pull request =) + + .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 + License ------- @@ -85,6 +70,12 @@ *OAuthLib is in active development, with most of OAuth1 complete and OAuth2 already in the works.* + 0.3.7: OAuth 1 optional encoding of Client.sign return values + + 0.3.6: Revert default urlencoding. + + 0.3.5: Default unicode conversion (utf-8) and urlencoding of input. + 0.3.4: A number of small features and bug fixes. 0.3.3: OAuth 1 Provider verify now return useful params diff -Nru python-oauthlib-0.3.4/oauthlib.egg-info/SOURCES.txt python-oauthlib-0.3.7/oauthlib.egg-info/SOURCES.txt --- python-oauthlib-0.3.4/oauthlib.egg-info/SOURCES.txt 2012-11-19 18:28:55.000000000 +0000 +++ python-oauthlib-0.3.7/oauthlib.egg-info/SOURCES.txt 2013-02-11 23:35:19.000000000 +0000 @@ -23,6 +23,8 @@ oauthlib/oauth2/draft25/parameters.py oauthlib/oauth2/draft25/tokens.py oauthlib/oauth2/draft25/utils.py +oauthlib/oauth2/ext/__init__.py +oauthlib/oauth2/ext/django.py tests/__init__.py tests/test_common.py tests/oauth1/__init__.py diff -Nru python-oauthlib-0.3.4/setup.py python-oauthlib-0.3.7/setup.py --- python-oauthlib-0.3.4/setup.py 2012-11-19 18:27:25.000000000 +0000 +++ python-oauthlib-0.3.7/setup.py 2013-02-11 23:34:15.000000000 +0000 @@ -24,13 +24,13 @@ setup( name='oauthlib', - version='0.3.4', + version='0.3.7', description='A generic, spec-compliant, thorough implementation of the OAuth request-signing logic', long_description=fread('README.rst'), author='Idan Gazit', author_email='idan@gazit.me', url='https://github.com/idan/oauthlib', - license=fread('LICENSE'), + license='BSD', packages=find_packages(exclude=('docs','tests','tests.*')), test_suite='nose.collector', tests_require=tests_require, diff -Nru python-oauthlib-0.3.4/tests/oauth1/rfc5849/test_client.py python-oauthlib-0.3.7/tests/oauth1/rfc5849/test_client.py --- python-oauthlib-0.3.4/tests/oauth1/rfc5849/test_client.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/tests/oauth1/rfc5849/test_client.py 2013-02-11 23:23:38.000000000 +0000 @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, unicode_literals -from oauthlib.oauth1.rfc5849 import Client +from oauthlib.oauth1.rfc5849 import Client, bytes_type from ...unittest import TestCase @@ -27,3 +27,32 @@ header["Authorization"].startswith('OAuth realm="baa-realm",')) # make sure sign() does not override the default realm self.assertEqual(client.realm, "moo-realm") + + +class ClientConstructorTests(TestCase): + + def test_convert_to_unicode_resource_owner(self): + client = Client('client-key', + resource_owner_key=b'owner key') + self.assertFalse(isinstance(client.resource_owner_key, bytes_type)) + self.assertEqual(client.resource_owner_key, 'owner key') + + def test_give_explicit_timestamp(self): + client = Client('client-key', timestamp='1') + params = dict(client.get_oauth_params()) + self.assertEqual(params['oauth_timestamp'], '1') + + def test_give_explicit_nonce(self): + client = Client('client-key', nonce='1') + params = dict(client.get_oauth_params()) + self.assertEqual(params['oauth_nonce'], '1') + + def test_decoding(self): + client = Client('client_key', decoding='utf-8') + uri, headers, body = client.sign('http://a.b/path?query', body='a=b', + headers={'Content-Type': 'application/x-www-form-urlencoded'}) + self.assertIsInstance(uri, bytes_type) + self.assertIsInstance(body, bytes_type) + for k, v in headers.items(): + self.assertIsInstance(k, bytes_type) + self.assertIsInstance(v, bytes_type) diff -Nru python-oauthlib-0.3.4/tests/oauth1/rfc5849/test_server.py python-oauthlib-0.3.7/tests/oauth1/rfc5849/test_server.py --- python-oauthlib-0.3.4/tests/oauth1/rfc5849/test_server.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/tests/oauth1/rfc5849/test_server.py 2013-01-16 18:42:10.000000000 +0000 @@ -73,7 +73,7 @@ return True def validate_redirect_uri(self, client_key, redirect_uri): - return True + return redirect_uri.startswith('http://client.example.com/') class ClientServer(Server): clients = ['foo'] @@ -116,7 +116,11 @@ return 'dummy' @property - def dummy_resource_owner(self): + def dummy_request_token(self): + return 'dumbo' + + @property + def dummy_access_token(self): return 'dumbo' def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, @@ -147,7 +151,7 @@ (client_key, request_token)))) def validate_redirect_uri(self, client_key, redirect_uri): - return True + return redirect_uri.startswith('http://client.example.com/') def get_client_secret(self, client_key): return 'super secret' @@ -190,7 +194,22 @@ uri, headers, body = c.sign('http://server.example.com:80/init') s = self.TestServer() - self.assertTrue(s.verify_request(uri, body=body, headers=headers)[0]) + self.assertTrue(s.verify_request(uri, body=body, headers=headers, + require_callback=True)[0]) + + def test_server_invalid_callback_request(self): + c = Client(self.CLIENT_KEY, + client_secret=self.CLIENT_SECRET, + resource_owner_key=self.RESOURCE_OWNER_KEY, + resource_owner_secret=self.RESOURCE_OWNER_SECRET, + callback_uri='http://attacker.example.com/callback' + ) + + uri, headers, body = c.sign('http://server.example.com:80/init') + + s = self.TestServer() + self.assertFalse(s.verify_request(uri, body=body, headers=headers, + require_callback=True)[0]) def test_not_implemented(self): s = Server() diff -Nru python-oauthlib-0.3.4/tests/oauth1/rfc5849/test_signatures.py python-oauthlib-0.3.7/tests/oauth1/rfc5849/test_signatures.py --- python-oauthlib-0.3.4/tests/oauth1/rfc5849/test_signatures.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/tests/oauth1/rfc5849/test_signatures.py 2013-02-11 23:08:36.000000000 +0000 @@ -13,7 +13,7 @@ class SignatureTests(TestCase): - uri_query = "b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2=&a3=2 q" + uri_query = "b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2=&a3=2+q" authorization_header = """OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2", oauth_token="kkk9d7dh3k39sjv7", diff -Nru python-oauthlib-0.3.4/tests/oauth2/draft25/test_grant_types.py python-oauthlib-0.3.7/tests/oauth2/draft25/test_grant_types.py --- python-oauthlib-0.3.4/tests/oauth2/draft25/test_grant_types.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/tests/oauth2/draft25/test_grant_types.py 2013-02-03 12:20:58.000000000 +0000 @@ -50,8 +50,8 @@ self.request_state = Request('http://a.b/path') self.request_state.state = 'abc' - mock_validator = mock.MagicMock() - self.auth = AuthorizationCodeGrant(request_validator=mock_validator) + self.mock_validator = mock.MagicMock() + self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) def test_create_authorization_grant(self): grant = self.auth.create_authorization_code(self.request) @@ -62,9 +62,8 @@ self.assertIn('state', grant) def test_create_token_response(self): - bearer = BearerToken() - bearer.save_token = mock.MagicMock() - token = self.auth.create_token_response(self.request, bearer) + bearer = BearerToken(self.mock_validator) + u, h, token, s = self.auth.create_token_response(self.request, bearer) token = json.loads(token) self.assertIn('access_token', token) self.assertIn('refresh_token', token) @@ -83,11 +82,12 @@ auth.validate_token_request, request) mock_validator.validate_client = mock.MagicMock(return_value=False) + mock_validator.validate_client_id = mock.MagicMock(return_value=False) request.code = 'waffles' - request.client = 'batman' self.assertRaises(UnauthorizedClientError, auth.validate_token_request, request) + request.client = 'batman' mock_validator.validate_client = mock.MagicMock(return_value=True) mock_validator.validate_code = mock.MagicMock(return_value=False) self.assertRaises(InvalidGrantError, @@ -97,9 +97,11 @@ class ImplicitGrantTest(TestCase): def setUp(self): + mock_client = mock.MagicMock() + mock_client.user.return_value = 'mocked user' self.request = Request('http://a.b/path') self.request.scopes = ('hello', 'world') - self.request.client = 'batman' + self.request.client = mock_client self.request.client_id = 'abcdef' self.request.response_type = 'token' self.request.state = 'xyz' @@ -109,14 +111,13 @@ self.auth = ImplicitGrant(request_validator=self.mock_validator) def test_create_token_response(self): - bearer = BearerToken() - bearer.save_token = mock.MagicMock() + bearer = BearerToken(self.mock_validator) orig_generate_token = common.generate_token - self.addCleanup(setattr, common, 'generage_token', orig_generate_token) + self.addCleanup(setattr, common, 'generate_token', orig_generate_token) common.generate_token = lambda *args, **kwargs: '1234' - uri, headers, body = self.auth.create_token_response( + uri, headers, body, status_code = self.auth.create_token_response( self.request, bearer) - correct_uri = 'https://b.c/p#access_token=1234&token_type=Bearer&expires_in=3600&state=xyz&scope=hello+world' + correct_uri = 'https://b.c/p#access_token=1234&token_type=Bearer&expires_in=3600&state=xyz' self.assertURLEqual(uri, correct_uri, parse_fragment=True) def test_error_response(self): @@ -126,20 +127,21 @@ class ResourceOwnerPasswordCredentialsGrantTest(TestCase): def setUp(self): + mock_client = mock.MagicMock() + mock_client.user.return_value = 'mocked user' self.request = Request('http://a.b/path') self.request.grant_type = 'password' self.request.username = 'john' self.request.password = 'doe' - self.request.client = 'mock authenticated' + self.request.client = mock_client self.request.scopes = ('mocked', 'scopes') self.mock_validator = mock.MagicMock() self.auth = ResourceOwnerPasswordCredentialsGrant( request_validator=self.mock_validator) def test_create_token_response(self): - bearer = BearerToken() - bearer.save_token = mock.MagicMock() - uri, headers, body = self.auth.create_token_response( + bearer = BearerToken(self.mock_validator) + uri, headers, body, status_code = self.auth.create_token_response( self.request, bearer) token = json.loads(body) self.assertIn('access_token', token) @@ -157,24 +159,24 @@ class ClientCredentialsGrantTest(TestCase): def setUp(self): + mock_client = mock.MagicMock() + mock_client.user.return_value = 'mocked user' self.request = Request('http://a.b/path') self.request.grant_type = 'client_credentials' - self.request.client = 'mock authenticated' + self.request.client = mock_client self.request.scopes = ('mocked', 'scopes') self.mock_validator = mock.MagicMock() self.auth = ClientCredentialsGrant( request_validator=self.mock_validator) def test_create_token_response(self): - bearer = BearerToken() - bearer.save_token = mock.MagicMock() - uri, headers, body = self.auth.create_token_response( + bearer = BearerToken(self.mock_validator) + uri, headers, body, status_code = self.auth.create_token_response( self.request, bearer) token = json.loads(body) self.assertIn('access_token', token) self.assertIn('token_type', token) self.assertIn('expires_in', token) - self.assertIn('refresh_token', token) def test_error_response(self): pass diff -Nru python-oauthlib-0.3.4/tests/oauth2/draft25/test_server.py python-oauthlib-0.3.7/tests/oauth2/draft25/test_server.py --- python-oauthlib-0.3.4/tests/oauth2/draft25/test_server.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/tests/oauth2/draft25/test_server.py 2013-02-02 19:58:57.000000000 +0000 @@ -1,78 +1,158 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals from ...unittest import TestCase +import json import mock -from oauthlib.oauth2.draft25 import AuthorizationEndpoint, TokenEndpoint -from oauthlib.oauth2.draft25 import ResourceEndpoint -from oauthlib.oauth2.draft25.grant_types import AuthorizationCodeGrant -from oauthlib.oauth2.draft25.grant_types import ImplicitGrant -from oauthlib.oauth2.draft25.grant_types import ResourceOwnerPasswordCredentialsGrant -from oauthlib.oauth2.draft25.grant_types import ClientCredentialsGrant +from oauthlib.oauth2 import draft25 +from oauthlib.oauth2.draft25 import grant_types, tokens, errors class AuthorizationEndpointTest(TestCase): def setUp(self): - mock_validator = mock.MagicMock() - auth_code = AuthorizationCodeGrant(request_validator=mock_validator) - implicit = ImplicitGrant(request_validator=mock_validator) + self.mock_validator = mock.MagicMock() + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + auth_code = grant_types.AuthorizationCodeGrant( + request_validator=self.mock_validator) + auth_code.save_authorization_code = mock.MagicMock() + implicit = grant_types.ImplicitGrant( + request_validator=self.mock_validator) + implicit.save_token = mock.MagicMock() response_types = { 'code': auth_code, 'token': implicit, } - self.endpoint = AuthorizationEndpoint(response_types=response_types) + token = tokens.BearerToken(self.mock_validator) + self.endpoint = draft25.AuthorizationEndpoint( + default_response_type='code', + default_token_type=token, + response_types=response_types) + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - pass + uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + uri, headers, body, status_code = self.endpoint.create_authorization_response(uri) + self.assertURLEqual(uri, 'http://back.to/me?code=abc&state=xyz') + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_implicit_grant(self): - pass + uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + uri, headers, body, status_code = self.endpoint.create_authorization_response(uri) + self.assertURLEqual(uri, 'http://back.to/me#access_token=abc&expires_in=3600&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True) def test_missing_type(self): - pass + uri = 'http://i.b/l?client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + self.mock_validator.validate_request = mock.MagicMock( + side_effect=errors.InvalidRequestError()) + uri, headers, body, status_code = self.endpoint.create_authorization_response(uri) + self.assertURLEqual(uri, 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.') def test_invalid_type(self): - pass + uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + self.mock_validator.validate_request = mock.MagicMock( + side_effect=errors.UnsupportedResponseTypeError()) + uri, headers, body, status_code = self.endpoint.create_authorization_response(uri) + self.assertURLEqual(uri, 'http://back.to/me?error=unsupported_response_type') class TokenEndpointTest(TestCase): def setUp(self): - mock_validator = mock.MagicMock() - auth_code = AuthorizationCodeGrant(request_validator=mock_validator) - password = ResourceOwnerPasswordCredentialsGrant(request_validator=mock_validator) - client = ClientCredentialsGrant(request_validator=mock_validator) - grant_types = { + def set_user(request): + request.user = mock.MagicMock() + request.client = mock.MagicMock() + request.client.client_id = 'mocked_client_id' + return True + + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = set_user + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + auth_code = grant_types.AuthorizationCodeGrant( + request_validator=self.mock_validator) + password = grant_types.ResourceOwnerPasswordCredentialsGrant( + request_validator=self.mock_validator) + client = grant_types.ClientCredentialsGrant( + request_validator=self.mock_validator) + supported_types = { 'authorization_code': auth_code, 'password': password, 'client_credentials': client, } - self.endpoint = TokenEndpoint(grant_types=grant_types) + token = tokens.BearerToken(self.mock_validator) + self.endpoint = draft25.TokenEndpoint('authorization_code', + default_token_type=token, grant_types=supported_types) + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - pass + body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + uri, headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': 3600, + 'access_token': 'abc', + 'refresh_token': 'abc', + 'state': 'xyz' + } + self.assertEqual(json.loads(body), token) + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_password_grant(self): - pass + body = 'grant_type=password&username=a&password=hello&scope=all+of+them' + uri, headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': 3600, + 'access_token': 'abc', + 'refresh_token': 'abc', + 'scope': 'all of them', + } + self.assertEqual(json.loads(body), token) + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_client_grant(self): - pass + body = 'grant_type=client_credentials&scope=all+of+them' + uri, headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': 3600, + 'access_token': 'abc', + 'scope': 'all of them', + } + self.assertEqual(json.loads(body), token) def test_missing_type(self): - pass + _, _, body, _ = self.endpoint.create_token_response('', body='') + token = {'error': 'unsupported_grant_type'} + self.assertEqual(json.loads(body), token) def test_invalid_type(self): - pass + body = 'grant_type=invalid' + _, _, body, _ = self.endpoint.create_token_response('', body=body) + token = {'error': 'unsupported_grant_type'} + self.assertEqual(json.loads(body), token) class ResourceEndpointTest(TestCase): def setUp(self): - self.endpoint = ResourceEndpoint() - - def test_token_validation(self): - pass - - def test_token_estimation(self): - pass + self.mock_validator = mock.MagicMock() + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + token = tokens.BearerToken(request_validator=self.mock_validator) + self.endpoint = draft25.ResourceEndpoint(default_token='Bearer', + token_types={'Bearer': token}) + + def test_defaults(self): + uri = 'http://a.b/path?some=query' + self.mock_validator.validate_bearer_token.return_value = False + valid, request = self.endpoint.verify_request(uri) + self.assertFalse(valid) + self.assertEqual(request.token_type, 'Bearer') diff -Nru python-oauthlib-0.3.4/tests/test_common.py python-oauthlib-0.3.7/tests/test_common.py --- python-oauthlib-0.3.4/tests/test_common.py 2012-11-19 18:23:36.000000000 +0000 +++ python-oauthlib-0.3.7/tests/test_common.py 2013-02-04 23:10:35.000000000 +0000 @@ -1,8 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals +import sys from oauthlib.common import * from .unittest import TestCase +if sys.version_info[0] == 3: + bytes_type = bytes +else: + bytes_type = lambda s, e: str(s) class CommonTests(TestCase): params_dict = {'foo': 'bar', 'baz': '123', } @@ -47,6 +52,17 @@ def test_extract_invalid(self): self.assertEqual(extract_params(object()), None) + def test_non_unicode_params(self): + r = Request(bytes_type('http://a.b/path?query', 'utf-8'), + http_method=bytes_type('GET', 'utf-8'), + body=bytes_type('you=shall+pass', 'utf-8'), + headers={bytes_type('a', 'utf-8'): bytes_type('b', 'utf-8')}) + self.assertEqual(r.uri, 'http://a.b/path?query') + self.assertEqual(r.http_method, 'GET') + self.assertEqual(r.body, 'you=shall+pass') + self.assertEqual(r.decoded_body, [('you', 'shall pass')]) + self.assertEqual(r.headers, {'a': 'b'}) + def test_none_body(self): r = Request(self.uri) self.assertEqual(r.decoded_body, None) @@ -64,7 +80,7 @@ self.assertEqual(r.decoded_body, []) def test_non_formencoded_string_body(self): - body = 'foo bar baz la la la!' + body = 'foo bar' r = Request(self.uri, body=body) self.assertEqual(r.decoded_body, None)