introspect_application

This commit is contained in:
mahdavi 2024-07-04 15:38:20 +03:30
parent 61878794a6
commit 022011d285
4 changed files with 51 additions and 238 deletions

View file

@ -28,6 +28,8 @@ 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
available to an application to be restricted on a per-application basis. available to an application to be restricted on a per-application basis.
""" """
# TODO: change it to owner
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
related_name="%(app_label)s_%(class)s", related_name="%(app_label)s_%(class)s",

View file

@ -1,7 +1,7 @@
from django.urls import re_path from django.urls import re_path
from oauth2_provider import views from oauth2_provider import views
from .views.introspect import IntrospectTokenView from .views.introspect import IntrospectTokenView, IntrospectApplicationView
app_name = "oauth2_provider" app_name = "oauth2_provider"
@ -11,6 +11,7 @@ base_urlpatterns = [
re_path(r"^token/$", views.TokenView.as_view(), name="token"), re_path(r"^token/$", views.TokenView.as_view(), name="token"),
re_path(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"), re_path(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"),
re_path(r"^introspect/$", IntrospectTokenView.as_view(), name="introspect"), re_path(r"^introspect/$", IntrospectTokenView.as_view(), name="introspect"),
re_path(r"^introspect_application/$", IntrospectApplicationView.as_view(), name="introspect-application"),
] ]

View file

