[Python-modules-commits] [django-oauth-toolkit] 01/07: Imported Upstream version 0.9.0

Michael Fladischer fladi at moszumanska.debian.org
Thu Aug 27 19:46:58 UTC 2015


This is an automated email from the git hooks/post-receive script.

fladi pushed a commit to branch master
in repository django-oauth-toolkit.

commit b9b658c70aa989b817f662c357bd5425992e9cd9
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date:   Sun Aug 2 20:49:23 2015 +0200

    Imported Upstream version 0.9.0
---
 .pep8                                             |  3 -
 .travis.yml                                       |  3 +
 README.rst                                        | 20 ++++--
 docs/changelog.rst                                | 16 +++++
 docs/glossary.rst                                 |  3 +-
 docs/settings.rst                                 |  5 ++
 docs/tutorial/tutorial.rst                        |  1 +
 docs/tutorial/tutorial_04.rst                     | 45 +++++++++++++
 docs/views/class_based.rst                        | 55 ++++++++++++++++
 docs/views/function_based.rst                     |  4 +-
 docs/views/mixins.rst                             |  5 ++
 docs/views/views.rst                              |  1 +
 oauth2_provider/__init__.py                       |  2 +-
 oauth2_provider/admin.py                          |  1 +
 oauth2_provider/compat.py                         |  2 +-
 oauth2_provider/decorators.py                     |  1 -
 oauth2_provider/ext/rest_framework/permissions.py |  1 -
 oauth2_provider/migrations/0001_initial.py        |  5 +-
 oauth2_provider/migrations/0002_08_updates.py     | 36 +++++++++++
 oauth2_provider/models.py                         | 21 +++++--
 oauth2_provider/oauth2_backends.py                | 46 ++++++++++++--
 oauth2_provider/oauth2_validators.py              | 30 +++++----
 oauth2_provider/settings.py                       |  3 +
 oauth2_provider/tests/test_auth_backends.py       |  2 +-
 oauth2_provider/tests/test_authorization_code.py  | 77 +++++++++++++++++++----
 oauth2_provider/tests/test_client_credential.py   | 10 ++-
 oauth2_provider/tests/test_decorators.py          |  1 +
 oauth2_provider/tests/test_mixins.py              | 18 +++++-
 oauth2_provider/tests/test_models.py              | 11 ++--
 oauth2_provider/tests/test_oauth2_backends.py     | 51 ++++++++++++++-
 oauth2_provider/tests/test_token_revocation.py    | 39 +++++++++---
 oauth2_provider/tests/test_validators.py          |  1 -
 oauth2_provider/validators.py                     |  1 +
 oauth2_provider/views/base.py                     |  3 +
 oauth2_provider/views/generic.py                  |  4 +-
 oauth2_provider/views/mixins.py                   | 28 +++++----
 requirements/base.txt                             |  2 +-
 setup.py                                          |  2 +-
 tox.ini                                           | 40 ++++++++++--
 39 files changed, 511 insertions(+), 88 deletions(-)

diff --git a/.pep8 b/.pep8
deleted file mode 100644
index b6bc68e..0000000
--- a/.pep8
+++ /dev/null
@@ -1,3 +0,0 @@
-[pep8]
-max-line-length = 120
-exclude = docs,migrations,.tox
diff --git a/.travis.yml b/.travis.yml
index d859c41..bfbfb94 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,12 +9,15 @@ env:
   - TOX_ENV=py27-django15
   - TOX_ENV=py27-django16
   - TOX_ENV=py27-django17
+  - TOX_ENV=py27-django18
   - TOX_ENV=py33-django15
   - TOX_ENV=py33-django16
   - TOX_ENV=py33-django17
+  - TOX_ENV=py33-django18
   - TOX_ENV=py34-django15
   - TOX_ENV=py34-django16
   - TOX_ENV=py34-django17
+  - TOX_ENV=py34-django18
   - TOX_ENV=docs
 
 install:
diff --git a/README.rst b/README.rst
index a9c17b2..42dec11 100644
--- a/README.rst
+++ b/README.rst
@@ -6,9 +6,6 @@ Django OAuth Toolkit
 .. image:: https://badge.fury.io/py/django-oauth-toolkit.png
     :target: http://badge.fury.io/py/django-oauth-toolkit
 
-.. image:: https://pypip.in/d/django-oauth-toolkit/badge.png
-        :target: https://crate.io/packages/django-oauth-toolkit?version=latest
-
 .. image:: https://travis-ci.org/evonove/django-oauth-toolkit.png
    :alt: Build Status
    :target: https://travis-ci.org/evonove/django-oauth-toolkit
@@ -41,7 +38,7 @@ Requirements
 ------------
 
 * Python 2.6, 2.7, 3.3, 3.4
-* Django 1.4, 1.5, 1.6, 1.7
+* Django 1.4, 1.5, 1.6, 1.7, 1.8
 
 Installation
 ------------
@@ -90,6 +87,21 @@ Roadmap / Todo list (help wanted)
 Changelog
 ---------
 
