CS396: Winter 2022

Intro to Web Development

CS396: Winter 2022

Assignments > HW5. PhotoApp: Authentication

Due on Sun, 03/06 @ 11:59PM. 40 Points.

Collaboration Policy

Same deal as always: you are welcome to work in pairs (optional). You must still maintain your own code files via your own GitHub repository, and you must deploy your own Heroku instance. You are welcome to collaborate on code and discuss code and strategies with your partner. 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:

  1. Interacting with the API you built in HW3
  2. Interacting with the user interface (UI) you built in HW4

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

As discussed in lecture, you can pass JWTs between the client and the server in a variety of different ways: through cookies, custom HTTP headers, the request body, and/or as query parameters. In this assignment, you will be using the JWT “cookie approach” to handle authentication from within your UI, and the JWT “HTTP Header approach” to handle authentication for your REST API.

  1. 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).
  2. The flask-jwt-extended library has a few convenience functions that will help you generate and set these cookies:

    • create_access_token() – generates the token
    • set_access_cookies() – sets the access cookies on the response header
  3. 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.
    • When the JWT access token expires, the system redirects the user to the login screen.

The HTTP Header Approach (for the REST API)

In the case of the REST API, your client may or may not be your Flask application. For instance, in HW3, the Python test suite and Postman were external clients. But in HW4, the client was your hw4.js file, which was part of your UI.

Browser Clients

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);
    });
Non-Browser 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:

Here is an example of how you might use fetch to access a protected resource from an external python app (or from Postman), using an access token:

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

Before making any changes to your photo-app folder, let’s create a snapshot of your current work (from HW04) by creating a hw04 branch using git:

  1. On your terminal or command line, navigate to your photo-app folder.
  2. Using git, create a branch called hw04 as follows: git checkout -b hw04
  3. Now type git branch and you should see several branches, including: main and hw04. Also note that you are currently on the hw04 branch (it should be green with an asterik next to it).
  4. Push your hw04 branch to GitHub as follows: git push -u origin hw04
  5. Open GitHub in your web browser and verify that your hw04 branch is there.
  6. Finally, on your local computer, switch back to your main branch (i.e., make main the active branch you’re working on) by typing git checkout main.
  7. Verify that you are on your main branch by typing git branch (you should see an asterik next to it).

2. Install dependencies:

3. 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

4. Download & incorporate the starter files

Download hw05.zip and unzip it.

hw05.zip

You should see a directory structure that looks like this:

hw05
├── app_updates.py
├── decorators.py
├── models
│   └── api_structure.py
├── populate.py
├── static
│   └── js
│       └── api.js
├── templates
│   ├── api
│   │   ├── api-docs.html
│   │   ├── include-endpoint-detail.html
│   │   └── include-form.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
    ├── authentication.py
    └── token.py

Please integrate the starter files as follows:

Source (hw05)   Destination (photo-app) Action
app_updates.py –> app_updates.py Add (new file)
decorators.py –> decorators.py Add (new file)
populate.py –> populate.py Replace
views/authentication.py –> views/authentication.py Add (new file)
views/token.py –> views/token.py Add (new file)
tests_updated –> tests_updated Add (entire folder)
templates/api –> templates/api Replace (entire folder)
templates/login.html –> templates/login.html Add (new file)
templates/navbar.html –> templates/navbar.html Replace
static/js/api.js –> static/js/api.js Replace
models/api_structure.py –> models/api_structure.py Replace
app_updates.py –> app.py Integrate

In regards to app_updates.py, please integrate the code into your app.py file:

5. Rebuild your database

When you’re done, please rebuild your database python3 populate.py (or however you’ve done it in the past). There was a bug in how the passwords were hashed in the users table, which needs to be fixed for this assignment to work.

6. 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:

  1. Securing the user interface
  2. Deprecating the hard-coded reference to User #12
  3. Securing the REST API
  4. 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.
  • When you're done, your tests_updated/test_login.py tests should pass.
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
Create a custom decorator(or use the existing on in `decorators.py`) to secure your / and /api endpoints:
  • If the user is logged in (i.e. a JWT cookie is present), allow them to access the page.
  • If the user is not logged in, redirect them to the login page.
2
4. Modify your JavaScript fetch statements 3 points
Update your JavaScript fetch requests (from HW4) so that they use the X-CSRF header token. 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 (5 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:

  1. Define a function that retrieves the User object based on the user_id that is embedded in the token.
  2. Add the @jwt.user_lookup_loader decorator to the top of the function. By doing this, you can use the built-in flask-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 any instance 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:

  1. Create an endpoint to issue a access / refresh token.
  2. Create an endpoint to issue a new access token (using your refresh token).
  3. 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"
}
  • username (string, required): The username of the person logging in.
  • password (string, required): The password of the person logging in.
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"
}
  • refresh_token (string, required): The refresh token that was previously issued to the user from the /api/token endpoint.
5
All routes Lockdown all endpoints. Every API endpoint in the system should now require a JWT token. Hint: use the @jwt_required() decorator from the flask-jwt-extended library. Ensure that all tests pass with the new test suite. 5

4. Deploying to Heroku (5 Points)

Please check in all of your changes / additional files to Git, and deploy your completed app to Heroku.

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
5 points Deprecate User #12
15 points REST API Tasks
5 points Deploy to Heroku

Canvas Submission

When you’re done, please submit the following to Canvas: