jazzband / django-oauth-toolkit

OAuth2 goodies for the Djangonauts!
https://django-oauth-toolkit.readthedocs.io
Other
3.13k stars 792 forks source link

How exactly is the @protected_resource decorator supposed to be used with scope? #1294

Closed Chappie74 closed 1 year ago

Chappie74 commented 1 year ago

For my project I would have added 3 additional scopes in the settings.py file.

oauth2_settings.DEFAULTS['SCOPES'] = oauth2_settings.DEFAULTS['SCOPES'] | {
    'application': 'Application scope',
    'customer': 'Customer scope',
    'merchant': 'Merchant scope'
}
oauth2_settings.DEFAULTS['DEFAULT_SCOPES'] = ['read', 'write']

For my use case I have separate views that issues tokens with separate scope. One login request might look like this.

{
    "access_token": "L4yez9nlfreW5ZnsvmTi9ATbfLvPVN",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "read write customer",
    "refresh_token": "osfNZGu0ZQKNoLKj0SVuhfSqbl6PZl"
}

or

{
    "access_token": "L4yez9nlfreW5ZnsvmTi9ATbfLvPVN",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "read write merchant",
    "refresh_token": "osfNZGu0ZQKNoLKj0SVuhfSqbl6PZl"
}

I then have views that are protected like this

@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
@protected_resource(scopes=['customer'])
def test_auth_view(request):

or like this

@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
@protected_resource(scopes=['merchant'])
def test_auth_view(request):

This works as intended. Tokens without the respective scope return 403. The problem I am having is when I want to protect a resource both the customer and merchant can access, which I am doing like so.

@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
@protected_resource(scopes=['customer', 'merchant'])
def test_auth_view(request):

With the above, both of them return a 403. I'm expected that if the scope on the token has either one it should work, however that is not the case.

Is this how it is supposed to work? Can someone provide some clarity on this please? Thanks in advance.

Chappie74 commented 1 year ago

I forgot to add I would have tested this

@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
@protected_resource(scopes=['read', 'write'])
def test_auth_view(request):

and both customer and merchant tokens work in this scenario. I'm guessing since both contain the ' read write` scope

Chappie74 commented 1 year ago

So I did some digging around and found in the code that there are two possible ways of handling the scope access.

found here image

It seems this package chose to implement the former (1), where all the scopes are checked by checking if the scope of the requested resource is a subset of the scopes the token has.

The approach I expected for my use case was the latter, where it would check if token has at least one of the specified resource scopes, which would then require an intersection check rather than subset.

https://github.com/jazzband/django-oauth-toolkit/blob/f730b645951e59d0f9638160748e1265e0b41c76/oauth2_provider/models.py#L409C24-L409C24

Chappie74 commented 1 year ago

To work with this approach I'll have to create a separate scope which would work as a combination scope and attach it to both merchant and customer tokens. Then use that single scope check for my view.