Skip to content

Commit e4e7a50

Browse files
committed
[DOP-21576] Move /auth/me -> /users/me; Remove state from redirect
1 parent e290703 commit e4e7a50

File tree

6 files changed

+57
-121
lines changed

6 files changed

+57
-121
lines changed

.env.local

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export DATA_RENTGEN__AUTH__KEYCLOAK__SERVER_URL=http://localhost:8080
2020
export DATA_RENTGEN__AUTH__KEYCLOAK__REALM_NAME=manually_created
2121
export DATA_RENTGEN__AUTH__KEYCLOAK__CLIENT_ID=manually_created
2222
export DATA_RENTGEN__AUTH__KEYCLOAK__CLIENT_SECRET=change_me
23-
export DATA_RENTGEN__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:3000/callback
23+
export DATA_RENTGEN__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:3000/auth/callback
2424
export DATA_RENTGEN__AUTH__KEYCLOAK__SCOPE=email
2525
export DATA_RENTGEN__AUTH__KEYCLOAK__VERIFY_SSL=False
2626
export DATA_RENTGEN__AUTH__PROVIDER=data_rentgen.server.providers.auth.keycloak_provider.KeycloakAuthProvider

data_rentgen/server/api/v1/router/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from data_rentgen.server.api.v1.router.location import router as location_router
99
from data_rentgen.server.api.v1.router.operation import router as operation_router
1010
from data_rentgen.server.api.v1.router.run import router as run_router
11+
from data_rentgen.server.api.v1.router.user import router as user_router
1112

1213
router = APIRouter(prefix="/v1")
1314
router.include_router(auth_router)
@@ -16,3 +17,4 @@
1617
router.include_router(location_router)
1718
router.include_router(operation_router)
1819
router.include_router(run_router)
20+
router.include_router(user_router)
Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
# SPDX-FileCopyrightText: 2024 MTS PJSC
22
# SPDX-License-Identifier: Apache-2.0
3-
import base64
43
from http import HTTPStatus
5-
from logging import getLogger
64
from typing import Annotated
75

8-
from fastapi import APIRouter, Depends, HTTPException, Request
6+
from fastapi import APIRouter, Depends, Request
97
from fastapi.security import OAuth2PasswordRequestForm
108

11-
from data_rentgen.db.models.user import User
129
from data_rentgen.dependencies import Stub
1310
from data_rentgen.server.errors.registration import get_error_responses
1411
from data_rentgen.server.errors.schemas.invalid_request import InvalidRequestSchema
@@ -22,11 +19,6 @@
2219
KeycloakAuthProvider,
2320
)
2421
from data_rentgen.server.schemas.v1.auth import AuthTokenSchema
25-
from data_rentgen.server.schemas.v1.user import UserResponseV1
26-
from data_rentgen.server.services import get_user
27-
28-
logger = getLogger(__name__)
29-
3022

