Skip to content

Commit 8d5365f

Browse files
author
maxim-lixakov
committed
[DOP-21268] - implement KeycloakAuthProvider
1 parent 15467e5 commit 8d5365f

File tree

21 files changed

+324
-117
lines changed

21 files changed

+324
-117
lines changed

.env.docker

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,20 @@ SYNCMASTER__CRYPTO_KEY=UBgPTioFrtH2unlC4XFDiGf5sYfzbdSf_VgiUSaQc94=
1818
# Postgres
1919
SYNCMASTER__DATABASE__URL=postgresql+asyncpg://syncmaster:changeme@db:5432/syncmaster
2020

21-
# Keycloack (MTS)
22-
SYNCMASTER__AUTH__KEYCLOAK_SERVER_URL=https://isso.mts.ru/auth/
23-
SYNCMASTER__AUTH__KEYCLOAK_REALM_NAME=mts
24-
SYNCMASTER__AUTH__KEYCLOAK_CLIENT_ID=syncmaster_dev
25-
SYNCMASTER__AUTH__KEYCLOAK_CLIENT_SECRET=secret
26-
SYNCMASTER__AUTH__KEYCLOAK_REDIRECT_URI=http://localhost:8000/callback
27-
SYNCMASTER__AUTH__KEYCLOAK_ADMIN_REDIRECT_URI=http://localhost:8000/admin/callback
28-
SYNCMASTER__AUTH__KEYCLOAK_SCOPE=email
21+
# KEYCLOAK Auth
22+
SYNCMASTER__AUTH__SERVER_URL=http://keycloak:8080/
23+
SYNCMASTER__AUTH__REALM_NAME=fastapi-realm
24+
SYNCMASTER__AUTH__CLIENT_ID=fastapi-client
25+
SYNCMASTER__AUTH__CLIENT_SECRET=VoLrqGz1HGjp6MiwzRaGWIu7z7imKIHb
26+
SYNCMASTER__AUTH__REDIRECT_URI=http://localhost:8000/v1/auth/callback
27+
SYNCMASTER__AUTH__ADMIN_REDIRECT_URI=http://localhost:8000/v1/auth/callback
28+
SYNCMASTER__AUTH__SCOPE=email
2929
SYNCMASTER__AUTH__KEYCLOAK_INTROSPECTION_DELAY=60
30-
SYNCMASTER__AUTH__PROVIDER=syncmaster.backend.providers.auth.keycloak.KeycloakAuthProvider
31-
SYNCMASTER__AUTH__KEYCLOAK_TOKEN_URL=https://isso.mts.ru/auth/realms/mts/protocol/openid-connect/token
30+
SYNCMASTER__AUTH__PROVIDER=syncmaster.backend.providers.auth.keycloak_provider.KeycloakAuthProvider
31+
SYNCMASTER__AUTH__KEYCLOAK_TOKEN_URL=http://keycloak:8080/realms/fastapi-realm/protocol/openid-connect/token
3232

33-
34-
SYNCMASTER__AUTH__KEYCLOAK__SERVER_URL=http://localhost:8080/auth/
35-
SYNCMASTER__AUTH__KEYCLOAK__REALM_NAME=fastapi-realm
36-
SYNCMASTER__AUTH__KEYCLOAK__CLIENT_ID=fastapi-client
37-
SYNCMASTER__AUTH__KEYCLOAK__CLIENT_SECRET=VoLrqGz1HGjp6MiwzRaGWIu7z7imKIHb
38-
SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:8000/callback
39-
SYNCMASTER__AUTH__KEYCLOAK__ADMIN_REDIRECT_URI=http://localhost:8000/admin/callback
40-
SYNCMASTER__AUTH__KEYCLOAK__SCOPE=email
41-
SYNCMASTER__AUTH__KEYCLOAK__INTROSPECTION_DELAY=60
42-
SYNCMASTER__AUTH__PROVIDER=syncmaster.backend.providers.auth.keycloak.KeycloakAuthProvider
43-
SYNCMASTER__AUTH__KEYCLOAK__TOKEN_URL=http://localhost:8080/auth/realms/fastapi-realm/protocol/openid-connect/token
44-
45-
46-
SYNCMASTER__AUTH__PROVIDER=syncmaster.backend.providers.auth.dummy.DummyAuthProvider
33+
# Dummy Auth
34+
SYNCMASTER__AUTH__PROVIDER=syncmaster.backend.providers.auth.dummy_provider.DummyAuthProvider
4735
SYNCMASTER__AUTH__ACCESS_TOKEN__SECRET_KEY=bae1thahr8Iyaisai0kohvoh1aeg5quu
4836

