In depth: User resources
There are multiple user resources, each one only having one or two methods in them.
Most of these resources respond to POST
requests as that's the method used for responding to arbitrary data, and not necessarily creating models and saving them in our server (e.g. when the user sends us data for logging in, nothing is created).
Since this file is a bit long, I'll skip putting the whole code in this page (remember you can check out the full code here). However we will review each of the resources fully below.
The user parser
This parser is defined outside a specific resource, since it is used by multiple resources. You do not need to define the parser inside a resource.
This parser will look for two fields in the request body (remember that's form data or JSON payload): username
and password
.
_user_parser = reqparse.RequestParser()
_user_parser.add_argument(
"username", type=str, required=True, help="This field cannot be blank."
)
_user_parser.add_argument(
"password", type=str, required=True, help="This field cannot be blank."
)
UserRegister
class UserRegister(Resource):
def post(self):
data = _user_parser.parse_args()
if UserModel.find_by_username(data["username"]):
return {"message": "A user with that username already exists."}, 400
user = UserModel(**data)
user.save_to_db()
return {"message": "User created successfully."}, 201
This resource looks at the data passed in by the request, and creates a new UserModel
unless a user already exists with that name.
Note that passwords are not encrypted in this sample application. If you were to take this to production, you would want to encrypt passwords before saving them to the database.
Using Python and the popular library passlib
, it's only a few extra lines of code. Our blog post contains a guide on how to implement password encryption in this type of application.
User
This resource is used only for testing, to retrieve and delete existing users. Before taking your application to production, you should delete this endpoint.
We have added it so you don't have to keep deleting your database (or coming up with increasingly random usernames) when you're developing your API.
class User(Resource):
@classmethod
def get(cls, user_id: int):
user = UserModel.find_by_id(user_id)
if not user:
return {"message": "User not found."}, 404
return user.json(), 200
@classmethod
def delete(cls, user_id: int):
user = UserModel.find_by_id(user_id)
if not user:
return {"message": "User not found."}, 404
user.delete_from_db()
return {"message": "User deleted."}, 200
UserLogin
This resource uses Flask-JWT-Extended to generate a JWT (also known as an access token) and a refresh token for a given user.
JWTs contain encoded payloads (in our case, the user's id
field) which we can then use to retrieve what user the JWT is for when it comes back in another request.
This resource checks the username/password combination to make sure it is correct.
class UserLogin(Resource):
def post(self):
data = _user_parser.parse_args()
user = UserModel.find_by_username(data["username"])
if user and safe_str_cmp(user.password, data["password"]):
access_token = create_access_token(identity=user.id, fresh=True)
refresh_token = create_refresh_token(user.id)
return {"access_token": access_token, "refresh_token": refresh_token}, 200
return {"message": "Invalid credentials!"}, 401
Note that the access token returned by this method is created with the create_access_token
function, with the fresh=True
parameter. Since this endpoint responds directly to a user login, the access token returned by it will always be fresh.
Use Werkzeug's safe_str_cmp
to compare strings when you don't control the inputs (e.g. user input, as is done here).
It makes sure that they are strings and works with all versions of Python–so you don't have to worry about pesky byte strings or anything like that!
UserLogout
This resource makes use of our blacklist. When a user makes a request to this resource, we save their access token's unique identifier (this is different from their user id
!) to the blacklist, so that that specific access token can not be used again.
If they want to log in again, they can—and a new access token will be generated.
class UserLogout(Resource):
@jwt_required
def post(self):
jti = get_raw_jwt()["jti"] # jti is "JWT ID", a unique identifier for a JWT.
user_id = get_jwt_identity()
BLACKLIST.add(jti)
return {"message": "User <id={}> successfully logged out.".format(user_id)}, 200
This uses the get_raw_jwt()
function of Flask-JWT-Extended. It gives us a dictionary of the various properties stored inside the decoded JWT.
TokenRefresh
This resource takes a refresh token, and gives us a non-fresh access token.
Access tokens generally expire about 10 minutes after they are generated (some last longer, sometimes up to a few days). Token refresh allows applications that use your API to keep requesting new access tokens without the user needing to keep logging in again and again.
However, this gives us non-fresh tokens. If we have a highly sensitive operation and we want to double-check the user is actually them, we need them to log in again. If that happens, we can mark any endpoint as requiring a fresh access token.
class TokenRefresh(Resource):
@jwt_refresh_token_required
def post(self):
current_user = get_jwt_identity()
new_token = create_access_token(identity=current_user, fresh=False)
return {"access_token": new_token}, 200