1+ import calendar
12import datetime
23import json
34import uuid
@@ -77,7 +78,7 @@ def _encode_access_token(identity, secret, algorithm, token_expire_delta,
7778 'user_claims' : user_claims ,
7879 }
7980 encoded_token = jwt .encode (token_data , secret , algorithm ).decode ('utf-8' )
80- _store_token_if_blacklist_enabled (token_data )
81+ _store_token (token_data , revoked = False )
8182 return encoded_token
8283
8384
@@ -102,7 +103,7 @@ def _encode_refresh_token(identity, secret, algorithm, token_expire_delta):
102103 'type' : 'refresh' ,
103104 }
104105 encoded_token = jwt .encode (token_data , secret , algorithm ).decode ('utf-8' )
105- _store_token_if_blacklist_enabled (token_data )
106+ _store_token (token_data , revoked = False )
106107 return encoded_token
107108
108109
@@ -179,25 +180,25 @@ def wrapper(*args, **kwargs):
179180 return wrapper
180181
181182
182- def _check_blacklist (jwt_data ):
183+ def _check_blacklist (token ):
183184 if not _blacklist_enabled ():
184185 return
185186
186187 store = _get_blacklist_store ()
187- token_type = jwt_data ['type' ]
188- jti = jwt_data ['jti' ]
188+ token_type = token ['type' ]
189+ jti = token ['jti' ]
189190
190191 # Only check access tokens if BLACKLIST_TOKEN_CHECKS is set to 'all`
191192 if token_type == 'access' and _blacklist_checks () == 'all' :
192- token_status = store [ jti ]
193- if token_status != 'active' :
194- raise RevokedTokenError ('{} has been revoked' . format )
193+ stored_data = json . loads ( store . get ( jti ))
194+ if stored_data [ 'revoked' ] != 'active' :
195+ raise RevokedTokenError ('Token has been revoked' )
195196
196197 # Always check refresh tokens
197198 if token_type == 'refresh' :
198- token_status = store [ jti ]
199- if token_status != 'active' :
200- raise RevokedTokenError ('{} has been revoked' . format )
199+ stored_data = json . loads ( store . get ( jti ))
200+ if stored_data [ 'revoked' ] != 'active' :
201+ raise RevokedTokenError ('Token has been revoked' )
201202
202203
203204def jwt_required (fn ):
@@ -220,7 +221,7 @@ def wrapper(*args, **kwargs):
220221 if jwt_data ['type' ] != 'access' :
221222 raise WrongTokenError ('Only access tokens can access this endpoint' )
222223
223- # See if the token has been revoked (based on blacklist options)
224+ # If blacklisting is enabled, see if this token has been revoked
224225 _check_blacklist (jwt_data )
225226
226227 # Save the jwt in the context so that it can be accessed later by
@@ -250,7 +251,7 @@ def wrapper(*args, **kwargs):
250251 if jwt_data ['type' ] != 'access' :
251252 raise WrongTokenError ('Only access tokens can access this endpoint' )
252253
253- # See if the token has been revoked (based on blacklist options)
254+ # If blacklisting is enabled, see if this token has been revoked
254255 _check_blacklist (jwt_data )
255256
256257 # Check if the token is fresh
@@ -324,6 +325,39 @@ def refresh_access_token():
324325 return jsonify (ret ), 200
325326
326327
328+ def get_stored_tokens ():
329+ if not _blacklist_enabled ():
330+ raise RuntimeError ("Blacklist must be enabled to list tokens" )
331+
332+ store = _get_blacklist_store ()
333+ return [json .loads (store .get (jti )) for jti in store .iter_keys ()]
334+
335+
336+ def _update_token (jti , revoked ):
337+ if not _blacklist_enabled ():
338+ raise RuntimeError ("Blacklist must be enabled to revoke a token" )
339+
340+ store = _get_blacklist_store ()
341+ try :
342+ token = store .get (jti )
343+ _store_token (token , revoked )
344+ except KeyError :
345+ # Token does not exist in the store. Could have been automatically
346+ # removed from the store via ttl expiring # (in case of redis or
347+ # memcached), or could have never been in the store, which probably
348+ # indicates a bug in the callers code.
349+ # TODO should this raise an error? Or silently return?
350+ return
351+
352+
353+ def revoke_token (jti ):
354+ return _update_token (jti , revoked = True )
355+
356+
357+ def unrevoke_token (jti ):
358+ return _update_token (jti , revoked = False )
359+
360+
327361def _get_secret_key ():
328362 key = current_app .config .get ('SECRET_KEY' , None )
329363 if not key :
@@ -351,28 +385,45 @@ def _store_supports_ttl(store):
351385 return getattr (store , 'ttl_support' , False )
352386
353387
354- def _store_token_if_blacklist_enabled (token ):
388+ def _utc_datetime_to_ts (dt ):
389+ return calendar .timegm (dt .utctimetuple ())
390+
391+
392+ def _ts_to_utc_datetime (ts ):
393+ datetime .datetime .utcfromtimestamp (ts )
394+
395+
396+ def _get_token_ttl (token ):
397+ expires = token ['exp' ]
398+ now = datetime .datetime .utcnow ()
399+ delta = expires - now
400+
401+ # If the token is already expired, return that it has a ttl of 0
402+ if delta .total_seconds () < 0 :
403+ return datetime .timedelta (0 )
404+ return delta
405+
406+
407+ def _store_token (token , revoked ):
355408 # If the blacklist isn't enabled, do nothing
356- if not _blacklist_enabled () or _blacklist_checks () is None :
409+ if not _blacklist_enabled ():
357410 return
358411
359- # If configured to only check refresh tokens and this isn't a refresh token, return
412+ # If configured to only check refresh tokens and this isn't a one, do nothing
360413 if _blacklist_checks () == 'refresh' and token ['type' ] != 'refresh' :
361414 return
362415
363- # TODO store data as json in the store (including jti, identity, and user claims)
416+ data_to_store = json .dumps ({
417+ 'token' : token ,
418+ 'last_used' : _utc_datetime_to_ts (datetime .datetime .utcnow ()),
419+ 'revoked' : revoked
420+ })
364421
365- # Otherwise store the token in the blacklist (with current status of active)
366422 store = _get_blacklist_store ()
367423 if _store_supports_ttl (store ):
368- config = current_app .config
369- if token ['type' ] == 'access' :
370- expire_delta = config .get ('JWT_ACCESS_TOKEN_EXPIRES' , ACCESS_EXPIRES )
371- else :
372- expire_delta = config .get ('JWT_REFRESH_TOKEN_EXPIRES' , REFRESH_EXPIRES )
373-
374- ttl = expire_delta + datetime .timedelta (minutes = 15 )
424+ # Add 15 minutes to token ttl to account for possible time drift
425+ ttl = _get_token_ttl (token ) + datetime .timedelta (minutes = 15 )
375426 ttl_secs = ttl .total_seconds ()
376- store .put (key = token ['jti' ], value = "active" , ttl_secs = ttl_secs )
427+ store .put (key = token ['jti' ], value = data_to_store , ttl_secs = ttl_secs )
377428 else :
378- store .put (key = token ['jti' ], value = "active" )
429+ store .put (key = token ['jti' ], value = data_to_store )
0 commit comments