fix(auth): схема bearerAuth в OpenAPI — кнопка Authorize в Swagger UI
Токен читался зависимостью как Header(), поэтому в openapi.json не было securitySchemes и Swagger UI не показывал Authorize (вопрос фронта 06.10.2026). Теперь HTTPBearer(auto_error=False) со scheme_name bearerAuth; значение заголовка собирается обратно и идёт в parse_bearer — тексты 401/403 и правила («три части» → 401) не изменились. Тесты: securitySchemes.bearerAuth есть, все маршруты кроме /dev/token и /healthcheck помечены security. make check: 1126 passed; интеграция auth/me/dev-token: 53 passed. Closes #29 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
This commit is contained in:
parent
4879fdca88
commit
227ab67809
@ -18,7 +18,8 @@ import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, Request, status
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from src.api.dependencies.db import SessionDep
|
||||
from src.common.exceptions import AuthenticationFailedError
|
||||
@ -61,6 +62,27 @@ def _login_forbidden(detail: str, email: str, user_id: uuid.UUID | None = None)
|
||||
)
|
||||
|
||||
|
||||
# Схема безопасности OpenAPI (issue #29): без неё Swagger UI не показывает Authorize, а заголовок выглядит как обычный
|
||||
# параметр. `auto_error=False`: нет заголовка или чужая схема → `None`, прежний 401 формирует `parse_bearer`.
|
||||
BEARER_SCHEME_NAME = "bearerAuth"
|
||||
bearer_scheme = HTTPBearer(
|
||||
scheme_name=BEARER_SCHEME_NAME,
|
||||
bearerFormat="JWT",
|
||||
auto_error=False,
|
||||
description=(
|
||||
"JWT РАИСА (прод) или dev-токен `POST /dev/token` (DEBUG). В Swagger: Authorize → токен без слова Bearer."
|
||||
),
|
||||
)
|
||||
BearerDep = Annotated[HTTPAuthorizationCredentials | None, Depends(bearer_scheme)]
|
||||
|
||||
|
||||
def header_value(credentials: HTTPAuthorizationCredentials | None) -> str | None:
|
||||
"""Значение заголовка `Authorization` как строка — для `parse_bearer` с прежними правилами (три части → 401)."""
|
||||
if credentials is None:
|
||||
return None
|
||||
return f"{credentials.scheme} {credentials.credentials}"
|
||||
|
||||
|
||||
def parse_bearer(authorization: str | None) -> str:
|
||||
"""Токен из значения заголовка `Authorization: Bearer <jwt>`; иначе 401 (схема без учёта регистра)."""
|
||||
if authorization is not None:
|
||||
@ -84,10 +106,10 @@ def get_token_verifier(request: Request) -> TokenVerifier | None:
|
||||
|
||||
async def verified_claims(
|
||||
verifier: Annotated[TokenVerifier | None, Depends(get_token_verifier)],
|
||||
authorization: Annotated[str | None, Header()] = None,
|
||||
credentials: BearerDep = None,
|
||||
) -> Claims:
|
||||
"""Claims проверенного токена. Не обращается к БД: все 401 решаются до неё."""
|
||||
token = parse_bearer(authorization)
|
||||
token = parse_bearer(header_value(credentials))
|
||||
if verifier is None:
|
||||
raise _token_rejected("аутентификация не настроена")
|
||||
try:
|
||||
|
||||
@ -199,3 +199,30 @@ def test_counterparty_mapping_routes_declare_documented_responses(schema: dict[s
|
||||
assert mapping["200"]["content"]["application/json"]["schema"]["$ref"].endswith("/MapCounterpartyOut")
|
||||
row = schema["components"]["schemas"]["MapCounterpartyOut"]["properties"]["row"]
|
||||
assert {"type": "null"} in row["anyOf"]
|
||||
|
||||
|
||||
# --- Схема безопасности (issue #29): Swagger UI показывает Authorize только при securitySchemes в OpenAPI ---
|
||||
|
||||
BEARER_SCHEME = "bearerAuth"
|
||||
# Маршруты без аутентификации: выдача dev-токена и healthcheck
|
||||
PUBLIC_PATHS = {"/api/v1/dev/token", "/healthcheck"}
|
||||
|
||||
|
||||
def test_openapi_declares_bearer_security_scheme(schema: dict[str, Any]) -> None:
|
||||
schemes = schema.get("components", {}).get("securitySchemes", {})
|
||||
bearer = schemes.get(BEARER_SCHEME, {})
|
||||
assert (bearer.get("type"), bearer.get("scheme"), bearer.get("bearerFormat")) == ("http", "bearer", "JWT"), schemes
|
||||
assert "Authorize" in bearer.get("description", "") # подсказка для Swagger UI
|
||||
|
||||
|
||||
def test_protected_routes_require_bearer_and_public_do_not(schema: dict[str, Any]) -> None:
|
||||
missing: list[str] = []
|
||||
for path, methods in schema["paths"].items():
|
||||
for method, operation in methods.items():
|
||||
requirements = operation.get("security", [])
|
||||
has_bearer = any(BEARER_SCHEME in requirement for requirement in requirements)
|
||||
if path in PUBLIC_PATHS:
|
||||
assert not has_bearer, f"{method.upper()} {path} — публичный маршрут помечен security"
|
||||
elif not has_bearer:
|
||||
missing.append(f"{method.upper()} {path}")
|
||||
assert missing == [], f"маршруты без схемы {BEARER_SCHEME} (Authorize в Swagger не подставит токен): {missing}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user