โ† pod_r3_v1g

fastapi_14786

resolved RESOLVED PASS ยท 9 tool calls ยท 41 s ยท fastapi/fastapi

Task input

๐Ÿ› Strip whitespaces from `Authorization` header credentials

## Description

Fixes whitespace handling in authorization header parameter extraction to comply with [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750#section-2.1).

## Problem

The current implementation preserves leading/trailing whitespace in the extracted credential parameter. While RFC 6750 allows one or more spaces (`1*SP`) between "Bearer" and the token, the token definition (`b64token`) doesn't include whitespace characters, meaning spaces should be treated as separators only.

This causes the extracted credentials to include extraneous whitespace, which can lead to authentication failures.

## Solution

Added `.strip()` to the parameter extraction to remove leading and trailing whitespace:
```python
return scheme, param.strip()
```

## Reproduction

### Before the fix:
```python
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

app = FastAPI()
security = HTTPBearer()

@app.get("/users/me")
def read_current_user(
    credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
):
    return {"scheme": credentials.scheme, "credentials": credentials.credentials}
```

**Test case 1: Single space (correct)**
```bash
curl -X 'GET' \
  'http://127.0.0.1:8000/users/me' \
  -H 'Authorization: Bearer xxx'
```
Response: `{"scheme":"Bearer","credentials":"xxx"}` โœ“

**Test case 2: Multiple spaces**
```bash
curl -X 'GET' \
  'http://127.0.0.1:8000/users/me' \
  -H 'Authorization: Bearer   xxx'
```
Response: `{"scheme":"Bearer","credentials":"  xxx"}` โœ—

**Test case 3: Trailing whitespace**
```bash
curl -X 'GET' \
  'http://127.0.0.1:8000/users/me' \
  -H 'Authorization: Bearer xxx '
```
Response: `{"scheme":"Bearer","credentials":"xxx "}` โœ—

### After the fix:

All three test cases now return: `{"scheme":"Bearer","credentials":"xxx"}` โœ“

## Changes

- Modified `get_authorization_scheme_param()` in `fastapi/security/utils.py` to strip whitespace from the parameter
- Added test cases to verify the fix handles multiple spaces and trailing whitespace correctly

## Checklist

- [x] Tests added to cover the bug fix
- [x] Existing tests pass

Tool calls (10)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"get_authorization_scheme_param\" fastapi/security/utils.py"
}
{"status": "ok", "stdout": "fastapi/security/utils.py:4:def get_authorization_scheme_param(\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "fastapi/security/utils.py:4:def get_authorization_scheme_param(\n", "stderr": "", "exit_code": 0}
2run_command
{
 "command": "sed -n '1,30p' fastapi/security/utils.py"
}
{"status": "ok", "stdout": "from typing import Optional\n\n\ndef get_authorization_scheme_param(\n authorization_header_value: Optional[str],\n) -> tuple[str, s
full
{"status": "ok", "stdout": "from typing import Optional\n\n\ndef get_authorization_scheme_param(\n    authorization_header_value: Optional[str],\n) -> tuple[str, str]:\n    if not authorization_header_value:\n        return \"\", \"\"\n    scheme, _, param = authorization_header_value.partition(\" \")\n    return scheme, param\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.security.utils import get_authorization_scheme_param\n\ntest_cases = [\n    (\"Bearer xxx\", (\"Bearer\", \"xxx\")),\n    (\"Bearer   xxx\", (\"Bearer\", \"xxx\")),\n    (\"Bearer xxx \", (\"Bearer\", \"xxx\")),\n    (\"Bearer xxx yyy\", (\"Bearer\", \"xxx yyy\")), # Should this be stripped? The RFC says b64token.\n]\n\nfor input_val, expected in test_cases:\n    result = get_authorization_scheme_param(input_val)\n    print(f\"Input: {repr(input_val)} | Expected: {expected} | Result: {result} | {'โœ“' if result == expected else 'โœ—'}\")\nPY\n"
}
{"status": "ok", "stdout": "Input: 'Bearer xxx' | Expected: ('Bearer', 'xxx') | Result: ('Bearer', 'xxx') | \u2713\nInput: 'Bearer xxx' | Expected: ('Bearer', '
full
{"status": "ok", "stdout": "Input: 'Bearer xxx' | Expected: ('Bearer', 'xxx') | Result: ('Bearer', 'xxx') | \u2713\nInput: 'Bearer   xxx' | Expected: ('Bearer', 'xxx') | Result: ('Bearer', '  xxx') | \u2717\nInput: 'Bearer xxx ' | Expected: ('Bearer', 'xxx') | Result: ('Bearer', 'xxx ') | \u2717\nInput: 'Bearer xxx yyy' | Expected: ('Bearer', 'xxx yyy') | Result: ('Bearer', 'xxx yyy') | \u2713\n", "stderr": "", "exit_code": 0}
4run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/security/utils.py\"); s = p.read_text()\nold = \"\"\"    scheme, _, param = authorization_header_value.partition(\" \")\n    return scheme, param\"\"\"\nnew = \"\"\"    scheme, _, param = authorization_header_value.partition(\" \")\n    return scheme, param.strip()\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new); compile(t, str(p), \"exec\"); p.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
5run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.security.utils import get_authorization_scheme_param\n\ntest_cases = [\n    (\"Bearer xxx\", (\"Bearer\", \"xxx\")),\n    (\"Bearer   xxx\", (\"Bearer\", \"xxx\")),\n    (\"Bearer xxx \", (\"Bearer\", \"xxx\")),\n    (\"Bearer xxx yyy\", (\"Bearer\", \"xxx yyy\")),\n]\n\nfor input_val, expected in test_cases:\n    result = get_authorization_scheme_param(input_val)\n    print(f\"Input: {repr(input_val)} | Expected: {expected} | Result: {result} | {'โœ“' if result == expected else 'โœ—'}\")\nPY\n"
}
{"status": "ok", "stdout": "Input: 'Bearer xxx' | Expected: ('Bearer', 'xxx') | Result: ('Bearer', 'xxx') | \u2713\nInput: 'Bearer xxx' | Expected: ('Bearer', '
full
{"status": "ok", "stdout": "Input: 'Bearer xxx' | Expected: ('Bearer', 'xxx') | Result: ('Bearer', 'xxx') | \u2713\nInput: 'Bearer   xxx' | Expected: ('Bearer', 'xxx') | Result: ('Bearer', 'xxx') | \u2713\nInput: 'Bearer xxx ' | Expected: ('Bearer', 'xxx') | Result: ('Bearer', 'xxx') | \u2713\nInput: 'Bearer xxx yyy' | Expected: ('Bearer', 'xxx yyy') | Result: ('Bearer', 'xxx yyy') | \u2713\n", "stderr": "", "exit_code": 0}
6run_command
{
 "command": "pytest tests/test_security/test_utils.py -q"
}
{"status": "error", "error_type": "CommandError", "error_message": "ERROR: file or directory not found: tests/test_security/test_utils.py\n\n", "details": {"std
full
{"status": "error", "error_type": "CommandError", "error_message": "ERROR: file or directory not found: tests/test_security/test_utils.py\n\n", "details": {"stdout": "\nno tests ran in 0.09s\n", "stderr": "ERROR: file or directory not found: tests/test_security/test_utils.py\n\n", "exit_code": 4}}
7run_command
{
 "command": "find tests -name \"*security*\""
}
{"status": "ok", "stdout": "tests/test_security_http_digest_description.py\ntests/test_security_oauth2.py\ntests/test_security_http_basic_optional.py\ntests/tes
full
{"status": "ok", "stdout": "tests/test_security_http_digest_description.py\ntests/test_security_oauth2.py\ntests/test_security_http_basic_optional.py\ntests/test_dependency_security_overrides.py\ntests/test_security_scopes_dont_propagate.py\ntests/test_security_openid_connect_description.py\ntests/test_security_http_base_description.py\ntests/test_security_api_key_cookie_optional.py\ntests/test_security_scopes.py\ntests/test_security_api_key_header.py\ntests/test_security_oauth2_authorization_code_bearer_scopes_openapi_simple.py\ntests/test_security_oauth2_password_bearer_optional_description.py\ntests/test_security_openid_connect_optional.py\ntests/test_security_scopes_sub_dependency.py\ntests/test_security_http_base_optional.py\ntests/test_security_api_key_query.py\ntests/test_security_http_bearer.py\ntests/test_security_api_key_query_description.py\ntests/test_security_oauth2_optional_description.py\ntests/test_webhooks_security.py\ntests/test_security_http_digest_optional.py\ntests/test_tutorial/test_security\ntests/test_security_http_basic_realm_description.py\ntests/test_security_http_base.py\ntests/test_security_http_bearer_optional.py\ntests/test_security_oauth2_authorization_code_bearer_description.py\ntests/test_security_api_key_header_description.py\ntests/test_security_oauth2_optional.py\ntests/test_security_oauth2_authorization_code_bearer.py\ntests/test_security_oauth2_password_bearer_optional.py\ntests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py\ntests/test_security_http_basic_realm.py\ntests/test_security_api_key_query_optional.py\ntests/test_security_api_key_cookie.py\ntests/test_security_openid_connect.py\ntests/test_top_level_security_scheme_in_openapi.py\ntests/test_security_api_key_cookie_description.py\ntests/test_security_http_bearer_description.py\ntests/test_security_api_key_header_optional.py\ntests/test_security_http_digest.py\n", "stderr": "", "exit_code": 0}
8run_command
{
 "command": "pytest tests/test_security_http_bearer.py -q"
}
{"status": "ok", "stdout": ".... [100%]\n=============================== warnings summary ===============================\n../../../../../../../Users/jp/repos/k
full
{"status": "ok", "stdout": "....                                                                     [100%]\n=============================== warnings summary ===============================\n../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45\n  /Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.\n    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]\n\n-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html\n4 passed, 1 warning in 0.32s\n", "stderr": "", "exit_code": 0}
9run_command
{
 "command": "submit_patch"
}
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bas
full
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bash: submit_patch: command not found\n", "exit_code": 127}}
10submit_patch
{}
{"status": "ok", "patch_size": 404, "files_changed": 1}
full
{"status": "ok", "patch_size": 404, "files_changed": 1}

Patch

diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py
index 002e68b4..fd349aec 100644
--- a/fastapi/security/utils.py
+++ b/fastapi/security/utils.py
@@ -7,4 +7,4 @@ def get_authorization_scheme_param(
     if not authorization_header_value:
         return "", ""
     scheme, _, param = authorization_header_value.partition(" ")
-    return scheme, param
+    return scheme, param.strip()

Test output

show
.........                                                                [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
9 passed, 1 warning in 0.43s