Skip to content

Commit

Permalink
Experiment authentication techniques.
Browse files Browse the repository at this point in the history
  • Loading branch information
aaugustin committed May 23, 2021
1 parent dd6d6bc commit 8ab85e5
Show file tree
Hide file tree
Showing 17 changed files with 549 additions and 0 deletions.
225 changes: 225 additions & 0 deletions experiments/authentication/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
#!/usr/bin/env python

import asyncio
import http
import http.cookies
import pathlib
import signal
import urllib.parse
import uuid

import websockets


# User accounts database

USERS = {}


def create_token(user, lifetime=1):
"""Create token for user and delete it once its lifetime is over."""
token = uuid.uuid4().hex
USERS[token] = user
asyncio.get_running_loop().call_later(lifetime, USERS.pop, token)
return token


def get_user(token):
"""Find user authenticated by token or return None."""
return USERS.get(token)


# Utilities


def get_cookie(raw, key):
cookie = http.cookies.SimpleCookie(raw)
morsel = cookie.get(key)
if morsel is not None:
return morsel.value


def get_query_param(path, key):
query = urllib.parse.urlparse(path).query
params = urllib.parse.parse_qs(query)
values = params.get(key, [])
if len(values) == 1:
return values[0]


# Main HTTP server

CONTENT_TYPES = {
".css": "text/css",
".html": "text/html; charset=utf-8",
".ico": "image/x-icon",
".js": "text/javascript",
}


async def serve_html(path, request_headers):
user = get_query_param(path, "user")
path = urllib.parse.urlparse(path).path
if path == "/":
if user is None:
page = "index.html"
else:
page = "test.html"
else:
page = path[1:]

try:
template = pathlib.Path(__file__).with_name(page)
except ValueError:
pass
else:
if template.is_file():
headers = {"Content-Type": CONTENT_TYPES[template.suffix]}
body = template.read_bytes()
if user is not None:
token = create_token(user)
body = body.replace(b"TOKEN", token.encode())
return http.HTTPStatus.OK, headers, body

return http.HTTPStatus.NOT_FOUND, {}, b"Not found\n"


async def noop_handler(websocket, path):
pass


# Send credentials as the first message in the WebSocket connection


async def first_message_handler(websocket, path):
token = await websocket.recv()
user = get_user(token)
if user is None:
await websocket.close(1011, "authentication failed")
return

await websocket.send(f"Hello {user}!")
message = await websocket.recv()
assert message == f"Goodbye {user}."


# Add credentials to the WebSocket URI in a query parameter


class QueryParamProtocol(websockets.WebSocketServerProtocol):
async def process_request(self, path, headers):
token = get_query_param(path, "token")
if token is None:
return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n"

user = get_user(token)
if user is None:
return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n"

self.user = user


async def query_param_handler(websocket, path):
user = websocket.user

await websocket.send(f"Hello {user}!")
message = await websocket.recv()
assert message == f"Goodbye {user}."


# Set a cookie on the domain of the WebSocket URI


class CookieProtocol(websockets.WebSocketServerProtocol):
async def process_request(self, path, headers):
if "Upgrade" not in headers:
template = pathlib.Path(__file__).with_name(path[1:])
headers = {"Content-Type": CONTENT_TYPES[template.suffix]}
body = template.read_bytes()
return http.HTTPStatus.OK, headers, body

token = get_cookie(headers.get("Cookie", ""), "token")
if token is None:
return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n"

user = get_user(token)
if user is None:
return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n"

self.user = user


async def cookie_handler(websocket, path):
user = websocket.user

await websocket.send(f"Hello {user}!")
message = await websocket.recv()
assert message == f"Goodbye {user}."


# Adding credentials to the WebSocket URI in user information


class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol):
async def check_credentials(self, username, password):
if username != "token":
return False

user = get_user(password)
if user is None:
return False

self.user = user
return True


async def user_info_handler(websocket, path):
user = websocket.user

await websocket.send(f"Hello {user}!")
message = await websocket.recv()
assert message == f"Goodbye {user}."


# Start all five servers


async def main():
# Set the stop condition when receiving SIGINT or SIGTERM.
loop = asyncio.get_running_loop()
stop = loop.create_future()
loop.add_signal_handler(signal.SIGINT, stop.set_result, None)
loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)

async with websockets.serve(
noop_handler,
host="",
port=8000,
process_request=serve_html,
), websockets.serve(
first_message_handler,
host="",
port=8001,
), websockets.serve(
query_param_handler,
host="",
port=8002,
create_protocol=QueryParamProtocol,
), websockets.serve(
cookie_handler,
host="",
port=8003,
create_protocol=CookieProtocol,
), websockets.serve(
user_info_handler,
host="",
port=8004,
create_protocol=UserInfoProtocol,
):
print("Running on http://localhost:8000/")
await stop
print("\rExiting")


