Django-rest-framework: DRF should authorize all OPTIONS requests by default

Created on 21 Nov 2017  ·  22Comments  ·  Source: encode/django-rest-framework

Following the discussion on #908

The permissions for OPTIONS (metadata) requests are handled incorrectly in DRF.

As per the W3C specs, all preflight OPTIONS requests are NOT authenticated, meaning the users will always get a 401 error when preflighting a request for authenticated endpoints, because modern browsers never send this as per the specs :

Otherwise, make a preflight request. Fetch the request URL from origin source origin using referrer source as override referrer source with the manual redirect flag and the block cookies flag set, using the method OPTIONS, and with the following additional constraints:

  • Include an Access-Control-Request-Method header with as header field value the request method (even when that is a simple method).
  • If author request headers is not empty include an Access-Control-Request-Headers header with as header field value a comma-separated list of the header field names from author request headers in lexicographical order, each converted to ASCII lowercase (even when one or more are a simple header).
  • Exclude the author request headers.
  • ➡️ Exclude user credentials.
  • Exclude the request entity body.

I think this should be the default behaviour here.
DRF should authorise all OPTIONS request by default for standard permission classes (IsAuthenticated, IsAdminUser, etc) and users may override this when they explicitly need to protect metadata info (infringing the standard CORS compatibility)

Steps to reproduce :

views.py

class MyView(APIView):

    permission_classes = (IsAuthenticated,)

urls.py

urlpatterns = [
    url(r'^myview/', MyView.as_view()),
]

console

http GET http://127.0.0.1:8000/myview/
HTTP/1.0 401 Unauthorized

http OPTIONS http://127.0.0.1:8000/myview/
HTTP/1.0 401 Unauthorized

Expected behaviour

http GET http://127.0.0.1:8000/myview/
HTTP/1.0 401 Unauthorized

http OPTIONS http://127.0.0.1:8000/myview/
HTTP/1.0 200 OK

Temporary workaroud

class IsAuthenticated(permissions.IsAuthenticated):

    def has_permission(self, request, view):
        if request.method == 'OPTIONS':
            return True
        return super(IsAuthenticated, self).has_permission(request, view)

Most helpful comment

DRF Version: 3.7.3
Python 3.6.3

The temporary workaround mentioned above did not work for me. All requests were being authenticated regardless of whether they were PUT, POST, GET, OPTIONS, etc.

It worked on changing it to:

# myapp/permissions.py
from rest_framework.permissions import IsAuthenticated

class AllowOptionsAuthentication(IsAuthenticated):
    def has_permission(self, request, view):
        if request.method == 'OPTIONS':
            return True
        return request.user and request.user.is_authenticated

And in settings.py:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.TokenAuthentication',),
    'DEFAULT_PERMISSION_CLASSES': (
        'myapp.permissions.AllowOptionsAuthentication',
    )
}

All 22 comments

DRF Version: 3.7.3
Python 3.6.3

The temporary workaround mentioned above did not work for me. All requests were being authenticated regardless of whether they were PUT, POST, GET, OPTIONS, etc.

It worked on changing it to:

# myapp/permissions.py
from rest_framework.permissions import IsAuthenticated

class AllowOptionsAuthentication(IsAuthenticated):
    def has_permission(self, request, view):
        if request.method == 'OPTIONS':
            return True
        return request.user and request.user.is_authenticated

And in settings.py:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.TokenAuthentication',),
    'DEFAULT_PERMISSION_CLASSES': (
        'myapp.permissions.AllowOptionsAuthentication',
    )
}

I've run into this same problem as well. I agree with the proposed solution and thanks for the temporary workaround!

Not 100% convinced that "should authorize all OPTIONS requests by default" is exactly the behavior we want since there are plenty of developers using REST framework who use the OPTIONS requests for cases other than CORS preflight requests.

However I think we probably do want to have a blessed CORS option. I don't know if that means we should include https://github.com/ottoyiu/django-cors-headers/ directly, or if we should reference it more highly.

Does this issue resolve once using that package?

This package is not really the soluton, it just appends CORS headers but doesn't fix the fact OPTIONS returns HTTP401 if DRF permission is not specifically granted for all un-authenticated requests.

IMO it should be introduced the other way around, ie allowed by default with a breaking change notice.
This is the expected behaviour as per W3C specs, and mentioning a 3rd party in doc to patch a wrong implementation is not really a clean solution...

So although there are people using OPTIONS for non-CORS related things (I am one of them), there are potentially a lot more implicitly expecting things working out of the box, since CORS mechanisms are being enforced by more and more browsers. In fact these mechanisms potentially generate 99%+ of OPTIONS requests over the web.

Interesting conflict. I total understand the problem with having an implicit allow by default on the OPTIONS method. I'm naturally more biased towards a more secure solution and I suppose it depends on the dev's purpose for DRF. So a default deny seems more appropriate to me.

It is also possible that the W3C spec for allowing OPTIONS only applies in the context of CORS. Obviously, DRF has no way of knowing the context and should be an setting provided to the dev.

