Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Django 1.7 compatibility and SchemedHttpResponseRedirect #186

Closed
wants to merge 12 commits into from
2 changes: 2 additions & 0 deletions oauth2_provider/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
__author__ = "Massimiliano Pippi & Federico Frenguelli"

VERSION = __version__ # synonym

default_app_config = 'oauth2_provider.apps.OAuth2ProviderConfig'
30 changes: 30 additions & 0 deletions oauth2_provider/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.apps import AppConfig


class OAuth2ProviderConfig(AppConfig):
name = 'oauth2_provider'
verbose_name = "OAuth2 provider"

def ready(self):
# Monkey-patch Meta.model and other root objects
from .models import get_application_model
Application = get_application_model()

# monkey-patch views/application.ApplicationOwnerIsUserMixin model
from .views.application import ApplicationOwnerIsUserMixin
ApplicationOwnerIsUserMixin.model = Application

# monkey-patch forms.RegistrationForm model
from .forms import RegistrationForm
RegistrationForm.Meta.model = Application

# monkey-patch oauth2_validators.Appliacation and GRANT_TYPE_MAPPING
from . import oauth2_validators
oauth2_validators.Application = Application
oauth2_validators.GRANT_TYPE_MAPPING = {
'authorization_code': (Application.GRANT_AUTHORIZATION_CODE,),
'password': (Application.GRANT_PASSWORD,),
'client_credentials': (Application.GRANT_CLIENT_CREDENTIALS,),
'refresh_token': (Application.GRANT_AUTHORIZATION_CODE, Application.GRANT_PASSWORD,
Application.GRANT_CLIENT_CREDENTIALS)
}
7 changes: 7 additions & 0 deletions oauth2_provider/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
else:
AUTH_USER_MODEL = 'auth.User'

try:
# Django's new application loading system
from django.apps import apps
get_model = apps.get_model
except ImportError:
from django.db.models import get_model

try:
from django.contrib.auth import get_user_model
except ImportError:
Expand Down
5 changes: 2 additions & 3 deletions oauth2_provider/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from django import forms

from .models import get_application_model


class AllowForm(forms.Form):
allow = forms.BooleanField(required=False)
Expand All @@ -24,5 +22,6 @@ class RegistrationForm(forms.ModelForm):
TODO: add docstring
"""
class Meta:
model = get_application_model()
# FIXME: monkey-patched in apps.py
model = None
fields = ('name', 'client_id', 'client_secret', 'client_type', 'authorization_grant_type', 'redirect_uris')
33 changes: 25 additions & 8 deletions oauth2_provider/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@
from django.core.urlresolvers import reverse
from django.db import models
from django.utils import timezone
try:
# Django's new application loading system
from django.apps import apps
get_model = apps.get_model
except ImportError:
from django.db.models import get_model
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from django.core.exceptions import ImproperlyConfigured
from django.utils.six.moves.urllib.parse import urlparse

from .settings import oauth2_settings
from .compat import AUTH_USER_MODEL
from .compat import AUTH_USER_MODEL, get_model
from .generators import generate_client_secret, generate_client_id
from .validators import validate_uris

Expand Down Expand Up @@ -96,6 +91,18 @@ def redirect_uri_allowed(self, uri):
"""
return uri in self.redirect_uris.split()

@property
def redirect_uri_schemes(self):
"""
Returns the set of schemes used by the :attr:`redirect_uris`.
"""
schemes = set()
for uri in self.redirect_uris.split():
parsed = urlparse(uri)
if parsed.scheme:
schemes.add(parsed.scheme)
return schemes

def clean(self):
from django.core.exceptions import ValidationError
if not self.redirect_uris \
Expand Down Expand Up @@ -239,7 +246,17 @@ def get_application_model():
except ValueError:
e = "APPLICATION_MODEL must be of the form 'app_label.model_name'"
raise ImproperlyConfigured(e)
app_model = get_model(app_label, model_name)

try:
# Django >=1.7
from django.apps import apps
try:
app_model = apps.get_model(app_label, model_name)
except LookupError:
app_model = None
except ImportError:
app_model = get_model(app_label, model_name)

if app_model is None:
e = "APPLICATION_MODEL refers to model {0} that has not been installed"
raise ImproperlyConfigured(e.format(oauth2_settings.APPLICATION_MODEL))
Expand Down
15 changes: 5 additions & 10 deletions oauth2_provider/oauth2_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,16 @@
from oauthlib.oauth2 import RequestValidator

from .compat import unquote_plus
from .models import Grant, AccessToken, RefreshToken, get_application_model
from .models import Grant, AccessToken, RefreshToken
from .settings import oauth2_settings

Application = get_application_model()
# FIXME: monkey-patched in apps.py
Application = None
# FIXME: monkey-patched in apps.py
GRANT_TYPE_MAPPING = {}

log = logging.getLogger('oauth2_provider')

GRANT_TYPE_MAPPING = {
'authorization_code': (Application.GRANT_AUTHORIZATION_CODE,),
'password': (Application.GRANT_PASSWORD,),
'client_credentials': (Application.GRANT_CLIENT_CREDENTIALS,),
'refresh_token': (Application.GRANT_AUTHORIZATION_CODE, Application.GRANT_PASSWORD,
Application.GRANT_CLIENT_CREDENTIALS)
}


class OAuth2Validator(RequestValidator):
def _extract_basic_auth(self, request):
Expand Down
4 changes: 2 additions & 2 deletions oauth2_provider/views/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
from braces.views import LoginRequiredMixin

from ..forms import RegistrationForm
from ..models import get_application_model


class ApplicationOwnerIsUserMixin(LoginRequiredMixin):
"""
This mixin is used to provide an Application queryset filtered by the current request.user.
"""
model = get_application_model()
# FIXME: monkey-patched in apps.py
model = None
fields = '__all__'

def get_queryset(self):
Expand Down
28 changes: 14 additions & 14 deletions oauth2_provider/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
from ..forms import AllowForm
from ..models import get_application_model
from .mixins import OAuthLibMixin

Application = get_application_model()
from .util import SchemedHttpResponseRedirect

log = logging.getLogger('oauth2_provider')

Expand Down Expand Up @@ -102,9 +101,9 @@ def form_valid(self, form):
allow = form.cleaned_data.get('allow')
uri, headers, body, status = self.create_authorization_response(
request=self.request, scopes=scopes, credentials=credentials, allow=allow)
self.success_url = uri
log.debug("Success url for the request: {0}".format(self.success_url))
return super(AuthorizationView, self).form_valid(form)
log.debug("Redirect uri for the request: {0}".format(uri))
application = get_application_model().objects.get(client_id=credentials['client_id']) # TODO: cache it!
return SchemedHttpResponseRedirect(uri, allowed_schemes=application.redirect_uri_schemes)

except OAuthToolkitError as error:
return self.error_response(error)
Expand All @@ -115,7 +114,7 @@ def get(self, request, *args, **kwargs):
kwargs['scopes_descriptions'] = [oauth2_settings.SCOPES[scope] for scope in scopes]
kwargs['scopes'] = scopes
# at this point we know an Application instance with such client_id exists in the database
application = Application.objects.get(client_id=credentials['client_id']) # TODO: cache it!
application = get_application_model().objects.get(client_id=credentials['client_id']) # TODO: cache it!
kwargs['application'] = application
kwargs.update(credentials)
self.oauth2_data = kwargs
Expand All @@ -127,26 +126,27 @@ def get(self, request, *args, **kwargs):
# a successful response depending on 'approval_prompt' url parameter
require_approval = request.GET.get('approval_prompt', oauth2_settings.REQUEST_APPROVAL_PROMPT)

def build_authorization_response():
uri, headers, body, status = self.create_authorization_response(
request=self.request, scopes=" ".join(scopes),
credentials=credentials, allow=True)
redirect_schemes = application.redirect_uri_schemes
return SchemedHttpResponseRedirect(uri, allowed_schemes=redirect_schemes)

# If skip_authorization field is True, skip the authorization screen even
# if this is the first use of the application and there was no previous authorization.
# This is useful for in-house applications-> assume an in-house applications
# are already approved.
if application.skip_authorization:
uri, headers, body, status = self.create_authorization_response(
request=self.request, scopes=" ".join(scopes),
credentials=credentials, allow=True)
return HttpResponseRedirect(uri)
return build_authorization_response()

elif require_approval == 'auto':
tokens = request.user.accesstoken_set.filter(application=kwargs['application'],
expires__gt=timezone.now()).all()
# check past authorizations regarded the same scopes as the current one
for token in tokens:
if token.allow_scopes(scopes):
uri, headers, body, status = self.create_authorization_response(
request=self.request, scopes=" ".join(scopes),
credentials=credentials, allow=True)
return HttpResponseRedirect(uri)
return build_authorization_response()

return self.render_to_response(self.get_context_data(**kwargs))

Expand Down
24 changes: 24 additions & 0 deletions oauth2_provider/views/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.http.response import HttpResponseRedirectBase


class SchemedHttpResponseRedirectBase(HttpResponseRedirectBase):
"""
HttpResponseRedirectBase-like class that accepts an `allowed_schemes`
positional argument to overwrite the default set of schemes.
Warning: if `allowed_schemes` is empty, no scheme is allowed.
"""

def __init__(self, redirect_to, *args, **kwargs):
try:
self.allowed_schemes = kwargs.pop('allowed_schemes')
except KeyError:
pass
super(SchemedHttpResponseRedirectBase, self).__init__(redirect_to, *args, **kwargs)


class SchemedHttpResponseRedirect(SchemedHttpResponseRedirectBase):
status_code = 302


class SchemedHttpResponsePermanentRedirect(SchemedHttpResponseRedirectBase):
status_code = 301