Skip to content

Commit 444715a

Browse files
committed
Token revoke endpoints initial commit
I'm not really happy with the readability of this code. I think refactoring the blacklist stuff into it's own file would help a lot. Also need to figure out how to deal with accessing and modifying the stored tokens based on which token is trying to do so. A user should only be able to see and modify their own access tokens, but we want to make this general enough where an applications admin can view all of the tokens. Perpahs a method like get_tokens(identity='blah'), where if identity is None it will return all the tokens. This probably wouldn't be verify effecient in the current setup though, because the key to the key-value store is the jti. So to look up the identity, we would need to json loads every item in the store, then filter them out after the fact. Maybe we could encode the jti and the token/identity seperately? Or something? think about this more...
1 parent 0dc84fd commit 444715a

File tree

3 files changed

+106
-36
lines changed

3 files changed

+106
-36
lines changed

flask_jwt_extended/__init__.py

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

flask_jwt_extended/app.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from flask_jwt_extended import JWTManager, jwt_required, fresh_jwt_required,\
88
create_refresh_access_tokens, create_fresh_access_token, refresh_access_token,\
9-
jwt_identity, jwt_claims
9+
jwt_identity, jwt_claims, revoke_token, unrevoke_token, get_stored_tokens
1010

1111
# Example users database
1212
USERS = {
@@ -114,13 +114,30 @@ def fresh_login():
114114
return create_fresh_access_token(identity=username)
115115

116116

117-
# TODO Endpoint for revoking a token
118-
119-
120-
# TODO Endpoint for un-revoking a token
121-
122-
123-
# TODO Endpoint for listing tokens
117+
# Endpoint for listing tokens
118+
@app.route('/auth/tokens', methods=['GET'])
119+
def list_tokens():
120+
# TODO you should put some extra protection on this, so a user can only
121+
# view their tokens, or some extra privillage roles so an admin can
122+
# view everyones token
123+
return jsonify(get_stored_tokens()), 200
124+
125+
126+
# Endpoint for revoking and unrevoking tokens
127+
@app.route('/auth/tokens/<string:jti>', methods=['PUT'])
128+
def revoke_jwt(jti):
129+
# TODO you should put some extra protection on this, so a user can only
130+
# modify their tokens
131+
revoke = request.json.get('revoke', None)
132+
if revoke is None:
133+
return jsonify({'msg': "Missing json argument: 'revoke'"}), 422
134+
if not isinstance(revoke, bool):
135+
return jsonify({'msg': "revoke' must be a boolean"}), 422
136+
137+
if revoke:
138+
revoke_token(jti)
139+
else:
140+
unrevoke_token(jti)
124141

125142

126143
# Endpoint for generating a non-fresh access token from the refresh token

flask_jwt_extended/utils.py

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import calendar
12
import datetime
23
import json
34
import 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

203204
def 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+
327361
def _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

Comments
 (0)