Homework 2 - Authentication




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.