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