Assignments > HW9: Authentication
Due on Fri, 04/28 @ 11:59PM. 35 Points.
Updates
Video Walkthroughs
Sarah has created a set of “get started” files that should help you with this assignment:
Errata & Notes
- There were a few bugs in the integration instructions / tests, so if you downloaded the starter files before 4/23 at 10PM, please re-download the starter files (and the tests_updated directory in particular).
- Within the views/posts.py file: please make sure that when you convert the Post objects to JSON, you pass the current_user into the to_dict() method. Example:
json.dumps(post.to_dict(user=self.current_user))
This is necessary for generatingcurrent_user_like_id
andcurrent_user_bookmark_id
(which are needed for the like / bookmark functionality to work with the React client).
1. Introduction
In this homework assignment, you are going to lock down your system so that only logged in users can interact with it by using JSON Web Tokens (JWTs). This requires the following changes:
1. REST API Changes
- Implement two new API endpoints (
/api/token
and/api/token/refresh
) so that third-party clients can also access your REST API. - Lock down all of your endpoints so that they require a valid JWT.
- Deprecate the hard-coded session variable for user #12 and replace it with code that retrieves the user if from the JWT.
2. User Interface Changes
- Create a login form to handle authentication via JWT cookies.
- Integrate your React App into Flask, and protect it.
2. Background & Relevant Concepts
1. Bearer Tokens (External) v. Cookies + CSRF Tokens (Internal)
You can pass JWTs between the client and the server in a variety of different ways: through cookies, through custom HTTP headers, through the request body, and/or as query parameters.
- External Clients: For HW4-HW6, you created an external client – located on a different server than the API – to interact with your REST API. To do this, you passed the JWT Bearer token in the HTTP. In this context, you did not need the CSRF token or cookies.
- Internal Clients: For HW9 (this homework), you will be using an internal client that will be hosted within the same web app as the REST API. This allows you to take advantage of some additional security measures, namely using an http-only JWT cookie, and incorporating a CSRF token in the HTTP header.
2. External Client Workflow
External clients do not rely on cookies. Instead, they usually pass authentication information via “Bearer Tokens” passed using HTTP headers. Given this, you need to implement the following features within the REST API:
- A way for a user to authenticate with the REST API order to receive an access and refresh token.
- Security measures on all of your REST API endpoints that require an access token.
As you have already seen, the followint code shows how you might fetch a protected resource from an external web application (running on a separate server), using a Bearer token:
For JavaScript clients that issue requests from other servers (not one that you own):
fetch("/api/posts", {
method: "GET",
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
})
.then(response => response.json())
.then(data => {
console.log(data);
});
For Python clients:
import requests
response = requests.get(
'http://localhost:5000/api/posts',
headers={
'Authorization': 'Bearer ' + access_token
}
)
print('Status Code:', response.status_code)
print(response.json())
3. Internal Client Workflow
Internal, browser-based clients can take advantage of additional security measures that aren’t available to external clients. Namely http-only JWT Cookies and CSRF tokens. Specifically:
- You will write code to generate a JWT cookie, which will sent back and forth between the browser and the server via request and response headers.
-
The
flask-jwt-extended
library has a few convenience functions that will help you generate and set these cookies:create_access_token()
– generates the tokenset_access_cookies()
– sets the access cookies on the response header
-
Workflow:
- User sends username and password to the server via a login form.
- If the credentials are valid, the server sets the JWT tokens using cookies.
- Because the JWT cookies are set, the system will know who is logged in.
- Web applications will also pass an
X-CSRF-TOKEN
as an extra security measure (see below).
- Web applications will also pass an
- When the JWT access token expires, the system redirects the user to the login screen.
Then, rather than using a Bearer Token, you will not only rely on the JWT http-only server cookie, but also on an X-CSRF-TOKEN
token, which will be included in all POST, PATCH, PUT, and DELETE requests. This token will protect against Cross-Site Request Forgery attacks. Here is an example of how you might use fetch to access a protected REST Endpoint from within your UI:
fetch("/api/posts", {
method: "GET",
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '5c4f034d-13d6-4aa2-b686-ee0add18426b'
}
})
.then(response => response.json())
.then(data => {
console.log(data);
});
4. The Flask-JWT-Extended Library
To help you implement the JWT workflow, you will be using the flask-jwt-extended
library, which offers some common JSON Web Token functionality that will help you. Please refer to the full documentation to get a more comprehensive explanation. Some links that we have found to be particularly helpful:
3. Setup
Download hw09.zip and unzip it.
You should see a directory structure that looks like this:
hw09
├── decorators.py
├── lib
├── models
│ └── api_structure.py
├── requirements.txt
├── static
├── templates
├── tests_updated
└── views
├── __init__.py
├── authentication.py
└── token.py
Please integrate the starter files VERY CAREFULLY (don’t rush) as follows:
1. Add (new files)
File / Folder | What is this file? |
---|---|
decorators.py |
Added a decorator whose job is to redirect to the login page if no credentials are found. |
lib (entire folder) |
Some utilities for integrating the API with your react app. |
tests_updated (entire folder) |
Updated tests that incorporate authentication. |
views/authentication.py |
View that handles the login / logout form functionality. |
views/token.py |
API Endpoint that issues access / refresh token if authorized credentials are provided. |
2. Replace
File / Folder | What Changed? |
---|---|
models/api_structure.py |
New routes (/login , /logout , /api/token , and /api/token/refresh ) added to the tester. |
requirements.txt |
Added new library / dependency called Flask-JWT-Extended |
static (entire folder) |
A few new helper JavaScript and CSS files. |
templates (entire folder) |
Now includes sample code for how to make requests with the authentication headers. |
views/__init__.py |
initialize_routes function updated to include new routes (/login , /logout , /api/token , and /api/token/refresh ). |
3. Install dependencies
On the command line / Terminal / shell, activate your virtual environment. Then, install the new Flask-JWT-Extended
dependency as follows:
python -m pip install -r requirements.txt
4. Create a new environment variable
In your .env
file, add a new environment variable for your JWT secret. You can make this secret anything you want:
JWT_SECRET=MY_SECRET
5. Run your old tests
Run your old tests (in the tests
directory). They should all still pass). By the end of the assignment, all of the new tests (in the tests_updated
directory) should pass.
4. REST API Tasks
4.1. Modify app.py
In order to integrate JWT security measures into your app, you will have to make some modifications to app.py
. Please complete the following 4 steps:
(1) Add the new import statements:
# import statement at the top
import flask_jwt_extended
(2) Replace CORS statement:
# replace CORS statement with this one:
# update:
cors = CORS(app,
resources={r"/api/*": {"origins": '*'}},
supports_credentials=True # new
)
(3) Turn on the JWT Manager:
# 4. Turn on the JWT Manager
app.config["JWT_SECRET_KEY"] = os.environ.get('JWT_SECRET')
app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies"]
app.config["JWT_COOKIE_SECURE"] = False
app.config['PROPAGATE_EXCEPTIONS'] = True
jwt = flask_jwt_extended.JWTManager(app)
(4) Add the JWT user lookup convenience function (https://flask-jwt-extended.readthedocs.io/en/stable/automatic_user_loading/)
# Include JWT starter code for querying the DB for user info:
@jwt.user_lookup_loader
def user_lookup_callback(_jwt_header, jwt_data):
# print('JWT data:', jwt_data)
user_id = jwt_data["sub"]
return User.query.filter_by(id=user_id).one_or_none()
4.2. Secure the REST API (20 Points)
After updating app.py
, you will make the following three changes to your REST API in order to implement JWT authentication:
Method/Route | Description | Parameters | Points | |
---|---|---|---|---|
1. | POST /api/token | Issues an access and refresh token if the correct credentials are posted to the endpoint. Example (truncated for readability): { "access_token": "e0e.dsc.3NI6Ij", "refresh_token": "e0e.mcm.6ktQ" } |
|
5 |
2. | POST /api/token/refresh |
Issues new access token if a valid refresh token is posted to the endpoint. Example (truncated for readability): { "access_token": "e0e.Ras.i3NyZ" } |
|
5 |
3. | All routes |
Lockdown all endpoints (see note below).
|
5 | |
4. | app.py |
Deprecate app.current_user (which is hardcoded to User #12) and use the user_id embedded in the JWT instead.
|
5 |
Note on Deprecating User #12
Deprecate app.current_user
by commenting out the following lines in app.py
# set logged in user
with app.app_context():
app.current_user = User.query.filter_by(id=12).one()
In its place, you will used a different approach suggested by the flask-jwt-extended
library, which involves:
- Defining a function that retrieves the User object based on the user_id that is embedded in the token.
- Adding the
@jwt.user_lookup_loader
decorator to the top of the function. By doing this, you can use the built-inflask_jwt_extended.current_user
property to access the logged in user (works like magic).
Sample code:
# defines the function for retrieving a user from the database
@jwt.user_lookup_loader
def user_lookup_callback(_jwt_header, jwt_data):
# print('JWT data:', jwt_data)
# https://flask-jwt-extended.readthedocs.io/en/stable/automatic_user_loading/
user_id = jwt_data["sub"]
return User.query.filter_by(id=user_id).one_or_none()
When you’re done, you will replace ALL instances of app.current_user
with flask_jwt_extended.current_user
.
- When you’re done with the 4 tasks listed above all of the tests in
tests_updated
should pass (just runrun_tests.py
) EXCEPT fortest_login.py
andtest_logout.py
(you’ll do that in the next step).
5. UI Tasks
In addition to modifying the REST API Endpoints, you are also going to secure your React client by integrating it into your Flask App and creating a login screen to protect it. To do this, please complete the following tasks:
5.1. Integrate Your React Client
Copy your react client folder (from HW6) into the root of your Flask app. Rename that folder to react-client
(you can call it anything, but let’s keep it consistent for simplicity).
5.2. Modify app.py
When you’re done, make the following changes to app.py
so that the base URL opens the React UI (if the user is logged in).
(1) Add the new import statements:
# import statement at the top
from lib.flask_multistatic import MultiStaticFlask as Flask
from flask import send_from_directory
(2) Tell flask which static folders it needs to be aware of:
# place the following after: app = Flask(__name__)
app.static_folder = [
os.path.join(app.root_path, 'react-client', 'build', 'static'),
os.path.join(app.root_path, 'static')
]
(3) Modify the root path to point to your React App:
@app.route('/')
# @decorators.jwt_or_login
def home():
# https://medium.com/swlh/how-to-deploy-a-react-python-flask-project-on-heroku-edb99309311
return send_from_directory(app.root_path + '/react-client/build', 'index.html')
5.3. Build Your React Client
Navigate into your react-client
folder from the command line and compile your react application by issuing the following command:
npm run build
This should create a new directory called “build” that will house your compiled React app. When you’re done, restart your flask app and navigate to http://127.0.0.1:5000. You should see your react app running.
5.4. Secure the User Interface (15 Points)
Secure the user interface as follows:
Task | Description | Points |
---|---|---|
1. Create login form for UI | 10 points | |
Create an HTML login form for your app (feel free to borrow code from the Lecture 25 files) by editing the templates/login.html html file. The form should POST to the /login endpoint.
|
2 | |
Ensure that the form is accessible by using the Wave Chrome extension. | 2 | |
Implement the /login POST endpoint by editing views/authentication.py . If the enpoint receives a valid username and password , it should set the JWT cookie in the response header and redirect the user to the home screen (/ ).
|
3 | |
If the /login POST endpoint does not receive a valid username and password, redisplay the form with an appropriate error message.
|
3 | |
2. Create logout form for UI | 3 points | |
Create logout endpoint (GET) by editing views/authentication.py . This endpoint should unset the JWT cookies and redirect the user to the /login page. When you're done, your tests_updated/test_logout.py tests should pass.
|
3 | |
3. Lockdown your UI Endpoints | 2 points | |
Use the @decorators.jwt_or_login (from decorators.py ) to secure your / and /api endpoints in app.py .
decorators.py to make sure you understand what this decorator is actually doing.
|
2 |
When you’re done, all of the tests_updated
tests should pass.
6. Extra Credit (10pts): Deploying to Heroku
If you plan to deploy your secured React + Flask app to Heroku, there are a few additional steps you need to complete:
- Create a
package.json
file at the root of your Flask app (note that this is in addition to thepackage.json
file inside of yourreact-client
directory) that has the JSON format shown below. - The second
package.json
file at the root of your app teaches Heroku to compile your react app from within thereact-client
subdirectory. - Ensure that your
react-client/node_modules
andreact-client/build
folders are excluded from git via your.gitignore
file. If you don’t know how to do this, ask Sarah. - Commit and push your changes to GitHub.
- On Heroku, create one additional environment variable called
JWT_SECRET
. We recommend that you use the same secret locally and on Heroku. To add an environment variable, go to the settings tab of your app and click the “Reveal Config Vars” button. - Deploy to Heroku (see instructions from HW7).
package.json file at the root of your Flask app (not the one in your react-client
folder):
{
"name": "photo-app-heroku-react-build-file",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "cd react-client && npm install && npm run build"
},
"dependencies": {
"cross-env": "^7.0.3"
}
}
7. What to Turn In
Please review the requirements above and ensure you have met them. Specifically:
Points | Category |
---|---|
20 points | REST API Tasks |
15 points | User Interface Related Tasks |
Moodle Submission
When you’re done, please submit the following to Moodle:
- Either a link to your GitHub Repo (preferred) or a zip file of your code (excluding
node_modules
and yourenv
) - If you worked with a partner, please list your partner
- (Optional) A link to your deployed Heroku instance: 10 points extra credit