4937
# RabbitMQ

.env.local

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export SYNCMASTER__CRYPTO_KEY=UBgPTioFrtH2unlC4XFDiGf5sYfzbdSf_VgiUSaQc94=
1919
export SYNCMASTER__DATABASE__URL=postgresql+asyncpg://syncmaster:changeme@localhost:5432/syncmaster
2020

2121
# Auth
22-
export SYNCMASTER__AUTH__PROVIDER=syncmaster.backend.providers.auth.dummy.DummyAuthProvider
22+
export SYNCMASTER__AUTH__PROVIDER=syncmaster.backend.providers.auth.dummy_provider.DummyAuthProvider
2323
export SYNCMASTER__AUTH__ACCESS_TOKEN__SECRET_KEY=bae1thahr8Iyaisai0kohvoh1aeg5quu
2424

2525
# RabbitMQ

poetry.lock

Lines changed: 56 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ python-json-logger = {version = "*", optional = true}
6868
asyncpg = { version = ">=0.29,<0.31", optional = true }
6969
apscheduler = { version = "^3.10.4", optional = true }
7070
starlette-exporter = {version = "^0.23.0", optional = true}
71+
itsdangerous = {version = "*", optional = true}
7172
python-keycloak = {version = "^4.7.0", optional = true}
7273
devtools = {version = "*", optional = true}
7374

