Skip to content

Commit 6abc4d2

Browse files
committed
Refactor blacklist functions into seperate file
1 parent 14aad4e commit 6abc4d2

File tree

3 files changed

+208
-132
lines changed

3 files changed

+208
-132
lines changed

flask_jwt_extended/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .jwt_manager import JWTManager
22
from .utils import (jwt_identity, jwt_claims, jwt_required, fresh_jwt_required,
33
create_refresh_access_tokens, refresh_access_token,
4-
create_fresh_access_token, revoke_token, unrevoke_token,
5-
get_stored_tokens)
4+
create_fresh_access_token)
5+
from .blacklist import (revoke_token, unrevoke_token, get_stored_tokens,
6+
get_all_stored_tokens)

flask_jwt_extended/blacklist.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Collection of code deals with storing and revoking tokens
2+
import calendar
3+
import datetime
4+
import json
5+
6+
from flask_jwt_extended.exceptions import RevokedTokenError
7+
from functools import wraps
8+
9+
from flask import current_app
10+
11+
from flask_jwt_extended.config import BLACKLIST_ENABLED, BLACKLIST_STORE, \
12+
BLACKLIST_TOKEN_CHECKS
13+
14+
15+
def _verify_blacklist_enabled(fn):
16+
"""
17+
Helper decorator that verifies the blacklist is enabled on any function
18+
that requires it
19+
"""
20+
@wraps(fn)
21+
def wrapper(*args, **kwargs):
22+
config = current_app.config
23+
24+
blacklist_enabled = config.get('JWT_BLACKLIST_ENABLED', BLACKLIST_ENABLED)
25+
if not blacklist_enabled:
26+
err = 'JWT_BLACKLIST_ENABLED must be True to access this functionality'
27+
raise RuntimeError(err)
28+
29+
store = current_app.config.get('JWT_BLACKLIST_STORE', BLACKLIST_STORE)
30+
if store is None:
31+
err = 'JWT_BLACKLIST_STORE must be set to access this functionality'
32+
raise RuntimeError(err)
33+
34+
check_type = config.get('JWT_BLACKLIST_TOKEN_CHECKS', BLACKLIST_TOKEN_CHECKS)
35+
if check_type not in ('all', 'refresh'):
36+
raise RuntimeError('Invalid option for JWT_BLACKLIST_TOKEN_CHECKS')
37+
38+
return fn(*args, **kwargs)
39+
return wrapper
40+
41+
42+
def _utc_datetime_to_ts(dt):
43+
return calendar.timegm(dt.utctimetuple())
44+
45+
46+
def _ts_to_utc_datetime(ts):
47+
datetime.datetime.utcfromtimestamp(ts)
48+
49+
50+
def _store_supports_ttl(store):
51+
"""
52+
Checks if this store supports a TTL on its keys, for automatic removal
53+
after the token has expired. For more info on this, see:
54+
http://pythonhosted.org/simplekv/#simplekv.TimeToLiveMixin
55+
"""
56+
return getattr(store, 'ttl_support', False)
57+
58+
59+
def _get_token_ttl(token):
60+
"""
61+
Returns a datetime.timdelta() of how long this token has left to live before
62+
it is expired
63+
"""
64+
expires = token['exp']
65+
now = datetime.datetime.utcnow()
66+
delta = expires - now
67+
68+
# If the token is already expired, return that it has a ttl of 0
69+
if delta.total_seconds() < 0:
70+
return datetime.timedelta(0)
71+
return delta
72+
73+
74+
def _get_token_from_store(jti):
75+
store = current_app.config.get('JWT_BLACKLIST_STORE', BLACKLIST_STORE)
76+
stored_str = store.get(jti).decode('utf-8')
77+
stored_data = json.loads(stored_str)
78+
return stored_data
79+
80+
81+
def _update_token(jti, revoked):
82+
try:
83+
stored_data = _get_token_from_store(jti)
84+
token = stored_data['token']
85+
store_token(token, revoked)
86+
except KeyError:
87+
# Token does not exist in the store. Could have been automatically
88+
# removed from the store via ttl expiring # (in case of redis or
89+
# memcached), or could have never been in the store, which probably
90+
# indicates a bug in the callers code.
91+
# TODO should this raise an error? Or silently return?
92+
raise
93+
94+
95+
@_verify_blacklist_enabled
96+
def revoke_token(jti):
97+
"""
98+
Revoke a token
99+
100+
:param jti: The jti of the token to revoke
101+
"""
102+
_update_token(jti, revoked=True)
103+
104+
105+
@_verify_blacklist_enabled
106+
def unrevoke_token(jti):
107+
"""
108+
Revoke a token
109+
110+
:param jti: The jti of the token to revoke
111+
"""
112+
_update_token(jti, revoked=False)
113+
114+
115+
@_verify_blacklist_enabled
116+
def get_stored_tokens(identity):
117+
"""
118+
Get a list of stored tokens for this identity. Each token will look like:
119+
120+
TODO
121+
"""
122+
# TODO this is *super* inefficient. Come up with a better way
123+
store = current_app.config.get('JWT_BLACKLIST_STORE', BLACKLIST_STORE)
124+
data = [json.loads(store.get(jti).decode('utf-8')) for jti in store.iter_keys()]
125+
return [d for d in data if d['identity'] == identity]
126+
127+
128+
@_verify_blacklist_enabled
129+
def get_all_stored_tokens():
130+
"""
131+
Get a list of stored tokens for every identity. Each token will look like:
132+
133+
TODO
134+
"""
135+
store = current_app.config.get('JWT_BLACKLIST_STORE', BLACKLIST_STORE)
136+
return [json.loads(store.get(jti).decode('utf-8')) for jti in store.iter_keys()]
137+
138+
139+
@_verify_blacklist_enabled
140+
def check_if_token_revoked(token):
141+
"""
142+
Checks if the given token has been revoked.
143+
"""
144+
config = current_app.config
145+
146+
store = config.get('JWT_BLACKLIST_STORE', BLACKLIST_STORE)
147+
check_type = config.get('JWT_BLACKLIST_TOKEN_CHECKS', BLACKLIST_TOKEN_CHECKS)
148+
token_type = token['type']
149+
jti = token['jti']
150+
151+
# Only check access tokens if BLACKLIST_TOKEN_CHECKS is set to 'all`
152+
if token_type == 'access' and check_type == 'all':
153+
stored_data = json.loads(store.get(jti).decode('utf-8'))
154+
if stored_data['revoked']:
155+
raise RevokedTokenError('Token has been revoked')
156+
157+
# Always check refresh tokens
158+
if token_type == 'refresh':
159+
stored_data = json.loads(store.get(jti).decode('utf-8'))
160+
if stored_data['revoked']:
161+
raise RevokedTokenError('Token has been revoked')
162+
163+
164+
@_verify_blacklist_enabled
165+
def store_token(token, revoked):
166+
"""
167+
Stores this token in our key-value store, with the given revoked status
168+
"""
169+
data_to_store = json.dumps({
170+
'token': token,
171+
'last_used': _utc_datetime_to_ts(datetime.datetime.utcnow()),
172+
'revoked': revoked
173+
}).encode('utf-8')
174+
175+
store = current_app.config.get('JWT_BLACKLIST_STORE', BLACKLIST_STORE)
176+
177+
if _store_supports_ttl(store):
178+
# Add 15 minutes to ttl to account for possible time drift
179+
ttl = _get_token_ttl(token) + datetime.timedelta(minutes=15)
180+
ttl_secs = ttl.total_seconds()
181+
store.put(token['jti'], data_to_store, ttl_secs=ttl_secs)
182+
else:
183+
store.put(token['jti'], data_to_store)

0 commit comments

Comments
 (0)