Introduction
You will continue to add features to your app from homework 1. In this assignment you will add authentication,
user accounts, and the protections that go along with them. By the end, users will be able to register, log in,
chat under their own identity, manage their profile, and log in with GitHub, all without exposing them to the
attacks discussed in lecture.
Note that, starting with this assignment, some objectives will require that you edit the provided front end, or
build
your own if you'd prefer.
All objectives must function properly using docker compose. When running "docker compose up" your app must be
fully functional on localhost port 8080.
Security Concerns
This assignment is where your server starts handling passwords and identities, so security matters more than
ever. Every security concern is clearly labeled. If you violate a security concern, all 5 objectives will be
scored 0 unless/until you submit a proper security essay for each security risk in your submission.
Deviations
In addition to modifying the front end, or building your own, you are free to deviate from the specific details
of each objective as long
as you still implement the features for each objective. This offers you flexibility to implement your app the
way you'd like, while still
learning the concepts of each objective. There is no limit to the amount you are allowed to deviate as
long as the concepts are
still covered.
If you choose to deviate from a stated objective, you must justify how this deviation still covers the
concepts required for the objective. Deviations never make a security concern acceptable. Any deviation that
avoids handling a security concern does not cover the concepts of the objective and is not allowed (e.g. Using
JWTs to avoid password storage).
Objective 1: Registration, Login, Logout, and @me
Goals:
- Allow users to register an account and log in to it
- Store passwords and auth tokens securely
- Allow users to log out, invalidating their auth token
- Add an endpoint that tells the front end who is logged in
Concepts Covered:
- Authentication
- Salting and hashing passwords
- Auth tokens and cookie directives
- Percent-encoding / URL-encoded forms
- SQL injection
- Confining secrets to a .env file
Pages
Add the following paths, rendered using layout.html in the same way as /index and /chat from HW1:
- "/register" - Render register.html
- "/login" - Render login.html
Registration
The provided register page sends a registration request containing a username and password in a URL-encoded
format. Your
server must parse the username and password from this body. You should study the provided front
end to find the format of these requests. Libraries that decode URL-encoded data, including decoding
individual percent-encoded characters, are not allowed. You must decode manually. It's recommended that
you write this as a helper method/function as login requests will be in the same format.
Handling every possible URL-encoded request could be daunting. The following limitations are added and
will be followed during grading:
- Usernames cannot contain any characters that are not alphanumeric. If a submitted username contains any
characters that are not alphanumeric, return a 400 Bad Request to the user
-
The password may contain any of the following special characters
{'!', '@', '#', '$', '%', '^', '&', '(', ')', '-', '_', '='}. If any other characters are used,
besides alphanumerics, should result in a 400 response.
You are welcome, and expected, to
hard-code these characters into your app.
Note that '%' is included in this list..
When a user registers, store their username, a unique id (generated in the same way as chat message ids), and a
salted hash of their password in your database.
Registration must fail, with a 400 response and a message of your choosing, if:
- The username is already taken. Duplicate usernames are not allowed
- The username contains any non-alphanumeric characters
- The password does not meet all of the following criteria:
- At least 8 characters long
- Contains at least 1 lowercase letter
- Contains at least 1 uppercase letter
- Contains at least 1 number
- Contains at least 1 special character
- Contains no characters other than alphanumerics and the special characters listed above
If registration is successful, respond with a 200 OK and any message of your choosing (The front end will
redirect to the login page and your message won't be displayed to the user).
Security Concern - Password storage: You must only store salted hashes of your users' passwords.
Security Concern - SQL injection: All database queries must be protected against SQL injection attacks.
Security Concern - Secrets in code: Your database password (and any other secrets) must not appear in
your source code, your Dockerfile, or your compose.yaml. Store these in a file named ".env" with placeholder
values (A value of "changeMe" is good since it is both a valid password for testing, and clear to anyone
deploying
the app that they must change it with a strong password. Leaving this blank can be better in the real world
since it prevents anyone from forgetting to change the password).
Tips:
-
If two users attempt to create an account with the same username simultaneously, it creates a race
condition.
Be sure to handle this in a way that cannot result in both users registering with the same username.
Login
The provided login page sends a URL-encoded username and password in the same format as registration. If the
username exists and the password matches the salted hash you stored, the user is authenticated.
When a user successfully logs in, generate a random auth token and set it as a cookie. This cookie:
- Must have the HttpOnly directive
- Must not be a session cookie (e.g. set Max-Age or Expires so it survives closing the browser)
- Must have at least 80 bits of entropy (You are allowed to use libraries to generate these tokens)
You must store a hash (hash only, do not salt) of each auth token in your database.
If login is successful, respond with a 200 OK and a message of your choice. The front end will redirect to the
homepage and this message will not be displayed. If login is not successful (e.g. the username doesn't exist or
the password is wrong), respond with a 400 and a message of your choosing.
Security Concern - Auth token cookie: The cookie storing your auth token must be set with the HttpOnly
directive.
Security Concern - Auth token storage: Only hashes of your auth tokens may be stored in your database. Do
not store the tokens as plain text.
Logout
Add a path for GET "/logout", which is used by the logout button on the provided front end. When a user
logs out:
- Their auth token must be removed from their cookies (To remove a cookie, set a cookie with the same name
and path with a Max-Age of 0)
- Their auth token must be invalidated in your database. The token they were issued must not work
anymore, even if your server later receives it as a cookie value (e.g. a token an attacker copied before
the user logged out)
- Respond with a 302 Found that redirects the user to your homepage
Who am I? (`GET /api/users/@me`)
Add an endpoint GET "/api/users/@me" that returns the profile of the user making the request. Return everything
stored for the user except their salted and hashed password and hashed auth token(s). The front end uses
this
endpoint to display the user's name on the page when they are logged in.
Response (JSON): {"username": string, "id": string}
If the requester is not logged in (no auth token, or a token that is invalid/logged out), respond with an empty
JSON object and a 401 status code.
As more data is added to user profiles in this and later assignments, that data may also be returned by this
endpoint.
Objective 2: Authenticated Chat
Goals:
- Send chat messages as a logged in user, using their identity instead of a guest name
- Ensure users can only modify and delete their own messages
- Give new users ownership of the guest messages they sent before registering
Concepts Covered:
- Authenticated endpoints
- 403 Forbidden
- Associating data with accounts
- Escaping HTML
In this objective you will add authentication to the chat feature from HW1. Guest chat should
continue to function for users who are not logged in, exactly as it did in HW1.
Authenticated Messages
Whenever a chat message is sent by an authenticated user, the "author" of the message must be their username
instead of a randomly generated guest name.
Update and Delete Messages
Update and delete permissions for messages sent by logged in users must be based on their account instead of
their guest cookie. A user must be able to log out, clear all their cookies, log back in, and still be able to
update and delete their own messages. Users may only update or delete their own messages. If a user attempts to
update or delete a message that is not theirs, respond with a 403 Forbidden. If it is their own, you may
choose the response (200 with a short message is fine).
Guests can still update and delete their own guest messages using their guest cookie, as in HW1.
Claiming Guest Messages
When a user registers, any messages that were sent by that browser while it was a guest must be claimed
by the new account. These messages now belong to the new user. Their author must be updated to the user's
username, and the user
can update and delete them like any other message they sent.
A guest message can only be claimed once. For example, if the same browser registers a second account, that
account must not claim the messages that were already claimed by the first account.
Tips:
- The registration request must include your guest cookie for you to know which messages to claim. Some
browsers will automatically add a "/api" path to a cookie set from an "/api/..." endpoint, so it will not be
sent to "/register". Setting the Path directive of your guest cookie to "/" can prevent this
- Store the id of the user who owns a message, in addition to (or instead of) the author's name. It will save
you time in the next objective.
Security Concern - Escape HTML: You must escape any HTML in user-provided content before it is displayed
to other users.
Security Concern - Guest cookies/token protection: Your cookie from HW1 that tracks guest users is now
used for authorization
since registered users can claim messages. These token must therefore have at least 80 bits of entropy. You
may/should optionally store only hashes of these tokens
Objective 3: Profile Settings
Goals:
- Allow users to change their display name and their password
- Reflect display name changes on all of the user's messages
Concepts Covered:
- Authenticated endpoints
- Updating user data
- Keeping derived data consistent
Add a path for "/settings" that renders
settings.html using layout.html.
In this objective you will let users edit their profile. Users will always log in with their username,
which never
changes. They also have a display name, which is the name shown in chat and can be changed at any time.
When a user registers, their display name starts out as their username.
Update Settings (`POST /api/users/settings`)
Add an endpoint POST "/api/users/settings" that lets a logged in user update their display name and/or their
password. The provided front end has labels of username and password on the form. This does not mean that
you are changing their username, but instead their display name.
- Requests from users who are not logged in must be rejected with a 401 Unauthorized
- If a new password is provided, it must meet the same criteria as registration and must be stored securely in
the same way. If it does not meet all criteria, respond with a 400 and a message of your choice, and do
not update anything (including their display name)
- A display name may have all the same characters as a password (Be sure to avoid HTML injection). If an
invalid
character is used, respond with a 400
- An empty display name field means the user only wants to change their password, and their display name must
remain unchanged
- An empty password field means the user only wants to change their display name, and their password must
remain unchanged
- If both fields are empty, nothing should be changed
- After a password is changed, the user must not be able to log in using their old password
Updating Messages
When a user changes their display name, all of their messages, past and future, must be sent to users
with the new name as the "author". This means that the next time any user requests "/api/chats", every message
written by this user, including messages they sent before the change and messages they claimed as a guest,
contains the new display name.
Tips:
- There are two common ways to do this. You can update the author of every message they've sent when they
change their display name, or you can store the id of the user on each message and look up their current
display name whenever messages are retrieved
- You may also want to require the user's current password before allowing a password change, as many real
sites do. This is not required
Security Concern - Escape HTML: You must not allow HTML in display names to be rendered. You may either
disallow certain characters, like we did with usernames, or allow any character and escape the HTML.
Objective 4: XSRF Tokens
Goals:
- Implement XSRF tokens to protect your authenticated endpoints from XSRF attacks
Concepts Covered:
- XSRF tokens
- Editing a front end and designing a full stack feature
This is the first objective where you will be required to make changes to the provided front end
in order to implement the required features. This will require some amount of architecting as you
design the implementation of this feature.
Your users' browsers automatically attach their auth_token cookie to every request sent to your server, even
certain cross-site requests. In this objective you will make sure that
only requests that were made by your front end can make changes on behalf of a logged in
user.
XSRF Tokens
Add XSRF tokens to your app and require them on every state-changing request made by an authenticated
user. This includes:
- Creating, updating, and deleting chat messages while logged in (Not required for guest users)
- Updating settings (POST "/api/users/settings")
- Logging out (see below)
Your implementation must meet all of the following:
- Generate a XSRF token, with at least 80 bits of entropy, for each logged in user (e.g. when they log in)
and store
it on your server so it is linked to that user's session. It must be different for every user and for
every login
- Send the token to the user when you render pages for them, and modify your front end so it sends the token
back with every state-changing request. For a form, this is typically a hidden input. For requests sent
with JavaScript (e.g. chat requests), read the token from the page and send it in the body or in a header
- When a state-changing request is received from an authenticated user, verify that the token in the request
is the one your server issued to that user. If the token is missing or incorrect, reject the request
with a 403 Forbidden and do not perform the action
- The token must be invalid after the user logs out (When they log back in, generate a new token for them)
Requests from users who are not logged in do not need XSRF tokens. This includes guest chat, registration, and
login.
Logging Out
The provided front end exposes a XSRF vulnerability that you will patch in this objective.
Specifically, logging out uses a GET request. Changing state in response to a GET request is exactly what
makes
XSRF easy to pull off (An attacker can log you out of any site using nothing more than an
<img src="http://example.com/logout"> tag on their site). Change logout so it is a
POST request that requires the user's XSRF
token, and update your front end so the logout button sends this request. Logging out must still delete
the cookie, invalidate the token, and redirect the user to the homepage.
Since this entire objective's purpose is to prevent a security risk, there are no security concerns
related to this objective that would trigger a security essay. Instead, if your implementation does
not properly prevent XSRF attacks, you will not earn credit for this objective [and not 0 on the entire assignment].
This also implies that you cannot recover your credit for this objective by writing an essay. This will
be true for all similar objectives (e.g. Using the state parameter in the next objective)
Tips:
-
Since you are writing both the front and back end for this feature, you have significant flexibility
in how you implement your tokens. As long as they successfully protect against XSRF attack, it will
be accepted. This, by design, will require you to make some thoughtful architecture decisions as
opposed to having the entire spec outlined for you
- To test, you can build a small HTML page that is served from a different origin (e.g. a different
localhost port)
and contains a form that submits to your server. While logged into your app, submit the form from the
"attacker" page and make sure your server rejects it
Objective 5: Login with GitHub (OAuth 2.0)
Goals:
- Allow users to log in using their GitHub account
- Use the OAuth 2.0 Authorization Code Flow securely
Concepts Covered:
- OAuth 2.0 Authorization Code Flow
- The "state" parameter (XSRF protection for OAuth)
- Client secrets and scopes
- Consuming third-party APIs
Enable the "login with GitHub" button. This button uses the GET "/authgithub" endpoint. Add functionality to
this endpoint to follow the OAuth 2.0 Authorization Code Flow. After the user authenticates their account
through GitHub, use the GitHub API to identify them, and log them in to your app.
You will use the Authorization Code Flow to build this feature, and you should follow
the documentation to guide your development. You will need a GitHub account to access the developer
settings, and this can be a free account. Follow the
Web Application Flow. No, you cannot use the Device Flow or the Non-Web Application Flow.
Use "http://localhost:8080/authcallback" as your redirect_uri. The course staff will use this exact string
to test your app. Request only the scopes that you need to identify the user. Asking
for more access than your app requires is bad practice.
Requirements
-
GitHub users must behave exactly the same as other users, including claiming guest messages on first login,
with
the only
difference being that they cannot login with a password. Note that when they log in, you will still issue
your own
auth token. Do not use their access token as an auth token
-
When logging in with GitHub, use a unique id provided by the API (GitHub numeric id is preferred since it
cannot change). You must not allow username collisions between GitHub and non-GitHub users. For example,
if someone registered (with username/password) choosing a username that matches someone's GitHub id,
when the user with that GitHub id logs in with GitHub it must not cause an issue (e.g. The GitHub user
must not be able to take over the other user's account or edit/delete their messages)
-
You must use the "state" parameter while obtaining the access grant from GitHub and
verify that you receive the expected state value at your callback URI. If the state does not match the one
issued to that particular user,
respond with a 400 response and end the login process. You have some freedom in what value to use for the
state, but it must successfully prevent a XSRF attack as discussed in lecture
-
You have a very sensitive value, your client secret, which must be protected. As with your database password,
this secret must only ever
appear in your .env file, and never hard-coded in your code or docker configuration. Anywhere you
need this value, it should be read, either directly or indirectly, from your .env file. Before
submitting, replace your secret with a placeholder so you don't leak it to the course staff.
Though it's not a security risk, you should do the same with your client ID. For testing, we
will paste our own id and secret into your
.env file before running your app
Security Concern - Client secret: Never share your app's client secret. Your submission must not contain
your client secret in any file, and your .env file must contain a placeholder
instead of your real secret.
Security Concern - Access tokens: Your users' access tokens must be protected. If they are leaked,
or exposed to an attack (e.g. SQL injection), in any way it is a security risk.. or simply don't store them.
Tips:
- You may use a library to send HTTP requests to the GitHub API. You are also allowed to use a
base64 library, if needed
- You do not need to keep the GitHub access token after you've identified the user. If you don't store it,
you can't leak it
- You are not required to handle refresh tokens. You should implement this for practice, but it's impractical to
test for a
course assignment since you'd have to wait for your access token to expire to properly test your feature
- For testing with your GitHub account, you can unlink your app from your account to retest the entire login
process (https://github.com/settings/apps/authorizations)
Submission
Submit all files for your server to Autolab in a .zip file
Your submission must include a ".env" file with placeholder values for all your sensitive values. This is
required so we
know your variable names.
Complete the homework submission form after submitting your zip file to Autolab:
Homework Submission Form
If you do not both submit your zip file to Autolab and complete the homework submission form before the due date,
your submission will not be graded. Do
not be one of the students who will get a 0 for not submitting the form after completing the assignment.
It is strongly recommended that you download and test your submission after submitting. To do this,
download
your zip file into a new directory, unzip your zip file, enter the directory where the files were unzipped,
run
"docker compose up --build --force-recreate --renew-anon-volumes", then navigate to localhost:8080 in your
browser and test all
your features. This
simulates exactly what the TAs will do during grading.
If you ignore this recommendation, don't be surprised when you do not earn credit even if your code functions
properly on your laptop.
Grading
Each objective will be scored on a 0-3 scale as follows:
| 3 (Complete) |
Clearly correct |
| 2 (Complete) |
Mostly correct, but with some minor issues
|
| 1 (Incomplete) |
Not all features outlined in this document are functional, but an attempt was made to
complete the objective
|
| 0 (Incomplete) |
No attempt to complete the objective or violation of the assignment (Ex. Using an HTTP library, except
the one for sending request to GitHub) |
| 0.X (Security Risk) |
If any security risk is found while testing, all objectives will be scored 0 unless a proper security
essay is submitted. X is the score that will be earned if a proper security essay is submitted on time
|
Note that for your final grade there is no difference between a 2 and 3, or a 0 and a 1.
| 3 |
Objective Complete |
| 2 |
Objective Complete
|
| 1 |
Objective Not Complete
|
| 0 |
Objective Not Complete
|
A Security Risk is any violation of a "Security Concern" that is explicitly labeled in this document.
Security Essay
If a security risk is found in your submission, you will be assigned a 0 for all 5 objectives.
However, you will still have an opportunity to earn credit for the objectives by submitting an essay about
the security issue you exposed. These essays must:
- Be at least 1000 words in length
- Explain the security issue from your submission with specific details about your code
- Describe how you fixed the issue in your submission with specific details about the code you changed
- Explain why this security issue is a concern and the damage that could be done if you exposed this issue
in production code with live users
If your submission contains multiple security risks, you may have to write multiple essays to recover your
homework grade
Any submission that does not meet all these criteria will be rejected and your objective will remain
incomplete.
Due Date: Security essays are due 1-week after grades are released. Submission instructions will be included in
your feedback on Autolab.
Any essay may be subject to an interview with the course staff to verify that you understand the importance
of
the security issue that you exposed. If an interview is required, you will be contacted by the course staff
for
scheduling. Decisions of whether or not an interview is required will be made at the discretion of the
course
staff.