Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Return 401 for API request with Invalid Authorization #3663

Closed

Conversation

carlosm22700
Copy link
Contributor

@carlosm22700 carlosm22700 commented Jan 15, 2024

Fixes

Related to #3626 by @sarayourfriend

Description

This PR implements middleware 'StrictAuthMiddleware' as a solution to issue #3626. The goal is to enforce a consistent response across the API for requests with invalid Authorization headers by returning a 401 Unauthorized status. If the request does not contain a header, it proceeds as anonymous.

During the development and testing phases, I tested different endpoints to see how they would handle various scenarios, including valid and invalid Authorization headers (tokens), as well as requests lacking these headers. By implementing this middleware, my goal was to avoid the need for repetitive modifications across individual views. It also avoids using individual permissions for specific resources and uses a wrapper instead.

However, I ran into some challenges with authenticated requests also returning 401 and the auth tests not passing when running just/api test. I would very much appreciate any feedback on my approach to this!

Testing

  • Invalid Token:
    curl -H "Authorization: Bearer invalid_token" -I "http://localhost:50280/v1/images/"
    curl -H "Authorization: Bearer invalid_token" -I "http://localhost:50280/v1/checkrates/"
    Expect a 401 Unauthorized response.

  • No Authorization Header (Anonymous request):
    curl -I "http://localhost:50280/v1/images/"
    Expect a 200 OK response, treating the request as anonymous.

Checklist

  • My pull request has a descriptive title.
  • Targets the default branch of the repository (main).
  • My commit messages follow best practices.
  • My code adheres to the established code style of the repository.
  • Tests for the changes have been added/updated (if applicable).
  • Documentation has been added/updated (if applicable).
  • Project runs locally without errors.
  • (If applicable) Ran the DAG documentation generator.wow too mu

Developer Certificate of Origin

Developer Certificate of Origin
Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
1 Letterman Drive
Suite D4700
San Francisco, CA, 94129

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.


Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.

@carlosm22700 carlosm22700 requested a review from a team as a code owner January 15, 2024 02:13
@openverse-bot openverse-bot added the 🚦 status: awaiting triage Has not been triaged & therefore, not ready for work label Jan 15, 2024
@carlosm22700 carlosm22700 reopened this Jan 15, 2024
@carlosm22700 carlosm22700 marked this pull request as draft January 15, 2024 06:22
@openverse-bot openverse-bot added 🟨 priority: medium Not blocking but should be addressed soon ✨ goal: improvement Improvement to an existing user-facing feature 🕹 aspect: interface Concerns end-users' experience with the software 🏷 status: label work required Needs proper labelling before it can be worked on labels Jan 15, 2024
@dhruvkb dhruvkb added 🧱 stack: api Related to the Django API and removed 🏷 status: label work required Needs proper labelling before it can be worked on 🚦 status: awaiting triage Has not been triaged & therefore, not ready for work labels Jan 15, 2024
@carlosm22700 carlosm22700 marked this pull request as ready for review January 15, 2024 21:03
@carlosm22700
Copy link
Contributor Author

Thanks for the help! @dhruvkb

Copy link
Member

@dhruvkb dhruvkb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem @carlosm22700. Thanks for the contribution!

A more elegant approach to this would be to use permission_classes (I believe L176 was a typo).

+ from oauth2_provider.contrib.rest_framework import TokenHasScope

@extend_schema(tags=["auth"])
  class CheckRates(APIView):
      throttle_classes = (OnePerSecond,)
+     permission_classes = (TokenHasScope,)
+     required_scopes = ('read',)

This automatically raises 401 when the token is not provided, invalid or expired (and also provides the reason in the WWW-Authenticated header as recommended by DRF).

E.g.

WWW-Authenticate: Bearer realm="api",error="invalid_token",error_description="The access token has expired."

or

WWW-Authenticate: Bearer realm="api",error="invalid_token",error_description="The access token is invalid."

@carlosm22700 carlosm22700 marked this pull request as draft January 18, 2024 04:24
@krysal
Copy link
Member

krysal commented Feb 8, 2024

@carlosm22700 Do you think you could resume this work in the following days? We would appreciate it if you let us know in any case.

@carlosm22700 carlosm22700 marked this pull request as ready for review February 9, 2024 03:47
@carlosm22700
Copy link
Contributor Author

Sorry for the delay, just wrapped up school for the semester!

Thanks for the review @dhruvkb @krysal. Would either of you mind taking another look to make sure everything looks good? I made sure to implement the changes recommended.

Copy link
Member

@dhruvkb dhruvkb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution @carlosm22700 and for implementing the changes from the review. LGTM!

Copy link
Contributor

@stacimc stacimc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution, @carlosm22700! Would you mind updating the PR description to include testing instructions? It looks like the description might also be slightly out of date.

If I'm reading the linked issue correctly, I think we want to return a 401 when an invalid authorization header is sent to any API endpoint. The code changes here only address the CheckRates view. For example, when testing I would expect that if I try:

> curl -H "Authorization: Bearer faketoken123" \
  -I "http://localhost:50280/v1/images/"

I should get a 401. Instead it continues to silently downgrade it to an unauthorized request (returns successfully until the unauthorized rate limit is exceeded, then 429s).

I'm also still getting a 403 when I call the CheckRates endpoint with a bad token, too 🤔 Though I may be misunderstanding the intended behavior there. @dhruvkb can you weigh in if I'm off base?