If CORS is intended, then allow all OPTIONS requests. CORS being unintended by default.

Actually I think we could consider a breaking change for a major version,with the current behaviour remaining available. Our existing OPTIONS behaviour has been in place since JSONP was what folks would use rather than CORS, so we’re probably due a refresh that removes the fairly ad-hoc set of default information we expose and instead concentrates solely on OPTIONS for CORS by default.

+1

@tomchristie do you have any idea when a major version update / fix would happen? I'm also hitting this issue.

@medakk 's workaround is perfect, meanwhile!

Milestoning this to make sure it gets proper consideration for 3.9

An alternate workaround is to handle this explicitly in the view. In the case where you have multiple permission_classes this is more DRY, since each of them would need this handling.

def check_permissions(self, request):
        if request.method == 'OPTIONS':
            return
        super(MyApiView, self).check_permissions(request)

Coming back to this having looked into it further - the https://github.com/OttoYiu/django-cors-headers package handles preflighted OPTIONS requests in middleware, and returns a response directly. It doesn't matter what REST framework does here because the request/response is intercepted before it hits REST framework at all.

Not obvious to me that we have an issue here.

Thanks. We aren't using django-cors-headers, but maybe we should be.
I still agree with the earlier comments that DRF shouldn't need an external package to handle W3C compliant CORS requests.

Yes there are bunch of way/3rd party packages to circumvent the issue.
But again, this ticket has been created because DRF is incompatible with the W3C CORS specs, and it should be fixed.

Nowadays, vast majority of OPTIONS requests performed to endpoints are for preflighting purposes, and it's probably deceptive for new DRF users to have to handle these issues manually because of DRF opinionated choices.

Nowadays, vast majority of OPTIONS requests performed to endpoints are for preflighting purposes, and it's probably deceptive for new DRF users to have to handle these issues manually because of DRF opinionated choices.

Just because CORS hijacked the OPTIONS method doesn't mean its security should be altered by default to accommodate. Especially if CORS support is properly implemented, it will work the way it needs to. Sorry, but I think developers that want this to be changed need to check their own opinions and not DRF maintainers.

Perhaps documentation enhancement to suggest that if CORS is needed, that a module to assist in the proper implementation be used instead of re-inventing the wheel.

Let's bring the level down.

Middleware is the sensible place to deal with CORS headers. We could build that into REST framework, but the existing package already has it covered really nicely.

The body that REST framework happens to return for OPTIONS requests is irrelevant here, as preflighted CORS requests ought to be intercepted by middleware anyways, and standard CORS requests can be of any HTTP method.

I'd be perfectly happy for REST framework to move to not serving up those response bodies by default, but that's slightly different to the issue of CORS support, and it's not so much that REST framework has opinionated behavior there, but rather that it has behavior that predates CORS becoming widely implemented.

It seems to me that the middleware solution alone is insufficient or at least requires some cooperation from DRF. If you look at the referenced code, you can see that global defaults are being used to supply the CORS headers. But how do I know globally what headers each view supports?

As such, I'd like to see a standard way for middleware to hook into these configuration options from viewsets/views: an allowed_cors_headers method for example.

I agree that this doesn't need to be handled by views, but it absolutely needs to respect the view's knowledge of what it can serve.

It might also be worthwhile to have a router-level override to apply to all views in the router.

Additional considerations:

  • varying based on origin
  • varying based on content-type

This is not my fav choice but the trend is going toward using a 3rd-party.

I think we can close this ticket once the documentation has been updated with proper informations for CORS users (such as: "Beware, DRF does not fulfil W3C CORS specs on OPTIONS requests. If you use OPTIONS for CORS don't forget to add a proper middleware otherwise your preflight requests might fail due to lack of authorization" etc)

The existing documentation https://www.django-rest-framework.org/topics/ajax-csrf-cors/#cors is perfectly reasonable, and dealing with CORS in middleware is the correct approach in any case.

But how do I know globally what headers each view supports?

You don't need to - you need to know what the CORS policy of the site is.

Happy to accept a pull request making the CORS docs more prominent, or including them somewhere else that's appropriate too. Other than that there's no actionable issue here.

My misunderstanding of the spec, oops.

In that case, support would be better included in the django sites contrib
package, not drf.

Le mar. 19 févr. 2019 10:22 a.m., Tom Christie notifications@github.com a
écrit :

The existing documentation
https://www.django-rest-framework.org/topics/ajax-csrf-cors/#cors is
perfectly reasonable, and dealing with CORS in middleware is the correct
approach in any case.

But how do I know globally what headers each view supports?

You don't need to - you need to know what the CORS policy of the site is.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/encode/django-rest-framework/issues/5616#issuecomment-465146969,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AFhtlM6bG-Bs2CvoO972pIfwvxLHbzAxks5vPAjAgaJpZM4Qlvkn
.

Was this page helpful?
0 / 5 - 0 ratings