Skip to content

Commit c2d5bd7

Browse files
committed
Adds creating token from complex object (refs #11)
1 parent 0226d30 commit c2d5bd7

File tree

5 files changed

+144
-2
lines changed

5 files changed

+144
-2
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ documentation is coming soon!
1515
installation
1616
basic_usage
1717
add_custom_data_claims
18+
tokens_from_complex_object
1819
refresh_tokens
1920
token_freshness
2021
changing_default_behavior

docs/tokens_from_complex_object.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Tokens from Complex Objects
2+
===========================
3+
4+
A common pattern will be to have your users information (such as username and
5+
password) stored on disk somewhere. Lets say for example that we have a database
6+
object stores a username, hashed password, and what roles this user is. In the
7+
access token, we want the identity to be the username or user_id. We also want
8+
to store the roles this user has access to in the access_token so that we don't
9+
have to look that information up from the database on every request. We could
10+
do this simple enough with the **user_claims_loader** mentioned in the last section.
11+
However, is we pass just the identity (username or userid) to the **user_claims_loader**,
12+
we would have to look up this user from the database twice. First time, when
13+
they access the login endpoint and we need to verify their username and password,
14+
and second time in the **user_claims_loader** function, so that we can fine what roles
15+
this user has. This isn't a huge deal, but obviously it could be more efficient.
16+
17+
This extension provides the ability to pass any object to the **create_access_token**
18+
method, which will then be passed to the **user_claims_loader** method. This lets
19+
us access the database only once. However, as we still need the identity to be
20+
a JSON serializable object unique to this user, we need
21+
to take an addition step and use the optional **identity_lookup** kwarg in the
22+
**create_access_token** method. This lets us tell the system how to get the identity from
23+
an object.
24+
25+
Here is an example of this in action
26+
27+
.. literalinclude:: ../examples/tokens_from_complex_objects.py
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from flask import Flask, jsonify, request
2+
from flask_jwt_extended import JWTManager, jwt_required, \
3+
create_access_token, get_jwt_identity
4+
5+
app = Flask(__name__)
6+
app.secret_key = 'super-secret' # Change this!
7+
8+
# Setup the Flask-JWT-Extended extension
9+
jwt = JWTManager(app)
10+
11+
12+
# This is a simple example of a complex object that we could build
13+
# a JWT from. In practice, this will normally be something that
14+
# requires a lookup from disk (such as SQLAlchemy)
15+
class UserObject:
16+
def __init__(self, username, roles):
17+
self.username = username
18+
self.roles = roles
19+
20+
21+
# This method will get whatever object is passed into the
22+
# create_access_token method.
23+
@jwt.user_claims_loader
24+
def add_claims_to_access_token(user):
25+
return {'roles': user.roles}
26+
27+
28+
@app.route('/login', methods=['POST'])
29+
def login():
30+
username = request.json.get('username', None)
31+
password = request.json.get('password', None)
32+
if username != 'test' and password != 'test':
33+
return jsonify({"msg": "Bad username or password"}), 401
34+
35+
# Create an example UserObject
36+
user = UserObject(username='test', roles=['foo', 'bar'])
37+
38+
# We can now pass this complex object directly to the
39+
# create_access_token method. This will allow us to access
40+
# the properties of this object in the user_claims_loader
41+
# function. Because this object is not json serializable itself,
42+
# we also need to provide a way to get some which is json
43+
# serializable and represents the identity of this token from
44+
# the complex object. We pass a function to the optional
45+
# identity_lookup kwarg, which tells the create_access_token
46+
# function how to get the identity from this object
47+
access_token = create_access_token(
48+
identity=user,
49+
identity_lookup=lambda u: u.username
50+
)
51+
52+
ret = {'access_token': access_token}
53+
return jsonify(ret), 200
54+
55+
56+
@app.route('/protected', methods=['GET'])
57+
@jwt_required
58+
def protected():
59+
current_user = get_jwt_identity()
60+
return jsonify({'hello_from': current_user}), 200
61+
62+
if __name__ == '__main__':
63+
app.run()

flask_jwt_extended/utils.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,30 @@ def create_refresh_token(identity):
312312
return refresh_token
313313

314314

315-
def create_access_token(identity, fresh=True):
315+
def create_access_token(identity, fresh=False, identity_lookup=None):
316+
"""
317+
Creates a new access token
318+
319+
:param identity: The identity of this token. This can be any data that is
320+
json serializable. It can also be an object, in which case
321+
you can pass a function to identity_lookup which tells us
322+
how to get the identity out of this object. This is useful
323+
so you don't need to query disk twice, once for initially
324+
finding the identity in your login endpoint, and once for
325+
setting addition data in the JWT via the user_claims_loader
326+
:param fresh: If this token should me markded as fresh, and can thus access
327+
fresh_jwt_required protected endpoints. Defaults to False
328+
:param identity_lookup: Function to generate a json serilizable identity
329+
from the identity object
330+
:return: A newly encoded JWT access token
331+
"""
316332
# Token options
317333
secret = _get_secret_key()
318334
access_expire_delta = get_access_expires()
319335
algorithm = get_algorithm()
320336
user_claims = current_app.jwt_manager.user_claims_callback(identity)
337+
if identity_lookup:
338+
identity = identity_lookup(identity)
321339

322340
access_token = _encode_access_token(identity, secret, algorithm, access_expire_delta,
323341
fresh=fresh, user_claims=user_claims)

tests/test_jwt_encode_decode.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from flask import Flask
77
from flask_jwt_extended.exceptions import JWTEncodeError, JWTDecodeError
88
from flask_jwt_extended.utils import _encode_access_token, _encode_refresh_token, \
9-
_decode_jwt
9+
_decode_jwt, create_access_token
10+
from flask_jwt_extended.jwt_manager import JWTManager
1011

1112

1213
class JWTEncodeDecodeTests(unittest.TestCase):
@@ -330,3 +331,35 @@ def test_decode_invalid_jwt(self):
330331
}
331332
encoded_token = jwt.encode(token_data, 'secret', 'HS256').decode('utf-8')
332333
_decode_jwt(encoded_token, 'secret', 'HS256')
334+
335+
def test_create_access_token_with_object(self):
336+
# Complex object to test building a JWT from. Normally if you are using
337+
# this functionality, this is something that would be retrieved from
338+
# disk somewhere (think sqlalchemy)
339+
class TestObject:
340+
def __init__(self, username, roles):
341+
self.username = username
342+
self.roles = roles
343+
344+
# Setup the flask stuff
345+
app = Flask(__name__)
346+
app.secret_key = 'super=secret'
347+
app.config['JWT_ALGORITHM'] = 'HS256'
348+
jwt = JWTManager(app)
349+
350+
@jwt.user_claims_loader
351+
def custom_claims(object):
352+
return {
353+
'roles': object.roles
354+
}
355+
356+
# Create the token using the complex object
357+
with app.test_request_context():
358+
user = TestObject(username='foo', roles=['bar', 'baz'])
359+
token = create_access_token(identity=user,
360+
identity_lookup=lambda obj: obj.username)
361+
362+
# Decode the token and make sure the values are set properly
363+
token_data = _decode_jwt(token, app.secret_key, app.config['JWT_ALGORITHM'])
364+
self.assertEqual(token_data['identity'], 'foo')
365+
self.assertEqual(token_data['user_claims']['roles'], ['bar', 'baz'])

0 commit comments

Comments
 (0)