Skip to content

Commit bd8058b

Browse files
committed
add jwt extended features
1 parent 28544fc commit bd8058b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+184
-47
lines changed

Diff for: __pycache__/blacklist.cpython-37.pyc

151 Bytes
Binary file not shown.

Diff for: app.py

+64-9
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,88 @@
11
import os
22

3-
from flask import Flask
3+
from flask import Flask, jsonify
44
from flask_restful import Api
5-
from flask_jwt import JWT
5+
from flask_jwt_extended import JWTManager
66

7-
from security import authenticate, identity
8-
from resources.user import UserRegister
7+
from blacklist import BLACKLIST
8+
from resources.user import User, UserRegister, UserLogin, UserLogout, TokenRefresh
99
from resources.item import Item, ItemList
1010
from resources.store import Store, StoreList
1111

1212
app = Flask(__name__)
13-
app.secret_key = 'some_secret_key'
13+
app.secret_key = 'some_secret_key' # app.config['JWT_SECRET_KEY']
1414
# Could be sqlite, mysql, postgresql, oracle ... without changing any of the code
1515
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///data.db') # second argument default value
1616
# Turns off Flask-SQLAlchemy tracking but no for SQLAlchemy
1717
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
18+
# Propagates internal errors to the user (not secure, but good for debugin)
19+
app.config['PROPAGATE_EXCEPTIONS'] = True
20+
app.config['JWT_BLACKLIST_ENABLED'] = True
21+
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh']
1822
api = Api(app)
1923

20-
# @app.before_first_request
21-
# def create_tables():
22-
# db.create_all()
24+
@app.before_first_request
25+
def create_tables():
26+
db.create_all()
2327

24-
jwt = JWT(app, authenticate, identity) # /auth
28+
jwt = JWTManager(app) # not creating /auth
2529

30+
@jwt.user_claims_loader
31+
def add_claims_to_jwt(identity):
32+
# Insted of hard-coding, you should read from a config file or db
33+
if identity == 1:
34+
return {'is_admin': True}
35+
return {'is_admin': False}
36+
37+
@jwt.token_in_blacklist_loader
38+
def check_if_token_in_blacklist(decrypted_token):
39+
return decrypted_token['jti'] in BLACKLIST
40+
41+
@jwt.expired_token_loader
42+
def expired_token_callback():
43+
return jsonify({
44+
'message': 'The token has expired',
45+
'error': 'token_expired'
46+
}), 401
47+
48+
@jwt.invalid_token_loader
49+
def invalid_token_callback(error):
50+
return jsonify({
51+
'message': 'Signature verification failed',
52+
'error': 'invalid_token'
53+
}), 401
54+
55+
@jwt.unauthorized_loader
56+
def unauthorized_callback(error):
57+
return jsonify({
58+
'message': 'Unauthorized to access this resource',
59+
'error': 'unauthorization_required'
60+
}), 401
61+
62+
@jwt.needs_fresh_token_loader
63+
def needs_fresh_token_callback(error):
64+
return jsonify({
65+
'message': 'Nedd fresh token to access this resource',
66+
'error': 'needs_fresh_token'
67+
}), 401
68+
69+
70+
@jwt.revoked_token_loader
71+
def revoked_token_callback(error):
72+
return jsonify({
73+
'message': 'You dont have permission any more',
74+
'error': 'revoked_token'
75+
}), 401
76+
77+
api.add_resource(User, '/user/<int:user_id>')
2678
api.add_resource(UserRegister,'/register')
79+
api.add_resource(UserLogin, '/login')
80+
api.add_resource(UserLogout, '/logout')
2781
api.add_resource(ItemList,'/items')
2882
api.add_resource(Item,'/item/<string:name>')
2983
api.add_resource(StoreList,'/stores')
3084
api.add_resource(Store,'/store/<string:name>')
85+
api.add_resource(TokenRefresh, '/refresh')
3186

3287
if __name__ == '__main__':
3388
from db import db

Diff for: blacklist.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
BLACKLIST = set()

Diff for: data.db

16 KB
Binary file not shown.

Diff for: models/__pycache__/item.cpython-37.pyc

29 Bytes
Binary file not shown.

Diff for: models/__pycache__/store.cpython-37.pyc

-4 Bytes
Binary file not shown.

Diff for: models/__pycache__/user.cpython-37.pyc

292 Bytes
Binary file not shown.

Diff for: models/item.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@ def __init__(self, name, price, store_id):
1616
self.store_id = store_id
1717

1818
def json(self):
19-
return {'name': self.name, 'price': self.price}
19+
return {
20+
'id': self.id,
21+
'name': self.name,
22+
'price': self.price,
23+
'store_id': self.store_id
24+
}
2025

2126
@classmethod
2227
def get_all(cls):
23-
return ItemModel.query.all()
28+
return cls.query.all()
2429

2530
@classmethod
2631
def find_by_name(cls,name):

Diff for: models/store.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ def __init__(self, name):
1313
self.name = name
1414

1515
def json(self):
16-
itemsJson = [item.json() for item in self.items.all()]
17-
return {'name': self.name, 'items': itemsJson}
16+
return {
17+
'id': self.id,
18+
'name': self.name,
19+
'items': [item.json() for item in self.items.all()]
20+
}
1821

1922
@classmethod
2023
def get_all(cls):
21-
return StoreModel.query.all()
24+
return cls.query.all()
2225

2326
@classmethod
2427
def find_by_name(cls,name):

Diff for: models/user.py

+10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ def __init__(self, username, password):
1111
self.username = username
1212
self.password = password
1313

14+
def json(self):
15+
return {
16+
'id': self.id,
17+
'username': self.username
18+
}
19+
1420
@classmethod
1521
def find_by_username(cls, username):
1622
return cls.query.filter_by(username=username).first()
@@ -21,4 +27,8 @@ def find_by_id(cls, _id):
2127

2228
def save_to_db(self):
2329
db.session.add(self)
30+
db.session.commit()
31+
32+
def delete(self):
33+
db.session.delete(self)
2434
db.session.commit()

Diff for: requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Flask
22
Flask-RESTful
33
Flask-SQLAlchemy
4-
Flask-JWT
4+
Flask-JWT-Extended
55
uwsgi
66
psycopg2

Diff for: resources/__pycache__/item.cpython-37.pyc

409 Bytes
Binary file not shown.

Diff for: resources/__pycache__/store.cpython-37.pyc

0 Bytes
Binary file not shown.

Diff for: resources/__pycache__/user.cpython-37.pyc

2 KB
Binary file not shown.

Diff for: resources/item.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from flask_restful import Resource, reqparse
2-
from flask_jwt import jwt_required
2+
from flask_jwt_extended import (
3+
jwt_required,
4+
get_jwt_claims,
5+
jwt_optional,
6+
get_jwt_identity,
7+
fresh_jwt_required
8+
)
39
from models.item import ItemModel
410

511
class Item(Resource):
@@ -26,7 +32,7 @@ def get(self, name):
2632
return item.json()
2733
return {'message': 'Item not found'},404
2834

29-
@jwt_required()
35+
@jwt_required
3036
def post(self, name):
3137
if ItemModel.find_by_name(name):
3238
return {'error': "An item with name {} already exists".format(name)}, 400
@@ -38,7 +44,7 @@ def post(self, name):
3844
return {'message': 'Error inserting item'}, 500
3945
return item.json(), 201
4046

41-
@jwt_required()
47+
@jwt_required
4248
def put(self, name):
4349
data = Item.parser.parse_args()
4450
item = ItemModel.find_by_name(name)
@@ -50,15 +56,26 @@ def put(self, name):
5056
item.save_to_db()
5157
return item.json(), 201
5258

53-
@jwt_required()
59+
@fresh_jwt_required
5460
def delete(self, name):
61+
claims = get_jwt_claims()
62+
if not claims['is_admin']:
63+
return {'message': 'Admin privilege required'}, 401
5564
item = ItemModel.find_by_name(name)
5665
if item:
5766
item.delete()
5867
return {'message': 'Item deleted'}
5968