@@ -90,6 +91,7 @@ backend = [
9091
"python-json-logger",
9192
"asyncpg",
9293
"devtools",
94+
"itsdangerous",
9395
"python-keycloak",
9496
# migrations only
9597
"celery",
@@ -185,6 +187,10 @@ ignore_missing_imports = true
185187
module = "pyarrow.*"
186188
ignore_missing_imports = true
187189

190+
[[tool.mypy.overrides]]
191+
module = "keycloak.*"
192+
ignore_missing_imports = true
193+
188194
[[tool.mypy.overrides]]
189195
module = "avro.*"
190196
ignore_missing_imports = true

syncmaster/backend/api/v1/auth.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
22
# SPDX-License-Identifier: Apache-2.0
3-
from typing import Annotated
3+
from typing import TYPE_CHECKING, Annotated
44

5-
from fastapi import APIRouter, Depends
5+
from fastapi import APIRouter, Depends, HTTPException, Request
6+
from fastapi.responses import RedirectResponse
67
from fastapi.security import OAuth2PasswordRequestForm
78

89
from syncmaster.backend.dependencies import Stub
910
from syncmaster.backend.providers.auth import AuthProvider
11+
from syncmaster.backend.utils.state import validate_state
1012
from syncmaster.errors.registration import get_error_responses
1113
from syncmaster.errors.schemas.invalid_request import InvalidRequestSchema
1214
from syncmaster.errors.schemas.not_authorized import NotAuthorizedSchema
1315
from syncmaster.schemas.v1.auth import AuthTokenSchema
1416

17+
if TYPE_CHECKING:
18+
from syncmaster.backend.providers.auth import (
19+
DummyAuthProvider,
20+
KeycloakAuthProvider,
21+
)
22+
1523
router = APIRouter(
1624
prefix="/auth",
1725
tags=["Auth"],
@@ -20,11 +28,11 @@
2028

2129

2230
@router.post("/token")
23-
async def login(
24-
auth_provider: Annotated[AuthProvider, Depends(Stub(AuthProvider))],
31+
async def token(
32+
auth_provider: Annotated["DummyAuthProvider", Depends(Stub(AuthProvider))],
2533
form_data: OAuth2PasswordRequestForm = Depends(),
2634
) -> AuthTokenSchema:
27-
token = await auth_provider.get_token(
35+
token = await auth_provider.get_token_password_grant(
2836
grant_type=form_data.grant_type,
2937
login=form_data.username,
3038
password=form_data.password,
@@ -33,3 +41,21 @@ async def login(
3341
client_secret=form_data.client_secret,
3442
)
3543
return AuthTokenSchema.parse_obj(token)
44+
45+
46+
@router.get("/callback")
47+
async def auth_callback(
48+
request: Request,
49+
code: str,
50+
state: str,
51+
auth_provider: Annotated["KeycloakAuthProvider", Depends(Stub(AuthProvider))],
52+
):
53+
original_redirect_url = validate_state(state)
54+
if not original_redirect_url:
55+
raise HTTPException(status_code=400, detail="Invalid state parameter")
56+
token = await auth_provider.get_token_authorization_code_grant(
57+
code=code,
58+
redirect_uri=auth_provider.settings.redirect_uri,
59+
)
60+
request.session["access_token"] = token["access_token"]
61+
return RedirectResponse(url=original_redirect_url)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
22
# SPDX-License-Identifier: Apache-2.0
33

4+
from syncmaster.backend.dependencies.get_access_token import get_access_token
45
from syncmaster.backend.dependencies.stub import Stub
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
2+
# SPDX-License-Identifier: Apache-2.0
3+
from fastapi import Request
4+
from fastapi.security.utils import get_authorization_scheme_param
5+
6+
7+
async def get_access_token(request: Request):
8+
authorization = request.headers.get("Authorization")
9+
scheme, token = get_authorization_scheme_param(authorization)
10+
if not authorization or scheme.lower() != "bearer":
11+
return None
12+
return token

syncmaster/backend/handler.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
from fastapi import HTTPException, Request, Response, status
77
from fastapi.exceptions import RequestValidationError
8+
from fastapi.responses import RedirectResponse
89
from pydantic import ValidationError
910

1011
from syncmaster.errors.base import APIErrorSchema, BaseErrorSchema
1112
from syncmaster.errors.registration import get_response_for_exception
1213
from syncmaster.exceptions import ActionNotAllowedError, SyncmasterError
14+
from syncmaster.exceptions.auth import AuthorizationError
1315
from syncmaster.exceptions.connection import (
1416
ConnectionDeleteError,
1517
ConnectionNotFoundError,
@@ -31,6 +33,7 @@
3133
QueueDeleteError,
3234
QueueNotFoundError,
3335
)
36+
from syncmaster.exceptions.redirect import RedirectException
3437
from syncmaster.exceptions.run import (
3538
CannotConnectToTaskQueueError,
3639
CannotStopRunError,
@@ -119,6 +122,14 @@ async def syncmsater_exception_handler(request: Request, exc: SyncmasterError):
119122
content=content,
120123
)
121124

125+
if isinstance(exc, RedirectException):
126+
return RedirectResponse(url=exc.redirect_url)
127+
128+
if isinstance(exc, AuthorizationError):
129+
content.code = "unauthorized"
130+
content.message = "Not authenticated"
131+
return exception_json_response(status=status.HTTP_401_UNAUTHORIZED, content=content)
132+
122133
if isinstance(exc, ConnectionDeleteError):
123134
content.code = "conflict"
124135
return exception_json_response(status=status.HTTP_409_CONFLICT, content=content)

syncmaster/backend/middlewares/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
apply_monitoring_metrics_middleware,
1010
)
1111
from syncmaster.backend.middlewares.request_id import apply_request_id_middleware
12+
from syncmaster.backend.middlewares.session import apply_session_middleware
1213
from syncmaster.settings import Settings
1314

1415

@@ -24,5 +25,6 @@ def apply_middlewares(
2425
apply_cors_middleware(application, settings.server.cors)
2526
apply_monitoring_metrics_middleware(application, settings.server.monitoring)
2627
apply_request_id_middleware(application, settings.server.request_id)
28+
apply_session_middleware(application)
2729

2830
return application
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
2+
# SPDX-License-Identifier: Apache-2.0
3+
import secrets
4+
5+
from fastapi import FastAPI
6+
from starlette.middleware.sessions import SessionMiddleware
7+
8+
9+
def apply_session_middleware(app: FastAPI) -> FastAPI:
10+
"""Add SessionMiddleware middleware to the application."""
11+
secret_key = secrets.token_urlsafe(32)
12+
app.add_middleware(SessionMiddleware, secret_key=secret_key)
13+
return app

0 commit comments

Comments
 (0)