Commit 5af6f955 by Trevor Austin

Add Week 6 notes and exercises

parent ae73ab68
from flask import Flask, render_template, request, jsonify
from functools import wraps
import mysql.connector
# import bcrypt
import configparser
import io
app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
config = configparser.ConfigParser()
config.read('secrets.cfg')
DB_NAME = 'passwords'
DB_USERNAME = config['secrets']['DB_USERNAME']
DB_PASSWORD = config['secrets']['DB_PASSWORD']
@app.route('/')
def index():
return app.send_static_file('index.html')
# -------------------------------- API ROUTES ----------------------------------
@app.route('/api/signup', methods=['POST'])
def signup ():
print(request.data)
body = request.get_json()
print(body)
username = body['username']
password = body['password']
connection = mysql.connector.connect(user=DB_USERNAME, database=DB_NAME, password=DB_PASSWORD)
cursor = connection.cursor()
query = "INSERT into users (username, password) VALUES (%s, %s)"
try:
cursor.execute(query, (username, password))
connection.commit()
return {}
except Exception as e:
print(e)
return {"username": username}, 302
finally:
cursor.close()
connection.close()
@app.route('/api/login', methods=['POST'])
def login ():
print(request.data)
body = request.get_json()
print(body)
username = body['username']
password = body['password']
connection = mysql.connector.connect(user=DB_USERNAME, database=DB_NAME, password=DB_PASSWORD)
cursor = connection.cursor()
query = "SELECT password FROM users WHERE username=%s"
try:
cursor.execute(query, (username,))
savedPassword = cursor.fetchone()[0]
print(password)
print(savedPassword)
if password == savedPassword:
return {}
return {}, 404
except Exception as e:
print(e)
return {}, 404
finally:
cursor.close()
connection.close()
-- sudo mysql -u root < 20200304T184500-create_database.sql
create database passwords CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- sudo mysql -u root weblog < 20200304T184700-create_tables.sql
create table users (
username VARCHAR(40) PRIMARY KEY,
password VARCHAR(20)
-- password BINARY(60) NOT NULL
);
[secrets]
PEPPER = some really long string that I keep secret
DB_USERNAME = trevor
DB_PASSWORD = password
<html lang="en" dir="ltr">
<head>
<title>Single-Page Login and Post</title>
<link rel="stylesheet" type="text/css" href="static/style.css">
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="static/script.js"></script>
</body>
</html>
class SignupAndLogin extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
signup = () => {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
fetch("http://127.0.0.1:5000/api/signup", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username, password: password})
}).then((response) => {
if(response.status == 200) {
alert("Created user "+username);
} else {
alert("A user "+username+" already exists");
}
});
}
login = () => {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
fetch("http://127.0.0.1:5000/api/login", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username, password: password})
}).then((response) => {
if(response.status == 200) {
alert("Logged in as "+username);
} else {
alert("Incorrect username and password");
}
});
}
render() {
return (
<div className="signup">
<h1>Signup and Login</h1>
<div className="signup_form">
<label htmlFor="username">Username</label>
<input id="username"></input>
<label htmlFor="password">Password</label>
<input id="password" type="password"></input>
<button className="form_button" onClick={this.signup}>
Signup
</button>
<button className="form_button" onClick={this.login}>
Login
</button>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
React.createElement(SignupAndLogin),
document.getElementById('root')
);
body {
margin: 0
}
.signup {
width: 960px;
margin: auto;
font-family: sans-serif;
}
h1 {
margin: 0;
padding-top: 10px;
padding-bottom: 5px;
}
.signup_form {
width: 400px;
border: 2px solid #AADDCC;
border-radius: 4px;
display: grid;
grid-template-columns: auto auto auto;
grid-gap: 10px 10px;
padding: 20px 40px 20px 40px;
}
.signup_form label {
grid-column: 1 / 2;
text-align: right;
margin-top: auto;
margin-bottom: auto;
}
.signup_form input {
grid-column: 2 / 4;
font-size: 24px;
border: 2px solid #AADDCC;
border-radius: 4px;
}
.signup_form button {
grid-column: 2;
}
button.form_button {
width: 100%;
font-size: 18px;
border: 2px solid #AADDCC;
border-radius: 4px;
}
# Exercise #6: Watch Party 2: The Single Page Experience
5 points
**DUE: Thursday, February 20 by 2:00pm**
### Instructions
For this exercise, we will build a *single-page* group chat web application with
asynchronous Javascript and a REST API written in Python with Flask.
Like the original, Watch Party 2 lets users start group chats with
disappearing messages, and invite up to five friends. This time, however, we
serve a static HTML page and never redirect or reload. Instead, the page
interacts purely with the JSON API.
As before, chats may contain up to 6 users and up to 30 messages. This time,
however, we prompt users for a username and password when they create a chat or
join one, and we remember which chats users are in. We store user passwords
securely in a database with bcrypt, salt, and pepper. (The rest of the
application's data can be stored in memory, as before).
The default page should allow users to create a new chat, prompting them for a
username when they do. Create a new chat by `POST`ing to the JSON API. The
response will contain a session_token, chat_id, and a magic invite link. Save
the session_token to local storage. Without reloading, display the chat screen
and push `chat/<chat_id>` to the URL bar and browser history. Display the invite
link on the page.
When a user visits the invite link, serve them the same single-page application.
The application should use the URL query parameters to try to authenticate via
the API. If successful (i.e. the invite link is valid and the room isn't full),
prompt them for a username and then show them the chat screen and set the URL
bar and browser history to `chat/<chat_id>`. Otherwise, show them the default
page and set the URL bar and browser history to `/`.
Users in the chat post messages using the API, and the front-end continuously
polls the API for new messages, so that they appear automatically in the chat.
Store the usernames on the server associated with the session_tokens, so users
do not have to supply them on each message.
Starting with the files included in this directory, implement the UI for Watch
Party in HTML, CSS, and Javascript, and serve it using server-side code written
in the [latest stable version of Python](https://www.python.org/downloads/release/python-381/)
([3.8.1](https://www.python.org/downloads/release/python-381/))
and [Flask]((https://www.python.org/downloads/release/python-381/)). You'll
notice that there are some routes in `app.py` to get you started:
- Prompt users to enter a username and password to sign up or log in
- Display a list of chats a logged in user is currently in, with a button to
create a new one.
- Give each chat a unique URL so users can return to it if they close their
tab or reload the page. Remember who they are if they do.
- Allow users to be in multiple chats in multiple tabs if they want.
- As other users type new messages in a chat, Watch Party should asynchronously
fetch them, and those messages should appear automatically without anyone
reloading or otherwise interacting with the page.
Watch Party can be visually very simple, but should render responsively on
desktops and on mobile. Make sure the message input for for a chat is always on
the screen regardless of where the user scrolls. Chats are intended to be
ephemeral and are not saved to a database or the filesystem (ie it's ok for them
just to exist in memory on the web server). For the purposes of this Exercise,
you do not need to worry about garbage collecting chats that are not in use.
Remember to include in your submission any classmates you collaborated with and
any materials you consulted. Watch Party (though it has somewhat different
requirements) is inspired by [yap.chat](https://yap.chat/).
### Rubric
One point each for:
- Single-Page UI: Have only one HTML file, containing the elements to display either the login screen, a list of chats the user is in, or the screen for a single chat (list messages sent, display the magic invite link, fields to enter a new message), and showing or hiding the elements as appropriate, without reloading the page.
- State from Navigation Bar: To navigate in-app without reloading the page, pull the chat_id and magic link magic_key from the URL navigation bar as needed and use them to determine which single-page elements to display.
- Push Single-Page State to the Navigation Bar: Use `history.pushState` to set which chat the user has navigated to, and to clear the magic link magic_key from the navigation bar after it has been used.
- Asyncronously Post and Fetch New Messages: Use asynchronous calls to the JSON API to post and fetch new messages. Continuously poll for new messages from other users and display them as they are written.
- Password Authentication: Store usernames and passwords in a MySQL or MariaDB
database.
from flask import Flask, render_template, request
from functools import wraps
app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
# sample_chats = {
# 1: {
# "authorized_users": {
# "session_token_0": {"username": "Alice", "expires": "2020-02-15T20:53:15Z"},
# "session_token_1": {"username": "Bob", "expires": "2020-02-15T20:57:22Z"}
# },
# "magic_key": "some_really_long_key_value"
# "messages": [
# {"username": "Alice", "body": "Hi Bob!"},
# {"username": "Bob", "body": "Hi Alice!"},
# {"username": "Alice", "body": "Knock knock"},
# {"username": "Bob", "body": "Who's there?"},
# ]
# }
# }
@app.route('/')
@app.route('/chat/<int:chat_id>')
def index(chat_id=None):
return app.send_static_file('index.html')
# -------------------------------- API ROUTES ----------------------------------
@app.route('/api/create', methods=['POST'])
def create ():
# TODO: create a chat data structure in global memory
# chat_id = len(chats.keys())
# TODO: also send a username
return {
"chat_id": 1,
"session_token": "some_token_value",
"magic_invite_link": "http://localhost:5000/chat/1?magic_key=some_really_long_key_value"
}
@app.route('/api/authenticate', methods=['POST'])
def authenticate():
# TODO: check if the request body contains a chat_id and the correct magic_key for that chat
# TODO: also send a username
return {"session_token": "some_token_value"}
@app.route('/api/messages', methods=['GET', 'POST'])
def messages ():
# TODO: check if the request body contains a chat_id and valid session token for that chat
if request.method == 'POST':
# TODO: add the message
pass
messages = {} # TODO: get the messages for this chat from global memory
return {
"messages": messages,
"magic_invite_link": "http://localhost:5000/chat/1?magic_key=some_really_long_key_value"
}
<html>
<head>
<title>Watch Party 2</title>
<link rel="stylesheet" type="text/css" href="/static/style.css"></link>
</head>
<body>
<div class="container">
<div class="splash">
<h1>Watch Party 2</h1>
<h2>The Single Page Experience</h2>
<button value="Create a Chat!"></button>
</div>
<div class="chat">
<h2>My Magic Link Goes here</h2>
<div class="messages">
</div>
<div class="message_input">
</div>
</div>
</div>
<script src="/static/script.js"></script>
</body>
</html>
// page loads
// if the navigation bar is just "/"
// hide the chat block
// show the splash screen block
// return
//
// else if the navigtion bar contains a magic link
// hide the splash screen block
// show the chat block
//
// if we already have a session_token for this chat in local storage:
// take the magic_key out of the url
// start polling for messages
// return
//
// else (we don't have a session_token for this chat in local storage):
// use the authenticate endpoint to try to exchange the magic key for a session_token
//
// if you get a token back
// put it in local storage
// take the magic_key out of the url
// start polling for messages
// return
//
// else (you didn't get a valid token back)
// hide the chat screen
// change url to "/"
// show the splash screen
// return
//
// else if the navigtion bar contains "/chat/<chat_id>"
// hide the splash screen block
// show the chat block
//
// if we have session_token for this chat in local storage:
// start polling for messages
// return
//
// else (no session token)
// hide the chat screen
// change url to "/"
// show the splash screen
// return
body {
}
.splash {
display: block;
}
.chat {
display: none;
}
class: center, middle
# MPCS 52553-2: Web Development
## Week 6: Secure Logins and Single Page Applications
---
class: agenda
# Secure Logins
- Assume attackers will get your DB
- Storing secure hashes
- Lab: Securely storing passwords with bcrypt, salt, and pepper
# Single Page Applications
- Static assets in Flask
- SessionStorage, LocalStorage, and Cookies
- Routing and history
- Lab: Watch Party 2: The Single Page Experience
---
# Password Security
You should do your best to keep your database and its contents safe. But
experience teaches us that human organizations are imperfect at best at keeping
database contents secure. We need to plan to minimize damage in the not-all-that
unlikely event that someone is able to steal its contents.
![Rami Malek as hacker Elliot Alderson from Mr Robot](mrrobot.jpg)
---
# Password Security
![Chart of Biggest Data Breaches](breaches.jpg)
From [CSO Magazine](https://www.csoonline.com/article/2130877/the-biggest-data-breaches-of-the-21st-century.html)
---
# Password Security
Databases of usernames and passwords are valuable because users re-use passwords
for multiple web services. They shouldn't, but they do. So even if you web
project doesn't have anything of value, a large enough list of usernames and
passwords will have some that also get an attacker access to users' bank
accounts, credit cards, or email addresses.
--
As developers we often won't have control over whether the database (or its
backups!) are stored securely, and it only takes one leak for the data to live
forever on the internet; there's no way to unring the bell. What we do have
control over though, is whether our databases store passwords at all in the
first place.
---
# Password Security: One-Way Hashing
The first thing you could do is, instead of storing passwords directly, you
could store a one-way hash. That lets you check whether the password a user
enters is the right one (just hash it and compare the hashes), and there's no
easy way to get the passwords from the hashes.
--
But just because there's no _easy_ way doesn't mean that there's no way. And
modern computers make lots of hard things possible!
--
You might try every possible password, or every likely one by using the words in
a dictionary. You might [cleverly precompute](https://en.wikipedia.org/wiki/Rainbow_table)
hashes in a space-efficient format for re-use.
---
# Password Security: Bcrypt, Salt and Pepper
To protect against those attacks, we're going to do two things: we're going to
use a hashing function (bcrypt) that's **expensive to compute**, so that
guessing passwords by brute force becomes prohibitively expensive, and we're
going to **salt** passwords with a random unique string, so attackers can't use
pre-computed lookup tables.
We'll also include a second kind of salting that's stored on the application
server and not on the database at all, so it's less likely to be stolen
alongside the database contents.
https://owasp.org/www-project-cheat-sheets/cheatsheets/Password_Storage_Cheat_Sheet
---
# Lab: Passwords
In the `/examples/passwords` directory we have a simple app that creates user
accounts with passwords and checks whether a login was successful. Right now it
is storing passwords in plaintext though. Modify it to:
- Store hashes created with `bcrypt`
- Store a unique salt for each row that's used to create the hash
- Incorporate a pepper stored in a config file
---
# Single Page Applications
https://en.wikipedia.org/wiki/Single-page_application
https://flask.palletsprojects.com/en/1.1.x/tutorial/static/
---
# Single Page Applications
https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
https://developer.mozilla.org/en-US/docs/Glossary/Cookie
---
# Single Page Applications
https://developer.mozilla.org/en-US/docs/Web/API/History_API
---
# Lab: Watch Party 2
## The Single Page Experience
<!DOCTYPE html>
<html>
<head>
<title>Web Development - Week 3</title>
<meta charset="utf-8">
<style>
@import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz);
@import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic);
@import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
body { font-family: 'Droid Serif'; }
h1, h2, h3 {
font-family: 'Yanone Kaffeesatz';
font-weight: normal;
}
img { width: 100%; }
.remark-code, .remark-inline-code { font-family: 'Ubuntu Mono'; }
.agenda h1 { margin-bottom: 0px; }
.agenda ul { margin-block-start: 0; color: #666 }
</style>
</head>
<body>
<textarea id="source">
</textarea>
<script src="https://remarkjs.com/downloads/remark-latest.min.js">
</script>
<script>
var slideshow = remark.create({sourceUrl: 'notes_week_6.md'});
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment