22
33import datetime
44import secrets
5+ import time
56import uuid
67
78from jose import jwt
1314from fastpubsub .database import Client as DBClient
1415from fastpubsub .database import SessionLocal
1516from fastpubsub .exceptions import InvalidClient
17+ from fastpubsub .logger import get_logger
1618from fastpubsub .models import (
1719 Client ,
1820 ClientToken ,
2426from fastpubsub .services .helpers import _delete_entity , _get_entity , utc_now
2527
2628password_hash = PasswordHash .recommended ()
29+ logger = get_logger (__name__ )
2730
2831
2932def generate_secret () -> str :
@@ -54,25 +57,44 @@ async def create_client(data: CreateClient) -> CreateClientResult:
5457 AlreadyExistsError: If a client with the same ID already exists.
5558 ValueError: If client data validation fails.
5659 """
57- async with SessionLocal () as session :
58- now = utc_now ()
59- secret = generate_secret ()
60- secret_hash = password_hash .hash (secret )
61- db_client = DBClient (
62- id = uuid .uuid7 (),
63- name = data .name ,
64- scopes = data .scopes ,
65- is_active = data .is_active ,
66- secret_hash = secret_hash ,
67- token_version = 1 ,
68- created_at = now ,
69- updated_at = now ,
70- )
71- session .add (db_client )
72-
73- await session .commit ()
60+ start_time = time .perf_counter ()
61+ logger .info (
62+ "creating client" ,
63+ extra = {"client_name" : data .name , "scopes" : data .scopes , "is_active" : data .is_active },
64+ )
7465
75- return CreateClientResult (id = db_client .id , secret = secret )
66+ try :
67+ async with SessionLocal () as session :
68+ now = utc_now ()
69+ secret = generate_secret ()
70+ secret_hash = password_hash .hash (secret )
71+ db_client = DBClient (
72+ id = uuid .uuid7 (),
73+ name = data .name ,
74+ scopes = data .scopes ,
75+ is_active = data .is_active ,
76+ secret_hash = secret_hash ,
77+ token_version = 1 ,
78+ created_at = now ,
79+ updated_at = now ,
80+ )
81+ session .add (db_client )
82+
83+ await session .commit ()
84+
85+ duration = time .perf_counter () - start_time
86+ logger .info (
87+ "client created" ,
88+ extra = {"client_id" : str (db_client .id ), "client_name" : data .name , "duration" : f"{ duration :.4f} s" },
89+ )
90+ return CreateClientResult (id = db_client .id , secret = secret )
91+ except Exception as e :
92+ duration = time .perf_counter () - start_time
93+ logger .error (
94+ "client creation failed" ,
95+ extra = {"client_name" : data .name , "error" : str (e ), "duration" : f"{ duration :.4f} s" },
96+ )
97+ raise
7698
7799
78100async def get_client (client_id : uuid .UUID ) -> Client :
@@ -175,31 +197,66 @@ async def issue_jwt_client_token(client_id: uuid.UUID, client_secret: str) -> Cl
175197 Raises:
176198 InvalidClient: If client credentials are invalid or client is disabled.
177199 """
178- async with SessionLocal () as session :
179- db_client = await _get_entity (session , DBClient , client_id , "Client not found" , raise_exception = False )
180- if not db_client :
181- raise InvalidClient ("Client not found" ) from None
182- if not db_client .is_active :
183- raise InvalidClient ("Client disabled" ) from None
184- if password_hash .verify (client_secret , db_client .secret_hash ) is False :
185- raise InvalidClient ("Client secret is invalid" ) from None
186-
187- now = utc_now ()
188- expires_in = now + datetime .timedelta (minutes = settings .auth_access_token_expire_minutes )
189- payload = {
190- "sub" : str (client_id ),
191- "exp" : expires_in ,
192- "iat" : now ,
193- "scope" : db_client .scopes ,
194- "ver" : db_client .token_version ,
195- }
196- access_token = jwt .encode (payload , key = settings .auth_secret_key , algorithm = settings .auth_algorithm )
197-
198- return ClientToken (
199- access_token = access_token ,
200- expires_in = int ((expires_in - now ).total_seconds ()),
201- scope = db_client .scopes ,
202- )
200+ start_time = time .perf_counter ()
201+ logger .info ("issuing jwt token" , extra = {"client_id" : str (client_id )})
202+
203+ try :
204+ async with SessionLocal () as session :
205+ db_client = await _get_entity (
206+ session , DBClient , client_id , "Client not found" , raise_exception = False
207+ )
208+ if not db_client :
209+ logger .warning ("token issuance failed: client not found" , extra = {"client_id" : str (client_id )})
210+ raise InvalidClient ("Client not found" ) from None
211+ if not db_client .is_active :
212+ logger .warning (
213+ "token issuance failed: client disabled" ,
214+ extra = {"client_id" : str (client_id ), "client_name" : db_client .name },
215+ )
216+ raise InvalidClient ("Client disabled" ) from None
217+ if password_hash .verify (client_secret , db_client .secret_hash ) is False :
218+ logger .warning (
219+ "token issuance failed: invalid secret" ,
220+ extra = {"client_id" : str (client_id ), "client_name" : db_client .name },
221+ )
222+ raise InvalidClient ("Client secret is invalid" ) from None
223+
224+ now = utc_now ()
225+ expires_in = now + datetime .timedelta (minutes = settings .auth_access_token_expire_minutes )
226+ payload = {
227+ "sub" : str (client_id ),
228+ "exp" : expires_in ,
229+ "iat" : now ,
230+ "scope" : db_client .scopes ,
231+ "ver" : db_client .token_version ,
232+ }
233+ access_token = jwt .encode (
234+ payload , key = settings .auth_secret_key , algorithm = settings .auth_algorithm
235+ )
236+
237+ duration = time .perf_counter () - start_time
238+ logger .info (
239+ "jwt token issued" ,
240+ extra = {
241+ "client_id" : str (client_id ),
242+ "client_name" : db_client .name ,
243+ "scopes" : db_client .scopes ,
244+ "expires_in_minutes" : settings .auth_access_token_expire_minutes ,
245+ "duration" : f"{ duration :.4f} s" ,
246+ },
247+ )
248+ return ClientToken (
249+ access_token = access_token ,
250+ expires_in = int ((expires_in - now ).total_seconds ()),
251+ scope = db_client .scopes ,
252+ )
253+ except Exception as e :
254+ duration = time .perf_counter () - start_time
255+ logger .error (
256+ "token issuance failed" ,
257+ extra = {"client_id" : str (client_id ), "error" : str (e ), "duration" : f"{ duration :.4f} s" },
258+ )
259+ raise
203260
204261
205262async def decode_jwt_client_token (access_token : str , auth_enabled : bool = True ) -> DecodedClientToken :
@@ -219,28 +276,69 @@ async def decode_jwt_client_token(access_token: str, auth_enabled: bool = True)
219276 InvalidClient: If token is invalid, expired, or client is disabled/revoked.
220277 """
221278 if not auth_enabled :
279+ logger .debug ("authentication disabled, returning test token" )
222280 return DecodedClientToken (client_id = uuid .uuid7 (), scopes = {"*" })
223281
282+ start_time = time .perf_counter ()
283+ logger .debug ("decoding jwt token" )
284+
224285 try :
225286 payload = jwt .decode (
226287 access_token ,
227288 key = settings .auth_secret_key ,
228289 algorithms = [settings .auth_algorithm ],
229290 )
230- except JWTError :
291+ except JWTError as e :
292+ logger .warning ("jwt token decode failed: invalid token" , extra = {"error" : str (e )})
231293 raise InvalidClient ("Invalid jwt token" ) from None
232294
233295 client_id = payload ["sub" ]
234296 scopes = payload ["scope" ]
235297 token_version = payload ["ver" ]
236298
237- async with SessionLocal () as session :
238- db_client = await _get_entity (session , DBClient , client_id , "Client not found" , raise_exception = False )
239- if not db_client :
240- raise InvalidClient ("Client not found" ) from None
241- if not db_client .is_active :
242- raise InvalidClient ("Client disabled" ) from None
243- if token_version != db_client .token_version :
244- raise InvalidClient ("Token revoked" ) from None
245-
246- return DecodedClientToken (client_id = uuid .UUID (client_id ), scopes = {scope for scope in scopes .split ()})
299+ try :
300+ async with SessionLocal () as session :
301+ db_client = await _get_entity (
302+ session , DBClient , client_id , "Client not found" , raise_exception = False
303+ )
304+ if not db_client :
305+ logger .warning (
306+ "jwt token validation failed: client not found" , extra = {"client_id" : client_id }
307+ )
308+ raise InvalidClient ("Client not found" ) from None
309+ if not db_client .is_active :
310+ logger .warning (
311+ "jwt token validation failed: client disabled" ,
312+ extra = {"client_id" : client_id , "client_name" : db_client .name },
313+ )
314+ raise InvalidClient ("Client disabled" ) from None
315+ if token_version != db_client .token_version :
316+ logger .warning (
317+ "jwt token validation failed: token revoked" ,
318+ extra = {
319+ "client_id" : client_id ,
320+ "client_name" : db_client .name ,
321+ "token_version" : token_version ,
322+ "current_version" : db_client .token_version ,
323+ },
324+ )
325+ raise InvalidClient ("Token revoked" ) from None
326+
327+ duration = time .perf_counter () - start_time
328+ logger .debug (
329+ "jwt token validated" ,
330+ extra = {
331+ "client_id" : client_id ,
332+ "client_name" : db_client .name ,
333+ "scopes" : scopes ,
334+ "duration" : f"{ duration :.4f} s" ,
335+ },
336+ )
337+ return DecodedClientToken (client_id = uuid .UUID (client_id ), scopes = {scope for scope in scopes .split ()})
338+ except Exception as e :
339+ duration = time .perf_counter () - start_time
340+ logger .error (
341+ "jwt token validation failed" ,
342+ extra = {"client_id" : client_id , "error" : str (e ), "duration" : f"{ duration :.4f} s" },
343+ )
344+ raise
0 commit comments