Skip to content

Commit b20f07c

Browse files
committed
Fixes and optimisations for the Murfey authentication API
* Forwards only essential headers to the auth server to prevent timeouts due to mismatch between header, body, and methods. * Migrate authentication server querying logic out into a helper function to minimise repetition.
1 parent 467c2d0 commit b20f07c

1 file changed

Lines changed: 98 additions & 85 deletions

File tree

src/murfey/server/api/auth.py

Lines changed: 98 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import secrets
44
import time
55
from logging import getLogger
6-
from typing import Dict
76
from uuid import uuid4
87

98
import aiohttp
@@ -18,7 +17,7 @@
1817
from passlib.context import CryptContext
1918
from pydantic import BaseModel
2019
from sqlmodel import Session, create_engine, select
21-
from typing_extensions import Annotated
20+
from typing_extensions import Annotated, Any
2221

2322
from murfey.server.murfey_db import murfey_db, url
2423
from murfey.util.api import url_path_for
@@ -50,7 +49,7 @@
5049
instrument_oauth2_scheme = lambda *args, **kwargs: None
5150
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
5251

53-
instrument_server_tokens: Dict[float, dict] = {}
52+
instrument_server_tokens: dict[float, dict] = {}
5453

5554
# Set up database engine
5655
try:
@@ -66,14 +65,30 @@ def hash_password(password: str) -> str:
6665

6766
"""
6867
=======================================================================================
69-
TOKEN VALIDATION FUNCTIONS
68+
VALIDATION FUNCTIONS
7069
=======================================================================================
7170
7271
Functions and helpers used to validate incoming requests from both the client and
73-
the frontend. 'validate_token()' and 'validate_instrument_token()' are imported
74-
int the other FastAPI modules and attached as dependencies to the routers.
72+
the frontend.
73+
74+
'validate_token()' and 'validate_instrument_token()' are imported in the other FastAPI
75+
modules and attached as dependencies to the routers. They validate the tokens passed
76+
around internally by Murfey to ensure that the request is valid.
77+
78+
'validate_instrument_server_session_access()' and 'validate_frontend_session_access()'
79+
are used to verify the IDs of sessions ot be accessed, and are attached as dependencies
80+
to them.
81+
82+
'validate_user_instrument_access()' is used to verify the instrument server name being
83+
accessed by the frontend, and is attached as a dependency as well.
7584
"""
7685

86+
# Essential headers used for authentication to forward along if present
87+
AUTH_HEADERS = (
88+
"authorization",
89+
"x-auth-request-access-token",
90+
)
91+
7792

7893
def check_user(username: str) -> bool:
7994
try:
@@ -84,6 +99,39 @@ def check_user(username: str) -> bool:
8499
return username in [u.username for u in users]
85100

86101