+
+0.9.0 [2015-07-28]
+~~~~~~~~~~~~~~~~~~
+
+* ``oauthlib_backend_class`` is now pluggable through Django settings
+* #127: ``application/json`` Content-Type is now supported using ``JSONOAuthLibCore``
+* #238: Fixed redirect uri handling in case of error
+* #229: Invalidate access tokens when getting a new refresh token
+* added support for oauthlib 1.0
+
+0.8.2 [2015-06-25]
+~~~~~~~~~~~~~~~~~~
+
+* Fix the migrations to be two-step and allow upgrade from 0.7.2
+
 0.8.1 [2015-04-27]
 ~~~~~~~~~~~~~~~~~~
 
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 3d876e9..839a34e 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,22 @@
 Changelog
 =========
 
+0.9.0 [2015-07-28]
+~~~~~~~~~~~~~~~~~~
+
+* ``oauthlib_backend_class`` is now pluggable through Django settings
+* #127: ``application/json`` Content-Type is now supported using ``JSONOAuthLibCore``
+* #238: Fixed redirect uri handling in case of error
+* #229: Invalidate access tokens when getting a new refresh token
+* added support for oauthlib 1.0
+
+
+0.8.2 [2015-06-25]
+------------------
+
+* Fix the migrations to be two-step and allow upgrade from 0.7.2
+
+
 0.8.1 [2015-04-27]
 ------------------
 
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 477eade..c1536f8 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -15,7 +15,8 @@ Glossary
         An application providing access to its own resources through an API protected following the OAuth2 spec.
 
     Application
-        TODO
+        An Application represents a Client on the Authorization server. Usually an Application is
+        created manually by client's developers after logging in on an Authorization Server.
 
     Client
         A client is an application authorized to access OAuth2-protected resources on behalf and with the authorization
diff --git a/docs/settings.rst b/docs/settings.rst
index f2b34ad..6fc46da 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -64,6 +64,11 @@ OAUTH2_VALIDATOR_CLASS
 The import string of the ``oauthlib.oauth2.RequestValidator`` subclass that
 validates every step of the OAuth2 process.
 
+OAUTH2_BACKEND_CLASS
+~~~~~~~~~~~~~~~~~~~~
+The import string for the ``oauthlib_backend_class`` used in the ``OAuthLibMixin``,
+to get a ``Server`` instance.
+
 SCOPES
 ~~~~~~
 A dictionnary mapping each scope name to its human description.
diff --git a/docs/tutorial/tutorial.rst b/docs/tutorial/tutorial.rst
index 8a0d03a..0de799a 100644
--- a/docs/tutorial/tutorial.rst
+++ b/docs/tutorial/tutorial.rst
@@ -7,3 +7,4 @@ Tutorials
    tutorial_01
    tutorial_02
    tutorial_03
+   tutorial_04
diff --git a/docs/tutorial/tutorial_04.rst b/docs/tutorial/tutorial_04.rst
new file mode 100644
index 0000000..e062c1a
--- /dev/null
+++ b/docs/tutorial/tutorial_04.rst
@@ -0,0 +1,45 @@
+Part 4 - Revoking an OAuth2 Token 
+====================================
+
+Scenario
+--------
+You've granted a user an :term:`Access Token`, following :doc:`part 1 <tutorial_01>` and now you would like to revoke that token, probably in response to a client request (to logout).
+
+Revoking a Token
+--------------
+Be sure that you've granted a valid token. If you've hooked in `oauth-toolkit` into your `urls.py` as specified in :doc:`part 1 <tutorial_01>`, you'll have a URL at `/o/revoke_token`. By submitting the appropriate request to that URL, you can revoke a user's :term:`Access Token`.
+
+`Oauthlib <https://github.com/idan/oauthlib>`_ is compliant with https://tools.ietf.org/html/rfc7009, so as specified, the revocation request requires:
+
+- token:  REQUIRED, this is the :term:`Access Token` you want to revoke 
+- token_type_hint: OPTIONAL, designating either 'access_token' or 'refresh_token'.    
+
+Note that these revocation-specific parameters are in addition to the authentication parameters already specified by your particular client type.   
+
+Setup a Request
+----------------
+Depending on the client type you're using, the token revocation request you may submit to the authentication server mayy vary. A `Public` client, for example, will not have access to your `Client Secret`. A revoke request from a public client would omit that secret, and take the form:
+
+::
+
+    POST /o/revoke_token/ HTTP/1.1
+    Content-Type: application/x-www-form-urlencoded
+    token=XXXX&client_id=XXXX
+
+Where token is :term:`Access Token` specified above, and client_id is the `Client id` obtained in 
+obtained in :doc:`part 1 <tutorial_01>`. If your application type is `Confidential` , it requires a `Client secret`, you will have to add it as one of the parameters: 
+
+::
+
+    POST /o/revoke_token/ HTTP/1.1
+    Content-Type: application/x-www-form-urlencoded
+    token=XXXX&client_id=XXXX&client_secret=XXXX
+
+
+The server will respond wih a `200` status code on successful revocation. You can use `curl` to make a revoke request on your server. If you have access to a local installation of your authorization server, you can test revoking a token with a request like that shown below, for a `Confidential` client. 
+
+::
+
+    curl --data "token=XXXX&client_id=XXXX&client_secret=XXXX" http://localhost:8000/o/revoke_token/
+
+
diff --git a/docs/views/class_based.rst b/docs/views/class_based.rst
index dde9d69..543ed58 100644
--- a/docs/views/class_based.rst
+++ b/docs/views/class_based.rst
@@ -1,3 +1,58 @@
 Class-based Views
 =================
 
