diff --git a/api/src/api/v1/auth.py b/api/src/api/v1/auth.py index a336382..a5b8d2e 100644 --- a/api/src/api/v1/auth.py +++ b/api/src/api/v1/auth.py @@ -3,8 +3,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from src.core.config import settings from src.core.security import ( + decode_keycloak_token, email_from_keycloak_payload, - inspect_keycloak_token, normalize_bearer_token, ) from src.db.session import get_db @@ -73,14 +73,11 @@ else: detail=_PROD_TOKEN_REQUIRED, headers={"WWW-Authenticate": "Bearer"}, ) - payload, reason = inspect_keycloak_token(raw_token) + payload = decode_keycloak_token(raw_token) if payload is None: - detail = _PROD_TOKEN_INVALID - if reason: - detail = f"{_PROD_TOKEN_INVALID}: {reason}" raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=detail, + detail=_PROD_TOKEN_INVALID, headers={"WWW-Authenticate": "Bearer"}, ) if not email_from_keycloak_payload(payload): diff --git a/api/src/core/security.py b/api/src/core/security.py index 309a402..c8871b1 100644 --- a/api/src/core/security.py +++ b/api/src/core/security.py @@ -1,5 +1,4 @@ from datetime import datetime, timedelta, timezone -import logging from typing import Optional import httpx @@ -10,7 +9,6 @@ from jose import JOSEError, JWTError, jwt from src.core.config import settings pwd_context = PasswordHasher() -logger = logging.getLogger(__name__) _KEYCLOAK_ALGORITHMS = ["RS256"] _KEYCLOAK_DECODE_OPTIONS = { @@ -112,33 +110,13 @@ def _decode_with_jwks(token: str, jwks: dict) -> dict: def decode_keycloak_token(token: str) -> Optional[dict]: """Проверяет Keycloak JWT по JWKS и возвращает payload.""" - payload, _reason = inspect_keycloak_token(token) - return payload - - -def inspect_keycloak_token(token: str) -> tuple[Optional[dict], str | None]: - """Как decode_keycloak_token, плюс текст причины отказа (для отладки логина).""" raw = normalize_bearer_token(token) if raw is None: - return None, "empty token" - alg = None - kid = None + return None try: - header = jwt.get_unverified_header(raw) - alg = header.get("alg") - kid = header.get("kid") - except Exception as exc: - return None, f"malformed jwt: {exc}" - try: - return _decode_with_jwks(raw, _get_jwks()), None - except (JOSEError, httpx.HTTPError, ValueError, TypeError, KeyError) as exc: - logger.warning("Keycloak JWT rejected: %s", exc) + return _decode_with_jwks(raw, _get_jwks()) + except (JOSEError, httpx.HTTPError, ValueError, TypeError, KeyError): try: - return _decode_with_jwks(raw, _get_jwks(force_refresh=True)), None - except (JOSEError, httpx.HTTPError, ValueError, TypeError, KeyError) as retry_exc: - logger.warning("Keycloak JWT rejected after JWKS refresh: %s", retry_exc) - reason = ( - f"{retry_exc.__class__.__name__}: {retry_exc}; " - f"alg={alg}; kid={kid}; jwks_url={settings.JWKS_URL}" - ) - return None, reason + return _decode_with_jwks(raw, _get_jwks(force_refresh=True)) + except (JOSEError, httpx.HTTPError, ValueError, TypeError, KeyError): + return None diff --git a/api/tests/unit/test_auth_api_prod_branch.py b/api/tests/unit/test_auth_api_prod_branch.py index c734034..c087ef5 100644 --- a/api/tests/unit/test_auth_api_prod_branch.py +++ b/api/tests/unit/test_auth_api_prod_branch.py @@ -72,8 +72,8 @@ def test_auth_login_non_debug_branch_success_and_unauthorized(monkeypatch): ) m.setattr( auth_module, - "inspect_keycloak_token", - lambda _token: ({"email": "prod-user@example.com"}, None), + "decode_keycloak_token", + lambda _token: {"email": "prod-user@example.com"}, ) client = TestClient(app) @@ -144,8 +144,8 @@ def test_auth_login_non_debug_rejects_missing_email(monkeypatch): with monkeypatch.context() as m: m.setattr( auth_module, - "inspect_keycloak_token", - lambda _token: ({"sub": "x"}, None), + "decode_keycloak_token", + lambda _token: {"sub": "x"}, ) client = TestClient(app) response = client.post("/auth/login", json={"token": "kc-token"}) @@ -172,16 +172,13 @@ def test_auth_login_non_debug_rejects_invalid_token(monkeypatch): with monkeypatch.context() as m: m.setattr( auth_module, - "inspect_keycloak_token", - lambda _token: (None, "Signature verification failed; alg=RS256"), + "decode_keycloak_token", + lambda _token: None, ) client = TestClient(app) response = client.post("/auth/login", json={"token": "bad"}) assert response.status_code == 401 - message = response.json()["message"] - assert message.startswith("Недействительный токен Keycloak") - assert "Signature verification failed" in message - assert "alg=RS256" in message + assert response.json()["message"] == "Недействительный токен Keycloak" finally: _restore_module(monkeypatch, "src.api.v1.auth", original_debug, previous) diff --git a/api/tests/unit/test_auth_service.py b/api/tests/unit/test_auth_service.py index 5221964..0c0a478 100644 --- a/api/tests/unit/test_auth_service.py +++ b/api/tests/unit/test_auth_service.py @@ -10,7 +10,6 @@ from src.core.security import ( clear_jwks_cache, create_refresh_token, decode_keycloak_token, - inspect_keycloak_token, verify_token, ) from src.services.auth_service import AuthService @@ -25,9 +24,6 @@ def test_decode_keycloak_token_rejects_garbage(monkeypatch): clear_jwks_cache() monkeypatch.setattr("src.core.security._get_jwks", lambda **_kwargs: {"keys": []}) assert decode_keycloak_token("not-a-jwt") is None - _payload, reason = inspect_keycloak_token("not-a-jwt") - assert _payload is None - assert reason def test_decode_keycloak_token_accepts_valid_rs256(monkeypatch):