@sarayourfriend
Copy link
Contributor

You're right about the intended scope, Staci. Any API endpoint sent with invalid authorisation header should raise a 401 to indicate the credentials are invalid, without downgrading the request to anonymous. What this means is:

  1. Any request with authorization header should have this check happen
  2. Any request without it should bypass the check and be treated as anonymous

To accomplish that, we need a wrapper around the authentication middleware, not a permissions class. The permissions class would be the right choice if we were trying to guard access to particular resources based on the scope of the authenticated request. That's not the goal, though.

If it helps clarify the intention and goal, we could use 400 instead of 401 for these, with a message indicating the authorization header credentials were invalid.

@carlosm22700 carlosm22700 marked this pull request as draft February 18, 2024 05:26
@carlosm22700 carlosm22700 marked this pull request as ready for review February 18, 2024 14:12
@carlosm22700
Copy link
Contributor Author

carlosm22700 commented Feb 18, 2024

Thank you all for your patience! If anyone could take a moment to review the updated PR or provide feedback, I'd greatly appreciate it! Your guidance means a lot while i continue to learn and contribute :)

@openverse-bot
Copy link
Collaborator

Based on the medium urgency of this PR, the following reviewers are being gently reminded to review this PR:

@sarayourfriend
@stacimc
This reminder is being automatically generated due to the urgency configuration.

Excluding weekend1 days, this PR was ready for review 4 day(s) ago. PRs labelled with medium urgency are expected to be reviewed within 4 weekday(s)2.

@carlosm22700, if this PR is not ready for a review, please draft it to prevent reviewers from getting further unnecessary pings.

Footnotes

  1. Specifically, Saturday and Sunday.

  2. For the purpose of these reminders we treat Monday - Friday as weekdays. Please note that the operation that generates these reminders runs at midnight UTC on Monday - Friday. This means that depending on your timezone, you may be pinged outside of the expected range.

Copy link
Contributor

@sarayourfriend sarayourfriend left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are on the right track, @carlosm22700, but there are two requested changes I've left. This is a bit of a tricky issue, and to understand the approach I've suggested, I had to dig through the code for django-oauth-toolkit to understand how it handles errors.

With these requested changes, I'd also like to request new unit tests to cover this, added in test_auth.py. The test should make a request with a bad token, and confirm that the response is a 401.

Comment on lines +25 to +26
"oauth2_provider.middleware.OAuth2TokenMiddleware",
"api.middleware.strict_auth_middleware.strict_auth_middleware",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging middleware must not be removed. The OAuth2 middleware is conditionally added in the settings/oauth.py module (

middleware = "oauth2_provider.middleware.OAuth2TokenMiddleware"
if middleware not in MIDDLEWARE:
MIDDLEWARE.append(middleware)
). To ensure the new middleware occurs after the OAuth2 middleware, we should append it to the MIDDLEWARE list when we append the OAuth2 one.

Suggested change
"oauth2_provider.middleware.OAuth2TokenMiddleware",
"api.middleware.strict_auth_middleware.strict_auth_middleware",
"api.middleware.response_headers_middleware.response_headers_middleware",

To fully resolve this issue, please update api/conf/settings/oauth.py to append strict_auth_middleware when we append the OAuth2 token middleware (in the same conditional).

Comment on lines +171 to +172
permission_classes = (TokenHasScope,)
required_scopes = ["read"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed and the TODO later in the file added back. The precise condition is already handled where the TODO was, and we should address this in a separate PR to avoid complicating this one.

Comment on lines +7 to +13
# Extract the Authorization header from the request
auth_header = request.headers.get('Authorization', None)

# If the Authorization header is present
if auth_header:
# Check if the user is anonymous or authentication failed
if request.user.is_anonymous or request.auth is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of how the OAuth2 DRF authenticator works, we can replace all of these checks with a check against oauth2_error on the request object:

https://github.com/jazzband/django-oauth-toolkit/blob/master/oauth2_provider/contrib/rest_framework/authentication.py#L30

The request attribute comes from this method, where you can see various error messages are turned as part of it:

https://github.com/jazzband/django-oauth-toolkit/blob/master/oauth2_provider/oauth2_validators.py#L219-L249

We haven't defined set scopes anywhere, so we don't need to worry about insufficient_scope, and can just assume the reason is for an invalid token. It'd be nice to return the error description as well, so users understand why it's invalid.

Something along these lines is where I would start:

oauth2_error = getattr(request, "oauth2_error", None)

if oauth2_error is None:
    return get_response(request)

raise AuthenticationFailed(
    detail=oauth2_error.get("error_description", None),
    code=oauth2_error.get("error", None),
)

@sarayourfriend sarayourfriend marked this pull request as draft February 23, 2024 01:05
@carlosm22700
Copy link
Contributor Author

working on this today, but I've already started implementing the changes! I've been having some issues with booting up my api using both just/api up and just/api init, so I was hoping to also address that on my end.

@sarayourfriend
Copy link
Contributor

@carlosm22700 have you moved this PR elsewhere/do you plan to continue working on it? Just wanted to clarify since you closed the PR, just let us know 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🕹 aspect: interface Concerns end-users' experience with the software ✨ goal: improvement Improvement to an existing user-facing feature 🟨 priority: medium Not blocking but should be addressed soon 🧱 stack: api Related to the Django API
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

None yet

6 participants