3123
router = APIRouter(
3224
prefix="/auth",
@@ -49,15 +41,9 @@ async def token(
4941
@router.get("/callback")
5042
async def auth_callback(
5143
request: Request,
52-
state: str,
5344
code: str,
5445
auth_provider: Annotated[KeycloakAuthProvider, Depends(Stub(AuthProvider))],
5546
):
56-
state = base64.b64decode(state.encode("utf-8")) # type: ignore[assignment]
57-
original_url = state.decode("utf-8") # type: ignore[attr-defined]
58-
59-
if not original_url:
60-
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid original_url")
6147
code_grant = await auth_provider.get_token_authorization_code_grant(
6248
code=code,
6349
redirect_uri=auth_provider.settings.keycloak.redirect_uri,
@@ -66,11 +52,3 @@ async def auth_callback(
6652
request.session["refresh_token"] = code_grant["refresh_token"]
6753

6854
return HTTPStatus.OK
69-
70-
71-
@router.get("/me")
72-
async def check_auth(
73-
current_user: User = Depends(get_user()),
74-
):
75-
logger.info("User check: %s", current_user.name)
76-
return UserResponseV1.model_validate(current_user)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# SPDX-FileCopyrightText: 2024 MTS PJSC
2+
# SPDX-License-Identifier: Apache-2.0
3+
from logging import getLogger
4+
5+
from fastapi import APIRouter, Depends
6+
7+
from data_rentgen.db.models.user import User
8+
from data_rentgen.server.errors.registration import get_error_responses
9+
from data_rentgen.server.errors.schemas.invalid_request import InvalidRequestSchema
10+
from data_rentgen.server.errors.schemas.not_authorized import (
11+
NotAuthorizedRedirectSchema,
12+
NotAuthorizedSchema,
13+
)
14+
from data_rentgen.server.schemas.v1.user import UserResponseV1
15+
from data_rentgen.server.services import get_user
16+
17+
logger = getLogger(__name__)
18+
19+
20+
router = APIRouter(
21+
prefix="/users",
22+
tags=["User"],
23+
responses=get_error_responses(include={NotAuthorizedSchema, InvalidRequestSchema, NotAuthorizedRedirectSchema}),
24+
)
25+
26+
27+
@router.get("/me")
28+
async def check_auth(
29+
current_user: User = Depends(get_user()),
30+
):
31+
logger.info("User check: %s", current_user.name)
32+
return UserResponseV1.model_validate(current_user)

data_rentgen/server/providers/auth/keycloak_provider.py

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
# SPDX-FileCopyrightText: 2024 MTS PJSC
22
# SPDX-License-Identifier: Apache-2.0
3-
import base64
43
import logging
5-
from http import HTTPStatus
64
from typing import Annotated, Any
75

8-
from fastapi import Depends, FastAPI, HTTPException, Request
6+
from fastapi import Depends, FastAPI, Request
97
from keycloak import KeycloakOpenID
10-
from keycloak.exceptions import KeycloakConnectionError
118

129
from data_rentgen.db.models import User
1310
from data_rentgen.dependencies import Stub
@@ -81,7 +78,7 @@ async def get_current_user(self, access_token: str, *args, **kwargs) -> User:
8178

8279
if not access_token:
8380
logger.debug("No access token found in session.")
84-
self.redirect_to_auth(str(request.url))
81+
self.redirect_to_auth()
8582

8683
# if user is disabled or blocked in Keycloak after the token is issued, he will
8784
# remain authorized until the token expires (not more than 15 minutes in MTS SSO)
@@ -96,7 +93,7 @@ async def get_current_user(self, access_token: str, *args, **kwargs) -> User:
9693

9794
if token_info is None:
9895
# If there is no token_info after refresh user get redirect
99-
self.redirect_to_auth(str(request.url))
96+
self.redirect_to_auth()
10097

10198
# these names are hardcoded in keycloak:
10299
# https://github.com/keycloak/keycloak/blob/3ca3a4ad349b4d457f6829eaf2ae05f1e01408be/core/src/main/java/org/keycloak/representations/IDToken.java
@@ -107,11 +104,7 @@ async def get_current_user(self, access_token: str, *args, **kwargs) -> User:
107104
return await self._uow.user.get_or_create(UserDTO(name=login)) # type: ignore[arg-type]
108105

109106
async def logout(self, refresh_token: str):
110-
try:
111-
return self.keycloak_openid.logout(refresh_token)
112-
except KeycloakConnectionError as err:
113-
logger.error("Error when trying to get token: %s", err)
114-
return None
107+
return self.keycloak_openid.logout(refresh_token)
115108

116109
def decode_token(self, access_token: str) -> dict[str, Any] | None:
117110
try:
@@ -127,21 +120,14 @@ def refresh_access_token(self, refresh_token: str, origin_url: str) -> tuple[str
127120
return new_tokens.get("access_token"), new_tokens.get("refresh_token")
128121
except Exception as err:
129122
logger.debug("Failed to refresh access token: %s", err)
130-
self.redirect_to_auth(origin_url)
123+
self.redirect_to_auth()
131124

132-
def redirect_to_auth(self, state: str = ""):
133-
try:
134-
state = base64.b64encode(state.encode("utf-8")) # type: ignore[assignment]
125+
def redirect_to_auth(self):
135126

136-
auth_url = self.keycloak_openid.auth_url(
137-
redirect_uri=self.settings.keycloak.redirect_uri,
138-
scope=self.settings.keycloak.scope,
139-
state=state.decode("utf-8"), # type: ignore[attr-defined]
140-
)
127+
auth_url = self.keycloak_openid.auth_url(
128+
redirect_uri=self.settings.keycloak.redirect_uri,
129+
scope=self.settings.keycloak.scope,
130+
)
141131

142-
except KeycloakConnectionError as err:
143-
logger.error("Failed connect to Keycloak: %s", err)
144-
# TODO: What exception should we raise here?
145-
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=err)
146132
logger.info("Raising redirect error with url: %s", auth_url)
147-
raise RedirectError(message="Authorize on provided url", details=auth_url)
133+
raise RedirectError(message="Please authorize using provided URL", details=auth_url)
Lines changed: 10 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import base64
21
import logging
32

43
import pytest
@@ -9,7 +8,6 @@
98
from data_rentgen.db.models import Dataset, User
109
from data_rentgen.server.settings import ServerApplicationSettings as Settings
1110
from data_rentgen.server.settings.auth.keycloak import KeycloakSettings
12-
from tests.test_server.utils.enrich import enrich_datasets
1311

1412
KEYCLOAK_PROVIDER = "data_rentgen.server.providers.auth.keycloak_provider.KeycloakAuthProvider"
1513
pytestmark = [pytest.mark.asyncio, pytest.mark.server]
@@ -33,23 +31,21 @@ async def test_get_keycloak_user_unauthorized(
3331
caplog,
3432
server_app_settings: Settings,
3533
):
36-
k_settings = KeycloakSettings.model_validate(server_app_settings.auth.keycloak)
34+
settings = KeycloakSettings.model_validate(server_app_settings.auth.keycloak)
3735

38-
response = await test_client.get("/v1/datasets")
36+
response = await test_client.get("/v1/users/me")
3937

4038
# redirect unauthorized user to Keycloak
41-
state = str(response.url).encode("utf-8")
42-
state = base64.b64encode(state)
4339
redirect_url = (
44-
f"{k_settings.server_url}/realms/{k_settings.realm_name}/protocol/openid-connect/auth?client_id="
45-
f"{k_settings.client_id}&response_type=code&redirect_uri={k_settings.redirect_uri}"
46-
f"&scope={k_settings.scope}&state={state.decode('utf-8')}&nonce="
40+
f"{settings.server_url}/realms/{settings.realm_name}/protocol/openid-connect/auth?client_id="
41+
f"{settings.client_id}&response_type=code&redirect_uri={settings.redirect_uri}"
42+
f"&scope={settings.scope}&state=&nonce="
4743
)
4844
assert response.status_code == 401
4945
assert response.json() == {
5046
"error": {
5147
"code": "auth_redirect",
52-
"message": "Authorize on provided url",
48+
"message": "Please authorize using provided URL",
5349
"details": redirect_url,
5450
},
5551
}
@@ -71,53 +67,24 @@ async def test_get_keycloak_user_authorized(
7167
test_client: AsyncClient,
7268
async_session: AsyncSession,
7369
user: User,
74-
datasets: list[Dataset],
7570
server_app_settings: Settings,
7671
create_session_cookie,
7772
mock_keycloak_well_known,
7873
mock_keycloak_realm,
7974
):
80-
datasets = await enrich_datasets(datasets, async_session)
8175
session_cookie = create_session_cookie(user)
8276
headers = {
8377
"Cookie": f"session={session_cookie}",
8478
}
8579

8680
response = await test_client.get(
87-
"/v1/datasets",
81+
"/v1/users/me",
8882
headers=headers,
8983
)
9084

9185
assert response.cookies.get("session") == session_cookie
9286
assert response.status_code == 200
93-
assert response.json() == {
94-
"meta": {
95-
"page": 1,
96-
"page_size": 20,
97-
"total_count": len(datasets),
98-
"pages_count": 1,
99-
"has_next": False,
100-
"has_previous": False,
101-
"next_page": None,
102-
"previous_page": None,
103-
},
104-
"items": [
105-
{
106-
"kind": "DATASET",
107-
"id": dataset.id,
108-
"format": dataset.format,
109-
"name": dataset.name,
110-
"location": {
111-
"id": dataset.location.id,
112-
"name": dataset.location.name,
113-
"type": dataset.location.type,
114-
"addresses": [{"url": address.url} for address in dataset.location.addresses],
115-
"external_id": dataset.location.external_id,
116-
},
117-
}
118-
for dataset in sorted(datasets, key=lambda x: x.name)
119-
],
120-
}
87+
assert response.json() == {"name": user.name}
12188

12289

12390
@responses.activate
@@ -135,7 +102,6 @@ async def test_get_keycloak_user_authorized(
135102
async def test_get_keycloak_user_expired_access_token(
136103
caplog,
137104
user: User,
138-
datasets: list[Dataset],
139105
test_client: AsyncClient,
140106
async_session: AsyncSession,
141107
server_app_settings: Settings,
@@ -144,45 +110,17 @@ async def test_get_keycloak_user_expired_access_token(
144110
mock_keycloak_realm,
145111
mock_keycloak_token_refresh,
146112
):
147-
datasets = await enrich_datasets(datasets, async_session)
148113
session_cookie = create_session_cookie(user, expire_in_msec=-100000000) # expired access token
149114
headers = {
150115
"Cookie": f"session={session_cookie}",
151116
}
152117

153118
with caplog.at_level(logging.DEBUG):
154119
response = await test_client.get(
155-
"/v1/datasets",
120+
"/v1/users/me",
156121
headers=headers,
157122
)
158123

159124
assert response.cookies.get("session") != session_cookie, caplog.text # cookie is updated
160125
assert response.status_code == 200
161-
assert response.json() == {
162-
"meta": {
163-
"page": 1,
164-
"page_size": 20,
165-
"total_count": len(datasets),
166-
"pages_count": 1,
167-
"has_next": False,
168-
"has_previous": False,
169-
"next_page": None,
170-
"previous_page": None,
171-
},
172-
"items": [
173-
{
174-
"kind": "DATASET",
175-
"id": dataset.id,
176-
"format": dataset.format,
177-
"name": dataset.name,
178-
"location": {
179-
"id": dataset.location.id,
180-
"name": dataset.location.name,
181-
"type": dataset.location.type,
182-
"addresses": [{"url": address.url} for address in dataset.location.addresses],
183-
"external_id": dataset.location.external_id,
184-
},
185-
}
186-
for dataset in sorted(datasets, key=lambda x: x.name)
187-
],
188-
}
126+
assert response.json() == {"name": user.name}

0 commit comments

Comments
 (0)