This assignment builds on the work you have done for Assignment 4.
In this part, you will secure your application by adding basic authentication and authorization, as well as some defensive features. Here is a high-level overview of what you will be doing:
- Implement a simple authentication mechanism.
- Protect server resources from unauthorized users by using session cookies.
- Sanitize user input to defend against Cross-Site Scripting attacks.
We continue to prohibit the use of third-party JavaScript frameworks, except those specifically mentioned, such as Express.js on the server side..
/client/
/assets/
/index.html
/login.html (ADDED in this assignment)
/style.css
/app.js
/Database.js
/SessionManager.js (ADDED in this assignment)
/server.js
/package.json
All your server-side code for this assignment goes in server.js
. In this assignment you will be adding 2 more files: login.html
and SessionManager.js
. We provide the initial implementation of SessionManager.js
here.
- Using HTTP cookies
- Express.js Guide
- Node.js
ws
module API documentation
-
(2 Points) [HTML] Until now, we have been using a hard-coded mock profile data in the client side. In the following tasks, you will build a simple authentication mechanism, and maintain a client session using cookies. It will involve the following steps:
-
Create a login page to be served at
/login
GET endpoint. -
Initialize a database table to store user data.
-
Create a POST endpoint to "sign in" the user, which involves creating a session in the server and setting the
Set-Cookie
HTTP header in the response, passing the session token. -
Protect the HTTP resource endpoints that only signed in users would be allowed to access.
-
A) As the first step, create a HTML page named
login.html
inside the root directory of the client application (next toindex.html
). This login page will not be a part of the single-page application, because we want the login page to be accessible to anyone, whileindex.html
accessible only to signed in users. Hence,login.html
will be served as an entirely separate HTML page, requiring a full reload.login.html
should contain aform
element, with themethod
attribute set to"POST"
and theaction
attribute set to"/login"
- this means that the form will be submitted to/login
endpoint viaPOST
request.- Inside the
form
, there should be a textinput
with thename
set to"username"
. - Inside the
form
, there should be a passwordinput
with thename
set to"password"
. - Inside the
form
, there should be a submitinput
orbutton
.
-
-
(1 Point) [JS] Before we can receive the login form handle the authentication steps, you will need to initialize the database and populate it with some user data. You will also need a way to read user information from the database.
- A) Use the
initUsers.mongo
script provided to populate the database - you will need toload("initUsers.mongo")
in the Mongo Shell, similar to how you initialize the other objects in the previous assignment.- If you look inside the
initUsers.mongo
script, you will notice that the password field has a rather complicated string - this is not the actual password. It is never a good idea to store sensitive data in plaintext in the database, because the damage will be huge if the database gets compromised. What the database stores here is the salted SHA256 hash of the password. We will get into more detail about this in Task 3.D.
- If you look inside the
- B) After initializing the database, create a method named
getUser(username)
inDatabase.js
. It accepts a singleusername
argument and queries theusers
collection for a document with theusername
field equal to the givenusername
. The method should return aPromise
that resolves to the user document if found,null
otherwise. - The 2 test users in the
initUsers.mongo
script are:alice
with password =secret
, andbob
with password =password
.
- A) Use the
-
(5 Points) [JS] Now that we have a login form and a user database, we will write the server-side logic to handle the login request and maintain a user session. By default, when we receive an HTTP request, there is no way for the server to tell which request came from which user, because HTTP messages do not contain the relevant information (i.e., HTTP is a stateless protocol). This is where a cookie becomes useful: to maintain some session data between a client and the server. A user session is realized by associating a given request with a user by inspecting the cookie included in the request.
In the following tasks, you will build a mechanism to maintain a user session in the server. At a high-level, the following needs to happen:
- When a client signs in successfully, the server needs to generate some unique token to be used privately between the client and the server. The server needs to remember this token. Then the server needs to tell the client to include this token in every request, so that it can tell if a request came from the signed-in client.
- The client needs to remember the token returned from the server, and make sure to include it in every request made to the protected endpoints.
- For every request made to the protected endpoints, the server needs to check the token included in the request, and determine whether the token is valid. If the token is invalid, the server needs to reject the request.
Some of the server-client interaction described above comes for free by using cookies to exchange the session token. Since cookie exchange is a standardized mechanism (RFC 6265), the browser does all the heavy-lifting for you; namely, you don't need to set the cookie yourself, the browser sets it automatically when the server includes a
Set-Cookie
header in the response. In this task, you will be building the server side of the session management. We have provided an initial implementation ofSessionManager
, whose methods you will complete.- A) In
server.js
, declare a variablesessionManager
, assigning aSessionManager
instance. - B) Complete the implementation of the
createSession
function inSessionManager
, performing the following operations:- Generate a random string token (e.g. you can use
crypto.randomBytes
). This token will be the session token you associate with a client. Make sure this token is random and long enough to guarantee uniqueness and confidentiality. To evaluate the "strength" of the generated token, we use the following ad-hoc formula:strength(token) = shannonEntropy(token) * length(token)
. If you're curious, you can read about Shannon Entropy here. - Create an object with a
username
property, assigning the givenusername
argument. You can also store some other metadata such as the timestamp when the token was created, and when the token should expire. Then, using the generated token as a key, store this object in thesessions
dictionary. - On the given
response
argument, set theSet-Cookie
header using the express methodresponse.cookie()
. The cookie name should be set tocpen322-session
, and the value set to the token you just generated. In addition, set theMax-Age
attribute on the cookie to the givenmaxAge
argument. If themaxAge
argument is not given, use some default value. maxAge
milliseconds after a session data is created, it should be deleted from thesessions
dictionary. You can use timers to achieve this.
- Generate a random string token (e.g. you can use
- C) In
server.js
, create aPOST
endpoint at the path/login
. This is where the login form will be submitted to. In thePOST
/login
handler, use thedb.getUser
method to look up the user data from the database.- If the user is not found, redirect back to the
/login
page. - If the user is found, you will have to now check that the password in the submitted form corresponds to the salted SHA256 hash stored in the database. Use the
isCorrectPassword
function you implement in Task 3.D (next subtask). to check the submitted password. If the password is correct, create a new user session using thecreateSession
function you just implemented, and then redirect the request to/
. If the password is incorrect, redirect back to the/login
page.
- If the user is not found, redirect back to the
- D) In the global scope of
server.js
, implement a helper function namedisCorrectPassword(password, saltedHash)
, refering to the following description to figure out how you can compare the two stringspassword
andsaltedHash
. You can use the built-incrypto
module to compute the SHA256 hash.- The stored password (
saltedHash
) in the database is made from concatenating 2 strings:- the first 20 characters is the "salt" string - it is just a random string we generate each time we want to store some password. We will refer to this string as the
salt
. - the remaining 44 characters is the
base64
representation of the SHA256 hash of the "salted password".
- the first 20 characters is the "salt" string - it is just a random string we generate each time we want to store some password. We will refer to this string as the
- The "salted password" is simply the concatenation of "plaintext password" (
password
) and thesalt
. - For testing, you'll need to export this function
- The stored password (
-
(7 Points) [JS] So far, we can authenticate a user and create a user session, but it is rather useless in protecting any of the pages; you can still access the app at
/
without signing in and the application does not know anything about the signed in user. To protect each of the resource endpoints, we will need to read the cookie value included in a request, check if the token exists in thesessions
object inside thesessionManager
, then proceed normally if the token is valid, or return HTTP 401 if not. We could perform these steps for each endpoint, but Express.js provides an easier way to do this using "middleware" functions.- A) Complete the implementation of the
middleware
function inSessionManager
, performing the following operations:- First, try to read the cookie information from the request. Since we're not using any Express.js extensions like
cookie-parser
, you will have to read it from the HTTP header and parse the string yourself. - If the cookie header was not found, "short-circuit" the middleware by calling the
next
function, passing in aSessionError
object, and returning immediately. This short-circuit mechanism is described here. - After parsing the cookie header, check if the token (cookie value) is found in the
sessions
object. If it was not found, short-circuit the middleware as above. If the session exists, assign the username associated with the session to a newusername
property on therequest
object passed to the middleware function. Additionally, assign a property namedsession
and set its value to the cookie value (the token). These two properties will be used by the next handler in the Express.js middleware chain. Then, callnext
with zero arguments to trigger the next middleware.
- First, try to read the cookie information from the request. Since we're not using any Express.js extensions like
- B) We need to customize how we deal with errors thrown by the above middleware. You can refer to the Express.js guide for defining custom error handlers. This error handler will be useful in the next task. In the error handler, check if the error is a
SessionError
instance (Note that this is exported asSessionManager.Error
if you look in theSessionManager
module). If it is, then you'll need to respond according to theAccept
header of the request. If theAccept
header specifiesapplication/json
, return HTTP 401 with the error message. Otherwise, redirect the request to the/login
page. If the error is not aSessionError
object, return HTTP 500. - C) You need to use the
middleware
function to protect some of the resources. You can do this simply by providing themiddleware
function as the argument before the final request handler function, as the Express.js API methods are variadic functions. Pay close attention to the order in which endpoints are defined, and the order in which the middlewares are added. Refer to the API documentations for further information. You should protect the following resource endpoints:/chat/:room_id/messages
/chat/:room_id
/chat
/profile
(you will create this in Task 6)/app.js
/index.html
/index
/
- Note: You will notice that while you want to protect the URL
/
, which maps to/index.html
, you want the rest of the directory such as/style.css
and/login.html
to be accessible without authentication. To do this, use the sameexpress.static
middleware to serve the protected files (index.html
andapp.js
) with theSessionManager.middleware
included. In Express.js, more restrictive paths are declared first, so you will need to declare these endpoints before the generic catch-all/
endpoint. You can also experiment with RegEx routes to selectively apply the middleware.
- A) Complete the implementation of the
-
(2 Points) [JS] In addition to the protected endpoints listed in Task 4, we also need to protect the WebSocket. Otherwise, rogue clients can connect to the WebSocket broker and send messages to the legitimate application users.
- A) In the
connection
event handler of thebroker
object, read and parse the cookie from the request headers. You can read the cookies from the second argument of theconnection
handler, which is ahttp.IncomingMessage
object. If the cookie is not present or the cookie value is invalid, close the client socket. If the cookie is valid, proceed as usual. - B) In the
message
handler of thebroker
client, ignore anyusername
field included in the message sent by a client. Instead, overwrite theusername
field with the username associated with the socket's session, before forwarding the message to the other clients. You can now omit theusername
field when you callsocket.send
inapp.js
.
- A) In the
-
(2 Point) [JS] We need to update the client application so that it uses the
profile
of the signed in user instead of the staticprofile
. To fetch theprofile
information dynamically, we also need to create the server-side endpoint.- A) In
server.js
, create aGET
/profile
endpoint, protected with the session middleware. This endpoint simply returns an object containing a propertyusername
- this value can be obtained from the Request object you augment in the session middleware. - B) In
app.js
, createService.getProfile
function, which makes aGET
request to the/profile
endpoint you created above. It should handle the response just like the other functions in theService
object do. Then, in themain
function, callService.getProfile
to get the username from the server, and assign this username in theprofile
object.
- A) In
-
(2 Points) [JS] Lastly, we need a way to "sign out" of the application. This involves creating a
/logout
endpoint and deleting the session data.- A) Complete the implementation of the
deleteSession
function inSessionManager
, performing the following operations:- Delete the
username
property of the given request. - Delete the
session
property of the given request. - Delete the session object associated with the request from the
sessions
object.
- Delete the
- B) In
server.js
, create aGET
/logout
endpoint. The request handler should delete the session associated with the request by callingsessionManager.deleteSession
. Then, send a redirect response to the login page. - While we don't assess you on this, you can also create a "Sign out" button/link in the client app.
- A) Complete the implementation of the
-
(3 Point) Authenticating users is only a small part of securing your application. Your application is still vulnerable to various attacks - such as a Cross-Site Scripting (XSS) attack. In this task you will implement defence against XSS attacks by sanitizing the input given by a user.
- If you do not see how your application is vulnerable to XSS attacks, open multiple tabs and navigate to a chat room to simulate a conversation. In the chat message, copy-and-paste the following javascript code:
You think I'm just chatting<img src="/assets/profile-icon.png" onload="alert('but I just mounted a XSS attack!');this.parentNode.removeChild(this)"/> normally
. This message will be sent to all other clients in the same room, and the client-application will render this string by adding it to the DOM. When yourChatView
renders this as a DOM element, you should observe an alert box. This is called the Cross-Site Scripting (XSS) attack. You can see why this is dangerous, because the code the attacker injects can access information belonging to other clients, for example, cookies (authentication tokens). - A) In the client-side application, locate the lines of code responsible for rendering the message received from the WebSocket. In that code block, sanitize the received message before appending it to the DOM.
- Think about how you want to "sanitize" a given user input. Do you want to simply invalidate texts containing
<script>
tag? Or should you be removing the tag but still show the body of the tag? What if you wanted to share code through the chat, and you wanted to show the entire script tag verbatim? We leave this choice up to you.
- Think about how you want to "sanitize" a given user input. Do you want to simply invalidate texts containing
- B) While sanitizing a malicious input just before rendering may suppress the attack (at least for now), the application is still vulnerable. Think about what happens in the server - the message goes to the
broker
, and it gets stored in the database as conversation objects. What would happen if, in the future, you replace your client application? or if your server interacts with a 3rd-party client application?- In addition to sanitizing the message in the client, also sanitize a given message in the
message
handler of thebroker
client. Ensure the dirty message does not get forwarded to the other clients, and does not get stored in the database.
- In addition to sanitizing the message in the client, also sanitize a given message in the
- If you do not see how your application is vulnerable to XSS attacks, open multiple tabs and navigate to a chat room to simulate a conversation. In the chat message, copy-and-paste the following javascript code:
This concludes the 5-part course project for CPEN322. Congratulations!
The testing infrastructure is identical to the one used in the previous assignment, except for the URL of the scripts used. Therefore, we will not elaborate on how to use the test script's API; you can refer to the "Testing" section in the previous assignment, and update the URLs of the test scripts accordingly.
-
Client-side:
- URL:
http://52.43.220.29/cpen322/test-a5.js
(this goes inindex.html
) - Default Values (can be customized with
cpen322.setDefault(key, val)
):testRoomId
:'room-1'
,cookieName
:'cpen322-session'
,testUser1
:{ username: 'alice', password: 'secret', saltedHash: '1htYvJoddV8mLxq3h7C26/RH2NPMeTDxHIxWn49M/G0wxqh/7Y3cM+kB1Wdjr4I=' }
testUser2
:{ username: 'bob', password: 'password', saltedHash: 'MIYB5u3dFYipaBtCYd9fyhhanQkuW4RkoRTUDLYtwd/IjQvYBgMHL+eoZi3Rzhw=' }
image
:'assets/everyone-icon.png'
,webSocketServer
:'ws://localhost:8000'
- Exported closure variables:
lobby
,chatView
- URL:
-
Server-side:
- URL:
http://52.43.220.29/cpen322/test-a5-server.js
(this goes in thecpen322.connect
function insideserver.js
. You still need to use the samecpen322-tester.js
module from Assignment 4) - Exported global variables:
app
,db
,messages
,messageBlockSize
,sessionManager
,isCorrectPassword
- URL:
-
NOTE: Unlike the test scripts in the previous assignments, this test script will not clean up the test objects it creates during the test. Previously, we could safely delete the test objects because we know that the objects were all in-memory and definitely belong to the application. In this assignment, the application connects to a database, which we consider as "external" to the application. Since we cannot make assumptions about the database service you're connecting to, we refrain from performing destructive operations. (Most of you would probably be running your own fresh database service, but we don't neglect the chance that some of you might be using an existing/shared database or connecting to a cloud-hosted service).
There are 8 tasks for this assignment (Total 24 Points):
- Task 1: 2 Points
- Task 2: 1 Points
- Task 3: 5 Points
- Task 4: 7 Points
- Task 5: 2 Points
- Task 6: 2 Points
- Task 7: 2 Points
- Task 8: 3 Points
Copy the commit hash from Github and enter it in Canvas.
For step-by-step instructions, refer to the tutorial.
These deadlines will be strictly enforced by the assignment submission system (Canvas).
- Sunday, Dec 4, 2022 23:59:59 PST