Skip to content

Commit 68552ca

Browse files
committed
✨ (Flask-JWT-Extended) Add section with everything written
1 parent 2f4d931 commit 68552ca

File tree

10 files changed

+700
-19
lines changed

10 files changed

+700
-19
lines changed

docs/docs/08_flask_jwt_extended/01_section_changes/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,17 @@ title: Changes in this section
33
description: Overview of the API endpoints we'll use for user registration and authentication.
44
---
55

6-
# Changes in this section
6+
# Changes in this section
7+
8+
In this section we will add the following endpoints:
9+
10+
| Method | Endpoint | Description |
11+
| -------------- | ----------------- | ----------------------------------------------------- |
12+
| `POST` | `/register` | Create user accounts given an `email` and `password`. |
13+
| `POST` | `/login` | Get a JWT given an `email` and `password`. |
14+
| 🔒 <br/> `POST` | `/logout` | Revoke a JWT. |
15+
| 🔒 <br/> `POST` | `/refresh` | Get a fresh JWT given a refresh JWT. |
16+
| `GET` | `/user/{user_id}` | (dev-only) Get info about a user given their ID. |
17+
| `DELETE` | `/user/{user_id}` | (dev-only) Delete a user given their ID. |
18+
19+
We will also protect some existing endpoints by requiring a JWT from clients. You can see which endpoints will be protected in [The API we'll build in this course](/docs/course_intro/what_is_rest_api/#the-api-well-build-in-this-course)

docs/docs/08_flask_jwt_extended/02_what_is_a_jwt/README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ description: Understand what a JWT is, what data it contains, and how it may be
55

66
# What is a JWT?
77

8-
- What is stored in a JWT?
9-
- When does a JWT expire?
10-
- What happens when the JWT expires?
11-
- How does a client use a JWT?
8+
A JWT is a signed JSON object with a specific structure. Our Flask app will sign the JWTs with the secret key, proving that _it generated them_.
9+
10+
The Flask app generates a JWT when a user logs in (with their username and password). In the JWT, we'll store the user ID. The client then stores the JWT and sends it to us on every request.
11+
12+
Because we can prove our app generated the JWT (through its signature), and we will receive the JWT with the user ID in every request, we can _treat requests that include a JWT as "logged in"_.
13+
14+
For example, if we want certain endpoints to only be accessible to logged-in users, all we do is require a JWT in them. Since the client can only get a JWT after logging in, we know that including a JWT is proof that the client logged in successfully at some point in the past.
15+
16+
And since the JWT includes the user ID inside it, when we receive a JWT we know _who logged in_ to get the JWT.
17+
18+
There's a lot more information about JWTs here: [https://jwt.io/introduction](https://jwt.io/introduction). This includes information such as:
19+
20+
- What is stored inside a JWT?
21+
- Are JWTs secure?

docs/docs/08_flask_jwt_extended/03_flask_jwt_extended_setup/README.md

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,75 @@ title: Flask-JWT-Extended setup
33
description: Install and set up the Flask-JWT-Extended extension with our REST API.
44
---
55

6-
# Many-to-many relationships
6+
# Flask-JWT-Extended setup
77

88
- [x] Set metadata above
9-
- [ ] Start writing!
9+
- [x] Start writing!
1010
- [x] Create `start` folder
1111
- [x] Create `end` folder
12-
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
12+
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
13+
14+
First, let's update our requirements:
15+
16+
```diff title="requirements.txt"
17+
+ flask-jwt-extended
18+
```
19+
20+
Then we must do two things:
21+
22+
- Add the extension to our `app.py`.
23+
- Set a secret key that the extension will use to _sign_ the JWTs.
24+
25+
```python title="app.py"
26+
from flask import Flask
27+
from flask_smorest import Api
28+
# highlight-start
29+
from flask_jwt_extended import JWTManager
30+
# highlight-end
31+
32+
from db import db
33+
34+
from resources.item import blp as ItemBlueprint
35+
from resources.store import blp as StoreBlueprint
36+
from resources.tag import blp as TagBlueprint
37+
38+
39+
def create_app(db_url=None):
40+
app = Flask(__name__)
41+
app.config["API_TITLE"] = "Stores REST API"
42+
app.config["API_VERSION"] = "v1"
43+
app.config["OPENAPI_VERSION"] = "3.0.3"
44+
app.config["OPENAPI_URL_PREFIX"] = "/"
45+
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
46+
app.config[
47+
"OPENAPI_SWAGGER_UI_URL"
48+
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
49+
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
50+
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
51+
app.config["PROPAGATE_EXCEPTIONS"] = True
52+
db.init_app(app)
53+
api = Api(app)
54+
55+
# highlight-start
56+
app.config["JWT_SECRET_KEY"] = "jose"
57+
jwt = JWTManager(app)
58+
# highlight-end
59+
60+
@app.before_first_request
61+
def create_tables():
62+
import models # noqa: F401
63+
64+
db.create_all()
65+
66+
api.register_blueprint(ItemBlueprint)
67+
api.register_blueprint(StoreBlueprint)
68+
api.register_blueprint(TagBlueprint)
69+
70+
return app
71+
```
72+
73+
:::caution
74+
The secret key set here, `"jose"`, is **not very safe**.
75+
76+
Instead you should generate a long and random secret key using something like `secrets.SystemRandom().getrandbits(128)`.
77+
:::

docs/docs/08_flask_jwt_extended/04_user_model_and_schema/README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,42 @@ description: Create the SQLAlchemy User model and marshmallow schema.
99
- [ ] Start writing!
1010
- [x] Create `start` folder
1111
- [x] Create `end` folder
12-
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
12+
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
13+
14+
Just as we did with items, stores, and tags, let's create two classes for our users:
15+
16+
- The SQLAlchemy model, to interact with the database.
17+
- The marshmallow schema, to deserialize data from clients and serialize it back to return data.
18+
19+
## The User SQLAlchemy model
20+
21+
```python title="models/user.py"
22+
from db import db
23+
24+
25+
class UserModel(db.Model):
26+
__tablename__ = "users"
27+
28+
id = db.Column(db.Integer, primary_key=True)
29+
username = db.Column(db.String(80), unique=True, nullable=False)
30+
password = db.Column(db.String(80), unique=True, nullable=False)
31+
```
32+
33+
Let's also add this class to `models/__init__.py` so it can then be imported by `app.py`:
34+
35+
```python title="models/__init__.py"
36+
from models.user import UserModel
37+
from models.item import ItemModel
38+
from models.tag import TagModel
39+
from models.store import StoreModel
40+
from models.item_tags import ItemsTags
41+
```
42+
43+
## The User marshmallow schema
44+
45+
```python title="schemas.py"
46+
class UserSchema(Schema):
47+
id = fields.Int(dump_only=True)
48+
username = fields.Str(required=True)
49+
password = fields.Str(required=True, load_only=True)
50+
```

docs/docs/08_flask_jwt_extended/05_registering_users_rest_api/README.md

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,115 @@ description: Learn how to add a registration endpoint to a REST API using Flask-
66
# How to add a register endpoint to the REST API
77

88
- [x] Set metadata above
9-
- [ ] Start writing!
9+
- [x] Start writing!
1010
- [x] Create `start` folder
1111
- [x] Create `end` folder
12-
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
12+
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
13+
14+
Registering users sounds like a conceptually very difficult thing, but let's break it down into steps:
15+
16+
- Receive username and password from the client (as JSON).
17+
- Check if a user with that username already exists.
18+
- If it doesn't...
19+
- Encrypt the password.
20+
- Add a new `UserModel` to the database.
21+
- Return a success message.
22+
23+
## Boilerplate set-up for a blueprint with Flask-Smorest
24+
25+
First, we need our imports and blueprint set-up. This is the same for pretty much every Flask-Smorest blueprint, so you already know how to do it!
26+
27+
```python title="resources/user.py"
28+
from flask.views import MethodView
29+
from flask_smorest import Blueprint, abort
30+
from passlib.hash import pbkdf2_sha256
31+
32+
from db import db
33+
from models import UserModel
34+
from schemas import UserSchema
35+
36+
37+
blp = Blueprint("Users", "users", description="Operations on users")
38+
```
39+
40+
## Creating the `UserRegister` resource
41+
42+
Now let's create the `MethodView` class, and register a route to it using the blueprint:
43+
44+
```python title="resources/user.py"
45+
from flask.views import MethodView
46+
from flask_smorest import Blueprint, abort
47+
from passlib.hash import pbkdf2_sha256
48+
49+
from db import db
50+
from models import UserModel
51+
from schemas import UserSchema
52+
53+
54+
blp = Blueprint("Users", "users", description="Operations on users")
55+
56+
57+
# highlight-start
58+
@blp.route("/register")
59+
class UserRegister(MethodView):
60+
@blp.arguments(UserSchema)
61+
def post(self, user_data):
62+
if UserModel.query.filter(UserModel.username == user_data["username"]).first():
63+
abort(409, message="A user with that username already exists.")
64+
65+
user = UserModel(
66+
username=user_data["username"],
67+
password=pbkdf2_sha256.hash(user_data["password"]),
68+
)
69+
db.session.add(user)
70+
db.session.commit()
71+
72+
return {"message": "User created successfully."}, 201
73+
# highlight-end
74+
```
75+
76+
## Creating a testing-only `User` resource
77+
78+
Let's also create a `User` resource that we will only use during testing. It allows us to retrieve information about a single user, or delete a user. This will be handy so that using Insomnia or Postman we can clear the registered users and we don't have to change our request arguments each time!
79+
80+
```python title="resources/user.py"
81+
@blp.route("/user/<int:user_id>")
82+
class User(MethodView):
83+
"""
84+
This resource can be useful when testing our Flask app.
85+
We may not want to expose it to public users, but for the
86+
sake of demonstration in this course, it can be useful
87+
when we are manipulating data regarding the users.
88+
"""
89+
90+
@classmethod
91+
@blp.response(200, UserSchema)
92+
def get(cls, user_id: int):
93+
user = UserModel.query.get_or_404(user_id)
94+
return user
95+
96+
@classmethod
97+
def delete(cls, user_id: int):
98+
user = UserModel.query.get_or_404(user_id)
99+
db.session.delete(user)
100+
db.session.commit()
101+
return {"message": "User deleted."}, 200
102+
```
103+
104+
## Register the user blueprint in `app.py`
105+
106+
Finally, let's go to `app.py` and register the blueprint!
107+
108+
```diff title="app.py"
109+
+from resources.user import blp as UserBlueprint
110+
from resources.item import blp as ItemBlueprint
111+
from resources.store import blp as StoreBlueprint
112+
from resources.tag import blp as TagBlueprint
113+
114+
...
115+
116+
+api.register_blueprint(UserBlueprint)
117+
api.register_blueprint(ItemBlueprint)
118+
api.register_blueprint(StoreBlueprint)
119+
api.register_blueprint(TagBlueprint)
120+
```

docs/docs/08_flask_jwt_extended/06_login_users_rest_api/README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,35 @@ description: Learn how to add a login endpoint that returns a JWT to a REST API
99
- [ ] Start writing!
1010
- [x] Create `start` folder
1111
- [x] Create `end` folder
12-
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
12+
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
13+
14+
Now that we've done registration, we can do log in! It's very similar.
15+
16+
Let's import `flask_jwt_extended.create_access_token` so that when we receive a valid username and password from the client, we can create a JWT and send it back:
17+
18+
```diff title="resources/user.py"
19+
from flask.views import MethodView
20+
from flask_smorest import Blueprint, abort
21+
+from flask_jwt_extended import create_access_token
22+
from passlib.hash import pbkdf2_sha256
23+
```
24+
25+
Then let's create our `UserLogin` resource.
26+
27+
```python title="resources/user.py"
28+
@blp.route("/login")
29+
class UserLogin(MethodView):
30+
@blp.arguments(UserSchema)
31+
def post(self, user_data):
32+
user = UserModel.query.filter(
33+
UserModel.username == user_data["username"]
34+
).first()
35+
36+
if user and pbkdf2_sha256.verify(user_data["password"], user.password):
37+
access_token = create_access_token(identity=user.id)
38+
return {"access_token": access_token}, 200
39+
40+
abort(401, message="Invalid credentials.")
41+
```
42+
43+
Here you can see the when we call `create_access_token(identity=user.id)` we pass in the user's `id`. This is what gets stored (among other things) inside the JWT, so when the client sends the JWT back on every request, we can tell who the JWT belongs to.

0 commit comments

Comments
 (0)