@ -20,17 +20,6 @@ UserModel = get_user_model()
class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223 class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
introspection_client = None
def get_introspection_client(self, introspection_client_id, introspection_client_secret):
if not OAuth2Validator.introspection_client:
OAuth2Validator.introspection_client = service_clients.Client(client_id=introspection_client_id,
client_secret=introspection_client_secret,
grant_type=service_clients.AccountsClient.GRANT_CLIENT_CREDENTIALS,
scopes=['introspection'])
return OAuth2Validator.introspection_client
def validate_user(self, username, password, client, request, *args, **kwargs): def validate_user(self, username, password, client, request, *args, **kwargs):
auth_fields = getattr(request, 'auth_fields', 'username:password').split(':') auth_fields = getattr(request, 'auth_fields', 'username:password').split(':')
@ -61,228 +50,3 @@ class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
return True return True
return False return False
def _get_token_from_gooyal_authentication_server(
self, token, introspection_url, introspection_token, introspection_credentials, introspection_client_id,
introspection_client_secret
):
"""Use external introspection endpoint to "crack open" the token.
:param introspection_url: introspection endpoint URL
:param introspection_token: Bearer token
:param introspection_credentials: Basic Auth credentials (id,secret)
:return: :class:`models.AccessToken`
Some RFC 7662 implementations (including this one) use a Bearer token while others use Basic
Auth. Depending on the external AS's implementation, provide either the introspection_token
or the introspection_credentials.
If the resulting access_token identifies a username (e.g. Authorization Code grant), add
that user to the UserModel. Also cache the access_token up until its expiry time or a
configured maximum time.
"""
headers = None
response = None
if introspection_token:
headers = {"Authorization": "Bearer {}".format(introspection_token)}
try:
response = requests.post(
introspection_url,
data={"token": token}, headers=headers
)
except requests.exceptions.RequestException:
log.exception("Introspection: Failed POST to %r in token lookup", introspection_url)
return None
elif introspection_credentials:
client_id = introspection_credentials[0].encode("utf-8")
client_secret = introspection_credentials[1].encode("utf-8")
basic_auth = base64.b64encode(client_id + b":" + client_secret)
headers = {"Authorization": "Basic {}".format(basic_auth.decode("utf-8"))}
try:
response = requests.post(
introspection_url,
data={"token": token}, headers=headers
)
except requests.exceptions.RequestException:
log.exception("Introspection: Failed POST to %r in token lookup", introspection_url)
return None
elif introspection_client_id and introspection_client_secret:
data = {"token": token}
introspection_client = self.get_introspection_client(introspection_client_id, introspection_client_secret)
response = introspection_client.request(url=introspection_url, method='post', data=data,
required_scopes=['introspection'], login_required=True)
try:
content: dict = response.json()
except ValueError:
log.exception("Introspection: Failed to parse response as json")
return None
user = None
if "active" in content and content["active"] is True:
if "username" in content:
user, content = oauth2_settings.INTROSPECTION_USER_CREATE_METHOD(token, content)
max_caching_time = datetime.now() + timedelta(
seconds=oauth2_settings.RESOURCE_SERVER_TOKEN_CACHING_SECONDS
)
if "exp" in content:
expires = datetime.utcfromtimestamp(content["exp"])
if expires > max_caching_time:
expires = max_caching_time
else:
expires = max_caching_time
scope = content.get("scope", "")
expires = make_aware(expires)
access_token, _created = AccessTokenModel.objects.update_or_create(
token=token,
defaults={
"user": user,
"application": None,
"scope": scope,
"expires": expires,
"detail": content,
})
# try:
# access_token = AccessTokenModel.objects.select_related("application", "user").get(token=token)
# except AccessTokenModel.DoesNotExist:
# access_token = AccessTokenModel.objects.create(
# user=user,
# token=token,
# application=None,
# scope=scope,
# expires=expires,
# detail=content
# )
# else:
# access_token.expires = expires
# access_token.scope = scope
# access_token.detail = content
# access_token.save()
return access_token
# 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
# introspection_client_id = oauth2_settings.RESOURCE_SERVER_CLIENT_ID
# introspection_client_secret = oauth2_settings.RESOURCE_SERVER_CLIENT_SECRET
#
# try:
# access_token = AccessTokenModel.objects.select_related("application", "user").get(token=token)
# except AccessTokenModel.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 or (introspection_client_id and
# introspection_client_secret)):
# access_token = self._get_token_from_gooyal_authentication_server(
# token,
# introspection_url,
# introspection_token,
# introspection_credentials,
# introspection_client_id,
# introspection_client_secret
# )
#
# 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
def _authenticate_basic_auth(self, request):
"""
Authenticates with HTTP Basic Auth.
Note: as stated in rfc:`2.3.1`, client_id and client_secret must be encoded with
"application/x-www-form-urlencoded" encoding algorithm.
"""
auth_string = self._extract_basic_auth(request)
if not auth_string:
return False
try:
encoding = request.encoding or settings.DEFAULT_CHARSET or "utf-8"
except AttributeError:
encoding = "utf-8"
try:
b64_decoded = base64.b64decode(auth_string)
except (TypeError, binascii.Error):
log.debug("Failed basic auth: %r can't be decoded as base64", auth_string)
return False
try:
auth_string_decoded = b64_decoded.decode(encoding)
except UnicodeDecodeError:
log.debug("Failed basic auth: %r can't be decoded as unicode by %r", auth_string, encoding)
return False
try:
client_id, client_secret = map(unquote_plus, auth_string_decoded.split(":", 1))
except ValueError:
log.debug("Failed basic auth, Invalid base64 encoding.")
return False
if self._load_application(client_id, request) is None:
log.debug("Failed basic auth: Application %s does not exist" % client_id)
return False
elif request.client.client_id != client_id:
log.debug("Failed basic auth: wrong client id %s" % client_id)
return False
# TODO: check why not work
elif not client_secret == request.client.client_secret:
log.debug("Failed basic auth: wrong client secret %s" % client_secret)
return False
else:
return True
def _authenticate_request_body(self, request):
"""
Try to authenticate the client using client_id and client_secret
parameters included in body.
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
try:
client_id = request.client_id
client_secret = request.client_secret
except AttributeError:
return False
if self._load_application(client_id, request) is None:
log.debug("Failed body auth: Application %s does not exists" % client_id)
return False
# TODO: check why not work
elif not client_secret == request.client.client_secret:
log.debug("Failed body auth: wrong client secret %s" % client_secret)
return False
else:
return True

View file

@ -5,7 +5,7 @@ from django.http import JsonResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from oauth2_provider.models import get_access_token_model from oauth2_provider.models import get_access_token_model, get_application_model
from oauth2_provider.views.generic import ClientProtectedScopedResourceView from oauth2_provider.views.generic import ClientProtectedScopedResourceView
@ -69,3 +69,49 @@ class IntrospectTokenView(ClientProtectedScopedResourceView):
:return: :return:
""" """
return self.get_token_response(request.POST.get("token", None)) return self.get_token_response(request.POST.get("token", None))
@method_decorator(csrf_exempt, name="dispatch")
class IntrospectApplicationView(ClientProtectedScopedResourceView):
required_scopes = ["introspection"]
# TODO: check application state
@staticmethod
def get_application_response(client_id=None):
try:
application = (
get_application_model().objects.get(client_id=client_id)
)
except ObjectDoesNotExist:
return JsonResponse({"active": False}, status=200)
else:
data = {
"active": True,
}
if application.user_id:
data["client_owner"] = str(application.user_id)
return JsonResponse(data)
def get(self, request, *args, **kwargs):
"""
Get the token from the URL parameters.
URL: https://example.com/introspect?token=mF_9.B5f-4.1JqM
:param request:
:param args:
:param kwargs:
:return:
"""
return self.get_application_response(request.GET.get("client_id", None))
def post(self, request, *args, **kwargs):
"""
Get the token from the body form parameters.
Body: token=mF_9.B5f-4.1JqM
:param request:
:param args:
:param kwargs:
:return:
"""
return self.get_application_response(request.POST.get("client_id", None))