11from typing import Any , Dict
22import httpx
33import jwt
4- from fastapi import Depends , HTTPException , WebSocket , status
4+ from fastapi import Depends , WebSocket , status
55from fastapi .security import OAuth2AuthorizationCodeBearer
66from jwt import PyJWKClient
77from loguru import logger
88
9+ from app .error import AuthException , DispatcherException
10+ from app .schemas .websockets import WSStatusMessage
11+
912from .config .settings import settings
1013
1114# Keycloak OIDC info
@@ -37,9 +40,9 @@ def _decode_token(token: str):
3740 )
3841 return payload
3942 except Exception :
40- raise HTTPException (
41- status_code = status .HTTP_401_UNAUTHORIZED ,
42- detail = "Could not validate credentials" ,
43+ raise AuthException (
44+ http_status = status .HTTP_401_UNAUTHORIZED ,
45+ message = "Could not validate credentials. Please retry signing in. " ,
4346 )
4447
4548
@@ -55,6 +58,7 @@ async def websocket_authenticate(websocket: WebSocket) -> str | None:
5558 """
5659 logger .debug ("Authenticating websocket" )
5760 token = websocket .query_params .get ("token" )
61+
5862 if not token :
5963 logger .error ("Token is missing from websocket authentication" )
6064 await websocket .close (code = 1008 , reason = "Missing token" )
@@ -63,9 +67,22 @@ async def websocket_authenticate(websocket: WebSocket) -> str | None:
6367 try :
6468 await websocket .accept ()
6569 return token
70+ except DispatcherException as ae :
71+ logger .error (f"Dispatcher exception detected: { ae .message } " )
72+ await websocket .send_json (
73+ WSStatusMessage (type = "error" , message = ae .message ).model_dump ()
74+ )
75+ await websocket .close (code = 1008 , reason = ae .error_code )
76+ return None
6677 except Exception as e :
67- logger .error (f"Invalid token in websocket authentication: { e } " )
68- await websocket .close (code = 1008 , reason = "Invalid token" )
78+ logger .error (f"Unexpected error occurred during websocket authentication: { e } " )
79+ await websocket .send_json (
80+ WSStatusMessage (
81+ type = "error" ,
82+ message = "Something went wrong during authentication. Please try again." ,
83+ ).model_dump ()
84+ )
85+ await websocket .close (code = 1008 , reason = "INTERNAL_ERROR" )
6986 return None
7087
7188
@@ -81,15 +98,15 @@ async def exchange_token_for_provider(
8198
8299 :return: The token response (dict) on success.
83100
84- :raise: Raises HTTPException with an appropriate status and message on error.
101+ :raise: Raises AuthException with an appropriate status and message on error.
85102 """
86103 token_url = f"{ KEYCLOAK_BASE_URL } /protocol/openid-connect/token"
87104
88105 # Check if the necessary settings are in place
89106 if not settings .keycloak_client_id or not settings .keycloak_client_secret :
90- raise HTTPException (
91- status_code = status .HTTP_500_INTERNAL_SERVER_ERROR ,
92- detail = "Token exchange not configured on the server (missing client credentials)." ,
107+ raise AuthException (
108+ http_status = status .HTTP_500_INTERNAL_SERVER_ERROR ,
109+ message = "Token exchange not configured on the server (missing client credentials)." ,
93110 )
94111
95112 payload = {
@@ -105,9 +122,12 @@ async def exchange_token_for_provider(
105122 resp = await client .post (token_url , data = payload )
106123 except httpx .RequestError as exc :
107124 logger .error (f"Token exchange network error for provider={ provider } : { exc } " )
108- raise HTTPException (
109- status_code = status .HTTP_502_BAD_GATEWAY ,
110- detail = "Failed to contact the identity provider for token exchange." ,
125+ raise AuthException (
126+ http_status = status .HTTP_502_BAD_GATEWAY ,
127+ message = (
128+ f"Could not authenticate with { provider } . Please contact APEx support or reach out "
129+ "through the <a href='https://forum.apex.esa.int/'>APEx User Forum</a>."
130+ ),
111131 )
112132
113133 # Parse response
@@ -117,9 +137,12 @@ async def exchange_token_for_provider(
117137 logger .error (
118138 f"Token exchange invalid JSON response (status={ resp .status_code } )"
119139 )
120- raise HTTPException (
121- status_code = status .HTTP_502_BAD_GATEWAY ,
122- detail = "Invalid response from identity provider during token exchange." ,
140+ raise AuthException (
141+ http_status = status .HTTP_502_BAD_GATEWAY ,
142+ message = (
143+ f"Could not authenticate with { provider } . Please contact APEx support or reach out "
144+ "through the <a href='https://forum.apex.esa.int/'>APEx User Forum</a>."
145+ ),
123146 )
124147
125148 if resp .status_code != 200 :
@@ -136,7 +159,16 @@ async def exchange_token_for_provider(
136159 else status .HTTP_502_BAD_GATEWAY
137160 )
138161
139- raise HTTPException (client_status , detail = body )
162+ raise AuthException (
163+ http_status = client_status ,
164+ message = (
165+ f"Please link your account with { provider } in your "
166+ "<a href='https://{settings.keycloak_host}/realms/{settings.keycloak_realm}/"
167+ "account'>Account Dashboard</a>"
168+ if body .get ("error" , "" ) == "not_linked"
169+ else f"Could not authenticate with { provider } : { err } "
170+ ),
171+ )
140172
141173 # Successful exchange, return token response (access_token, expires_in, etc.)
142174 return body
0 commit comments