if __name__ == "__main__":
asyncio.run(main())
15 changes: 15 additions & 0 deletions experiments/authentication/cookie.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Cookie | WebSocket Authentication</title>
<link href="style.css" rel="stylesheet">
</head>
<body class="test">
<p class="test">[??] Cookie</p>
<p class="ok">[OK] Cookie</p>
<p class="ko">[KO] Cookie</p>
<script src="script.js"></script>
<script src="cookie.js"></script>
<iframe src="http://localhost:8003/cookie_iframe.html" style="display: none;"></iframe>
</body>
</html>
36 changes: 36 additions & 0 deletions experiments/authentication/cookie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// wait for "load" rather than "DOMContentLoaded"
// to ensure that the iframe has finished loading
window.addEventListener("load", () => {
// create message channel to communicate
// with the iframe
const channel = new MessageChannel();
const port1 = channel.port1;

// receive WebSocket events from the iframe
const expected = getExpectedEvents();
var actual = [];
port1.onmessage = ({ data }) => {
// respond to messages
if (data.type == "message") {
port1.postMessage({
type: "message",
message: `Goodbye ${data.data.slice(6, -1)}.`,
});
}
// run tests
actual.push(data);
testStep(expected, actual);
};

// send message channel to the iframe
const iframe = document.querySelector("iframe");
const origin = "http://localhost:8003";
const ports = [channel.port2];
iframe.contentWindow.postMessage("", origin, ports);

// send token to the iframe
port1.postMessage({
type: "open",
token: token,
});
});
9 changes: 9 additions & 0 deletions experiments/authentication/cookie_iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Cookie iframe | WebSocket Authentication</title>
</head>
<body>
<script src="cookie_iframe.js"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions experiments/authentication/cookie_iframe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// receive message channel from the parent window
window.addEventListener("message", ({ ports }) => {
const port2 = ports[0];
var websocket;
port2.onmessage = ({ data }) => {
switch (data.type) {
case "open":
websocket = initWebSocket(data.token, port2);
break;
case "message":
websocket.send(data.message);
break;
case "close":
websocket.close(data.code, data.reason);
break;
}
};
});

// open WebSocket connection and relay events to the parent window
function initWebSocket(token, port2) {
document.cookie = `token=${token}; SameSite=Strict`;
const websocket = new WebSocket("ws://localhost:8003/");

websocket.addEventListener("open", ({ type }) => {
port2.postMessage({ type });
});
websocket.addEventListener("message", ({ type, data }) => {
port2.postMessage({ type, data });
});
websocket.addEventListener("close", ({ type, code, reason, wasClean }) => {
port2.postMessage({ type, code, reason, wasClean });
});

return websocket;
}
Binary file added experiments/authentication/favicon.ico
Binary file not shown.
14 changes: 14 additions & 0 deletions experiments/authentication/first_message.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>First message | WebSocket Authentication</title>
<link href="style.css" rel="stylesheet">
</head>
<body class="test">
<p class="test">[??] First message</p>
<p class="ok">[OK] First message</p>
<p class="ko">[KO] First message</p>
<script src="script.js"></script>
<script src="first_message.js"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions experiments/authentication/first_message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
window.addEventListener("DOMContentLoaded", () => {
const websocket = new WebSocket("ws://localhost:8001/");
websocket.onopen = () => websocket.send(token);

websocket.onmessage = ({ data }) => {
// event.data is expected to be "Hello <user>!"
websocket.send(`Goodbye ${data.slice(6, -1)}.`);
};

runTest(websocket);
});
12 changes: 12 additions & 0 deletions experiments/authentication/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>WebSocket Authentication</title>
<link href="style.css" rel="stylesheet">
</head>
<body>
<form method="GET">
<input name="user" placeholder="username">
</form>
</body>
</html>
14 changes: 14 additions & 0 deletions experiments/authentication/query_param.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Query parameter | WebSocket Authentication</title>
<link href="style.css" rel="stylesheet">
</head>
<body class="test">
<p class="test">[??] Query parameter</p>
<p class="ok">[OK] Query parameter</p>
<p class="ko">[KO] Query parameter</p>
<script src="script.js"></script>
<script src="query_param.js"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions experiments/authentication/query_param.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
window.addEventListener("DOMContentLoaded", () => {
const uri = `ws://localhost:8002/?token=${token}`;
const websocket = new WebSocket(uri);

websocket.onmessage = ({ data }) => {
// event.data is expected to be "Hello <user>!"
websocket.send(`Goodbye ${data.slice(6, -1)}.`);
};

runTest(websocket);
});
Loading

0 comments on commit 8ab85e5

Please sign in to comment.