Implemented login page and new jwt-based sessions. Watching cleanup.

This commit is contained in:
Leo Vasanko
2023-10-18 01:06:27 +03:00
committed by Leo Vasanko
parent bd680e3668
commit 429a7dfb16
6 changed files with 166 additions and 59 deletions

117
cista/auth.py Normal file
View File

@@ -0,0 +1,117 @@
import hmac
import re
from time import time
from unicodedata import normalize
import argon2
import msgspec
from html5tagger import Document
from sanic import Blueprint, html, json, redirect
from . import session
from .config import User, config
_argon = argon2.PasswordHasher()
_droppyhash = re.compile(r'^([a-f0-9]{64})\$([a-f0-9]{8})$')
def _pwnorm(password):
return normalize('NFC', password).strip().encode()
def login(username: str, password: str):
un = _pwnorm(username)
pw = _pwnorm(password)
try:
u = config.users[un.decode()]
except KeyError:
raise ValueError("Invalid username")
# Verify password
need_rehash = False
if not u.hash:
raise ValueError("Account disabled")
if (m := _droppyhash.match(u.hash)) is not None:
h, s = m.groups()
h2 = hmac.digest(pw + s.encode() + un, b"", "sha256").hex()
if not hmac.compare_digest(h, h2):
raise ValueError("Invalid password")
# Droppy hashes are weak, do a hash update
need_rehash = True
else:
try:
_argon.verify(u.hash, pw)
except Exception:
raise ValueError("Invalid password")
if _argon.check_needs_rehash(u.hash):
need_rehash = True
# Login successful
if need_rehash:
u.set_password(password)
now = int(time())
u.lastSeen = now
return u
def set_password(user: User, password: str):
user.hash = _argon.hash(_pwnorm(password))
class LoginResponse(msgspec.Struct):
user: str = ""
privileged: bool = False
error: str = ""
authbp = Blueprint("auth")
@authbp.get("/login")
async def login_page(request):
doc = Document("Cista Login")
with doc.div(id="login"):
with doc.form(method="POST", autocomplete="on"):
doc.h1("Login")
doc.input(name="username", placeholder="Username", autocomplete="username").br
doc.input(type="password", name="password", placeholder="Password", autocomplete="current-password").br
doc.input(type="submit", value="Login")
s = session.get(request)
if s:
name = s["username"]
with doc.form(method="POST", action="/logout"):
doc.input(type="submit", value=f"Logout {name}")
flash = request.cookies.flash
if flash:
doc.p(flash)
res = html(doc)
if flash:
res.cookies.delete_cookie("flash")
if s is False:
session.delete(res)
return res
@authbp.post("/login")
async def login_post(request):
json_format = request.headers.content_type == "application/json"
if json_format:
username = request.json["username"]
password = request.json["password"]
else:
print(request.form)
username = request.form["username"][0]
password = request.form["password"][0]
if not username or not password:
raise ValueError("Missing username or password")
user = login(username, password)
if json_format:
res = json({
"status": "authenticated",
"user": username,
"privileged": user.privileged,
})
else:
res = redirect("/")
res.cookies.add_cookie("flash", "Logged in", host_prefix=True, max_age=5)
session.create(res, username)
return res
@authbp.post("/logout")
async def logout_post(request):
res = redirect("/")
session.delete(res)
res.cookies.add_cookie("flash", "Logged out",host_prefix=True, max_age=5)
return res