failed CONTEXT UNSUBMITTED agent_error · 16 tool calls · 280 s · fastapi/fastapi
🐛 Use `401` status code in security classes when credentials are missing
## Warning
This description is partially outdated after changes described in [this comment](https://github.com/fastapi/fastapi/pull/13786#pullrequestreview-3501695067).
## 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
* **Standard:**
* https://datatracker.ietf.org/doc/html/rfc7617
* **Actions:**
* No needed. This scheme already acts according to the standard in terms of returning 401 status code with `WWW-Authenticate` header on a lack of credentials
* **Notes:**
* `realm` is required according to the RFC, but optional in the current implementation. Fixing this would introduce breaking changes. Considering this is not a problem for people who want to follow the standard, I suggest we leave it as it is.
### HTTP Digest
* **Standard:**
* https://datatracker.ietf.org/doc/html/rfc7616
* **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 (#9825, #3071)
* Should we add a note that `HTTPDigest` is just a stub?
### HTTP Bearer, OAuth2 schemes, OIDC
* **Standard:**
* https://datatracker.ietf.org/doc/html/rfc6750
* **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](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1) 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
* https://github.com/fastapi/fastapi/issues/10177
* https://github.com/fastapi/fastapi/pull/2120
* https://github.com/fastapi/fastapi/pull/5332
* https://github.com/fastapi/fastapi/pull/12507
* https://github.com/fastapi/fastapi/pull/5310
* https://github.com/fastapi/fastapi/pull/13729
* https://github.com/fastapi/fastapi/pull/13514
* https://github.com/fastapi/fastapi/pull/13326
| # | Tool | Arguments | Result |
|---|---|---|---|
| 1 | run_command | {
"command": "grep -rn \"APIKeyQuery\" fastapi/security | head -20"
} | {"status": "ok", "stdout": "fastapi/security/__init__.py:3:from .api_key import APIKeyQuery as APIKeyQuery\nfastapi/security/api_key.py:24:class APIKeyQuery(APIfull{"status": "ok", "stdout": "fastapi/security/__init__.py:3:from .api_key import APIKeyQuery as APIKeyQuery\nfastapi/security/api_key.py:24:class APIKeyQuery(APIKeyBase):\nfastapi/security/api_key.py:43: from fastapi.security import APIKeyQuery\nfastapi/security/api_key.py:47: query_scheme = APIKeyQuery(name=\"api_key\")\nfastapi/security/api_key.py:87: By default, if the query parameter is not provided, `APIKeyQuery` will\n", "stderr": "", "exit_code": 0} |
| 2 | run_command | {
"command": "grep -rn \"APIKeyBase\" fastapi/security/api_key.py"
} | {"status": "ok", "stdout": "fastapi/security/api_key.py:12:class APIKeyBase(SecurityBase):\nfastapi/security/api_key.py:24:class APIKeyQuery(APIKeyBase):\nfastafull{"status": "ok", "stdout": "fastapi/security/api_key.py:12:class APIKeyBase(SecurityBase):\nfastapi/security/api_key.py:24:class APIKeyQuery(APIKeyBase):\nfastapi/security/api_key.py:116:class APIKeyHeader(APIKeyBase):\nfastapi/security/api_key.py:204:class APIKeyCookie(APIKeyBase):\n", "stderr": "", "exit_code": 0} |
| 3-4 ×2 | run_command | {
"command": "cat fastapi/security/api_key.py"
} | {"status": "ok", "stdout": "from typing import Optional\n\nfrom annotated_doc import Doc\nfrom fastapi.openapi.models import APIKey, APIKeyIn\nfrom fastapi.secufull{"status": "ok", "stdout": "from typing import Optional\n\nfrom annotated_doc import Doc\nfrom fastapi.openapi.models import APIKey, APIKeyIn\nfrom fastapi.security.base import SecurityBase\nfrom starlette.exceptions import HTTPException\nfrom starlette.requests import Request\nfrom starlette.status import HTTP_403_FORBIDDEN\nfrom typing_extensions import Annotated\n\n\nclass APIKeyBase(SecurityBase):\n @staticmethod\n def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:\n if not api_key:\n if auto_error:\n raise HTTPException(\n status_code=HTTP_403_FORBIDDEN, detail=\"Not authenticated\"\n )\n return None\n return api_key\n\n\nclass APIKeyQuery(APIKeyBase):\n \"\"\"\n API key authentication using a query parameter.\n\n This defines the name of the query parameter that should be provided in the request\n with the API key and integrates that into the OpenAPI documentation. It extracts\n the key value sent in the query parameter automatically and provides it as the\n dependency result. But it doesn't define how to send that API key to the client.\n\n ## Usage\n\n Create an instance object and use that object as the dependency in `Depends()`.\n\n The dependency result will be a string containing the key value.\n\n ## Example\n\n ```python\n from fastapi import Depends, FastAPI\n from fastapi.security import APIKeyQuery\n\n app = FastAPI()\n\n query_scheme = APIKeyQuery(name=\"api_key\")\n\n\n @app.get(\"/items/\")\n async def read_items(api_key: str = Depends(query_scheme)):\n return {\"api_key\": api_key}\n ```\n \"\"\"\n\n def __init__(\n self,\n *,\n name: Annotated[\n str,\n Doc(\"Query parameter name.\"),\n ],\n scheme_name: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Securi |
| 5 | run_command | {
"command": "tail -n +200 fastapi/security/api_key.py"
} | {"status": "ok", "stdout": " api_key = request.headers.get(self.model.name)\n return self.check_api_key(api_key, self.auto_error)\n\n\nclass APIKeyCookie(APIKeyfull{"status": "ok", "stdout": " api_key = request.headers.get(self.model.name)\n return self.check_api_key(api_key, self.auto_error)\n\n\nclass APIKeyCookie(APIKeyBase):\n \"\"\"\n API key authentication using a cookie.\n\n This defines the name of the cookie that should be provided in the request with\n the API key and integrates that into the OpenAPI documentation. It extracts\n the key value sent in the cookie automatically and provides it as the dependency\n result. But it doesn't define how to set that cookie.\n\n ## Usage\n\n Create an instance object and use that object as the dependency in `Depends()`.\n\n The dependency result will be a string containing the key value.\n\n ## Example\n\n ```python\n from fastapi import Depends, FastAPI\n from fastapi.security import APIKeyCookie\n\n app = FastAPI()\n\n cookie_scheme = APIKeyCookie(name=\"session\")\n\n\n @app.get(\"/items/\")\n async def read_items(session: str = Depends(cookie_scheme)):\n return {\"session\": session}\n ```\n \"\"\"\n\n def __init__(\n self,\n *,\n name: Annotated[str, Doc(\"Cookie name.\")],\n scheme_name: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme name.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n description: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme description.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n auto_error: Annotated[\n bool,\n Doc(\n \"\"\"\n By default, if the cookie is not provided, `APIKeyCookie` will\n automatically cancel the request a |
| 6 | run_command | {
"command": "ls fastapi/security/"
} | {"status": "ok", "stdout": "__init__.py\napi_key.py\nbase.py\nhttp.py\noauth2.py\nopen_id_connect_url.py\nutils.py\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "__init__.py\napi_key.py\nbase.py\nhttp.py\noauth2.py\nopen_id_connect_url.py\nutils.py\n", "stderr": "", "exit_code": 0} |
| 7 | run_command | {
"command": "cat fastapi/security/http.py"
} | {"status": "ok", "stdout": "import binascii\nfrom base64 import b64decode\nfrom typing import Optional\n\nfrom annotated_doc import Doc\nfrom fastapi.exceptionsfull{"status": "ok", "stdout": "import binascii\nfrom base64 import b64decode\nfrom typing import Optional\n\nfrom annotated_doc import Doc\nfrom fastapi.exceptions import HTTPException\nfrom fastapi.openapi.models import HTTPBase as HTTPBaseModel\nfrom fastapi.openapi.models import HTTPBearer as HTTPBearerModel\nfrom fastapi.security.base import SecurityBase\nfrom fastapi.security.utils import get_authorization_scheme_param\nfrom pydantic import BaseModel\nfrom starlette.requests import Request\nfrom starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN\nfrom typing_extensions import Annotated\n\n\nclass HTTPBasicCredentials(BaseModel):\n \"\"\"\n The HTTP Basic credentials given as the result of using `HTTPBasic` in a\n dependency.\n\n Read more about it in the\n [FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/).\n \"\"\"\n\n username: Annotated[str, Doc(\"The HTTP Basic username.\")]\n password: Annotated[str, Doc(\"The HTTP Basic password.\")]\n\n\nclass HTTPAuthorizationCredentials(BaseModel):\n \"\"\"\n The HTTP authorization credentials in the result of using `HTTPBearer` or\n `HTTPDigest` in a dependency.\n\n The HTTP authorization header value is split by the first space.\n\n The first part is the `scheme`, the second part is the `credentials`.\n\n For example, in an HTTP Bearer token scheme, the client will send a header\n like:\n\n ```\n Authorization: Bearer deadbeef12346\n ```\n\n In this case:\n\n * `scheme` will have the value `\"Bearer\"`\n * `credentials` will have the value `\"deadbeef12346\"`\n \"\"\"\n\n scheme: Annotated[\n str,\n Doc(\n \"\"\"\n The HTTP authorization scheme extracted from the header value.\n \"\"\"\n ),\n ]\n credentials: Annotated[\n str,\n Doc(\n \"\"\"\n The HTTP authorization credentials extracted from t |
| 8 | run_command | {
"command": "tail -n +200 fastapi/security/http.py"
} | {"status": "ok", "stdout": " status_code=HTTP_401_UNAUTHORIZED,\n detail=\"Not authenticated\",\n headers=unauthorized_headers,\n )\n else:\n return None\n invafull{"status": "ok", "stdout": " status_code=HTTP_401_UNAUTHORIZED,\n detail=\"Not authenticated\",\n headers=unauthorized_headers,\n )\n else:\n return None\n invalid_user_credentials_exc = HTTPException(\n status_code=HTTP_401_UNAUTHORIZED,\n detail=\"Invalid authentication credentials\",\n headers=unauthorized_headers,\n )\n try:\n data = b64decode(param).decode(\"ascii\")\n except (ValueError, UnicodeDecodeError, binascii.Error):\n raise invalid_user_credentials_exc # noqa: B904\n username, separator, password = data.partition(\":\")\n if not separator:\n raise invalid_user_credentials_exc\n return HTTPBasicCredentials(username=username, password=password)\n\n\nclass HTTPBearer(HTTPBase):\n \"\"\"\n HTTP Bearer token authentication.\n\n ## Usage\n\n Create an instance object and use that object as the dependency in `Depends()`.\n\n The dependency result will be an `HTTPAuthorizationCredentials` object containing\n the `scheme` and the `credentials`.\n\n ## Example\n\n ```python\n from typing import Annotated\n\n from fastapi import Depends, FastAPI\n from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer\n\n app = FastAPI()\n\n security = HTTPBearer()\n\n\n @app.get(\"/users/me\")\n def read_current_user(\n credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]\n ):\n return {\"scheme\": credentials.scheme, \"credentials\": credentials.credentials}\n ```\n \"\"\"\n\n def __init__(\n self,\n *,\n bearerFormat: Annotated[Optional[str], Doc(\"Bearer token format.\")] = None,\n scheme_name: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme name.\n\n It w |
| 9 | run_command | {
"command": "tail -n +350 fastapi/security/http.py"
} | {"status": "ok", "stdout": " def read_current_user(\n credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]\n ):\n return {\"scheme\": credentfull{"status": "ok", "stdout": " def read_current_user(\n credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]\n ):\n return {\"scheme\": credentials.scheme, \"credentials\": credentials.credentials}\n ```\n \"\"\"\n\n def __init__(\n self,\n *,\n scheme_name: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme name.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n description: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme description.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n auto_error: Annotated[\n bool,\n Doc(\n \"\"\"\n By default, if the HTTP Digest is not provided, `HTTPDigest` will\n automatically cancel the request and send the client an error.\n\n If `auto_error` is set to `False`, when the HTTP Digest is not\n available, instead of erroring out, the dependency result will\n be `None`.\n\n This is useful when you want to have optional authentication.\n\n It is also useful when you want to have authentication that can be\n provided in one of multiple optional ways (for example, in HTTP\n Digest or in a cookie).\n \"\"\"\n ),\n ] = True,\n ):\n self.model = HTTPBaseModel(scheme=\"digest\", description=description)\n self.scheme_name = scheme_name or self.__class__.__name__\n self.auto_error = auto_error\n\n async def __call__(\n self, request: Request\n ) -> Optional[HTTPAuthorizationCredentials]:\n |
| 10 | run_command | {
"command": "cat fastapi/security/oauth2.py"
} | {"status": "ok", "stdout": "from typing import Any, Dict, List, Optional, Union, cast\n\nfrom annotated_doc import Doc\nfrom fastapi.exceptions import HTTPExcepfull{"status": "ok", "stdout": "from typing import Any, Dict, List, Optional, Union, cast\n\nfrom annotated_doc import Doc\nfrom fastapi.exceptions import HTTPException\nfrom fastapi.openapi.models import OAuth2 as OAuth2Model\nfrom fastapi.openapi.models import OAuthFlows as OAuthFlowsModel\nfrom fastapi.param_functions import Form\nfrom fastapi.security.base import SecurityBase\nfrom fastapi.security.utils import get_authorization_scheme_param\nfrom starlette.requests import Request\nfrom starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN\n\n# TODO: import from typing when deprecating Python 3.9\nfrom typing_extensions import Annotated\n\n\nclass OAuth2PasswordRequestForm:\n \"\"\"\n This is a dependency class to collect the `username` and `password` as form data\n for an OAuth2 password flow.\n\n The OAuth2 specification dictates that for a password flow the data should be\n collected using form data (instead of JSON) and that it should have the specific\n fields `username` and `password`.\n\n All the initialization parameters are extracted from the request.\n\n Read more about it in the\n [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).\n\n ## Example\n\n ```python\n from typing import Annotated\n\n from fastapi import Depends, FastAPI\n from fastapi.security import OAuth2PasswordRequestForm\n\n app = FastAPI()\n\n\n @app.post(\"/login\")\n def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):\n data = {}\n data[\"scopes\"] = []\n for scope in form_data.scopes:\n data[\"scopes\"].append(scope)\n if form_data.client_id:\n data[\"client_id\"] = form_data.client_id\n if form_data.client_secret:\n data[\"client_secret\"] = form_data.client_secret\n return data\n ```\n\n Note that for OAuth2 the scope `items:read` is a single scope in an |
| 11 | run_command | {
"command": "tail -n +300 fastapi/security/oauth2.py"
} | {"status": "ok", "stdout": " grant_type=grant_type,\n username=username,\n password=password,\n scope=scope,\n client_id=client_id,\n client_secret=client_secrefull{"status": "ok", "stdout": " grant_type=grant_type,\n username=username,\n password=password,\n scope=scope,\n client_id=client_id,\n client_secret=client_secret,\n )\n\n\nclass OAuth2(SecurityBase):\n \"\"\"\n This is the base class for OAuth2 authentication, an instance of it would be used\n as a dependency. All other OAuth2 classes inherit from it and customize it for\n each OAuth2 flow.\n\n You normally would not create a new class inheriting from it but use one of the\n existing subclasses, and maybe compose them if you want to support multiple flows.\n\n Read more about it in the\n [FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/).\n \"\"\"\n\n def __init__(\n self,\n *,\n flows: Annotated[\n Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]],\n Doc(\n \"\"\"\n The dictionary of OAuth2 flows.\n \"\"\"\n ),\n ] = OAuthFlowsModel(),\n scheme_name: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme name.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n description: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme description.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n auto_error: Annotated[\n bool,\n Doc(\n \"\"\"\n By default, if no HTTP Authorization header is provided, required for\n OAuth2 authentication, it will automatically cancel the request and\n send the client an error.\n\n |
| 12 | run_command | {
"command": "tail -n +400 fastapi/security/oauth2.py"
} | {"status": "ok", "stdout": "\n def __init__(\n self,\n tokenUrl: Annotated[\n str,\n Doc(\n \"\"\"\n The URL to obtain the OAuth2 token. This would be the *pathfull{"status": "ok", "stdout": "\n def __init__(\n self,\n tokenUrl: Annotated[\n str,\n Doc(\n \"\"\"\n The URL to obtain the OAuth2 token. This would be the *path operation*\n that has `OAuth2PasswordRequestForm` as a dependency.\n \"\"\"\n ),\n ],\n scheme_name: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme name.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n scopes: Annotated[\n Optional[Dict[str, str]],\n Doc(\n \"\"\"\n The OAuth2 scopes that would be required by the *path operations* that\n use this dependency.\n \"\"\"\n ),\n ] = None,\n description: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme description.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n auto_error: Annotated[\n bool,\n Doc(\n \"\"\"\n By default, if no HTTP Authorization header is provided, required for\n OAuth2 authentication, it will automatically cancel the request and\n send the client an error.\n\n If `auto_error` is set to `False`, when the HTTP Authorization header\n is not available, instead of erroring out, the dependency result will\n be `None`.\n\n This is useful when you want to have optional authentication.\n\n It is also useful when you want to have authentication that can be\n provided in one of multiple optional ways (for example, |
| 13 | run_command | {
"command": "tail -n +480 fastapi/security/oauth2.py"
} | {"status": "ok", "stdout": " )\n )\n super().__init__(\n flows=flows,\n scheme_name=scheme_name,\n description=description,\n auto_error=auto_error,\n )\n\n asyfull{"status": "ok", "stdout": " )\n )\n super().__init__(\n flows=flows,\n scheme_name=scheme_name,\n description=description,\n auto_error=auto_error,\n )\n\n async def __call__(self, request: Request) -> Optional[str]:\n authorization = request.headers.get(\"Authorization\")\n scheme, param = get_authorization_scheme_param(authorization)\n if not authorization or scheme.lower() != \"bearer\":\n if self.auto_error:\n raise HTTPException(\n status_code=HTTP_401_UNAUTHORIZED,\n detail=\"Not authenticated\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n else:\n return None\n return param\n\n\nclass OAuth2AuthorizationCodeBearer(OAuth2):\n \"\"\"\n OAuth2 flow for authentication using a bearer token obtained with an OAuth2 code\n flow. An instance of it would be used as a dependency.\n \"\"\"\n\n def __init__(\n self,\n authorizationUrl: str,\n tokenUrl: Annotated[\n str,\n Doc(\n \"\"\"\n The URL to obtain the OAuth2 token.\n \"\"\"\n ),\n ],\n refreshUrl: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n The URL to refresh the token and obtain a new one.\n \"\"\"\n ),\n ] = None,\n scheme_name: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme name.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n scopes: Annotated[\n Optional[Dict[str, str]],\n Doc(\n \"\"\"\n The OAuth2 scopes that would be required by |
| 14 | run_command | {
"command": "cat fastapi/security/open_id_connect_url.py"
} | {"status": "ok", "stdout": "from typing import Optional\n\nfrom annotated_doc import Doc\nfrom fastapi.openapi.models import OpenIdConnect as OpenIdConnectModelfull{"status": "ok", "stdout": "from typing import Optional\n\nfrom annotated_doc import Doc\nfrom fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel\nfrom fastapi.security.base import SecurityBase\nfrom starlette.exceptions import HTTPException\nfrom starlette.requests import Request\nfrom starlette.status import HTTP_403_FORBIDDEN\nfrom typing_extensions import Annotated\n\n\nclass OpenIdConnect(SecurityBase):\n \"\"\"\n OpenID Connect authentication class. An instance of it would be used as a\n dependency.\n \"\"\"\n\n def __init__(\n self,\n *,\n openIdConnectUrl: Annotated[\n str,\n Doc(\n \"\"\"\n The OpenID Connect URL.\n \"\"\"\n ),\n ],\n scheme_name: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme name.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n description: Annotated[\n Optional[str],\n Doc(\n \"\"\"\n Security scheme description.\n\n It will be included in the generated OpenAPI (e.g. visible at `/docs`).\n \"\"\"\n ),\n ] = None,\n auto_error: Annotated[\n bool,\n Doc(\n \"\"\"\n By default, if no HTTP Authorization header is provided, required for\n OpenID Connect authentication, it will automatically cancel the request\n and send the client an error.\n\n If `auto_error` is set to `False`, when the HTTP Authorization header\n is not available, instead of erroring out, the dependency result will\n be `None`.\n\n This is useful when you want to have optional authentication.\n\n It is also |
| 15 | run_command | {
"command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\nimport re\n\n# Process fastapi/security/api_key.py\np_api = Path(\"fastapi/security/api_key.py\")\ns_api = p_api.read_text()\n\n# Update imports\ns_api = s_api.replace(\"from starlette.status import HTTP_403_FORBIDDEN\", \"from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN\")\n\n# Update APIKeyBase.check_api_key\n# We need to introduce not_authenticated_status_code.\n# Since check_api_key is a staticmethod, we might need to change it to an instance method or pass the status code.\n# Looking at the existing code, check_api_key is called as self.check_api_key(api_key, self.auto_error)\n# So it's better to make it an instance method.\n\n# Let's refine APIKeyBase and its subclasses.\n# I'll replace the whole APIKeyBase class and update the calls in subclasses.\n\n# However, the task says \"the not_authenticated_status_code is introduced\".\n# This means it should be an attribute of the security class instance.\n\n# 1. Update APIKeyBase.__init__ and check_api_key\n# Wait, APIKeyBase doesn't have an __init__. Its subclasses do.\n# Let's add it to APIKeyBase or just handle it in subclasses.\n# APIKeyBase is used as a base for APIKeyQuery, APIKeyHeader, APIKeyCookie.\n# They all have __init__.\n\n# Let's do it carefully.\n# Replace check_api_key staticmethod with an instance method and add the param to __init__ of subclasses.\n\n# Actually, it's easier to just modify the subclasses.\n\n# For APIKeyBase:\nold_base = \"\"\"class APIKeyBase(SecurityBase):\n @staticmethod\n def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:\n if not api_key:\n if auto_error:\n raise HTTPException(\n status_code=HTTP_403_FORBIDDEN, detail=\"Not authenticated\"\n )\n return None\n return api_key\"\"\"\n\n# We will make it an instance method and use self.not_authenticated_status_code\nnew_base = \"\"\"class APIKeyBase(SecurityBase):\n def check_api_key(\n self,\n api_key: Optional[str],\n auto_error: bool,\n not_authenticated_status_code: int,\n ) -> Optional[str]:\n if not api_key:\n if auto_error:\n raise HTTPException(\n status_code=not_authenticated_status_code, detail=\"Not authenticated\"\n )\n return None\n return api_key\"\"\"\n\ns_api = s_api.replace(old_base, new_base)\n\n# Update APIKeyQuery.__init__ and __call__\n# Need to add not_authenticated_status_code to __init__ and pass it to check_api_key.\n\n# APIKeyQuery.__init__\n# Find the end of __init__'s arguments\n# This is tricky with regex. Let's use a simpler approach.\n# Since I can't easily use regex for multi-line replacements of specific parts of __init__,\n# I'll just replace the whole class definition if it's small enough, or use a more precise replacement.\n\n# Let's try to replace the __init__ of APIKeyQuery.\n# I will use a marker for replacements.\n\n# Actually, I can just write the whole file if I'm careful, but it's too large.\n# Let's use a more robust way to add the parameter.\n\n# Let's replace the __init__ methods and __call__ methods of all three APIKey classes.\n\n# For APIKeyQuery:\n# Find __init__ end and add not_authenticated_status_code.\n# Find __call__ and update check_api_key call.\n\n# Let's do it for each class.\n# I'll use a helper function in Python to do the replacements.\n\ndef update_apikey_class(content, class_name):\n # Add not_authenticated_status_code to __init__ arguments\n # Find the docstring of __init__ and add the argument after auto_error\n # This is still hard.\n\n # Let's just replace the whole __init__ and __call__ for each class.\n return content\n\n# I'll use sed-like replacement via python for each class.\n# But wait, I can just replace the whole file if I have the content.\n# I'll read the file and recons | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 16 | run_command | {
"command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\nimport re\n\ndef update_file(path, replacements):\n content = Path(path).read_text()\n for old, new in replacements:\n content = content.replace(old, new)\n Path(path).write_text(content)\n\n# 1. fastapi/security/api_key.py\napi_key_replacements = [\n (\"from starlette.status import HTTP_403_FORBIDDEN\", \"from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN\"),\n (\n \"\"\"class APIKeyBase(SecurityBase):\n @staticmethod\n def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:\n if not api_key:\n if auto_error:\n raise HTTPException(\n status_code=HTTP_403_FORBIDDEN, detail=\"Not authenticated\"\n )\n return None\n return api_key\"\"\",\n \"\"\"class APIKeyBase(SecurityBase):\n def check_api_key(\n self,\n api_key: Optional[str],\n auto_error: bool,\n not_authenticated_status_code: int,\n ) -> Optional[str]:\n if not api_key:\n if auto_error:\n raise HTTPException(\n status_code=not_authenticated_status_code, detail=\"Not authenticated\"\n )\n return None\n return api_key\"\"\"\n )\n]\nupdate_file(\"fastapi/security/api_key.py\", api_key_replacements)\n\n# Now update the 3 classes in api_key.py: APIKeyQuery, APIKeyHeader, APIKeyCookie\n# Each needs not_authenticated_status_code in __init__ and passed to check_api_key in __call__.\n\n# I will read the file and use regex for this because the __init__s are similar.\n\ns_api = Path(\"fastapi/security/api_key.py\").read_text()\n\n# Regex to find __init__ of APIKey classes and add the parameter\n# This is complex. Let's just use a simple replacement for the parts I know.\n\n# For APIKeyQuery\ns_api = s_api.replace(\n \" auto_error: Annotated[\",\n \" not_authenticated_status_code: Annotated[\\n int,\\n Doc(\\\"Status code to return if the API key is missing.\\\"),\\n ] = HTTP_401_UNAUTHORIZED,\\n auto_error: Annotated[\"\n)\n# Wait, I need to add it to the __init__ of each. But they all have auto_error.\n# The above will add it to all of them.\n\n# Now need to update __init__'s last line: self.auto_error = auto_error\n# to include self.not_authenticated_status_code = not_authenticated_status_code\n\n# This is tricky because they are at the end of __init__.\n# Let's search for \"self.auto_error = auto_error\" and replace it.\n\ns_api = s_api.replace(\" self.auto_error = auto_error\", \" self.not_authenticated_status_code = not_authenticated_status_code\\n self.auto_error = auto_error\")\n\n# Now update __call__\n# find \"return self.check_api_key(api_key, self.auto_error)\"\n# replace with \"return self.check_api_key(api_key, self.auto_error, self.not_authenticated_status_code)\"\n\ns_api = s_api.replace(\"return self.check_api_key(api_key, self.auto_error)\", \"return self.check_api_key(api_key, self.auto_error, self.not_authenticated_status_code)\")\n\nPath(\"fastapi/security/api_key.py\").write_text(s_api)\n\n# 2. fastapi/security/http.py\n# HTTPBase should have not_authenticated_status_code and use it in __call__.\n# HTTPBearer and HTTPDigest inherit from HTTPBase.\n\nhttp_replacements = [\n (\n \"\"\"class HTTPBase(SecurityBase):\n def __init__(\n self,\n *,\n scheme: str,\n scheme_name: Optional[str] = None,\n description: Optional[str] = None,\n auto_error: bool = True,\n ):\n self.model = HTTPBaseModel(scheme=scheme, description=description)\n self.scheme_name = scheme_name or self.__class__.__name__\n self.auto_error = auto_error\"\"\",\n \"\"\"class HTTPBase(SecurityBase):\n def __init__(\n self,\n *,\n scheme: str,\n scheme | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py
index 496c815a..2a6ec320 100644
--- a/fastapi/security/api_key.py
+++ b/fastapi/security/api_key.py
@@ -5,21 +5,30 @@ from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException
from starlette.requests import Request
-from starlette.status import HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
from typing_extensions import Annotated
class APIKeyBase(SecurityBase):
- @staticmethod
- def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:
+ def check_api_key(
+ self,
+ api_key: Optional[str],
+ auto_error: bool,
+ not_authenticated_status_code: int,
+ ) -> Optional[str]:
if not api_key:
if auto_error:
raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
+ status_code=not_authenticated_status_code,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": self.format_www_authenticate_header_value()},
)
return None
return api_key
+ def format_www_authenticate_header_value(self) -> str:
+ return f'ApiKey in="{self.model.in_}", name="{self.model.name}"'
+
class APIKeyQuery(APIKeyBase):
"""
@@ -80,6 +89,10 @@ class APIKeyQuery(APIKeyBase):
"""
),
] = None,
+ not_authenticated_status_code: Annotated[
+ int,
+ Doc("Status code to return if the API key is missing."),
+ ] = HTTP_401_UNAUTHORIZED,
auto_error: Annotated[
bool,
Doc(
@@ -106,11 +119,12 @@ class APIKeyQuery(APIKeyBase):
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
+ self.not_authenticated_status_code = not_authenticated_status_code
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.query_params.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key, self.auto_error, self.not_authenticated_status_code)
class APIKeyHeader(APIKeyBase):
@@ -169,6 +183,10 @@ class APIKeyHeader(APIKeyBase):
"""
),
] = None,
+ not_authenticated_status_code: Annotated[
+ int,
+ Doc("Status code to return if the API key is missing."),
+ ] = HTTP_401_UNAUTHORIZED,
auto_error: Annotated[
bool,
Doc(
@@ -194,11 +212,12 @@ class APIKeyHeader(APIKeyBase):
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
+ self.not_authenticated_status_code = not_authenticated_status_code
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.headers.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key, self.auto_error, self.not_authenticated_status_code)
class APIKeyCookie(APIKeyBase):
@@ -257,6 +276,10 @@ class APIKeyCookie(APIKeyBase):
"""
),
] = None,
+ not_authenticated_status_code: Annotated[
+ int,
+ Doc("Status code to return if the API key is missing."),
+ ] = HTTP_401_UNAUTHORIZED,
auto_error: Annotated[
bool,
Doc(
@@ -282,8 +305,9 @@ class APIKeyCookie(APIKeyBase):
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
+ self.not_authenticated_status_code = not_authenticated_status_code
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.cookies.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key, self.auto_error, self.not_authenticated_status_code)
diff --git a/fastapi/security/http.py b/fastapi/security/http.py
index 3a598565..fbe28998 100644
--- a/fastapi/security/http.py
+++ b/fastapi/security/http.py
@@ -75,10 +75,13 @@ class HTTPBase(SecurityBase):
scheme_name: Optional[str] = None,
description: Optional[str] = None,
auto_error: bool = True,
+ not_authenticated_status_code: int = HTTP_401_UNAUTHORIZED,
):
self.model = HTTPBaseModel(scheme=scheme, description=description)
self.scheme_name = scheme_name or self.__class__.__name__
+ self.not_authenticated_status_code = not_authenticated_status_code
self.auto_error = auto_error
+ self.not_authenticated_status_code = not_authenticated_status_code
async def __call__(
self, request: Request
@@ -88,7 +91,9 @@ class HTTPBase(SecurityBase):
if not (authorization and scheme and credentials):
if self.auto_error:
raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
+ status_code=self.not_authenticated_status_code,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"}
)
else:
return None
@@ -159,6 +164,7 @@ class HTTPBasic(HTTPBase):
"""
),
] = None,
+ not_authenticated_status_code: Annotated[int, Doc("Status code to return if the authentication is missing.")] = HTTP_401_UNAUTHORIZED,
auto_error: Annotated[
bool,
Doc(
@@ -183,6 +189,7 @@ class HTTPBasic(HTTPBase):
self.model = HTTPBaseModel(scheme="basic", description=description)
self.scheme_name = scheme_name or self.__class__.__name__
self.realm = realm
+ self.not_authenticated_status_code = not_authenticated_status_code
self.auto_error = auto_error
async def __call__( # type: ignore
@@ -274,6 +281,7 @@ class HTTPBearer(HTTPBase):
"""
),
] = None,
+ not_authenticated_status_code: Annotated[int, Doc("Status code to return if the authentication is missing.")] = HTTP_401_UNAUTHORIZED,
auto_error: Annotated[
bool,
Doc(
@@ -297,6 +305,7 @@ class HTTPBearer(HTTPBase):
):
self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description)
self.scheme_name = scheme_name or self.__class__.__name__
+ self.not_authenticated_status_code = not_authenticated_status_code
self.auto_error = auto_error
async def __call__(
@@ -307,7 +316,9 @@ class HTTPBearer(HTTPBase):
if not (authorization and scheme and credentials):
if self.auto_error:
raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
+ status_code=self.not_authenticated_status_code,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"}
)
else:
return None
@@ -377,6 +388,7 @@ class HTTPDigest(HTTPBase):
"""
),
] = None,
+ not_authenticated_status_code: Annotated[int, Doc("Status code to return if the authentication is missing.")] = HTTP_401_UNAUTHORIZED,
auto_error: Annotated[
bool,
Doc(
@@ -399,6 +411,7 @@ class HTTPDigest(HTTPBase):
):
self.model = HTTPBaseModel(scheme="digest", description=description)
self.scheme_name = scheme_name or self.__class__.__name__
+ self.not_authenticated_status_code = not_authenticated_status_code
self.auto_error = auto_error
async def __call__(
@@ -409,7 +422,9 @@ class HTTPDigest(HTTPBase):
if not (authorization and scheme and credentials):
if self.auto_error:
raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
+ status_code=self.not_authenticated_status_code,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"}
)
else:
return None
diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py
index 5e99798e..80e678ab 100644
--- a/fastapi/security/open_id_connect_url.py
+++ b/fastapi/security/open_id_connect_url.py
@@ -46,6 +46,7 @@ class OpenIdConnect(SecurityBase):
"""
),
] = None,
+ not_authenticated_status_code: Annotated[int, Doc("Status code to return if the authentication is missing.")] = HTTP_401_UNAUTHORIZED,
auto_error: Annotated[
bool,
Doc(
@@ -71,15 +72,13 @@ class OpenIdConnect(SecurityBase):
openIdConnectUrl=openIdConnectUrl, description=description
)
self.scheme_name = scheme_name or self.__class__.__name__
+ self.not_authenticated_status_code = not_authenticated_status_code
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization")
if not authorization:
- if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ if self.auto_error:raise HTTPException(status_code=self.not_authenticated_status_code, detail="Not authenticated")
else:
return None
return authorization
==================================== ERRORS ====================================
____________ ERROR collecting tests/test_security_api_key_cookie.py ____________
tests/test_security_api_key_cookie.py:1: in <module>
from fastapi import Depends, FastAPI, Security
fastapi/__init__.py:7: in <module>
from .applications import FastAPI as FastAPI
fastapi/applications.py:17: in <module>
from fastapi import routing
fastapi/routing.py:38: in <module>
from fastapi.dependencies.models import Dependant
fastapi/dependencies/models.py:8: in <module>
from fastapi.security.base import SecurityBase
fastapi/security/__init__.py:15: in <module>
from .open_id_connect_url import OpenIdConnect as OpenIdConnect
fastapi/security/open_id_connect_url.py:12: in <module>
class OpenIdConnect(SecurityBase):
fastapi/security/open_id_connect_url.py:49: in OpenIdConnect
not_authenticated_status_code: Annotated[int, Doc("Status code to return if the authentication is missing.")] = HTTP_401_UNAUTHORIZED,
^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'HTTP_401_UNAUTHORIZED' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 error in 0.51s