102+
async def submit_to_auth_endpoint(
103+
url_subpath: str,
104+
request: Request,
105+
token: str,
106+
) -> dict[str, Any]:
107+
"""
108+
Helper function to forward incoming requests to an authentication server
109+
to verify that they are allowed to inspect the
110+
"""
111+
112+
# Forward only essentials auth-related headers
113+
headers = {
114+
key: value
115+
for key, value in dict(request.headers).items()
116+
if key.lower() in AUTH_HEADERS
117+
}
118+
if security_config.auth_type == "password":
119+
headers["authorization"] = f"Bearer {token}"
120+
cookies = (
121+
{security_config.cookie_key: token}
122+
if security_config.auth_type == "cookie"
123+
else {}
124+
)
125+
async with aiohttp.ClientSession(cookies=cookies) as session:
126+
async with session.get(
127+
f"{auth_url}/{url_subpath}",
128+
headers=headers,
129+
) as response:
130+
success = response.status == 200
131+
validation_outcome: dict[str, Any] = await response.json()
132+
return validation_outcome if success and validation_outcome else {"valid": False}
133+
134+
87135
async def validate_token(
88136
token: Annotated[str, Depends(oauth2_scheme)],
89137
request: Request,
@@ -94,25 +142,9 @@ async def validate_token(
94142
try:
95143
# Validate using auth URL if provided; will error if invalid
96144
if auth_url:
97-
# Extract and forward headers as-is
98-
headers = dict(request.headers)
99-
# Update/add authorization header if authenticating using password
100-
if security_config.auth_type == "password":
101-
headers["authorization"] = f"Bearer {token}"
102-
# Forward the cookie along if authenticating using cookie
103-
cookies = (
104-
{security_config.cookie_key: token}
105-
if security_config.auth_type == "cookie"
106-
else {}
107-
)
108-
async with aiohttp.ClientSession(cookies=cookies) as session:
109-
async with session.get(
110-
f"{auth_url}/validate_token",
111-
headers=headers,
112-
) as response:
113-
success = response.status == 200
114-
validation_outcome = await response.json()
115-
if not (success and validation_outcome.get("valid")):
145+
if not (
146+
await submit_to_auth_endpoint("validate_token", request, token)
147+
).get("valid"):
116148
raise JWTError
117149
# If authenticating using cookies; an auth URL MUST be provided
118150
else:
@@ -199,20 +231,6 @@ async def validate_instrument_token(
199231
return None
200232

201233

202-
"""
203-
=======================================================================================
204-
SESSION ID VALIDATION
205-
=======================================================================================
206-
207-
Annotated ints are defined here that trigger validation of the session IDs in incoming
208-
requests, verifying that the session is allowed to access the particular visit.
209-
210-
The 'MurfeySessionID...' types are imported and used in the type hints of the endpoint
211-
functions in the other FastAPI routers, depending on whether requests from the frontend
212-
or the instrument are expected.
213-
"""
214-
215-
216234
def get_visit_name(session_id: int) -> str:
217235
with Session(engine) as murfey_db:
218236
return (
@@ -222,46 +240,6 @@ def get_visit_name(session_id: int) -> str:
222240
)
223241

224242

225-
async def submit_to_auth_endpoint(url_subpath: str, token: str) -> None:
226-
if auth_url:
227-
headers = (
228-
{}
229-
if security_config.auth_type == "cookie"
230-
else {"Authorization": f"Bearer {token}"}
231-
)
232-
cookies = (
233-
{security_config.cookie_key: token}
234-
if security_config.auth_type == "cookie"
235-
else {}
236-
)
237-
async with aiohttp.ClientSession(cookies=cookies) as session:
238-
async with session.get(
239-
f"{auth_url}/{url_subpath}",
240-
headers=headers,
241-
) as response:
242-
success = response.status == 200
243-
validation_outcome: dict = await response.json()
244-
if not (success and validation_outcome.get("valid")):
245-
logger.warning("Unauthorised visit access request from frontend")
246-
raise HTTPException(
247-
status_code=status.HTTP_401_UNAUTHORIZED,
248-
detail="You do not have access to this visit",
249-
headers={"WWW-Authenticate": "Bearer"},
250-
)
251-
252-
253-
async def validate_frontend_session_access(
254-
session_id: int,
255-
token: Annotated[str, Depends(oauth2_scheme)],
256-
) -> int:
257-
"""
258-
Validates whether a frontend request can access information about this session
259-
"""
260-
visit_name = get_visit_name(session_id)
261-
await submit_to_auth_endpoint(f"validate_visit_access/{visit_name}", token)
262-
return session_id
263-
264-
265243
async def validate_instrument_server_session_access(
266244
session_id: int,
267245
token: Annotated[str, Depends(instrument_oauth2_scheme)],
@@ -294,25 +272,60 @@ async def validate_instrument_server_session_access(
294272
return session_id
295273

296274

275+
async def validate_frontend_session_access(
276+
session_id: int,
277+
request: Request,
278+
token: Annotated[str, Depends(oauth2_scheme)],
279+
) -> int:
280+
"""
281+
Validates whether a frontend request can access information about this session
282+
"""
283+
visit_name = get_visit_name(session_id)
284+
if auth_url:
285+
if not (
286+
await submit_to_auth_endpoint(
287+
f"validate_visit_access/{visit_name}",
288+
request,
289+
token,
290+
)
291+
).get("valid"):
292+
raise HTTPException(
293+
status_code=status.HTTP_401_UNAUTHORIZED,
294+
detail="You do not have access to this visit",
295+
headers={"WWW-Authenticate": "Bearer"},
296+
)
297+
return session_id
298+
299+
297300
async def validate_user_instrument_access(
298301
instrument_name: str,
302+
request: Request,
299303
token: Annotated[str, Depends(oauth2_scheme)],
300304
) -> str:
301305
"""
302306
Validates whether a frontend request can access information about this instrument
303307
"""
304-
await submit_to_auth_endpoint(
305-
f"validate_instrument_access/{instrument_name}", token
306-
)
308+
if auth_url:
309+
if not (
310+
await submit_to_auth_endpoint(
311+
f"validate_instrument_access/{instrument_name}",
312+
request,
313+
token,
314+
)
315+
).get("valid"):
316+
raise HTTPException(
317+
status_code=status.HTTP_401_UNAUTHORIZED,
318+
detail="You do not have access to this instrument",
319+
headers={"WWW-Authenticate": "Bearer"},
320+
)
307321
return instrument_name
308322

309323

310-
# Set validation conditions for the session ID based on where the request is from
311-
MurfeySessionIDFrontend = Annotated[int, Depends(validate_frontend_session_access)]
324+
# Create annotated session ID and instrument name for endpoints that need to verify them
312325
MurfeySessionIDInstrument = Annotated[
313326
int, Depends(validate_instrument_server_session_access)
314327
]
315-
328+
MurfeySessionIDFrontend = Annotated[int, Depends(validate_frontend_session_access)]
316329
MurfeyInstrumentNameFrontend = Annotated[str, Depends(validate_user_instrument_access)]
317330

318331

0 commit comments

Comments
 (0)