+Django OAuth Toolkit provides generic classes useful to implement OAuth2 protected endpoints
+using the *Class Based View* approach.
+
+
+.. class:: ProtectedResourceView(ProtectedResourceMixin, View):
+
+    A view that provides OAuth2 authentication out of the box. To implement a protected
+    endpoint, just define your CBV as::
+
+        class MyEndpoint(ProtectedResourceView):
+            """
+            A GET endpoint that needs OAuth2 authentication
+            """
+            def get(self, request, *args, **kwargs):
+                return HttpResponse('Hello, World!')
+
+    **Please notice**: ``OPTION`` method is not OAuth2 protected to allow preflight requests.
+
+.. class:: ScopedProtectedResourceView(ScopedResourceMixin, ProtectedResourceView):
+
+    A view that provides OAuth2 authentication and scopes handling out of the box. To implement
+    a protected endpoint, just define your CBV specifying the ``required_scopes`` field::
+
+        class MyScopedEndpoint(ScopedProtectedResourceView):
+            required_scopes = ['can_make_it can_break_it']
+
+            """
+            A GET endpoint that needs OAuth2 authentication
+            and a set of scopes: 'can_make_it' and 'can_break_it'
+            """
+            def get(self, request, *args, **kwargs):
+                return HttpResponse('Hello, World!')
+
+
+.. class:: ReadWriteScopedResourceView(ReadWriteScopedResourceMixin, ProtectedResourceView):
+
+    A view that provides OAuth2 authentication and read/write default scopes.
+    ``GET``, ``HEAD``, ``OPTIONS`` http methods require ``read`` scope, others methods
+    need the ``write`` scope. If you need, you can always specify an additional list of
+    scopes in the ``required_scopes`` field::
+
+        class MyRWEndpoint(ReadWriteScopedResourceView):
+            required_scopes = ['has_additional_powers']  # optional
+
+            """
+            A GET endpoint that needs OAuth2 authentication
+            and the 'read' scope. If required_scopes was specified,
+            clients also need those scopes.
+            """
+            def get(self, request, *args, **kwargs):
+                return HttpResponse('Hello, World!')
+
+
+Generic views in DOT are obtained composing a set of mixins you can find in the :doc:`views.mixins <mixins>`
+module: feel free to use those mixins directly if you want to provide your own class based views.
diff --git a/docs/views/function_based.rst b/docs/views/function_based.rst
index de6b872..cc0650b 100644
--- a/docs/views/function_based.rst
+++ b/docs/views/function_based.rst
@@ -46,7 +46,7 @@ Django OAuth Toolkit provides decorators to help you in protecting your function
     box. GET, HEAD, OPTIONS http methods require "read" scope.
     Otherwise "write" scope is required::
 
-        from oauth2_provider.decorators import protected_resource
+        from oauth2_provider.decorators import rw_protected_resource
 
         @rw_protected_resource()
         def my_view(request):
@@ -56,7 +56,7 @@ Django OAuth Toolkit provides decorators to help you in protecting your function
 
     If you need, you can ask for other scopes over "read" and "write"::
 
-        from oauth2_provider.decorators import protected_resource
+        from oauth2_provider.decorators import rw_protected_resource
 
         @rw_protected_resource(scopes=['exotic_scope'])
         def my_view(request):
diff --git a/docs/views/mixins.rst b/docs/views/mixins.rst
new file mode 100644
index 0000000..be3541a
--- /dev/null
+++ b/docs/views/mixins.rst
@@ -0,0 +1,5 @@
+Mixins for Class Based Views
+============================
+
+.. automodule:: oauth2_provider.views.mixins
+   :members:
\ No newline at end of file
diff --git a/docs/views/views.rst b/docs/views/views.rst
index c92b927..34afef9 100644
--- a/docs/views/views.rst
+++ b/docs/views/views.rst
@@ -9,3 +9,4 @@ Django OAuth Toolkit provides a set of pre-defined views for different purposes:
    function_based
    class_based
    application
+   mixins
diff --git a/oauth2_provider/__init__.py b/oauth2_provider/__init__.py
index 435cf1b..9ffed34 100644
--- a/oauth2_provider/__init__.py
+++ b/oauth2_provider/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '0.8.1'
+__version__ = '0.9.0'
 
 __author__ = "Massimiliano Pippi & Federico Frenguelli"
 
