fix(workspace): validate Keycloak JWT for production auth

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Mortezakoohjani 2026-09-12 12:02:03 +03:30
parent a483b47369
commit a846b3e7f1
3 changed files with 36 additions and 6 deletions

View File

@ -22,14 +22,22 @@ class Settings(BaseSettings):
auth_required: bool = True auth_required: bool = True
entitlement_stub: bool = True entitlement_stub: bool = True
keycloak_enabled: bool = True keycloak_enabled: bool = True
keycloak_server_url: str = "http://localhost:8080" keycloak_server_url: str = Field(default="http://localhost:8080", validation_alias="KEYCLOAK_SERVER_URL")
keycloak_public_url: str = "" keycloak_public_url: str = Field(default="", validation_alias="KEYCLOAK_PUBLIC_URL")
keycloak_realm: str = "superapp" keycloak_realm: str = Field(default="superapp", validation_alias="KEYCLOAK_REALM")
jwt_algorithm: str = "RS256" jwt_algorithm: str = "RS256"
jwt_audience: str = "account" jwt_audience: str = "account"
jwt_verify_signature: bool = True jwt_verify_signature: bool = True
cors_origins: str = "http://localhost:3000,http://127.0.0.1:3000" cors_origins: str = "http://localhost:3000,http://127.0.0.1:3000"
@property
def keycloak_public_base(self) -> str:
return (self.keycloak_public_url or self.keycloak_server_url).rstrip("/")
@property
def keycloak_public_realm_url(self) -> str:
return f"{self.keycloak_public_base}/realms/{self.keycloak_realm}"
@property @property
def cors_origin_list(self): def cors_origin_list(self):
return [x.strip() for x in self.cors_origins.split(",") if x.strip()] return [x.strip() for x in self.cors_origins.split(",") if x.strip()]

View File

@ -1,16 +1,34 @@
from functools import lru_cache
from fastapi import Depends from fastapi import Depends
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.core.config import settings from app.core.config import settings
from shared.auth import JWTSettings, JWTValidator
from shared.exceptions import UnauthorizedError from shared.exceptions import UnauthorizedError
from shared.security import CurrentUser from shared.security import CurrentUser
bearer = HTTPBearer(auto_error=False) bearer = HTTPBearer(auto_error=False)
@lru_cache
def get_jwt_validator() -> JWTValidator:
return JWTValidator(
JWTSettings(
keycloak_enabled=settings.keycloak_enabled,
keycloak_server_url=settings.keycloak_server_url,
keycloak_realm=settings.keycloak_realm,
jwt_algorithm=settings.jwt_algorithm,
jwt_audience=settings.jwt_audience,
jwt_verify_signature=settings.jwt_verify_signature,
jwt_issuer=settings.keycloak_public_realm_url,
)
)
async def get_current_user(credentials: HTTPAuthorizationCredentials | None = Depends(bearer)): async def get_current_user(credentials: HTTPAuthorizationCredentials | None = Depends(bearer)):
if not settings.auth_required: if not settings.auth_required:
return CurrentUser(user_id="test-user", username="test", roles=["tenant_admin"]) return CurrentUser(user_id="test-user", username="test", roles=["tenant_admin"])
if not credentials: if credentials is None or not credentials.credentials:
raise UnauthorizedError("Authentication required") raise UnauthorizedError("توکن احراز هویت ارائه نشده است")
return CurrentUser(user_id="token-user", username="token", roles=[]) return await get_jwt_validator().validate(credentials.credentials)

View File

@ -460,6 +460,10 @@ services:
WORKSPACE_DATABASE_URL_SYNC: ${WORKSPACE_DATABASE_URL_SYNC:-postgresql+psycopg://superapp:superapp_password@postgres:5432/workspace_db} WORKSPACE_DATABASE_URL_SYNC: ${WORKSPACE_DATABASE_URL_SYNC:-postgresql+psycopg://superapp:superapp_password@postgres:5432/workspace_db}
CORE_SERVICE_URL: ${CORE_SERVICE_URL:-http://core-service:8000} CORE_SERVICE_URL: ${CORE_SERVICE_URL:-http://core-service:8000}
AUTH_REQUIRED: ${AUTH_REQUIRED:-true} AUTH_REQUIRED: ${AUTH_REQUIRED:-true}
KEYCLOAK_SERVER_URL: ${KEYCLOAK_SERVER_URL:-http://keycloak:8080}
KEYCLOAK_PUBLIC_URL: ${KEYCLOAK_PUBLIC_URL:-http://localhost:8080}
KEYCLOAK_REALM: ${KEYCLOAK_REALM:-superapp}
JWT_VERIFY_SIGNATURE: ${JWT_VERIFY_SIGNATURE:-true}
ports: ports:
- "8013:8013" - "8013:8013"
volumes: volumes: