11from __future__ import annotations
22
33import hashlib
4+ import random
45from collections .abc import Callable , Iterable
56from typing import Any , ClassVar
67
@@ -307,6 +308,17 @@ def authenticate(self, request: Request):
307308 return self .transform_auth (user_id , None )
308309
309310
311+ class TokenStrLookupRequired (Exception ):
312+ """
313+ Used in combination with `apitoken.use-and-update-hash-rate` option.
314+
315+ If raised, calling code should peform API token lookups based on its
316+ plaintext value and not its hashed value.
317+ """
318+
319+ pass
320+
321+
310322@AuthenticationSiloLimit (SiloMode .REGION , SiloMode .CONTROL )
311323class UserAuthTokenAuthentication (StandardAuthentication ):
312324 token_name = b"bearer"
@@ -328,11 +340,17 @@ def _find_or_update_token_by_hash(self, token_str: str) -> ApiToken | ApiTokenRe
328340
329341 hashed_token = hashlib .sha256 (token_str .encode ()).hexdigest ()
330342
343+ rate = options .get ("apitoken.use-and-update-hash-rate" )
344+ random_rate = random .random ()
345+
331346 if SiloMode .get_current_mode () == SiloMode .REGION :
332347 try :
333- # Try to find the token by its hashed value first
334- return ApiTokenReplica .objects .get (hashed_token = hashed_token )
335- except ApiTokenReplica .DoesNotExist :
348+ if rate > random_rate :
349+ # Try to find the token by its hashed value first
350+ return ApiTokenReplica .objects .get (hashed_token = hashed_token )
351+ else :
352+ raise TokenStrLookupRequired
353+ except (ApiTokenReplica .DoesNotExist , TokenStrLookupRequired ):
336354 try :
337355 # If we can't find it by hash, use the plaintext string
338356 return ApiTokenReplica .objects .get (token = token_str )
@@ -342,10 +360,13 @@ def _find_or_update_token_by_hash(self, token_str: str) -> ApiToken | ApiTokenRe
342360 else :
343361 try :
344362 # Try to find the token by its hashed value first
345- return ApiToken .objects .select_related ("user" , "application" ).get (
346- hashed_token = hashed_token
347- )
348- except ApiToken .DoesNotExist :
363+ if rate > random_rate :
364+ return ApiToken .objects .select_related ("user" , "application" ).get (
365+ hashed_token = hashed_token
366+ )
367+ else :
368+ raise TokenStrLookupRequired
369+ except (ApiToken .DoesNotExist , TokenStrLookupRequired ):
349370 try :
350371 # If we can't find it by hash, use the plaintext string
351372 api_token = ApiToken .objects .select_related ("user" , "application" ).get (
@@ -355,9 +376,11 @@ def _find_or_update_token_by_hash(self, token_str: str) -> ApiToken | ApiTokenRe
355376 # If the token does not exist by plaintext either, it is not a valid token
356377 raise AuthenticationFailed ("Invalid token" )
357378 else :
358- # Update it with the hashed value if found by plaintext
359- api_token .hashed_token = hashed_token
360- api_token .save (update_fields = ["hashed_token" ])
379+ if rate > random_rate :
380+ # Update it with the hashed value if found by plaintext
381+ api_token .hashed_token = hashed_token
382+ api_token .save (update_fields = ["hashed_token" ])
383+
361384 return api_token
362385
363386 def accepts_auth (self , auth : list [bytes ]) -> bool :
0 commit comments