-
-
Notifications
You must be signed in to change notification settings - Fork 522
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Experiment authentication techniques.
- Loading branch information
Showing
17 changed files
with
549 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
Oops, something went wrong.