CSCI 344: Spring 2025

Advanced Web Technology

CSCI 344: Spring 2025

UNCA Logo

Assignments > HW6: Adding Security + Authentication

Due on Wed, 05/07 @ 11:59PM. 35 Points.

Video Walkthroughs for Homework 6

I have created some video walkthroughs to help you with Homework 6, which have been posted to Google Drive.

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:

REST API Changes

User Interface Changes

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.

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:

  1. A way for a user to authenticate with the REST API order to receive an access and refresh token.
  2. 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):

async function getPosts() {
    const response = await fetch("http://localhost:5000/api/posts/?limit=3", {
        method: "GET",
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer access_token'
        }
    });
    const data = await response.json();
    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:

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

    1. User sends username and password to the server via a login form.
    2. If the credentials are valid, the server sets the JWT tokens using cookies.
    3. 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).
    4. 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:

async function getPosts() {
    const response = await fetch("http://localhost:5000/api/posts/?limit=3", {
        method: "GET",
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': csrf_token
        }
    });
    const data = await response.json();
    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

1. Duplicate your COMPLETED hw05 folder

Before you do anything with the new code, make a copy of your hw05 homework folder. Rename the duplicated folder hw06. Your csci344/homework folder should now look something like this:

homework
├── hw01
├── hw02
├── hw03
├── hw04
├── hw05
└── hw06  <-- this folder is an exact copy of HW5

Verify your code still works

On your terminal:

  1. Navigate to your hw06 folder.
  2. Create a new python virtual environment and install the dependencies: poetry install
  3. Run your flask server: poetry run flask run --debug
  4. Run your tests (keep your flask server running, but run your tests in a new terminal shell):
    • cd tests
    • poetry run python run_tests.py

2. Integrate the new files

Download hw06-modifications.zip, unzip it, and also save it in your homework folder.

hw06-modifications.zip

Your csci344/homework folder should now look like this:

homework
├── hw01
├── hw02
├── hw03
├── hw04
├── hw05
├── hw06
└── hw06-modifications
    ├── decorators.py
    ├── models
    │   ├── __init__.py
    │   └── api_navigator.py
    ├── static
    │   ├── css
    │   └── js
    ├── templates
    │   ├── api
    │   ├── includes
    │   └── login.html
    ├── tests_updated
    └── views
        ├── __init__.py
        ├── authentication.py
        └── token.py

Please integrate the starter files VERY CAREFULLY from hw06-modifications into hw06 as follows:

1. Add (new files)

File / Folder What is this file?
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.
decorators.py Used to lockdown your UI to work with the login form.

2. Replace

File / Folder What Changed?
models/__init__.py Adding a few additional import statements.
models/api_navigator.py New routes (/login, /logout, /api/token, and /api/token/refresh) added to the tester.
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, add the new required dependencies to support JSON Web Tokens (this may already be installed, but just in case):

poetry add Flask-JWT-Extended

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 one more time (in the tests directory). They should all still pass. Now, navigate to tests_updated and run all of those tests. They should not be currently passing, but they will pass after this assignment is complete!

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:
# 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"]
    user_id = int(user_id) # cast to an integer
    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"
}
  • username (string, required): The username of the person logging in.
  • password (string, required): The password of the person logging in.
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"
}
Done for you!
  • refresh_token (string, required): The refresh token that was previously issued to the user from the /api/token endpoint.
0
3. All routes Lockdown all endpoints (see note below).
  • Every API endpoint in the system should now require a JWT token using the @flask_jwt_extended.jwt_required() decorator.
  • Replace app.current_user with flask_jwt_extended.current_user.
  • All tests should pass with the new test suite.
10
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()

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 run run_tests.py) EXCEPT for test_login.py and test_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 HW4) into the root of the static directory in your Flask app. Rename that folder to react-client (you can call it anything, but let’s keep it consistent for simplicity).

New Replace selected React files with the new ones

This step has been added since the videos were created. Please download the following zip file and replace your current version of the three React files with the ones listed below: hw06-react-updates.zip

.
├── src
│   ├── index.jsx
│   └── server-requests.jsx
└── vite.config.js

5.2. Modify app.py

When you’re done, you will need to make a few changes to app.py.

First, import two new modules at the top:

from flask import send_from_directory
import decorators # utility that Sarah made

Next, replace the routes that are defined below this line of code…

# Initialize routes for all of your API endpoints:
initialize_routes(api, flask_jwt_extended.current_user)

…with these routes…

# Route for serving static react files
@app.route("/<path:filename>")
def custom_static(filename):
    try:
        return send_from_directory(app.root_path + "/static/react-client/dist", filename)
    except FileNotFoundError:
        return "File not found, probably because you're in development mode.", 404


@app.route("/")
@decorators.jwt_or_login
def home():
    if os.getenv("ENVIRONMENT") != "development":
        return send_from_directory(
        app.root_path + "/static/react-client/dist", "index.html"
    )
    else:
        return f'''
            Hello, {flask_jwt_extended.current_user.username}.
            In development mode, React is served by Vite.
            In production mode, this route will serve your react app.
        '''


@app.route("/api")
@app.route("/api/")
@decorators.jwt_or_login
def api_docs():
    access_token = request.cookies.get("access_token_cookie")
    csrf = request.cookies.get("csrf_access_token")

    navigator = ApiNavigator(flask_jwt_extended.current_user)
    return render_template(
        "api/api-docs.html",
        user=flask_jwt_extended.current_user,
        endpoints=navigator.get_endpoints(),
        access_token=access_token,
        csrf=csrf,
        url_root=request.url_root[0:-1],  # trim trailing slash
    )


# enables flask app to run using "python3 app.py"
if __name__ == "__main__":
    app.run()

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 “dist” that will house your compiled React app.

Finally, add one more environment variable called ENVIRONMENT and set it’s value to production, which indicates that you have a react app that will be served from the static directory.

ENVIRONMENT=production

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.
  • When you're done, your tests_updated/test_login.py tests should pass.
3
2. Create logout form for UI 0 points
Done for you! 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. 0
3. Lockdown your UI Endpoints 5 points
Use the @decorators.jwt_or_login (from decorators.py) to secure your / and /api endpoints in app.py.
  • 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.
The code has already been provided for you (but is commented out). You will uncomment it. Please also examine decorators.py to make sure you understand what this decorator is actually doing.
5

When you’re done, all of the tests_updated tests should pass.

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