Django Oauth Toolkit: User data over introspection

1.5k views Asked by At

Current Scenario:

I'm using Introspect to validate access token on the authentication server. This call returns only 'username' of the user from the authentication server and saves it in the resource server. The Id of the same user on the authentication server and the resource server are no necessarily the same.

Desired Scenario:

I want to receive more data about the user (email, phone number, address, etc..) and save it in the resource server.

What I have done so far:

I modified the django-oauth-toolkit/oauth2_provider/views/introspect.py/ get_token_response to return the data I need.

What is remaining:

How do I save those data in the resource server? or is it better to make an api call to the authentication server whenever I require the user data?

2

There are 2 answers

0
Usoof On BEST ANSWER

I achieved this by modifying get_token_response in IntrospectTokenView in the Auth-Server

def get_token_response(token_value=None):
        try:
            token = get_access_token_model().objects.select_related(
                "user", "application"
                ).get(token=token_value)
        except ObjectDoesNotExist:
            return HttpResponse(
                content=json.dumps({"active": False}),
                status=401,
                content_type="application/json"
            )
        else:
            if token.is_valid():
                data = {
                    "active": True,
                    "scope": token.scope,
                    "exp": int(calendar.timegm(token.expires.timetuple())),
                }
                if token.application:
                    data["client_id"] = token.application.client_id
                if token.user:
                    data["username"] = token.user.get_username()
# TODO: DANGER ZONE
# Pass extra parameters
# ------------------------------------------------------------------------------
                    data["email"] = token.user.email
                    data["phone_number"] = token.user.phone_number
                    data["is_company"] = token.user.is_company
                    customer = token.user.customer
                    data["designation"] = customer.designation
                    company = customer.company
                    data["company"] = company.company_name
# ------------------------------------------------------------------------------
                return HttpResponse(content=json.dumps(data), status=200, content_type="application/json")
            else:
                return HttpResponse(content=json.dumps({
                    "active": False,
                }), status=200, content_type="application/json")

and _get_token_from_authentication_server in OAuth2Validator in the Resource-Server

def _get_token_from_authentication_server(
            self, token, introspection_url, introspection_token, introspection_credentials
    ):
        headers = None
        if introspection_token:
            headers = {"Authorization": "Bearer {}".format(introspection_token)}
        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

        # Log an exception when response from auth server is not successful
        if response.status_code != http.client.OK:
            log.exception("Introspection: Failed to get a valid response "
                          "from authentication server. Status code: {}, "
                          "Reason: {}.".format(response.status_code,
                                               response.reason))
            return None

        try:
            content = response.json()
        except ValueError:
            log.exception("Introspection: Failed to parse response as json")
            return None

        if "active" in content and content["active"] is True:
            if "username" in content:
                user, _created = UserModel.objects.get_or_create(
                    **{UserModel.USERNAME_FIELD: content["username"]}
                )
# TODO: DANGER ZONE
# Adding extra data to user profile and create company
# ------------------------------------------------------------------------------
                user.email = content["email"]
                user.phone_number = content["phone_number"]
                user.is_company = content["is_company"]

                customer, _created_customer = CustomerModel.objects.get_or_create(
                    user = user
                )
                customer.designation = content["designation"]

                company, _created_company = CompanyModel.objects.get_or_create(
                    company_name = content["company"]
                )
                customer.company = company

                customer.save()
                user.save()
# ------------------------------------------------------------------------------
            else:
                user = None

            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 = AccessToken.objects.update_or_create(
                token=token,
                defaults={
                    "user": user,
                    "application": None,
                    "scope": scope,
                    "expires": expires,
                })

            return access_token

. Now I'm wondering how can I extend the classes and add the extra codes instead of directly modifying the source code? Appreciate any help.

0
Nathan On

Answering on how to extend IntrospectTokenView to override get_token_response method. Make a new "oauth2" directory on the same level as the other Django app folders. Add new empty __init__.py and views.py files.

oauth2/views.py:

from calendar import timegm
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse
from oauth2_provider.views.introspect import IntrospectTokenView
from oauth2_provider.models import get_access_token_model


class CustomIntrospectView(IntrospectTokenView):

    @staticmethod
    def get_token_response(token_value=None):
        try:
            token = (
                get_access_token_model().objects.select_related("user", "application").get(token=token_value)
            )
        except ObjectDoesNotExist:
            return JsonResponse({"active": False}, status=401)
        else:
            if token.is_valid():
                data = {
                    "active": True,
                    "scope": token.scope,
                    "exp": int(timegm(token.expires.timetuple())),
                }
                if token.application:
                    data["client_id"] = token.application.client_id
                if token.user:
                    data["username"] = token.user.get_username()
                    # add extra key-value pairs here:
                    data["email"] = token.user.email
                    data["phone_number"] = token.user.phone_number
                    data["is_company"] = token.user.is_company
                    customer = token.user.customer
                    data["designation"] = customer.designation
                    company = customer.company
                    data["company"] = company.company_name
                return JsonResponse(data)
            else:
                return JsonResponse({"active": False})

Depending on how you include oauth2_views to main project urls.py you need to include this modified view class. If you follow Django Oauth Toolkit documentation, you need to change:

...

urlpatterns = [
    path('admin/', admin.site.urls),
    path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
    # ...
]

To:

from django.contrib import admin
from django.urls import path, include
from oauth2.views import CustomIntrospectView
from oauth2_provider import views as oauth2_views


oauth2_endpoint_views = [
    path('authorize/', oauth2_views.AuthorizationView.as_view(), name="authorize"),
    path('token/', oauth2_views.TokenView.as_view(), name="token"),
    path('revoke-token/', oauth2_views.RevokeTokenView.as_view(), name="revoke-token"),
    path('introspect/', CustomIntrospectView.as_view(), name="introspect"),
]

urlpatterns = [
    path('admin/', admin.site.urls),
    path('o/', include((oauth2_endpoint_views, 'oauth2_provider'), namespace='oauth2_provider')),
    # ...
]

Reference used: https://django-oauth-toolkit.readthedocs.io/en/latest/advanced_topics.html#overriding-views