diff --git a/oauth2_provider/admin.py b/oauth2_provider/admin.py
index ac789be..d3c764a 100644
--- a/oauth2_provider/admin.py
+++ b/oauth2_provider/admin.py
@@ -2,6 +2,7 @@ from django.contrib import admin
 
 from .models import Grant, AccessToken, RefreshToken, get_application_model
 
+
 class RawIDAdmin(admin.ModelAdmin):
     raw_id_fields = ('user',)
 
diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py
index a466c1e..4266c34 100644
--- a/oauth2_provider/compat.py
+++ b/oauth2_provider/compat.py
@@ -36,4 +36,4 @@ try:
     from django.apps import apps
     get_model = apps.get_model
 except ImportError:
-    from django.db.models import get_model
\ No newline at end of file
+    from django.db.models import get_model
diff --git a/oauth2_provider/decorators.py b/oauth2_provider/decorators.py
index 01944a4..d1448f7 100644
--- a/oauth2_provider/decorators.py
+++ b/oauth2_provider/decorators.py
@@ -19,7 +19,6 @@ def protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Se
             # An access token is required to get here...
             # ...
             pass
-    
     """
     _scopes = scopes or []
 
diff --git a/oauth2_provider/ext/rest_framework/permissions.py b/oauth2_provider/ext/rest_framework/permissions.py
index c5d4c4d..e60415d 100644
--- a/oauth2_provider/ext/rest_framework/permissions.py
+++ b/oauth2_provider/ext/rest_framework/permissions.py
@@ -59,4 +59,3 @@ class TokenHasReadWriteScope(TokenHasScope):
             read_write_scope = oauth2_settings.WRITE_SCOPE
 
         return required_scopes + [read_write_scope]
-
diff --git a/oauth2_provider/migrations/0001_initial.py b/oauth2_provider/migrations/0001_initial.py
index 7b0b40a..bb1b518 100644
--- a/oauth2_provider/migrations/0001_initial.py
+++ b/oauth2_provider/migrations/0001_initial.py
@@ -26,8 +26,7 @@ class Migration(migrations.Migration):
                 ('authorization_grant_type', models.CharField(max_length=32, choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')])),
                 ('client_secret', models.CharField(default=oauth2_provider.generators.generate_client_secret, max_length=255, db_index=True, blank=True)),
                 ('name', models.CharField(max_length=255, blank=True)),
-                ('skip_authorization', models.BooleanField(default=False)),
-                ('user', models.ForeignKey(related_name='oauth2_provider_application', to=settings.AUTH_USER_MODEL)),
+                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
             ],
             options={
                 'abstract': False,
@@ -41,7 +40,7 @@ class Migration(migrations.Migration):
                 ('expires', models.DateTimeField()),
                 ('scope', models.TextField(blank=True)),
                 ('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL)),
-                ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
+                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
             ],
         ),
         migrations.CreateModel(
diff --git a/oauth2_provider/migrations/0002_08_updates.py b/oauth2_provider/migrations/0002_08_updates.py
new file mode 100644
index 0000000..a95aedb
--- /dev/null
+++ b/oauth2_provider/migrations/0002_08_updates.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from oauth2_provider.settings import oauth2_settings
+from django.db import models, migrations
+import oauth2_provider.validators
+import oauth2_provider.generators
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('oauth2_provider', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+             model_name='Application',
+             name='skip_authorization',
+             field=models.BooleanField(default=False),
+             preserve_default=True,
+        ),
+        migrations.AlterField(
+            model_name='Application',
+            name='user',
+            field=models.ForeignKey(related_name='oauth2_provider_application', to=settings.AUTH_USER_MODEL),
+            preserve_default=True,
+        ),
+        migrations.AlterField(
+            model_name='AccessToken',
+            name='user',
+            field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True),
+            preserve_default=True,
+        ),
+    ]
diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py
index 1307b6e..1d26726 100644
--- a/oauth2_provider/models.py
+++ b/oauth2_provider/models.py
@@ -94,8 +94,8 @@ class AbstractApplication(models.Model):
             parsed_uri = urlparse(uri)
 
             if (parsed_allowed_uri.scheme == parsed_uri.scheme and
-                parsed_allowed_uri.netloc == parsed_uri.netloc and
-                parsed_allowed_uri.path == parsed_uri.path):
+                    parsed_allowed_uri.netloc == parsed_uri.netloc and
+                    parsed_allowed_uri.path == parsed_uri.path):
 
                 aqs_set = set(parse_qsl(parsed_allowed_uri.query))
                 uqs_set = set(parse_qsl(parsed_uri.query))
@@ -175,8 +175,7 @@ class AccessToken(models.Model):
     * :attr:`user` The Django user representing resources' owner
     * :attr:`token` Access token
     * :attr:`application` Application instance
-    * :attr:`expires` Expire time in seconds, defaults to
-                      :data:`settings.ACCESS_TOKEN_EXPIRE_SECONDS`
+    * :attr:`expires` Date and time of token expiration, in DateTime format
     * :attr:`scope` Allowed scopes
     """
     user = models.ForeignKey(AUTH_USER_MODEL, blank=True, null=True)
@@ -213,6 +212,13 @@ class AccessToken(models.Model):
 
         return resource_scopes.issubset(provided_scopes)
 
+    def revoke(self):
+        """
+        Convenience method to uniform tokens' interface, for now
+        simply remove this token from the database in order to revoke it.
+        """
+        self.delete()
+
     def __str__(self):
         return self.token
 
@@ -237,6 +243,13 @@ class RefreshToken(models.Model):
     access_token = models.OneToOneField(AccessToken,
                                         related_name='refresh_token')
 
+    def revoke(self):
+        """
+        Delete this refresh token along with related access token
+        """
+        AccessToken.objects.get(id=self.access_token.id).revoke()
+        self.delete()
+
     def __str__(self):
         return self.token
 
diff --git a/oauth2_provider/oauth2_backends.py b/oauth2_provider/oauth2_backends.py
index b7a318c..4c7e40a 100644
--- a/oauth2_provider/oauth2_backends.py
+++ b/oauth2_provider/oauth2_backends.py
@@ -1,5 +1,7 @@
 from __future__ import unicode_literals
 
+import json
+
 from oauthlib import oauth2
 from oauthlib.common import urlencode, urlencoded, quote
 
@@ -33,10 +35,20 @@ class OAuthLibCore(object):
     def _extract_params(self, request):
         """
         Extract parameters from the Django request object. Such parameters will then be passed to
-        OAuthLib to build its own Request object
+        OAuthLib to build its own Request object. The body should be encoded using OAuthLib urlencoded
         """
         uri = self._get_escaped_full_path(request)
         http_method = request.method
+        headers = self.extract_headers(request)
+        body = urlencode(self.extract_body(request))
+        return uri, http_method, body, headers
+
+    def extract_headers(self, request):
+        """
+        Extracts headers from the Django request object
+        :param request: The current django.http.HttpRequest object
+        :return: a dictionary with OAuthLib needed headers
+        """
         headers = request.META.copy()
         if 'wsgi.input' in headers:
             del headers['wsgi.input']
@@ -44,8 +56,16 @@ class OAuthLibCore(object):
             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
+
+        return headers
+
+    def extract_body(self, request):
+        """
+        Extracts the POST body from the Django request object
+        :param request: The current django.http.HttpRequest object
+        :return: provided POST parameters
+        """
+        return request.POST.items()
 
     def validate_authorization_request(self, request):
         """
@@ -136,6 +156,24 @@ class OAuthLibCore(object):
         return valid, r
 
 
+class JSONOAuthLibCore(OAuthLibCore):
+    """
+    Extends the default OAuthLibCore to parse correctly requests with application/json Content-Type
+    """
+    def extract_body(self, request):
+        """
+        Extracts the JSON body from the Django request object
+        :param request: The current django.http.HttpRequest object
+        :return: provided POST parameters "urlencodable"
+        """
+        try:
+            body = json.loads(request.body.decode('utf-8')).items()
+        except ValueError:
+            body = ""
+
+        return body
+
+
 def get_oauthlib_core():
     """
     Utility function that take a request and returns an instance of
@@ -144,4 +182,4 @@ def get_oauthlib_core():
     from oauthlib.oauth2 import Server
 
     server = Server(oauth2_settings.OAUTH2_VALIDATOR_CLASS())
-    return OAuthLibCore(server)
+    return oauth2_settings.OAUTH2_BACKEND_CLASS(server)
diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py
index aea3f5e..cc669f0 100644
--- a/oauth2_provider/oauth2_validators.py
+++ b/oauth2_provider/oauth2_validators.py
@@ -55,7 +55,10 @@ class OAuth2Validator(RequestValidator):
         if not auth_string:
             return False
 
-        encoding = request.encoding or 'utf-8'
+        try:
+            encoding = request.encoding
+        except AttributeError:
+            encoding = 'utf-8'
 
         try:
             b64_decoded = base64.b64decode(auth_string)
@@ -90,11 +93,11 @@ class OAuth2Validator(RequestValidator):
         Remember that this method is NOT RECOMMENDED and SHOULD be limited to clients unable to
         directly utilize the HTTP Basic authentication scheme. See rfc:`2.3.1` for more details.
         """
-        #TODO: check if oauthlib has already unquoted client_id and client_secret
-        client_id = request.client_id
-        client_secret = request.client_secret
-
-        if not client_id or not client_secret:
+        # TODO: check if oauthlib has already unquoted client_id and client_secret
+        try:
+            client_id = request.client_id
+            client_secret = request.client_secret
+        except AttributeError:
             return False
 
         if self._load_application(client_id, request) is None:
@@ -143,8 +146,12 @@ class OAuth2Validator(RequestValidator):
         if self._extract_basic_auth(request):
             return True
 
-        if request.client_id and request.client_secret:
-            return True
+        try:
+            if request.client_id and request.client_secret:
+                return True
+        except AttributeError:
+            log.debug("Client id or client secret not provided, proceed evaluating if authentication is required...")
+            pass
 
         self._load_application(request.client_id, request)
         if request.client:
@@ -286,7 +293,7 @@ class OAuth2Validator(RequestValidator):
         if request.refresh_token:
             # remove used refresh token
             try:
-                RefreshToken.objects.get(token=request.refresh_token).delete()
+                RefreshToken.objects.get(token=request.refresh_token).revoke()
             except RefreshToken.DoesNotExist:
                 assert()  # TODO though being here would be very strange, at least log the error
 
@@ -332,10 +339,11 @@ class OAuth2Validator(RequestValidator):
 
         token_type = token_types.get(token_type_hint, AccessToken)
         try:
-            token_type.objects.get(token=token).delete()
+            token_type.objects.get(token=token).revoke()
         except ObjectDoesNotExist:
             for other_type in [_t for _t in token_types.values() if _t != token_type]:
-                other_type.objects.filter(token=token).delete()
+                # slightly inefficient on Python2, but the queryset contains only one instance
+                list(map(lambda t: t.revoke(), other_type.objects.filter(token=token)))
 
     def validate_user(self, username, password, client, request, *args, **kwargs):
         """
diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py
index fa7eef1..db57686 100644
--- a/oauth2_provider/settings.py
+++ b/oauth2_provider/settings.py
@@ -34,6 +34,7 @@ DEFAULTS = {
     'CLIENT_SECRET_GENERATOR_CLASS': 'oauth2_provider.generators.ClientSecretGenerator',
     'CLIENT_SECRET_GENERATOR_LENGTH': 128,
     'OAUTH2_VALIDATOR_CLASS': 'oauth2_provider.oauth2_validators.OAuth2Validator',
+    'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.OAuthLibCore',
     'SCOPES': {"read": "Reading scope", "write": "Writing scope"},
     'READ_SCOPE': 'read',
     'WRITE_SCOPE': 'write',
@@ -52,6 +53,7 @@ MANDATORY = (
     'CLIENT_ID_GENERATOR_CLASS',
     'CLIENT_SECRET_GENERATOR_CLASS',
     'OAUTH2_VALIDATOR_CLASS',
+    'OAUTH2_BACKEND_CLASS',
     'SCOPES',
     'ALLOWED_REDIRECT_URI_SCHEMES',
 )
@@ -61,6 +63,7 @@ IMPORT_STRINGS = (
     'CLIENT_ID_GENERATOR_CLASS',
     'CLIENT_SECRET_GENERATOR_CLASS',
     'OAUTH2_VALIDATOR_CLASS',
+    'OAUTH2_BACKEND_CLASS',
 )
 
 
diff --git a/oauth2_provider/tests/test_auth_backends.py b/oauth2_provider/tests/test_auth_backends.py
index 631834b..2494466 100644
--- a/oauth2_provider/tests/test_auth_backends.py
+++ b/oauth2_provider/tests/test_auth_backends.py
@@ -76,7 +76,7 @@ class TestOAuth2Backend(BaseTest):
         'oauth2_provider.backends.OAuth2Backend',
         'django.contrib.auth.backends.ModelBackend',
     ),
-    MIDDLEWARE_CLASSES=MIDDLEWARE_CLASSES+('oauth2_provider.middleware.OAuth2TokenMiddleware',)
+    MIDDLEWARE_CLASSES=MIDDLEWARE_CLASSES + ('oauth2_provider.middleware.OAuth2TokenMiddleware',)
 )
 class TestOAuth2Middleware(BaseTest):
 
diff --git a/oauth2_provider/tests/test_authorization_code.py b/oauth2_provider/tests/test_authorization_code.py
index 9277226..5c51c35 100644
--- a/oauth2_provider/tests/test_authorization_code.py
+++ b/oauth2_provider/tests/test_authorization_code.py
@@ -10,9 +10,8 @@ from django.core.urlresolvers import reverse
 from django.utils import timezone
 
 from ..compat import urlparse, parse_qs, urlencode, get_user_model
-from ..models import get_application_model, Grant, AccessToken
+from ..models import get_application_model, Grant, AccessToken, RefreshToken
 from ..settings import oauth2_settings
-from ..oauth2_validators import OAuth2Validator
 from ..views import ProtectedResourceView
 
 from .test_utils import TestCaseUtils
@@ -35,7 +34,7 @@ class BaseTest(TestCaseUtils, TestCase):
         self.dev_user = UserModel.objects.create_user("dev_user", "dev at user.com", "123456")
 
         oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ['http', 'custom-scheme']
