Assignments > HW5. PhotoApp: Authentication
Due on Fri, 05/27 @ 11:59PM. 35 Points.
Update (Tuesday, 5/17 @10:00AM)
One of your classmates just informed me (Sarah) that I accidentally posted code that had the answers (in the views)! LOL. Not to worry – there’s still some work involved w/this assignment – but less work that I intended :). Please do still look at how the answers were implemented so that you understand what’s going on. Y’all have been working hard, and I appreciate your engagment with this class all quarter. And Happy Dillo Day!
Collaboration Policy
Same deal as always:
- You are welcome to work in pairs (optional).
- You may share a GitHub repo.
- You must deploy your own Heroku instance.
- If you collaborate, you’ll just list your partner in the comments section of Canvas.
1. Introduction
In this homework assignment, you are going to lock down your system so that only logged in users can interact with it. This includes two big categories of changes:
- User Interface Changes:
- Create a login form to handle authentication via JWT cookies.
- Modify your JavaScript
fetch
requests to pass theX-CSRF-TOKEN
in the HTTP request header.
- 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. - Deprecate the hard-coded session variable for user #12 and replace it with code that retrieves the user if from the JWT.
- Lock down all of your endpoints (from HW3) so that they require a valid JWT.
- Implement two new API endpoints (
To do this, we will be using JSON Web Tokens (JWTs). Please review the Lecture 15 materials for the basic JWT workflow.
1. Cookies versus authorization headers
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. In this assignment, you will be two methods:
- Within your web application interface, you will use an integrated approach that relies on an http-only JWT cookie and a CSRF token.
- For third-party apps (i.e., apps outside of your web application who also interact with your REST API), you will pass the JWT as an “Authentication header” using a “Bearer token”. In this context, you will not need the CSRF token, nor will you use cookies.
1.1. The Cookie Approach (for Browser-Based Interactions)
- Your Flask UI will rely on JWT cookies. You will write code to generate these cookies on the server. Subsequently, these cookies will sent back and forth between the browser and the server via request and response headers (respectively).
-
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.
For internal clients that use your REST API, you need to embed something called an X-CSRF-TOKEN
in the header of your fetch requests. Here is an example of how you might use fetch to access a protected REST Endpoint from within your UI (like you did in HW4):
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);
});
1.2. The Authorization Bearer Approach (for Third-Party Clients)
For external clients, you need to offer them another way to access your REST API without actually having to log into your UI. To do this, you will implement:
- 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.
Here is an example of how you might use fetch to access a protected resource from an external python app, 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())
2. 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:
2. Setup
1. Configure your local git repo
- On your terminal or command line, navigate to your photo-app folder.
- Make sure that
hw04
is still your active branch by typinggit branch
. - Make sure that you commit all of your HW4 changes before creating a new branch.
- Next, create a branch called hw05 as follows:
git checkout -b hw05
. Since you created yourhw05
branch from yourhw04
branch, yourhw05
branch should be currently be identical to yourhw04
branch. - Type
git branch
to ensure that you are indeed on thehw05
branch (it should be green with an asterik next to it). - Push your hw05 branch to GitHub as follows:
git push -u origin hw05
- Open GitHub in your web browser and verify that your hw05 branch is there.
2. Integrate the new files
Download hw05.zip and unzip it.
You should see a directory structure that looks like this:
hw05
├── app.py
├── decorators.py
├── models
│ └── api_structure.py
├── requirements.txt
├── static
│ └── js
│ ├── api.js
│ └── utilities.js
├── templates
│ ├── api
│ │ ├── api-docs.html
│ │ ├── include-endpoint-detail.html
│ │ ├── include-form.html
│ │ └── navbar-api.html
│ ├── includes
│ │ └── navbar.html
│ └── login.html
├── tests_updated
│ ├── __init__.py
│ ├── run_tests.py
│ ├── test_bookmarks.py
│ ├── test_comments.py
│ ├── test_followers.py
│ ├── test_following.py
│ ├── test_like_post.py
│ ├── test_login.py
│ ├── test_logout.py
│ ├── test_posts.py
│ ├── test_profile.py
│ ├── test_stories.py
│ ├── test_suggestions.py
│ ├── test_token.py
│ └── utils.py
└── views
├── __init__.py
├── authentication.py
└── token.py
Please integrate the starter files VERY CAREFULLY (don’t rush) as follows:
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. |
static/js/utilities.js |
Helper file for handling cookies and escaping HTML (to prevent against XSS attacks). |
tests_updated (entire folder) |
Updated tests that incorporate authentication. |
templates/login.html |
New Login form stub. |
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. |
Replace
File / Folder | What Changed? |
---|---|
app.py |
Added some new code that handles JWT Authentication and figuring out the current user |
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/js/api.js |
Fetch requests updated to include authentication token in the header. |
templates/api (entire folder) |
Now includes sample code for how to make requests with the authentication headers. |
templates/includes/navbar.html |
Logout button now connected to logout route. |
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.
3. Your Tasks
For this assignment, you will be implementing an authentication system for your REST API and for your app. There are 4 tasks you need to complete:
- Securing the user interface
- Deprecating the hard-coded reference to User #12
- Securing the REST API
- Deploying to Heroku
1. Securing the User Interface (15 Points)
In order to implement authentication within your Photo App UI, you will:
Task | Description | Points |
---|---|---|
1. Create login form for UI | 7 points | |
Create an HTML login form for your app (feel free to borrow code from the Lecture 15 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. | 1 | |
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 (/ ).
|
2 | |
If the /login POST endpoint does not receive a valid username and password, redisplay the form with an appropriate error message.
|
2 | |
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 | |
4. Modify your JavaScript fetch statements | 3 points | |
Update your JavaScript fetch requests (from HW4) so that they use the X-CSRF-TOKEN in the request header. Otherwise, your POST, DELETE, and PATCH requests will be rejected by your API. For an example of how to do this, please see static/js/api.js .
|
3 |
2. Deprecating User #12 (4 Points)
Now that you’ve implemented a way for your user to log, you need display the logged in user’s data. However, the way the application is currently configured, you’re still displaying User #12. To fix this, you will need to deprecate app.current_user
, which relies on the following code in app.py
# set logged in user
with app.app_context():
app.current_user = User.query.filter_by(id=12).one()
Luckily, the flask-jwt-extended
library provides a way to do this. The approach:
- Define a function that retrieves the User object based on the user_id that is embedded in the token.
- Add 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:
First, define a function for retrieving a user from the database using the embedded JWT user_id:
# 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, replace ALL instances of app.current_user
with flask_jwt_extended.current_user
.
3. Securing the REST API (15 Points)
You will make the following changes to your REST API in order to implement JWT authentication:
- Create an endpoint to issue a access / refresh token.
- Create an endpoint to issue a new access token (using your refresh token).
- Lock down all of your endpoints.
Method/Route | Description | Parameters | Points |
---|---|---|---|
POST /api/token | Issues an access and refresh token based on the credentials posted to the API Endpoint. Example (truncated for readability): { "access_token": "e0e.dsc.3NI6Ij", "refresh_token": "e0e.mcm.6ktQ" } |
|
5 |
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 |
All routes |
Lockdown all endpoints.
|
5 |
- When you’re done with parts 1-3, all of the tests in
tests_updated
should pass (just runrun_tests.py
).
4. Deploying to Heroku (1 Point)
Please commit all of your changes / additional files to git. When you’re done, create a NEW Heroku app for your hw05 branch. You will follow the same process as you did for the HW3 Heroku deployment, with two differences:
- Instead of creating a brand new Heroku Postgres database, just use the same DB_URL environment variable that you used in HW3. In other words, your HW3 and HW4 Heroku deployments will share a database.
- Make sure you push your hw05 branch to Heroku: git push heroku hw05:main
- You will have to 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.
4. What to Turn In
Please review the requirements above and ensure you have met them. Specifically:
Points | Category |
---|---|
15 points | User Interface Related Tasks |
4 points | Deprecate User #12 |
15 points | REST API Tasks |
1 points | Deploy to Heroku |
Canvas Submission
When you’re done, please submit the following to Canvas:
- A zip file of your code
- A comment that includes the following:
- A link to your Heroku app
- The name of your partner (if applicable)