From 943630270b2224e884d3e89fbc88df6efea636f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D1=80=D1=82=D1=8B=D0=BD=D0=BE=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Tue, 28 Jul 2026 15:04:12 +0300 Subject: [PATCH] [DOP-39448] Sync KeycloakAuthProvider config with SyncMaster --- config.docker.yml | 9 +- config.yml | 11 +- data_rentgen/server/api/v1/router/auth.py | 16 +-- data_rentgen/server/middlewares/__init__.py | 2 - data_rentgen/server/middlewares/session.py | 18 --- .../server/providers/auth/base_provider.py | 6 +- .../server/providers/auth/dummy_provider.py | 2 +- .../providers/auth/keycloak_provider.py | 35 +++--- .../providers/auth/personal_token_provider.py | 2 +- data_rentgen/server/settings/auth/keycloak.py | 118 +++++++++++++++--- data_rentgen/server/settings/server.py | 5 - data_rentgen/server/settings/session.py | 85 ------------- .../changelog/next_release/508.breaking.md | 28 +++++ mddocs/docs/reference/server/auth/keycloak.md | 4 + .../reference/server/configuration/session.md | 5 - tests/test_server/fixtures/keycloak.py | 30 ++--- .../test_auth/test_keycloak_auth.py | 8 +- 17 files changed, 192 insertions(+), 192 deletions(-) delete mode 100644 data_rentgen/server/middlewares/session.py delete mode 100644 data_rentgen/server/settings/session.py create mode 100644 mddocs/docs/changelog/next_release/508.breaking.md delete mode 100644 mddocs/docs/reference/server/configuration/session.md diff --git a/config.docker.yml b/config.docker.yml index c64434f3..bd8fec78 100644 --- a/config.docker.yml +++ b/config.docker.yml @@ -11,9 +11,6 @@ kafka: server: debug: false - session: - enabled: true - secret_key: session_secret_key cors: enabled: true allow_origins: [http://localhost:3000] @@ -34,13 +31,15 @@ auth: # Keycloak Auth # provider: data_rentgen.server.providers.auth.keycloak_provider.KeycloakAuthProvider keycloak: - server_url: http://keycloak:8080 + api_url: http://keycloak:8080 realm_name: create_realm_manually client_id: create_client_manually client_secret: generated_by_keycloak - redirect_uri: http://localhost:3000/auth-callback + ui_callback_url: http://localhost:3000/auth-callback scope: email verify_ssl: false + cookie: + secret_key: cookie_secret_key ui: api_browser_url: http://localhost:8000 diff --git a/config.yml b/config.yml index 92347b22..ee5e6b69 100644 --- a/config.yml +++ b/config.yml @@ -17,9 +17,6 @@ consumer: server: debug: true - session: - enabled: true - secret_key: session_secret_key cors: enabled: true allow_origins: [http://localhost:3000] @@ -40,13 +37,15 @@ auth: # Keycloak Auth # provider: data_rentgen.server.providers.auth.keycloak_provider.KeycloakAuthProvider keycloak: - server_url: http://localhost:8080 + api_url: http://localhost:8080 realm_name: create_realm_manually client_id: create_client_manually - client_secret: change_me - redirect_uri: http://localhost:3000/auth-callback + client_secret: generated_by_keycloak + ui_callback_url: http://localhost:3000/auth-callback scope: email verify_ssl: false + cookie: + secret_key: cookie_secret_key ui: api_browser_url: http://localhost:8000 diff --git a/data_rentgen/server/api/v1/router/auth.py b/data_rentgen/server/api/v1/router/auth.py index 15d1b440..9cc7306d 100644 --- a/data_rentgen/server/api/v1/router/auth.py +++ b/data_rentgen/server/api/v1/router/auth.py @@ -16,11 +16,7 @@ NotAuthorizedSchema, ) from data_rentgen.server.errors.schemas.not_implemented import NotImplementedErrorSchema -from data_rentgen.server.providers.auth import ( - AuthProvider, - DummyAuthProvider, - KeycloakAuthProvider, -) +from data_rentgen.server.providers.auth import AuthProvider from data_rentgen.server.schemas.v1.auth import AuthTokenSchema from data_rentgen.server.services import PersonalTokenPolicy, get_user @@ -43,7 +39,7 @@ ), ) async def token( - auth_provider: Annotated[DummyAuthProvider, Depends(Stub(AuthProvider))], + auth_provider: Annotated[AuthProvider, Depends(Stub(AuthProvider))], form_data: Annotated[OAuth2PasswordRequestForm, Depends()], ) -> AuthTokenSchema: user_token = await auth_provider.get_token_password_grant( @@ -68,7 +64,7 @@ async def token( async def auth_callback( request: Request, code: str, - auth_provider: Annotated[KeycloakAuthProvider, Depends(Stub(AuthProvider))], + auth_provider: Annotated[AuthProvider, Depends(Stub(AuthProvider))], ): code_grant = await auth_provider.get_token_authorization_code_grant( code=code, @@ -94,9 +90,7 @@ async def auth_callback( async def logout( request: Request, current_user: Annotated[User, Depends(get_user(personal_token_policy=PersonalTokenPolicy.DENY))], - auth_provider: Annotated[KeycloakAuthProvider, Depends(Stub(AuthProvider))], + auth_provider: Annotated[AuthProvider, Depends(Stub(AuthProvider))], ): - refresh_token = request.session.get("refresh_token", None) - request.session.clear() - await auth_provider.logout(user=current_user, refresh_token=refresh_token) + await auth_provider.logout(user=current_user, request=request) return Response(status_code=HTTPStatus.NO_CONTENT) diff --git a/data_rentgen/server/middlewares/__init__.py b/data_rentgen/server/middlewares/__init__.py index b7e37861..2cc3d3e3 100644 --- a/data_rentgen/server/middlewares/__init__.py +++ b/data_rentgen/server/middlewares/__init__.py @@ -12,7 +12,6 @@ ) from data_rentgen.server.middlewares.openapi import apply_openapi_middleware from data_rentgen.server.middlewares.request_id import apply_request_id_middleware -from data_rentgen.server.middlewares.session import apply_session_middleware from data_rentgen.server.middlewares.static_files import apply_static_files from data_rentgen.server.settings import ServerSettings @@ -29,6 +28,5 @@ def apply_middlewares( apply_application_version_middleware(application, settings.application_version) apply_openapi_middleware(application, settings.openapi) apply_static_files(application, settings.static_files) - apply_session_middleware(application, settings.session) return application diff --git a/data_rentgen/server/middlewares/session.py b/data_rentgen/server/middlewares/session.py deleted file mode 100644 index 3145ab8a..00000000 --- a/data_rentgen/server/middlewares/session.py +++ /dev/null @@ -1,18 +0,0 @@ -# SPDX-FileCopyrightText: 2024-present MTS PJSC -# SPDX-License-Identifier: Apache-2.0 -from fastapi import FastAPI -from starlette.middleware.sessions import SessionMiddleware - -from data_rentgen.server.settings.session import SessionSettings - - -def apply_session_middleware(app: FastAPI, settings: SessionSettings) -> FastAPI: - """Add SessionMiddleware middleware to the application.""" - if not settings.enabled: - return app - - settings_dict = settings.model_dump(exclude={"secret_key", "enabled"}) - settings_dict["secret_key"] = settings.secret_key.get_secret_value() # type: ignore[union-attr] - - app.add_middleware(SessionMiddleware, **settings_dict) - return app diff --git a/data_rentgen/server/providers/auth/base_provider.py b/data_rentgen/server/providers/auth/base_provider.py index a1f66710..6e434991 100644 --- a/data_rentgen/server/providers/auth/base_provider.py +++ b/data_rentgen/server/providers/auth/base_provider.py @@ -113,9 +113,5 @@ async def get_token_authorization_code_grant( """ @abstractmethod - async def logout( - self, - user: User, - refresh_token: str, - ) -> None: + async def logout(self, user: User, request: Request) -> None: """This method should implement user logout logic""" diff --git a/data_rentgen/server/providers/auth/dummy_provider.py b/data_rentgen/server/providers/auth/dummy_provider.py index 2c637f6b..9ee7c526 100644 --- a/data_rentgen/server/providers/auth/dummy_provider.py +++ b/data_rentgen/server/providers/auth/dummy_provider.py @@ -118,6 +118,6 @@ async def get_token_authorization_code_grant( msg = "Authorization code grant is not supported by DummyAuthProvider" raise NotImplementedError(msg) - async def logout(self, user: User, refresh_token: str | None) -> None: + async def logout(self, user: User, request: Request) -> None: msg = "Logout method is not implemented for DummyAuthProvider" raise NotImplementedError(msg) diff --git a/data_rentgen/server/providers/auth/keycloak_provider.py b/data_rentgen/server/providers/auth/keycloak_provider.py index 5065ddcf..67012636 100644 --- a/data_rentgen/server/providers/auth/keycloak_provider.py +++ b/data_rentgen/server/providers/auth/keycloak_provider.py @@ -6,6 +6,7 @@ from fastapi import Depends, FastAPI, Request from jwcrypto.common import JWException from keycloak import KeycloakOpenID, KeycloakOperationError +from starlette.middleware.sessions import SessionMiddleware from data_rentgen.db.models import User from data_rentgen.dependencies import Stub @@ -30,7 +31,7 @@ def __init__( self.settings = settings self._uow = unit_of_work self.keycloak_openid = KeycloakOpenID( - server_url=self.settings.keycloak.server_url, + server_url=str(self.settings.keycloak.api_url).rstrip("/") + "/", client_id=self.settings.keycloak.client_id, realm_name=self.settings.keycloak.realm_name, client_secret_key=self.settings.keycloak.client_secret.get_secret_value(), @@ -49,6 +50,13 @@ async def get_settings(): app.dependency_overrides[AuthProvider] = cls app.dependency_overrides[KeycloakAuthProviderSettings] = get_settings + + app.add_middleware( + SessionMiddleware, + secret_key=settings.cookie.secret_key.get_secret_value(), + session_cookie=settings.cookie.name, + **settings.cookie.model_dump(exclude={"secret_key", "name"}), + ) return app async def get_token_password_grant( @@ -67,18 +75,14 @@ async def get_token_authorization_code_grant( return await self.keycloak_openid.a_token( grant_type="authorization_code", code=code, - redirect_uri=self.settings.keycloak.redirect_uri, + redirect_uri=self.settings.keycloak.ui_callback_url, ) except KeycloakOperationError as e: logger.exception("Fail to get token from keycloak: %s", e) # noqa: TRY401 msg = "Failed to get token" raise AuthorizationError(msg) from e - async def get_current_user( - self, - access_token: str | None, - request: Request, - ) -> User: + async def get_current_user(self, access_token: str | None, request: Request) -> User: # we ignore explicit token passed via Authorization header access_token = request.session.get("access_token") if not access_token: @@ -100,13 +104,13 @@ async def get_current_user( if token_info is None: await self.redirect_to_auth() - # this name is hardcoded in keycloak: + # field name is hardcoded in keycloak: # https://github.com/keycloak/keycloak/blob/3ca3a4ad349b4d457f6829eaf2ae05f1e01408be/core/src/main/java/org/keycloak/representations/IDToken.java - login = token_info.get("preferred_username") # type: ignore[union-attr] + login = token_info.get("preferred_username") if not login: msg = "Invalid token" raise AuthorizationError(msg) - return await self._uow.user.get_or_create(UserDTO(name=login)) # type: ignore[arg-type] + return await self._uow.user.get_or_create(UserDTO(name=login)) async def decode_token(self, access_token: str) -> dict[str, Any] | None: try: @@ -115,7 +119,7 @@ async def decode_token(self, access_token: str) -> dict[str, Any] | None: logger.debug("Access token is invalid or expired: %s", err) return None - async def refresh_access_token(self, refresh_token: str) -> tuple[str, str]: # type: ignore[return] + async def refresh_access_token(self, refresh_token: str) -> tuple[str, str]: try: new_tokens = await self.keycloak_openid.a_refresh_token(refresh_token) logger.debug("Access token refreshed") @@ -126,7 +130,7 @@ async def refresh_access_token(self, refresh_token: str) -> tuple[str, str]: # async def redirect_to_auth(self) -> NoReturn: auth_url = await self.keycloak_openid.a_auth_url( - redirect_uri=self.settings.keycloak.redirect_uri, + redirect_uri=self.settings.keycloak.ui_callback_url, scope=self.settings.keycloak.scope, ) @@ -136,7 +140,10 @@ async def redirect_to_auth(self) -> NoReturn: details=auth_url, ) - async def logout(self, user: User, refresh_token: str | None) -> None: + async def logout(self, user: User, request: Request) -> None: + request.session.pop("access_token", None) + + refresh_token = request.session.pop("refresh_token", None) if not refresh_token: logger.debug("No refresh token found in session.") return @@ -144,6 +151,6 @@ async def logout(self, user: User, refresh_token: str | None) -> None: try: await self.keycloak_openid.a_logout(refresh_token) except KeycloakOperationError as err: + logger.debug("Can't logout user %s: %s", user.name, err) msg = f"Can't logout user: {user.name}" - logger.debug("%s. Error: %s", msg, err) raise LogoutError(msg) from err diff --git a/data_rentgen/server/providers/auth/personal_token_provider.py b/data_rentgen/server/providers/auth/personal_token_provider.py index 35e97ce3..1f9c6d9a 100644 --- a/data_rentgen/server/providers/auth/personal_token_provider.py +++ b/data_rentgen/server/providers/auth/personal_token_provider.py @@ -212,6 +212,6 @@ async def get_token_authorization_code_grant( msg = "Authorization code grant is not supported by PersonalTokenAuthProvider" raise NotImplementedError(msg) - async def logout(self, user: User, refresh_token: str | None) -> None: + async def logout(self, user: User, request: Request) -> None: msg = "Logout method is not implemented for PersonalTokenAuthProvider" raise NotImplementedError(msg) diff --git a/data_rentgen/server/settings/auth/keycloak.py b/data_rentgen/server/settings/auth/keycloak.py index 77cf7453..f719fc9c 100644 --- a/data_rentgen/server/settings/auth/keycloak.py +++ b/data_rentgen/server/settings/auth/keycloak.py @@ -1,16 +1,97 @@ # SPDX-FileCopyrightText: 2024-present MTS PJSC # SPDX-License-Identifier: Apache-2.0 -from pydantic import BaseModel, Field, SecretStr +import textwrap +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, HttpUrl, SecretStr class KeycloakSettings(BaseModel): - server_url: str = Field(..., description="Keycloak server URL") - client_id: str = Field(..., description="Keycloak client ID") - realm_name: str = Field(..., description="Keycloak realm name") - client_secret: SecretStr = Field(..., description="Keycloak client secret") - redirect_uri: str = Field(..., description="Redirect URI") + api_url: HttpUrl = Field(description="Keycloak API URL") + client_id: str = Field(description="Keycloak client ID") + client_secret: SecretStr = Field(description="Keycloak client secret") + realm_name: str = Field(description="Keycloak realm name") + ui_callback_url: str = Field(description="SyncMaster UI auth callback endpoint") verify_ssl: bool = Field(default=True, description="Verify SSL certificates") - scope: str = Field("openid", description="Keycloak scope") + scope: str = Field(default="openid", description="Keycloak scope") + + +class KeycloakCookieSettings(BaseModel): + """Keycloak cookie Middleware Settings. + + See [SessionMiddleware](https://www.starlette.io/middleware/#sessionmiddleware) documentation. + + !!! note + + You can pass here any extra option supported by `SessionMiddleware`, + even if it is not mentioned in documentation. + + Examples + -------- + + For development environment: + + ```yaml title="config.yml" + auth: + provider: data_rentgen.server.providers.auth.keycloak_provider.KeycloakAuthProvider + keycloak: ... + cookie: + secret_key: cookie_secret + name: custom_name + max_age: null + same_site: lax + https_only: false + domain: localhost + ``` + For production environment: + + ```yaml title="config.yml" + auth: + provider: data_rentgen.server.providers.auth.keycloak_provider.KeycloakAuthProvider + keycloak: ... + cookie: + secret_key: cookie_secret + name: custom_name + max_age: 2678400 # 31 days + same_site: strict + https_only: true + domain: example.com + ``` + """ + + secret_key: SecretStr = Field( + description=textwrap.dedent( + """ + Secret key for encrypting cookies. + + Can be any string. It is recommended to generate random value for every application instance, e.g.: + + ```shell + pwgen 32 1 + ``` + """, + ), + ) + name: str = Field( + default="session", + description="Name of the session cookie. Change this if there are multiple application under the same domain.", + ) + max_age: int | None = Field( + default=14 * 24 * 60 * 60, + description="Session expiry time in seconds. Defaults to 2 weeks.", + ) + same_site: Literal["strict", "lax", "none"] = Field( + default="lax", + description="Prevents cookie from being sent with cross-site requests.", + ) + path: str = Field(default="/", description="Path to restrict session cookie access.") + https_only: bool = Field(default=False, description="Secure flag for HTTPS-only access.") + domain: str | None = Field( + default=None, + description="Domain for sharing cookies between subdomains or cross-domains.", + ) + + model_config = ConfigDict(extra="allow") class KeycloakAuthProviderSettings(BaseModel): @@ -21,18 +102,23 @@ class KeycloakAuthProviderSettings(BaseModel): ```yaml title="config.yml" auth: - provider: data_rentgen.server.providers.auth.keycloak_provider.KeycloakAuthProvider - keycloak: - server_url: http://keycloak:8080 - redirect_uri: http://localhost:8000/auth-callback - realm_name: fastapi_realm - client_id: fastapi_client - client_secret: generated_by_keycloak - scope: email - verify_ssl: false + provider: data_rentgen.server.providers.auth.keycloak_provider.KeycloakAuthProvider + keycloak: + api_url: http://keycloak:8080 + ui_callback_url: http://localhost:3000/auth-callback + realm_name: fastapi_realm + client_id: fastapi_client + client_secret: generated_by_keycloak + scope: email + verify_ssl: false + cookie: + secret_key: cookie_secret ``` """ keycloak: KeycloakSettings = Field( description="Keycloak settings", ) + cookie: KeycloakCookieSettings = Field( + description="Keycloak cookie settings", + ) diff --git a/data_rentgen/server/settings/server.py b/data_rentgen/server/settings/server.py index bd49b45e..8b0e7553 100644 --- a/data_rentgen/server/settings/server.py +++ b/data_rentgen/server/settings/server.py @@ -10,7 +10,6 @@ from data_rentgen.server.settings.monitoring import MonitoringSettings from data_rentgen.server.settings.openapi import OpenAPISettings from data_rentgen.server.settings.request_id import RequestIDSettings -from data_rentgen.server.settings.session import SessionSettings from data_rentgen.server.settings.static_files import StaticFilesSettings @@ -71,7 +70,3 @@ class ServerSettings(BaseModel): default_factory=OpenAPISettings, description="[OpenAPI.json settings][configuration-server-openapi]", ) - session: SessionSettings = Field( - default_factory=SessionSettings, # type: ignore[arg-type] - description="[Session settings][configuration-server-session]", - ) diff --git a/data_rentgen/server/settings/session.py b/data_rentgen/server/settings/session.py deleted file mode 100644 index 5235c197..00000000 --- a/data_rentgen/server/settings/session.py +++ /dev/null @@ -1,85 +0,0 @@ -# SPDX-FileCopyrightText: 2024-present MTS PJSC -# SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations - -import textwrap - -from pydantic import BaseModel, ConfigDict, Field, SecretStr, ValidationInfo, field_validator - -DEFAULT_MAX_AGE = 1_209_600 - - -class SessionSettings(BaseModel): - """Session Middleware Settings. - - Required for [auth-server-keycloak][]. - - See [SessionMiddleware](https://www.starlette.io/middleware/#sessionmiddleware) documentation. - - !!! note - - You can pass here any extra option supported by `SessionMiddleware`, - even if it is not mentioned in documentation. - - Examples - -------- - - ```yaml title="config.yml" - server: - session: - enabled: true - secret_key: secret - session_cookie: custom_cookie_name - max_age: null # cookie will last as long as the browser session - same_site: strict - https_only: true - domain: example.com - ``` - """ - - enabled: bool = Field( - default=True, - description="Set to `True` to enable SessionMiddleware", - ) - secret_key: SecretStr | None = Field( - default=None, - description=textwrap.dedent( - """ - Secret key for encrypting cookies. - - Can be any string. It is recommended to generate random value for every application instance, e.g.: - - ```shell - pwgen 32 1 - ``` - """, - ), - ) - session_cookie: str | None = Field( - default="session", - description="Name of the session cookie. Change this if there are multiple application under the same domain.", - ) - max_age: int | None = Field( - default=DEFAULT_MAX_AGE, - description="Session expiry time in seconds. Defaults to 2 weeks.", - ) - same_site: str | None = Field( - default="lax", - description="Prevents cookie from being sent with cross-site requests.", - ) - path: str | None = Field(default="/", description="Path to restrict session cookie access.") - https_only: bool = Field(default=False, description="Secure flag for HTTPS-only access.") - domain: str | None = Field( - default=None, - description="Domain for sharing cookies between subdomains or cross-domains.", - ) - - model_config = ConfigDict(extra="allow") - - @field_validator("secret_key", mode="after") - @classmethod - def _validate_secret_key(cls, value: SecretStr | None, info: ValidationInfo) -> SecretStr | None: - if not value and info.data.get("enabled"): - msg = "secret_key is required" - raise ValueError(msg) - return value diff --git a/mddocs/docs/changelog/next_release/508.breaking.md b/mddocs/docs/changelog/next_release/508.breaking.md new file mode 100644 index 00000000..7031442a --- /dev/null +++ b/mddocs/docs/changelog/next_release/508.breaking.md @@ -0,0 +1,28 @@ +Moved `server.session` middleware settings to `auth` block. Also renamed some fields in `auth.keycloak` settings block. + +Before: + +```yaml +auth: + provider: ... + keycloak: + server_url: ... + redirect_url: ... + +server: + session: + enabled: true + secret_key: ... +``` + +Now: + +```yaml +auth: + provider: + keycloak: + api_url: ... + ui_callback_url: ... + cookie: + secret_key: ... +``` diff --git a/mddocs/docs/reference/server/auth/keycloak.md b/mddocs/docs/reference/server/auth/keycloak.md index c91ebd2b..2dc4ac21 100644 --- a/mddocs/docs/reference/server/auth/keycloak.md +++ b/mddocs/docs/reference/server/auth/keycloak.md @@ -99,6 +99,10 @@ end options: show_root_heading: true +::: data_rentgen.server.settings.auth.keycloak.KeycloakCookieSettings + options: + show_root_heading: true + ::: data_rentgen.server.settings.auth.keycloak.KeycloakSettings options: show_root_heading: true diff --git a/mddocs/docs/reference/server/configuration/session.md b/mddocs/docs/reference/server/configuration/session.md deleted file mode 100644 index c09d7354..00000000 --- a/mddocs/docs/reference/server/configuration/session.md +++ /dev/null @@ -1,5 +0,0 @@ -# Session cookie settings { #configuration-server-session } - -These settings used to control cookie settings used for storing user session data. - -::: data_rentgen.server.settings.session.SessionSettings diff --git a/tests/test_server/fixtures/keycloak.py b/tests/test_server/fixtures/keycloak.py index 10d52a76..1c3e1326 100644 --- a/tests/test_server/fixtures/keycloak.py +++ b/tests/test_server/fixtures/keycloak.py @@ -11,7 +11,7 @@ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from itsdangerous import TimestampSigner -from data_rentgen.server.settings.auth.keycloak import KeycloakSettings +from data_rentgen.server.settings.auth.keycloak import KeycloakCookieSettings, KeycloakSettings @pytest.fixture(scope="session") @@ -49,9 +49,11 @@ def get_public_key_pem(public_key): @pytest.fixture def create_session_cookie(rsa_keys, server_app_settings): + cookie_settings = KeycloakCookieSettings.model_validate(server_app_settings.auth.cookie) + def _create_session_cookie(user, expire_in_msec=5000) -> str: private_pem = rsa_keys["private_pem"] - session_secret_key = server_app_settings.server.session.secret_key.get_secret_value() + session_secret_key = cookie_settings.secret_key.get_secret_value() payload = { "sub": str(user.id), @@ -89,10 +91,10 @@ def _create_session_cookie(user, expire_in_msec=5000) -> str: @pytest.fixture def mock_keycloak_well_known(server_app_settings, respx_mock): keycloak_settings = KeycloakSettings.model_validate(server_app_settings.auth.keycloak) - server_url = keycloak_settings.server_url + api_url = keycloak_settings.api_url realm_name = keycloak_settings.realm_name - realm_url = f"{server_url}/realms/{realm_name}" + realm_url = f"{api_url}realms/{realm_name}" well_known_url = f"{realm_url}/.well-known/openid-configuration" openid_url = f"{realm_url}/protocol/openid-connect" respx_mock.get(well_known_url).respond( @@ -112,11 +114,11 @@ def mock_keycloak_well_known(server_app_settings, respx_mock): @pytest.fixture def mock_keycloak_realm(server_app_settings, rsa_keys, respx_mock): keycloak_settings = KeycloakSettings.model_validate(server_app_settings.auth.keycloak) - server_url = keycloak_settings.server_url + api_url = keycloak_settings.api_url realm_name = keycloak_settings.realm_name public_pem_str = get_public_key_pem(rsa_keys["public_key"]) - realm_url = f"{server_url}/realms/{realm_name}" + realm_url = f"{api_url}realms/{realm_name}" openid_url = f"{realm_url}/protocol/openid-connect" respx_mock.get(realm_url).respond( status_code=200, @@ -133,7 +135,7 @@ def mock_keycloak_realm(server_app_settings, rsa_keys, respx_mock): @pytest.fixture def mock_keycloak_token_refresh(user, server_app_settings, rsa_keys, respx_mock): keycloak_settings = KeycloakSettings.model_validate(server_app_settings.auth.keycloak) - server_url = keycloak_settings.server_url + api_url = keycloak_settings.api_url realm_name = keycloak_settings.realm_name # generate new access and refresh tokens @@ -156,7 +158,7 @@ def mock_keycloak_token_refresh(user, server_app_settings, rsa_keys, respx_mock) ) new_refresh_token = "mock_new_refresh_token" - realm_url = f"{server_url}/realms/{realm_name}" + realm_url = f"{api_url}realms/{realm_name}" openid_url = f"{realm_url}/protocol/openid-connect" respx_mock.post(f"{openid_url}/token").respond( status_code=200, @@ -173,10 +175,10 @@ def mock_keycloak_token_refresh(user, server_app_settings, rsa_keys, respx_mock) @pytest.fixture def mock_keycloak_logout(server_app_settings, respx_mock): keycloak_settings = KeycloakSettings.model_validate(server_app_settings.auth.keycloak) - server_url = keycloak_settings.server_url + api_url = keycloak_settings.api_url realm_name = keycloak_settings.realm_name - realm_url = f"{server_url}/realms/{realm_name}" + realm_url = f"{api_url}realms/{realm_name}" openid_url = f"{realm_url}/protocol/openid-connect" respx_mock.post(f"{openid_url}/logout").respond( status_code=204, @@ -187,10 +189,10 @@ def mock_keycloak_logout(server_app_settings, respx_mock): @pytest.fixture def mock_keycloak_logout_bad_request(server_app_settings, respx_mock): keycloak_settings = KeycloakSettings.model_validate(server_app_settings.auth.keycloak) - server_url = keycloak_settings.server_url + api_url = keycloak_settings.api_url realm_name = keycloak_settings.realm_name - realm_url = f"{server_url}/realms/{realm_name}" + realm_url = f"{api_url}realms/{realm_name}" openid_url = f"{realm_url}/protocol/openid-connect" respx_mock.post(f"{openid_url}/logout").respond( status_code=400, @@ -201,9 +203,9 @@ def mock_keycloak_logout_bad_request(server_app_settings, respx_mock): @pytest.fixture def mock_keycloak_certs(server_app_settings, rsa_keys, respx_mock): keycloak_settings = KeycloakSettings.model_validate(server_app_settings.auth.keycloak) - server_url = keycloak_settings.server_url + api_url = keycloak_settings.api_url realm_name = keycloak_settings.realm_name - realm_url = f"{server_url}/realms/{realm_name}" + realm_url = f"{api_url}realms/{realm_name}" openid_url = f"{realm_url}/protocol/openid-connect" def encode_number_base64(n: int): diff --git a/tests/test_server/test_auth/test_keycloak_auth.py b/tests/test_server/test_auth/test_keycloak_auth.py index 6231a763..e86a63f8 100644 --- a/tests/test_server/test_auth/test_keycloak_auth.py +++ b/tests/test_server/test_auth/test_keycloak_auth.py @@ -35,8 +35,8 @@ async def test_keycloak_get_current_user_unauthorized( # redirect unauthorized user to Keycloak redirect_url = ( - f"{settings.server_url}/realms/{settings.realm_name}/protocol/openid-connect/auth?client_id=" - f"{settings.client_id}&response_type=code&redirect_uri={settings.redirect_uri}" + f"{settings.api_url}realms/{settings.realm_name}/protocol/openid-connect/auth?client_id=" + f"{settings.client_id}&response_type=code&redirect_uri={settings.ui_callback_url}" f"&scope={settings.scope}&state=&nonce=" ) assert response.status_code == HTTPStatus.UNAUTHORIZED, response.json() @@ -189,8 +189,8 @@ async def test_keycloak_auth_logout( next_response = await test_client.get("v1/users/me") # redirect unauthorized user to Keycloak redirect_url = ( - f"{settings.server_url}/realms/{settings.realm_name}/protocol/openid-connect/auth?client_id=" - f"{settings.client_id}&response_type=code&redirect_uri={settings.redirect_uri}" + f"{settings.api_url}realms/{settings.realm_name}/protocol/openid-connect/auth?client_id=" + f"{settings.client_id}&response_type=code&redirect_uri={settings.ui_callback_url}" f"&scope={settings.scope}&state=&nonce=" ) assert next_response.status_code == HTTPStatus.UNAUTHORIZED, response.json()