Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions config.docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ kafka:

server:
debug: false
session:
enabled: true
secret_key: session_secret_key
cors:
enabled: true
allow_origins: [http://localhost:3000]
Expand All @@ -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
Expand Down
11 changes: 5 additions & 6 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ consumer:

server:
debug: true
session:
enabled: true
secret_key: session_secret_key
cors:
enabled: true
allow_origins: [http://localhost:3000]
Expand All @@ -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
Expand Down
16 changes: 5 additions & 11 deletions data_rentgen/server/api/v1/router/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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)
2 changes: 0 additions & 2 deletions data_rentgen/server/middlewares/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
18 changes: 0 additions & 18 deletions data_rentgen/server/middlewares/session.py

This file was deleted.

6 changes: 1 addition & 5 deletions data_rentgen/server/providers/auth/base_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
2 changes: 1 addition & 1 deletion data_rentgen/server/providers/auth/dummy_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
35 changes: 21 additions & 14 deletions data_rentgen/server/providers/auth/keycloak_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(),
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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")
Expand All @@ -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,
)

Expand All @@ -136,14 +140,17 @@ 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

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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading