introspect in access token

This commit is contained in:
Sayyid Hamid Mahdavi 2020-08-25 10:47:37 +04:30
parent 46230c3cfe
commit 442cf026cb
7 changed files with 220 additions and 175 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ delme*.py
static static
*.pyc *.pyc
.env .env
/venv/

View file

@ -25,7 +25,7 @@ class ApplicationAdmin(ApplicationAdmin):
@admin.register(Resource) @admin.register(Resource)
class ResourceAdmin(admin.ModelAdmin): class ResourceAdmin(admin.ModelAdmin):
list_display = ("name", "token", "user", "expires") list_display = ("name", "user", "expires")
@admin.register(Scope) @admin.register(Scope)

View file

@ -0,0 +1,48 @@
# Generated by Django 3.0.8 on 2020-08-25 06:15
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('gooyal_oauth2', '0005_auto_20200712_1219'),
]
operations = [
migrations.RemoveField(
model_name='resource',
name='created',
),
migrations.RemoveField(
model_name='resource',
name='token',
),
migrations.RemoveField(
model_name='resource',
name='updated',
),
migrations.AddField(
model_name='application',
name='resource',
field=models.OneToOneField(blank=True, help_text='The resource of application.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='application', to='gooyal_oauth2.Resource'),
),
migrations.AddField(
model_name='scope',
name='resource',
field=models.ForeignKey(blank=True, help_text='The resource of scope.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='scopes', to='gooyal_oauth2.Resource'),
),
migrations.AlterField(
model_name='application',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='gooyal_oauth2_application', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='resource',
name='name',
field=models.CharField(max_length=255),
),
]

View file

@ -12,6 +12,20 @@ from oauth2_provider.settings import oauth2_settings
import uuid import uuid
class Resource(models.Model):
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
name = models.CharField(max_length=255)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True,
related_name="resources"
)
expires = models.DateTimeField()
def __str__(self):
return self.name
class Application(AbstractApplication): class Application(AbstractApplication):
""" """
Application model for use with Django OAuth Toolkit that allows the scopes Application model for use with Django OAuth Toolkit that allows the scopes
@ -23,6 +37,13 @@ class Application(AbstractApplication):
on_delete=models.PROTECT on_delete=models.PROTECT
) )
allowed_scope = models.TextField(blank=True) allowed_scope = models.TextField(blank=True)
resource = models.OneToOneField(
Resource,
models.PROTECT,
blank=True, null=True,
help_text='The resource of application.',
related_name='application'
)
@property @property
def allowed_scopes(self): def allowed_scopes(self):
@ -34,45 +55,20 @@ class Application(AbstractApplication):
return app_scopes.intersection(all_scopes) return app_scopes.intersection(all_scopes)
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)
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]
def __str__(self):
return self.name
class Scope(models.Model): class Scope(models.Model):
""" """
Django model for an OAuth scope. Django model for an OAuth scope.
""" """
#: The application that created the scope #: The application that created the scope
#  NOTE: This is not used to limit access to the scope in any way - we want the # NOTE: This is not used to limit access to the scope in any way - we want the
# scope to be available to other applications in order to request access # scope to be available to other applications in order to request access
#   to the resource it protects! # to the resource it protects!
application = models.ForeignKey( application = models.ForeignKey(
oauth2_settings.APPLICATION_MODEL, oauth2_settings.APPLICATION_MODEL,
models.CASCADE, models.CASCADE,
#  This field is nullable because it is only set for scopes created by # This field is nullable because it is only set for scopes created by
#  external resource servers, which have a corresponding OAuth application # external resource servers, which have a corresponding OAuth application
#  record on the authorisation server # record on the authorisation server
blank=True, null=True, blank=True, null=True,
help_text='The application to which the scope belongs.', help_text='The application to which the scope belongs.',
related_name='scopes' related_name='scopes'

View file

@ -41,41 +41,41 @@ class MultiGatewayOAuth2Validator(OAuth2Validator): # pylint: disable=w0223
return False return False
class IntrospectOAuth2Validator(OAuth2Validator): # class IntrospectOAuth2Validator(OAuth2Validator):
def validate_bearer_token(self, token, scopes, request): # def validate_bearer_token(self, token, scopes, request):
""" # """
When users try to access resources, check that provided token is valid # When users try to access resources, check that provided token is valid
""" # """
if not token: # if not token:
return False # return False
#
introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL # introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL
introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN # introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN
introspection_credentials = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS # introspection_credentials = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS
#
try: # try:
access_token = Resource.objects.select_related("user").get(token=token) # access_token = AccessToken.objects.select_related("application", "user").get(token=token)
except Resource.DoesNotExist: # except AccessToken.DoesNotExist:
access_token = None # access_token = None
#
# if there is no token or it's invalid then introspect the token if there's an external OAuth server # # 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 not access_token or not access_token.is_valid(scopes):
if introspection_url and (introspection_token or introspection_credentials): # if introspection_url and (introspection_token or introspection_credentials):
access_token = self._get_token_from_authentication_server( # access_token = self._get_token_from_authentication_server(
token, # token,
introspection_url, # introspection_url,
introspection_token, # introspection_token,
introspection_credentials # introspection_credentials
) # )
#
if access_token and access_token.is_valid(scopes): # if access_token and access_token.is_valid(scopes):
request.client = access_token.application # request.client = access_token.application
request.user = access_token.user or (access_token.application and access_token.application.user) # request.user = access_token.user or (access_token.application and access_token.application.user)
request.scopes = scopes # request.scopes = scopes
#
# this is needed by django rest framework # # this is needed by django rest framework
request.access_token = access_token # request.access_token = access_token
return True # return True
else: # else:
self._set_oauth2_error_on_request(request, access_token, scopes) # self._set_oauth2_error_on_request(request, access_token, scopes)
return False # return False

View file

@ -12,113 +12,113 @@ from oauth2_provider.oauth2_backends import OAuthLibCore
from oauth2_provider.views import ClientProtectedScopedResourceView from oauth2_provider.views import ClientProtectedScopedResourceView
from oauthlib.oauth2 import Server from oauthlib.oauth2 import Server
from apps.gooyal_oauth2.validators import IntrospectOAuth2Validator # from apps.gooyal_oauth2.validators import IntrospectOAuth2Validator
@method_decorator(csrf_exempt, name="dispatch") # @method_decorator(csrf_exempt, name="dispatch")
class IntrospectTokenView(ClientProtectedScopedResourceView): # class IntrospectTokenView(ClientProtectedScopedResourceView):
""" # """
Implements an endpoint for token introspection based # Implements an endpoint for token introspection based
on RFC 7662 https://tools.ietf.org/html/rfc7662 # on RFC 7662 https://tools.ietf.org/html/rfc7662
#
To access this view the request must pass a OAuth2 Bearer Token # To access this view the request must pass a OAuth2 Bearer Token
which is allowed to access the scope `introspection`. # which is allowed to access the scope `introspection`.
""" # """
required_scopes = ["introspection"] # required_scopes = ["introspection"]
#
@staticmethod # @staticmethod
def get_token_response(token_value=None): # def get_token_response(token_value=None):
try: # try:
token = get_access_token_model().objects.get(token=token_value) # token = get_access_token_model().objects.get(token=token_value)
except ObjectDoesNotExist: # except ObjectDoesNotExist:
return HttpResponse( # return HttpResponse(
content=json.dumps({"active": False}), # content=json.dumps({"active": False}),
status=401, # status=401,
content_type="application/json" # content_type="application/json"
) # )
else: # else:
if token.is_valid(): # if token.is_valid():
data = { # data = {
"active": True, # "active": True,
"scope": token.scope, # "scope": token.scope,
"exp": int(calendar.timegm(token.expires.timetuple())), # "exp": int(calendar.timegm(token.expires.timetuple())),
} # }
if token.application: # if token.application:
data["client_id"] = token.application.client_id # data["client_id"] = token.application.client_id
if token.user: # if token.user:
data["username"] = token.user.get_username() # data["username"] = token.user.get_username()
return HttpResponse(content=json.dumps(data), status=200, content_type="application/json") # return HttpResponse(content=json.dumps(data), status=200, content_type="application/json")
else: # else:
return HttpResponse(content=json.dumps({ # return HttpResponse(content=json.dumps({
"active": False, # "active": False,
}), status=200, content_type="application/json") # }), status=200, content_type="application/json")
#
def get(self, request, *args, **kwargs): # def get(self, request, *args, **kwargs):
""" # """
Get the token from the URL parameters. # Get the token from the URL parameters.
URL: https://example.com/introspect?token=mF_9.B5f-4.1JqM # URL: https://example.com/introspect?token=mF_9.B5f-4.1JqM
#
:param request: # :param request:
:param args: # :param args:
:param kwargs: # :param kwargs:
:return: # :return:
""" # """
return self.get_token_response(request.GET.get("token", None)) # return self.get_token_response(request.GET.get("token", None))
#
def post(self, request, *args, **kwargs): # def post(self, request, *args, **kwargs):
""" # """
Get the token from the body form parameters. # Get the token from the body form parameters.
Body: token=mF_9.B5f-4.1JqM # Body: token=mF_9.B5f-4.1JqM
#
:param request: # :param request:
:param args: # :param args:
:param kwargs: # :param kwargs:
:return: # :return:
""" # """
return self.get_token_response(request.POST.get("token", None)) # return self.get_token_response(request.POST.get("token", None))
def protected_resource(scopes=None): # def protected_resource(scopes=None):
""" # """
Implementation of protected_resource decorator that saves the client on the # Implementation of protected_resource decorator that saves the client on the
request for the view function to use. # request for the view function to use.
#
Cribbed from django-oauth-toolkit. # Cribbed from django-oauth-toolkit.
""" # """
_scopes = scopes or [] # _scopes = scopes or []
#
def decorator(view_func): # def decorator(view_func):
@functools.wraps(view_func) # @functools.wraps(view_func)
def _validate(request, *args, **kwargs): # def _validate(request, *args, **kwargs):
validator = IntrospectOAuth2Validator() # validator = IntrospectOAuth2Validator()
core = OAuthLibCore(Server(validator)) # core = OAuthLibCore(Server(validator))
valid, oauthlib_req = core.verify_request(request, scopes=_scopes) # valid, oauthlib_req = core.verify_request(request, scopes=_scopes)
if valid: # if valid:
request.client = oauthlib_req.client # request.client = oauthlib_req.client
request.resource_owner = oauthlib_req.user # request.resource_owner = oauthlib_req.user
return view_func(request, *args, **kwargs) # return view_func(request, *args, **kwargs)
return HttpResponseForbidden() # return HttpResponseForbidden()
#
return _validate # return _validate
#
return decorator # return decorator
#
#
@require_http_methods(['GET', 'POST']) # @require_http_methods(['GET', 'POST'])
@csrf_exempt # @csrf_exempt
@protected_resource(scopes=['introspection']) # @protected_resource(scopes=['introspection'])
def introspect_token(request): # def introspect_token(request):
""" # """
Version of the introspection view protected by a regular scope instead of # Version of the introspection view protected by a regular scope instead of
read-write scopes. # read-write scopes.
#
Also allows for the required scope to be changed using a setting. # Also allows for the required scope to be changed using a setting.
""" # """
if request.method == 'GET': # if request.method == 'GET':
token = request.GET.get("token", None) # token = request.GET.get("token", None)
else: # else:
token = request.POST.get("token", None) # token = request.POST.get("token", None)
return IntrospectTokenView.get_token_response(token) # return IntrospectTokenView.get_token_response(token)
# @require_POST # @require_POST

View file

@ -20,7 +20,7 @@ from django.contrib.auth.views import LogoutView
from django.urls import path, include from django.urls import path, include
from django.contrib import admin from django.contrib import admin
from apps.gooyal_oauth2.views.introspect import introspect_token # from apps.gooyal_oauth2.views.introspect import introspect_token
from apps.transactions.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt, \ from apps.transactions.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt, \
ServiceTransactionVerify, ServiceTransactionSubmit ServiceTransactionVerify, ServiceTransactionSubmit
from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView, \ from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView, \
@ -35,8 +35,8 @@ urlpatterns = [
path('logout/', LogoutView.as_view(), name='logout'), path('logout/', LogoutView.as_view(), name='logout'),
path('', home, name='home'), path('', home, name='home'),
path('oauth2/introspect', introspect_token), # path('oauth2/introspect', introspect_token),
path('oauth2/introspect/', introspect_token, name='introspect'), # path('oauth2/introspect/', introspect_token, name='introspect'),
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
# url(r'^oauth2/register_scope/$', register_scope, name = 'register-scope'), # url(r'^oauth2/register_scope/$', register_scope, name = 'register-scope'),