Skip to content

Conversation

@YuriiMotov
Copy link
Member

@YuriiMotov YuriiMotov commented Jun 11, 2025

Warning

This description is partially outdated after changes described in this comment.

Description

This PR is an attempt to finally solve the issue with security tools returning error responses with status code 403 instead of 401 when credentials are not provided.

Breaking changes and workaround

These changes can break projects that rely on old behavior.
In order to mitigate this, the not_authenticated_status_code is introduced. If set to 403, it will make it work the same way as it was before changes (return 403 status code).
This option should be treated as a temporary workaround to give developers more time to update Clients to follow the new behavior.

Changes and reasoning

APIKeyQuery, APIKeyHeader, APIKeyCookie

  • Standard:
    • These schemes are not covered by standards, but developers usually follow the same rules as for other standards
  • Actions:
    • The default status code for not providing API key was changed from 403 to 401.
    • Temporary not_authenticated_status_code parameter can be used to revert this behavior back to returning 403 error code without sending WWW-Authenticate.
  • Notes:
    • It’s considered to be a good practice to include in WWW-Authenticate information needed to understand how the key is supposed to be passed. I implemented default format (WWW-Authenticate: ApiKey in="...", name="..." ), but it’s possible to override the template for WWW-Authenticate by subclassing and defining the format_www_authenticate_header_value method

HTTP Basic

HTTP Digest

  • Standard:
  • Actions:
    • The default status code for not providing the authorization parameter was changed from 403 to 401.
    • WWW-Authenticate is just a stub for now (just WWW-Authenticate: Digest) (see notes)
    • Temporary not_authenticated_status_code parameter can be used to revert this behavior back to returning 403 error code without sending WWW-Authenticate.
  • Notes:
    • Since the current HTTPDigest implementation is just a stub, we can’t follow standards (we don’t generate nonce's, don’t have realm, …). I suggest we just change the error status code and add a stub for WWW-Authenticate (just WWW-Authenticate: Digest). For now HTTPDigest can’t be used as it is, so, this is not a problem.
    • We can later add full implementation of Digest scheme. There have been made several attempts to implement it (Update HTTPDigest to match RFC 7616 #9825, 🔒 Match HTTP Digest specs from RFC 7616 #3071)
    • Should we add a note that HTTPDigest is just a stub?

HTTP Bearer, OAuth2 schemes, OIDC

  • Standard:
  • Actions:
    • For OAuth2PasswordBearer and OAuth2AuthorizationCodeBearer: not needed.
      • They already return a 401 error code. Implementation is probably not 100% correct (see notes), but considering nobody argued, I think we can leave it as it is for now.
    • For HTTPBearer and OpenIdConnect:
      • The default status code for not providing the authorization parameter was changed from 403 to 401. The suggested implementation will be in line with the current implementations of OAuth2PasswordBearer and OAuth2AuthorizationCodeBearer.
      • Temporary not_authenticated_status_code parameter added to HTTPBearer can be used to revert this behavior back to returning 403 error code without sending WWW-Authenticate.
  • Notes:
    • It’s recommended to return 400 error response if the parameter is missed or of an unsupported type, but this is not a strict requirement (word SHOULD is used). I suggest we ignore this and follow the approach that is consistent with other schemes.
    • The format of WWW-Authenticate is not clearly described: It’s said that the value "Bearer" MUST be followed by one or more auth-param values. At the same time, all auth-param attributes are optional. In examples they always add realm. Since we don’t have realm, I suggest we just skip it and send just WWW-Authenticate: Bearer
    • We can later improve the WWW-Authenticate format by adding realm and scope

Links

@YuriiMotov YuriiMotov added the bug Something isn't working label Jun 11, 2025
@tiangolo tiangolo changed the title 🐛 Use 401 status code in security classes when credentials missed 🐛 Use 401 status code in security classes when credentials are missing Jun 17, 2025
@bjmc
Copy link

bjmc commented Jul 15, 2025

Thank you for opening this @YuriiMotov - we've just been surprised by an API reporting 403 for a missing token when we we'd be expecting to see 401 and trigger a login flow on the client side.

We'd be glad to have this this fix merged. 🙏

(fwiw, I agree with the decision to follow other implementations and return 401 not 400)

@stg609
Copy link

stg609 commented Aug 6, 2025

Any progress?

@Cito
Copy link

Cito commented Aug 6, 2025

We have been also noticing this and are waiting for a fix for a long time.

@svlandeg svlandeg linked an issue Aug 27, 2025 that may be closed by this pull request
@cleder
Copy link

cleder commented Aug 28, 2025

I ran into this too when using schemathesis testing a route protected by HTTPBearer

  | schemathesis.core.failures.FailureGroup: Schemathesis found 2 distinct failures
  | 
  | - Missing header not rejected
  | 
  |     Missing header not rejected (got 403, expected 401)

Copy link

@cleder cleder left a comment

Choose a reason for hiding this comment

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

The if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: checks look a bit clunky, but given this is of a temporary nature it is probably fine

@github-actions

This comment was marked as resolved.

@github-actions github-actions bot added the conflicts Automatically generated when a PR has a merge conflict label Sep 20, 2025
@github-actions

This comment was marked as resolved.

@github-actions github-actions bot removed the conflicts Automatically generated when a PR has a merge conflict label Oct 24, 2025
Copy link
Member

@tiangolo tiangolo left a comment

Choose a reason for hiding this comment

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

Awesome, thank you @YuriiMotov! 🙌

And thank you for all the investigation and notes with all the research. 🤓 ☕


I made the following updates:

  • Simplify the WWW-Authenticate challenge for API key, as this is a custom challenge we are defining, I didn't want to also define custom parameters, but leave the simplest form.
  • Refactor the implementation to not have an extra parameter for defining the status code, instead, put the creation of the error in a method of the class, this way if someone wants to use the old behavior, they can create a subclass and override only that method.
  • Refactor the HTTP classes to all return the same message for errors ("Not authenticated"), instead of some times returning "Invalid authentication credentials", this simplifies the code a bit, and in particular makes it simpler to explain to users how to use the old behavior.
  • Simplify and refactor a bit the implementation.
  • Add new docs for how to use the old behavior by subclassing, with new source examples and tests for them.
  • Add notes and warnings about stub classes, as you suggested.

I also noted that the spec says that the realm is now optional, but only in a footer note (https://datatracker.ietf.org/doc/html/rfc7235#appendix-A). 😂 Anyway, we are good with that now.


This will be available in FastAPI 0.122.0, released in the next few hours. 🎉

@tiangolo tiangolo merged commit 51ad909 into fastapi:master Nov 24, 2025
45 checks passed
nilslindemann added a commit to nilslindemann/fastapi that referenced this pull request Dec 1, 2025
YuriiMotov added a commit that referenced this pull request Dec 2, 2025
* Sync with #14217

* Sync with #14359

* Sync with #13786

* Sync with #14070

* Sync with #14120

* Sync with #14211

* Sync with #14405

* "to deploy" -> "deployen"

The LLM used that translation a lot ithis convinced me that "deployen" it is the better word. "bereitstellen" (or "ausliefern") is still used for "to serve".

---------

Co-authored-by: Motov Yurii <[email protected]>
Co-authored-by: Yurii Motov <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTPBearer security scheme is returning 403 instead or 401

6 participants