Skip to content

Commit 398ed30

Browse files
committed
Added flask-jwt-extended sample code
1 parent 89b4587 commit 398ed30

File tree

14 files changed

+446
-0
lines changed

14 files changed

+446
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
__pycache__/
44
*.db
55
.DS_Store
6+
venv/
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Flask-jwt-extended Section
2+
3+
This section presents the basic usage of an active flask JWT extension called `flask-jwt-extended`. We inherited and simplified the project structure from section 6 to demonstrate how to apply `flask-jwt-extended` to our project.
4+
5+
## Features
6+
- JWT authentication
7+
- Token refreshing
8+
- Fresh token vs. Non-fresh token
9+
- Adding claims to JWT payload
10+
- Blacklist and token revoking
11+
- Customize JWT response/error message callbacks
12+
13+
## Related Resources
14+
15+
### UserLogin
16+
- `POST: /login`
17+
- Description: authenticate a user and ,if authenticated, respond with an access token and a refresh token.
18+
19+
### UserFreshLogin
20+
- `POST: /fresh_login`
21+
- Description: authenticate a user and ,if authenticated, respond with a fresh access token.
22+
23+
### Item
24+
25+
- `GET: /item/<name>`
26+
- Description: require a valid JWT to access this endpoint.
27+
- Request header: `Authorization Bearer <access_token>`
28+
- `POST: /item/<name>`
29+
- Description: require a valid and fresh JWT to access this endpoint.
30+
- Request header: `Authorization Bearer <fresh_access_token>`
31+
32+
### ItemList
33+
34+
- `GET: /items`
35+
- Description: require a valid JWT and appropriate user privilege to access this endpoint. The user privilege is provided as claims within the JWT payload.
36+
- Request header: `Authorization Bearer <access_token>`
37+
38+
### TokenRefresh
39+
40+
- `POST: /refresh`
41+
- Description: This endpoint is used to refresh an expired access token. If the refresh token is valid, respond with a new valid access token.
42+
- Request header: `Authorization Bearer <refresh_token>`
43+
44+
45+
46+
47+

sections_flask-jwt-extended/app.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from flask import Flask, jsonify
2+
from flask_restful import Api
3+
from flask_jwt_extended import JWTManager
4+
5+
from resources.refresh_token import TokenRefresh
6+
from resources.user import UserRegister, UserLogin, UserFreshLogin
7+
from resources.item import Item, ItemList
8+
from resources.store import Store, StoreList
9+
10+
app = Flask(__name__)
11+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'
12+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
13+
api = Api(app)
14+
"""
15+
JWT related configurations began. The following functions includes:
16+
1) add claims to each jwt
17+
2) customize the token expired error message
18+
"""
19+
app.config['JWT_SECRET_KEY'] = 'super-secret'
20+
app.config['JWT_BLACKLIST_ENABLED'] = True # enable blacklist feature
21+
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh'] # allow blacklisting for access and refresh tokens
22+
jwt = JWTManager(app)
23+
24+
"""
25+
claims are data we choose to attached to each jwt payload
26+
and for each jwt protected endpoint, we can retrieve these claims via `get_jwt_claims()`
27+
one possible use case for claims are access level control, which is shown below
28+
"""
29+
30+
31+
@jwt.user_claims_loader
32+
def add_claims_to_jwt(identity):
33+
if identity == 1:
34+
return {'isAdmin': True}
35+
return {'isAdmin': False}
36+
37+
38+
39+
black_list = [2, 4, 6] # user.id that are black listed
40+
41+
42+
# this method will check if a token is blacklisted, and will be called automatically when blacklist is enabled
43+
@jwt.token_in_blacklist_loader
44+
def check_if_token_in_blacklist(decrypted_token):
45+
return decrypted_token['identity'] in black_list
46+
47+
48+
# the following callbacks are used for customizing jwt response/error messages
49+
@jwt.expired_token_loader
50+
def expired_token_callback():
51+
return jsonify({
52+
'message': 'The token has expired.',
53+
'error': 'token_expired'
54+
}), 401
55+
56+
57+
@jwt.invalid_token_loader
58+
def invalid_token_callback(error):
59+
return jsonify({
60+
'message': 'Signature verification failed.',
61+
'error': 'invalid_token'
62+
}), 401
63+
64+
65+
@jwt.unauthorized_loader
66+
def missing_token_callback(error):
67+
return jsonify({
68+
"description": "Request does not contain an access token.",
69+
'error': 'authorization_required'
70+
}), 401
71+
72+
73+
@jwt.needs_fresh_token_loader
74+
def token_not_fresh_callback():
75+
return jsonify({
76+
"description": "The token is not fresh.",
77+
'error': 'fresh_token_required'
78+
}), 401
79+
80+
81+
@jwt.revoked_token_loader
82+
def revoked_token_callback():
83+
return jsonify({
84+
"description": "The token has been revoked.",
85+
'error': 'token_revoked'
86+
}), 401
87+
88+
# JWT configuration ends ###################################
89+
90+
91+
@app.before_first_request
92+
def create_tables():
93+
db.create_all()
94+
95+
api.add_resource(TokenRefresh, '/refresh')
96+
api.add_resource(Store, '/store/<string:name>')
97+
api.add_resource(StoreList, '/stores')
98+
api.add_resource(Item, '/item/<string:name>')
99+
api.add_resource(ItemList, '/items')
100+
api.add_resource(UserRegister, '/register')
101+
api.add_resource(UserLogin, '/login')
102+
api.add_resource(UserFreshLogin, '/fresh_login')
103+
104+
if __name__ == '__main__':
105+
from db import db
106+
107+
db.init_app(app)
108+
app.run(port=5000, debug=True)