-        
+
         self.application = Application(
             name="Test Application",
             redirect_uris="http://localhost http://example.com http://example.it custom-scheme://example.com",
@@ -74,7 +73,6 @@ class TestAuthorizationCodeView(BaseTest):
         response = self.client.get(url)
         self.assertEqual(response.status_code, 302)
 
-
     def test_pre_auth_invalid_client(self):
         """
         Test error for an invalid client_id with response_type: code
@@ -147,11 +145,11 @@ class TestAuthorizationCodeView(BaseTest):
 
     def test_pre_auth_approval_prompt(self):
         """
-
+        TODO
         """
         tok = AccessToken.objects.create(user=self.test_user, token='1234567890',
                                          application=self.application,
-                                         expires=timezone.now()+datetime.timedelta(days=1),
+                                         expires=timezone.now() + datetime.timedelta(days=1),
                                          scope='read write')
         self.client.login(username="test_user", password="123456")
         query_string = urlencode({
@@ -173,13 +171,13 @@ class TestAuthorizationCodeView(BaseTest):
 
     def test_pre_auth_approval_prompt_default(self):
         """
-
+        TODO
         """
         self.assertEqual(oauth2_settings.REQUEST_APPROVAL_PROMPT, 'force')
 
         AccessToken.objects.create(user=self.test_user, token='1234567890',
                                    application=self.application,
-                                   expires=timezone.now()+datetime.timedelta(days=1),
+                                   expires=timezone.now() + datetime.timedelta(days=1),
                                    scope='read write')
         self.client.login(username="test_user", password="123456")
         query_string = urlencode({
@@ -195,13 +193,13 @@ class TestAuthorizationCodeView(BaseTest):
 
     def test_pre_auth_approval_prompt_default_override(self):
         """
-
+        TODO
         """
         oauth2_settings.REQUEST_APPROVAL_PROMPT = 'auto'
 
         AccessToken.objects.create(user=self.test_user, token='1234567890',
                                    application=self.application,
-                                   expires=timezone.now()+datetime.timedelta(days=1),
+                                   expires=timezone.now() + datetime.timedelta(days=1),
                                    scope='read write')
         self.client.login(username="test_user", password="123456")
         query_string = urlencode({
@@ -263,7 +261,7 @@ class TestAuthorizationCodeView(BaseTest):
 
         response = self.client.get(url)
         self.assertEqual(response.status_code, 302)
-        self.assertIn("error=unauthorized_client", response['Location'])
+        self.assertIn("error=unsupported_response_type", response['Location'])
 
     def test_code_post_auth_allow(self):
         """
@@ -425,6 +423,27 @@ class TestAuthorizationCodeView(BaseTest):
         self.assertIn("http://example.com?foo=bar", response['Location'])
         self.assertIn("code=", response['Location'])
 
+    def test_code_post_auth_failing_redirection_uri_with_querystring(self):
+        """
+        Test that in case of error the querystring of the redirection uri is preserved
+
+        See https://github.com/evonove/django-oauth-toolkit/issues/238
+        """
+        self.client.login(username="test_user", password="123456")
+
+        form_data = {
+            'client_id': self.application.client_id,
+            'state': 'random_state_string',
+            'scope': 'read write',
+            'redirect_uri': 'http://example.com?foo=bar',
+            'response_type': 'code',
+            'allow': False,
+        }
+
+        response = self.client.post(reverse('oauth2_provider:authorize'), data=form_data)
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual("http://example.com?foo=bar&error=access_denied", response['Location'])
+
     def test_code_post_auth_fails_when_redirect_uri_path_is_invalid(self):
         """
         Tests that a redirection uri is matched using scheme + netloc + path
@@ -528,6 +547,37 @@ class TestAuthorizationCodeTokenView(BaseTest):
         content = json.loads(response.content.decode("utf-8"))
         self.assertTrue('invalid_grant' in content.values())
 
+    def test_refresh_invalidates_old_tokens(self):
+        """
+        Ensure existing refresh tokens are cleaned up when issuing new ones
+        """
+        self.client.login(username="test_user", password="123456")
+        authorization_code = self.get_auth()
+
+        token_request_data = {
+            'grant_type': 'authorization_code',
+            'code': authorization_code,
+            'redirect_uri': 'http://example.it'
+        }
+        auth_headers = self.get_basic_auth_header(self.application.client_id, self.application.client_secret)
+
+        response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers)
+        content = json.loads(response.content.decode("utf-8"))
+
+        rt = content['refresh_token']
+        at = content['access_token']
+
+        token_request_data = {
+            'grant_type': 'refresh_token',
+            'refresh_token': rt,
+            'scope': content['scope'],
+        }
+        response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertFalse(RefreshToken.objects.filter(token=rt).exists())
+        self.assertFalse(AccessToken.objects.filter(token=at).exists())
+
     def test_refresh_no_scopes(self):
         """
         Request an access token using a refresh token without passing any scope
@@ -634,7 +684,8 @@ class TestAuthorizationCodeTokenView(BaseTest):
             'scope': content['scope'],
         }
 
-        with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator.rotate_refresh_token', return_value=False):
+        with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator.rotate_refresh_token',
+                        return_value=False):
             response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers)
             self.assertEqual(response.status_code, 200)
             response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers)
@@ -742,7 +793,7 @@ class TestAuthorizationCodeTokenView(BaseTest):
             'code': authorization_code,
             'redirect_uri': 'http://example.it',
             'client_id': self.application.client_id,
-            'client_secret':  self.application.client_secret,
+            'client_secret': self.application.client_secret,
         }
 
         response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data)
