[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