6069
class ItemList(Resource):
70+
@jwt_optional
6171
def get(self):
72+
user_id = get_jwt_identity()
6273
items = ItemModel.get_all()
63-
return {'items': [i.json() for i in items ]}
64-
# list(map(lambda i: i.json(), items))
74+
# If logged in
75+
if user_id:
76+
return {'items': [i.json() for i in items ]}, 200
77+
# list(map(lambda i: i.json(), items))
78+
return {
79+
'items': [i.name for i in items],
80+
'message': 'More info if logged in'
81+
}, 200

Diff for: resources/user.py

+72-15
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,79 @@
11
from flask_restful import Resource, reqparse
2-
from models.user import UserModel
2+
from flask_jwt_extended import (
3+
create_access_token,
4+
create_refresh_token,
5+
jwt_refresh_token_required,
6+
get_jwt_identity,
7+
jwt_required,
8+
get_raw_jwt
9+
)
10+
from werkzeug.security import safe_str_cmp
11+
from models.user import UserModel
12+
from blacklist import BLACKLIST
313

4-
class UserRegister(Resource):
5-
parser = reqparse.RequestParser()
6-
parser.add_argument('username',
7-
type=str,
8-
required=True,
9-
help="This field cannot be left blank"
10-
)
11-
parser.add_argument('password',
12-
type=str,
13-
required=True,
14-
help="This field cannot be left blank"
15-
)
14+
_user_parser = reqparse.RequestParser()
15+
_user_parser.add_argument('username',
16+
type=str,
17+
required=True,
18+
help="This field cannot be left blank"
19+
)
20+
_user_parser.add_argument('password',
21+
type=str,
22+
required=True,
23+
help="This field cannot be left blank"
24+
)
1625

26+
class UserRegister(Resource):
1727
def post(self):
18-
data = UserRegister.parser.parse_args()
28+
data = _user_parser.parse_args()
1929
if UserModel.find_by_username(data['username']):
2030
return {'message': 'Username already exists'}, 400
2131
UserModel(**data).save_to_db()
22-
return {'message': 'User created successfully'}, 201
32+
return {'message': 'User created successfully'}, 201
33+
34+
class User(Resource):
35+
@classmethod
36+
def get(cls, user_id):
37+
user = UserModel.find_by_id(user_id)
38+
if not user:
39+
return {'message': 'User not found'}, 404
40+
return user.json()
41+
42+
@classmethod
43+
def delete(cls, user_id):
44+
user = UserModel.find_by_id(user_id)
45+
if not user:
46+
return {'message': 'User not found'}, 404
47+
user.delete()
48+
return {'message': 'User deleted'}, 200
49+
50+
class UserLogin(Resource):
51+
@classmethod
52+
def post(cls):
53+
data = _user_parser.parse_args()
54+
user = UserModel.find_by_username(data['username'])
55+
# This is what the authenticate() function used to do
56+
if user and safe_str_cmp(user.password, data['password']):
57+
# This is what the identity() function used to do
58+
access_token = create_access_token(identity=user.id, fresh=True)
59+
refresh_token = create_refresh_token(user.id)
60+
return {
61+
'access_token': access_token,
62+
'refresh_token': refresh_token
63+
}, 200
64+
return {'message': 'Invalid credentials'}, 401
65+
66+
class UserLogout(Resource):
67+
@jwt_required
68+
def post(self):
69+
jti = get_raw_jwt()['jti']
70+
BLACKLIST.add(jti)
71+
return {'message': 'Successfully logged out'}, 200
72+
73+
74+
class TokenRefresh(Resource):
75+
@jwt_refresh_token_required
76+
def post(self):
77+
current_user = get_jwt_identity()
78+
new_token = create_access_token(identity=current_user, fresh=False)
79+
return {'access_token': new_token}, 200

Diff for: security.py

-11
This file was deleted.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/abc.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/base64.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/bisect.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/codecs.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/copy.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/enum.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/heapq.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/imp.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/io.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/locale.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/ntpath.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/os.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/random.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/re.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/shutil.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/stat.cpython-37.pyc

-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/struct.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/token.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

Diff for: venv/lib/python3.7/__pycache__/types.cpython-37.pyc

-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.
-29 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)