termux
termux

Reputation: 341

Disable or restrict /o/applications (django rest framework, oauth2)

I am currently writing a REST API using Django rest framework, and oauth2 for authentication (using django-oauth-toolkit). I'm very happy with both of them, making exactly what I want.

However, I have one concern. I'm passing my app to production, and realized there might be a problem with the /o/applications/ view, which is accessible to everyone! I found myself surprised to not see anything in the doc about it, neither when I try to google it. Did I miss something?

Some ideas where to either making a custom view, requiring authentication as super-user (but this would be weird, as it would mix different kind of authentication, wouldn't it?), or add a dummy route to 401 or 403 view to /o/applications/. But these sound quite hacky to me... isn't it any official "best" solution to do it? I'd be very surprised if I'm the first one running into this issue, I must have missed something...

Thanks by advance!

Upvotes: 6

Views: 1534

Answers (3)

ryanc16
ryanc16

Reputation: 1

I ran into the same problem and it was very frustrating. I ended up coming up with an admittedly less than ideal solution that involves re-defining the built-in views from oauth2_provider for the routes you want protected from regular users.

  1. Create a new directory in your application called oauth and create a views.py and urls.py.
  2. In views.py:
    a. Create two new mixins StaffRequiredMixin and ApplicationOwnerIsStaffMixin. Both require user.is_staff to be True.
    b. Redefine the views found in oauth2_provider.views.application.py by copy/pasting and update them to use the new mixins.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.forms.models import modelform_factory
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from oauth2_provider import models as oauth2_provider_models


class StaffRequiredMixin(LoginRequiredMixin):
    """Verify that the current user is staff."""
    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_staff:
            return self.handle_no_permission()
        return super().dispatch(request, *args, **kwargs)

class ApplicationOwnerIsStaffMixin(StaffRequiredMixin):
    """
    This mixin is used to provide an Application queryset filtered by request.user.is_staff.
    """

    fields = "__all__"

    def get_queryset(self):
        return oauth2_provider_models.get_application_model().objects.filter(user=self.request.user.is_staff)

class ApplicationRegistration(StaffRequiredMixin, CreateView):
    """
    View used to register a new Application for the request.user
    """

    template_name = "oauth2_provider/application_registration_form.html"

    def get_form_class(self):
        """
        Returns the form class for the application model
        """
        return modelform_factory(
            oauth2_provider_models.get_application_model(),
            fields=(
                "name",
                "client_id",
                "client_secret",
                "client_type",
                "authorization_grant_type",
                "redirect_uris",
                "algorithm",
            ),
        )

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)


class ApplicationDetail(ApplicationOwnerIsStaffMixin, DetailView):
    """
    Detail view for an application instance owned by the request.user
    """

    context_object_name = "application"
    template_name = "oauth2_provider/application_detail.html"


class ApplicationList(ApplicationOwnerIsStaffMixin, ListView):
    """
    List view for all the applications owned by the request.user
    """

    context_object_name = "applications"
    template_name = "oauth2_provider/application_list.html"


class ApplicationDelete(ApplicationOwnerIsStaffMixin, DeleteView):
    """
    View used to delete an application owned by the request.user
    """

    context_object_name = "application"
    success_url = reverse_lazy("oauth2_provider:list")
    template_name = "oauth2_provider/application_confirm_delete.html"


class ApplicationUpdate(ApplicationOwnerIsStaffMixin, UpdateView):
    """
    View used to update an application owned by the request.user
    """

    context_object_name = "application"
    template_name = "oauth2_provider/application_form.html"

    def get_form_class(self):
        """
        Returns the form class for the application model
        """
        return modelform_factory(
            oauth2_provider_models.get_application_model(),
            fields=(
                "name",
                "client_id",
                "client_secret",
                "client_type",
                "authorization_grant_type",
                "redirect_uris",
                "algorithm",
            ),
        )
  1. In urls.py:
    a. Redefine the urlpatterns to match that of oauth2_provider.urls.py and substitute in the new views for all the application/ and authorized_tokens/ based routes.
    b. Notice the last two lines of the file. In order to make all the other routes accessible for authorization requests and granting tokens, etc. we need to include the default views for those here as well.
from django.urls import re_path
from . import views
from oauth2_provider import views as oauth2_provider_views
from oauth2_provider import urls as oauth2_provider_urls

app_name = "oauth"
urlpatterns = [
    # Application management views
    re_path(r"^applications/$", views.ApplicationList.as_view(), name="list"),
    re_path(r"^applications/register/$", views.ApplicationRegistration.as_view(), name="register"),
    re_path(r"^applications/(?P<pk>[\w-]+)/$", views.ApplicationDetail.as_view(), name="detail"),
    re_path(r"^applications/(?P<pk>[\w-]+)/delete/$", views.ApplicationDelete.as_view(), name="delete"),
    re_path(r"^applications/(?P<pk>[\w-]+)/update/$", views.ApplicationUpdate.as_view(), name="update"),
    # Token management views
    re_path(r"^authorized_tokens/$", oauth2_provider_views.AuthorizedTokensListView.as_view(), name="authorized-token-list"),
    re_path(r"^authorized_tokens/(?P<pk>[\w-]+)/delete/$", oauth2_provider_views.AuthorizedTokenDeleteView.as_view(), name="authorized-token-delete")
]
urlpatterns += oauth2_provider_urls.base_urlpatterns
urlpatterns += oauth2_provider_urls.oidc_urlpatterns
  1. In your main applications configuration urls.py
    a. Update it to use your new oauth routes and views.
    b. If you had the one from the django_oauth_toolkit example, replace it with this one.
urlpatterns = [
  path("", ..., name="home"),
  ...
# Remove this one
# path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
# Add this one
  path("o/", include("your_application.oauth.urls", namespace="oauth2_provider")),
  ...
]

Upvotes: 0

AcckiyGerman
AcckiyGerman

Reputation: 365

Use only base urls: authorize/, token/, revoke_token/

from oauth2_provider.urls import base_urlpatterns, app_name

urlpatterns = [
    ...,  # some other urls

    # oauth2 urls
    path('o/', include((base_urlpatterns, app_name), namespace=app_name)
]

Instead of using all urls, as in official example:

    path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),

Upvotes: 3

termux
termux

Reputation: 341

Solution found!

In fact, the reason why /o/application was accessible, is because I had a super admin session open.

Everything is great, then :)

Upvotes: 1

Related Questions