introspect in access token
This commit is contained in:
parent
46230c3cfe
commit
442cf026cb
7 changed files with 220 additions and 175 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ delme*.py
|
||||||
static
|
static
|
||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
|
/venv/
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
48
apps/gooyal_oauth2/migrations/0006_auto_20200825_0615.py
Normal file
48
apps/gooyal_oauth2/migrations/0006_auto_20200825_0615.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue