jazzband / django-oauth-toolkit

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

Introspection endpoint doesn't require "introspection" scope when used in client-credentials flow #1451

Open makulatur opened 1 month ago

makulatur commented 1 month ago

Hi. I am not entirely sure if this is a bug or I am misunderstanding something about OAuth2, I'd like to apologize in advance if it's the latter.

Describe the bug

I followed the tutorial to create an access token and tried using the IntrospectTokenView with that token. This view is a ClientProtectedScopedResourceView that includes required_scopes = ["introspection"]. My understanding of the documentation around these made me believe that if I use a bearer token without the appropriate scope, my request will fail. To my surprise, the call succeeds regardless of the scopes in the token I use.

To Reproduce

Expected behavior Even when using the client-credentials flow, I expect a token that doesn't have the introspection scope to be rejected when calling an endpoint declaring required_scopes = ["introspection"].

Version 2.4.0

Additional context I googled around and found this Auth0 thread for a similar question. There it is suggested that scopes in a token are not considered at all if using client-credentials. However, in that example the client itself has associated scopes. This isn't the case for the default setup of DOT, as far as I can see.

As it stands, when using client-credentials, any token can be inspected by any client. If this is the desired behavior that's fine with me. If not, however, you might consider a small change to IntrospectTokenView.get_token_response that uses cls.required_scopes:

@classmethod
def get_token_response(cls, token_value=None):
    try:
        ...
    except ObjectDoesNotExist:
        return JsonResponse({"active": False}, status=200)
    else:
        if token.is_valid(cls.required_scopes):
            ...
n2ygk commented 1 month ago

I think you are right that the introspection client (what DOT calls an application) should be somehow constrained in whether it is authorized to introspect. However I don't think using token.is_valid(cls.required_scopes) is correct as that is checking the token that is being introspected rather than the token of the introspector. Also only a Authorization: Bearer <token> would have an access token with a scope. When Authorization: Basic <base64(user:pass)> is used, there is no place for a scope as there is no Access Token and technically this is not a client credentials flow as implemented, so it seems any application with client_credentials grant_type can be used for introspection. Hopefully most actual users have authorization_code grant_type.

Introspection Basic auth was added pretty recently (2019 in #725:-) so I suspect the earlier Bearer-only version of introspection worked correctly [1]. When support for Basic was added it broke this checking. Prior to that PR, introspection could only be done with a Bearer access token.

I expect a PR to fix this would somehow have to constrain applications with the client_credentials as discussed in #709

BTW, the part of the discussion in #709 about constraining which applications a particular introspection application can introspect might be a little too much. In my experience with a commercial product, an introspector can introspect any access token. And, with OIDC, anyone with a given Access Token can essentially introspect it via the Userinfo endpoint.

However, in the commercial product, the introspector is specially identified with a "fake" grant type of ACCESS_TOKEN_VALIDATION. In other words it is flagged as somehow special and allowed to invoke the introspection endpoint rather than just allowing anyone with client_credentials to do so. I believe this historically predates the creation of a standardized introspection endpoint which came out 3 years after OAuth2

So to summarize, using Authorization: Basic is technically not an OAuth2 client credentials flow at all and conflating that was probably a mistake. If it were a real client credentials flow, the introspector client would have made anAuthorization: Basic client credentials request to the AS's token endpoint, been given an Access Token, and then presented that Access Token in an Authorization: Bearer header.

[1] Further, given that any client can request any scope, I think that means that any client's access token (Bearer) can be an introspector!

n2ygk commented 1 month ago

Confirmed in testing that anyone can request introspection scope (e.g. using authorization code flow) and then use the Bearer token to introspect any access token.

Not a huge concern since the access token needs to be protected no matter what as possession means access to whatever resources allow it.

makulatur commented 1 month ago

Hey, thanks for the very quick response time.

However I don't think using token.is_valid(cls.required_scopes) is correct as that is checking the token that is being introspected rather than the token of the introspector.

facepalm - of course.

When Authorization: Basic <base64(user:pass)> is used, there is no place for a scope as there is no Access Token and technically this is not a client credentials flow as implemented, so it seems any application with client_credentials grant_type can be used for introspection. Hopefully most actual users have authorization_code grant_type.

Sorry, I am still new to this thing. According to this article on the OAuth website the "client needs to authenticate themselves for this request. Typically the service will allow either additional request parameters client_id and client_secret, or accept the client ID and secret in the HTTP Basic auth header." This reads to me as if it's acceptable to use auth basic in the client credentials flow. Maybe I'm missing something.

I expect a PR to fix this would somehow have to constrain applications with the client_credentials as discussed in #709

I dug around a bit and it seems to me like the OAuth2Validator already has most of the tooling to retrieve the client from a request with basic auth, but the functions are not yet refactored in a way that they are easy to use inside the IntrospectTokenView.

So to summarize, using Authorization: Basic is technically not an OAuth2 client credentials flow at all and conflating that was probably a mistake. If it were a real client credentials flow, the introspector client would have made anAuthorization: Basic client credentials request to the AS's token endpoint, been given an Access Token, and then presented that Access Token in an Authorization: Bearer header.

Thanks for the summary, this seems logical to me (although I still wonder about the article above).