diff --git a/oauth2_provider/tests/test_client_credential.py b/oauth2_provider/tests/test_client_credential.py
index f9c4e2c..8136f5d 100644
--- a/oauth2_provider/tests/test_client_credential.py
+++ b/oauth2_provider/tests/test_client_credential.py
@@ -14,6 +14,7 @@ from django.views.generic import View
 from oauthlib.oauth2 import BackendApplicationServer
 
 from ..models import get_application_model, AccessToken
+from ..oauth2_backends import OAuthLibCore
 from ..oauth2_validators import OAuth2Validator
 from ..settings import oauth2_settings
 from ..views import ProtectedResourceView
@@ -109,11 +110,13 @@ class TestExtendedRequest(BaseTest):
     @classmethod
     def setUpClass(cls):
         cls.request_factory = RequestFactory()
+        super(TestExtendedRequest, cls).setUpClass()
 
     def test_extended_request(self):
         class TestView(OAuthLibMixin, View):
             server_class = BackendApplicationServer
             validator_class = OAuth2Validator
+            oauthlib_backend_class = OAuthLibCore
 
             def get_scopes(self):
                 return ['read', 'write']
@@ -166,7 +169,10 @@ class TestClientResourcePasswordBased(BaseTest):
             'username': 'test_user',
             'password': '123456'
         }
-        auth_headers = self.get_basic_auth_header(urllib.quote_plus(self.application.client_id), urllib.quote_plus(self.application.client_secret))
+        auth_headers = self.get_basic_auth_header(
+            urllib.quote_plus(self.application.client_id),
+            urllib.quote_plus(self.application.client_secret))
+
         response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
 
@@ -183,5 +189,3 @@ class TestClientResourcePasswordBased(BaseTest):
         view = ResourceView.as_view()
         response = view(request)
         self.assertEqual(response, "This is a protected resource")
-
-
diff --git a/oauth2_provider/tests/test_decorators.py b/oauth2_provider/tests/test_decorators.py
index c55b034..b9e22bc 100644
--- a/oauth2_provider/tests/test_decorators.py
+++ b/oauth2_provider/tests/test_decorators.py
@@ -19,6 +19,7 @@ class TestProtectedResourceDecorator(TestCase, TestCaseUtils):
     @classmethod
     def setUpClass(cls):
         cls.request_factory = RequestFactory()
+        super(TestProtectedResourceDecorator, cls).setUpClass()
 
     def setUp(self):
         self.user = UserModel.objects.create_user("test_user", "test at user.com", "123456")
diff --git a/oauth2_provider/tests/test_mixins.py b/oauth2_provider/tests/test_mixins.py
index 484a542..4cb6f20 100644
--- a/oauth2_provider/tests/test_mixins.py
+++ b/oauth2_provider/tests/test_mixins.py
@@ -8,6 +8,7 @@ from django.http import HttpResponse
 from oauthlib.oauth2 import Server
 
 from ..views.mixins import OAuthLibMixin, ScopedResourceMixin, ProtectedResourceMixin
+from ..oauth2_backends import OAuthLibCore
 from ..oauth2_validators import OAuth2Validator
 
 
@@ -15,12 +16,23 @@ class BaseTest(TestCase):
     @classmethod
     def setUpClass(cls):
         cls.request_factory = RequestFactory()
+        super(BaseTest, cls).setUpClass()
 
 
 class TestOAuthLibMixin(BaseTest):
+    def test_missing_oauthlib_backend_class(self):
+        class TestView(OAuthLibMixin, View):
+            server_class = Server
+            validator_class = OAuth2Validator
+
+        test_view = TestView()
+
+        self.assertRaises(ImproperlyConfigured, test_view.get_oauthlib_backend_class)
+
     def test_missing_server_class(self):
         class TestView(OAuthLibMixin, View):
             validator_class = OAuth2Validator
+            oauthlib_backend_class = OAuthLibCore
 
         test_view = TestView()
 
@@ -29,6 +41,7 @@ class TestOAuthLibMixin(BaseTest):
     def test_missing_validator_class(self):
         class TestView(OAuthLibMixin, View):
             server_class = Server
+            oauthlib_backend_class = OAuthLibCore
 
         test_view = TestView()
 
@@ -38,6 +51,7 @@ class TestOAuthLibMixin(BaseTest):
         class TestView(OAuthLibMixin, View):
             server_class = Server
             validator_class = OAuth2Validator
+            oauthlib_backend_class = OAuthLibCore
 
         request = self.request_factory.get("/fake-req")
         request.user = "fake"
@@ -52,13 +66,13 @@ class TestOAuthLibMixin(BaseTest):
         class TestView(OAuthLibMixin, View):
             server_class = Server
             validator_class = OAuth2Validator
-            oauthlib_core_class = AnotherOauthLibBackend
+            oauthlib_backend_class = AnotherOauthLibBackend
 
         request = self.request_factory.get("/fake-req")
... 456 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-oauth-toolkit.git



More information about the Python-modules-commits mailing list