From 56ebabad7feb7ac3ccd5c5722982feee5a3f4888 Mon Sep 17 00:00:00 2001 From: mahdavi Date: Thu, 11 Jun 2020 12:58:08 +0430 Subject: [PATCH] basic resource model --- apps/gooyal_oauth2/models.py | 25 +++++++- apps/gooyal_oauth2/validators.py | 44 +++++++++++++ apps/gooyal_oauth2/views.py | 103 +++++++++++++++++++++++++++++++ gooyal_accounts/urls.py | 3 + 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 apps/gooyal_oauth2/views.py diff --git a/apps/gooyal_oauth2/models.py b/apps/gooyal_oauth2/models.py index c484d5c..c97ddb4 100644 --- a/apps/gooyal_oauth2/models.py +++ b/apps/gooyal_oauth2/models.py @@ -5,9 +5,10 @@ Django models for the gooyal-restrict-scopes package. import requests from django.conf import settings from django.db import models -from oauth2_provider.models import AbstractApplication +from oauth2_provider.models import AbstractApplication, AbstractAccessToken from oauth2_provider.scopes import get_scopes_backend from oauth2_provider.settings import oauth2_settings +import uuid class Application(AbstractApplication): @@ -95,3 +96,25 @@ class Scope(models.Model): defaults={'description': description, 'is_default': is_default} ) return True + + +class Resource(AbstractAccessToken): + source_refresh_token = None + id = None + application = None + + uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True) + name = models.CharField(max_length=255, blank=True) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True, + related_name="resources" + ) + expires = models.DateTimeField() + token = models.CharField(max_length=255, unique=True, ) # introspect token + + scope = 'introspection' + scopes = {'introspection': 'Introspect token scope'} + + def allow_scopes(self, scopes): + return scopes == [self.scope] diff --git a/apps/gooyal_oauth2/validators.py b/apps/gooyal_oauth2/validators.py index 90b7a92..e3b073d 100755 --- a/apps/gooyal_oauth2/validators.py +++ b/apps/gooyal_oauth2/validators.py @@ -1,6 +1,9 @@ from oauth2_provider.oauth2_validators import OAuth2Validator from django.contrib.auth import get_user_model +from oauth2_provider.settings import oauth2_settings + +from apps.gooyal_oauth2.models import Resource USER_MODEL = get_user_model() @@ -45,3 +48,44 @@ class MultiGatewayOAuth2Validator(OAuth2Validator): # pylint: disable=w0223 return True return False + + +class IntrospectOAuth2Validator(OAuth2Validator): + def validate_bearer_token(self, token, scopes, request): + """ + When users try to access resources, check that provided token is valid + """ + if not token: + return False + + introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL + introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN + introspection_credentials = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS + + try: + access_token = Resource.objects.select_related("application", "user").get(token=token) + except Resource.DoesNotExist: + access_token = None + + # if there is no token or it's invalid then introspect the token if there's an external OAuth server + if not access_token or not access_token.is_valid(scopes): + if introspection_url and (introspection_token or introspection_credentials): + access_token = self._get_token_from_authentication_server( + token, + introspection_url, + introspection_token, + introspection_credentials + ) + + if access_token and access_token.is_valid(scopes): + request.client = access_token.application + request.user = access_token.user + request.scopes = scopes + + # this is needed by django rest framework + request.access_token = access_token + return True + else: + self._set_oauth2_error_on_request(request, access_token, scopes) + return False + diff --git a/apps/gooyal_oauth2/views.py b/apps/gooyal_oauth2/views.py new file mode 100644 index 0000000..4d72e8d --- /dev/null +++ b/apps/gooyal_oauth2/views.py @@ -0,0 +1,103 @@ +""" +Django views for the gooyal-dynamic-scopes package. +""" + +import json +import functools + +from django.conf import settings +from django.http import HttpResponse, HttpResponseForbidden +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods, require_POST + +from oauthlib.oauth2 import Server + +from oauth2_provider.oauth2_backends import OAuthLibCore +from oauth2_provider.views import IntrospectTokenView + +from apps.gooyal_oauth2.validators import IntrospectOAuth2Validator +from .models import Scope + + +def protected_resource(scopes=None): + """ + Implementation of protected_resource decorator that saves the client on the + request for the view function to use. + + Cribbed from django-oauth-toolkit. + """ + _scopes = scopes or [] + + def decorator(view_func): + @functools.wraps(view_func) + def _validate(request, *args, **kwargs): + validator = IntrospectOAuth2Validator() + core = OAuthLibCore(Server(validator)) + valid, oauthlib_req = core.verify_request(request, scopes=_scopes) + if valid: + request.client = oauthlib_req.client + request.resource_owner = oauthlib_req.user + return view_func(request, *args, **kwargs) + return HttpResponseForbidden() + + return _validate + + return decorator + + +@require_http_methods(['GET', 'POST']) +@csrf_exempt +@protected_resource(scopes=[settings.INTROSPECT_SCOPE]) +def introspect_token(request): + """ + Version of the introspection view protected by a regular scope instead of + read-write scopes. + + Also allows for the required scope to be changed using a setting. + """ + if request.method == 'GET': + token = request.GET.get("token", None) + else: + token = request.POST.get("token", None) + return IntrospectTokenView.get_token_response(token) + + +# @require_POST +# @csrf_exempt +# @protected_resource(scopes=[settings.REGISTER_SCOPE_SCOPE]) +# def register_scope(request): +# """ +# Implements an endpoint for registering a scope. +# """ +# #  Get the scope data from the request body +# scope_data = json.loads(request.body) if request.body else {} +# try: +# try: +# #  If a scope with the given name already exists, find it +# scope = Scope.objects.get(name=scope_data['name']) +# except Scope.DoesNotExist: +# #  If no scope with the given name exists, create it +# _ = Scope.objects.create( +# application=request.client, +# name=scope_data['name'], +# description=scope_data['description'], +# is_default=scope_data.get('is_default', False) +# ) +# #  Respond with a 201 Created +# return HttpResponse(status=201) +# except KeyError as exc: +# #  A key missing in the data should be reported as a bad request +# return HttpResponse( +# status=400, +# content="'{}' must be given in request data".format(exc.args[0]), +# content_type='text/plain' +# ) +# #  If the scope does exist, check that the current application is the +# #  owner of the scope before updating it +# if scope.application and scope.application == request.client: +# scope.description = scope_data['description'] +# scope.is_default = scope_data.get('is_default', False) +# scope.save() +# return HttpResponse(status=200) +# else: +# return HttpResponse(status=403) diff --git a/gooyal_accounts/urls.py b/gooyal_accounts/urls.py index 299b65b..db1cba4 100644 --- a/gooyal_accounts/urls.py +++ b/gooyal_accounts/urls.py @@ -20,6 +20,7 @@ from django.contrib.auth.views import LogoutView from django.urls import path, include from django.contrib import admin +from apps.gooyal_oauth2.views import introspect_token from apps.transactions.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt, \ ServiceTransactionVerify, ServiceTransactionSubmit from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView, \ @@ -34,6 +35,8 @@ urlpatterns = [ path('logout/', LogoutView.as_view(), name='logout'), path('', home, name='home'), + path('oauth2/introspect', introspect_token), + path('oauth2/introspect/', introspect_token, name='introspect'), path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), # url(r'^oauth2/register_scope/$', register_scope, name = 'register-scope'),