sections_flask-jwt-extended/db.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from flask_sqlalchemy import SQLAlchemy
2+
3+
db = SQLAlchemy()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from db import db
2+
3+
class ItemModel(db.Model):
4+
__tablename__ = 'items'
5+
6+
id = db.Column(db.Integer, primary_key=True)
7+
name = db.Column(db.String(80))
8+
price = db.Column(db.Float(precision=2))
9+
10+
store_id = db.Column(db.Integer, db.ForeignKey('stores.id'))
11+
store = db.relationship('StoreModel')
12+
13+
def __init__(self, name, price, store_id):
14+
self.name = name
15+
self.price = price
16+
self.store_id = store_id
17+
18+
def json(self):
19+
return {'name': self.name, 'price': self.price}
20+
21+
@classmethod
22+
def find_by_name(cls, name):
23+
return cls.query.filter_by(name=name).first()
24+
25+
def save_to_db(self):
26+
db.session.add(self)
27+
db.session.commit()
28+
29+
def delete_from_db(self):
30+
db.session.delete(self)
31+
db.session.commit()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from db import db
2+
3+
class StoreModel(db.Model):
4+
__tablename__ = 'stores'
5+
6+
id = db.Column(db.Integer, primary_key=True)
7+
name = db.Column(db.String(80))
8+
9+
items = db.relationship('ItemModel', lazy='dynamic')
10+
11+
def __init__(self, name):
12+
self.name = name
13+
14+
def json(self):
15+
return {'name': self.name, 'items': [item.json() for item in self.items.all()]}
16+
17+
@classmethod
18+
def find_by_name(cls, name):
19+
return cls.query.filter_by(name=name).first()
20+
21+
def save_to_db(self):
22+
db.session.add(self)
23+
db.session.commit()
24+
25+
def delete_from_db(self):
26+
db.session.delete(self)
27+
db.session.commit()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from db import db
2+
3+
class UserModel(db.Model):
4+
__tablename__ = 'users'
5+
6+
id = db.Column(db.Integer, primary_key=True)
7+
username = db.Column(db.String(80))
8+
password = db.Column(db.String(80))
9+
10+
def __init__(self, username, password):
11+
self.username = username
12+
self.password = password
13+
14+
def save_to_db(self):
15+
db.session.add(self)
16+
db.session.commit()
17+
18+
@classmethod
19+
def find_by_username(cls, username):
20+
return cls.query.filter_by(username=username).first()
21+
22+
@classmethod
23+
def find_by_id(cls, _id):
24+
return cls.query.filter_by(id=_id).first()
108 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)