From: PhiTux Date: Wed, 18 Dec 2024 21:17:58 +0000 (+0100) Subject: login with httpOnly-cookie working X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=1fa89dc8a2bf1638bdf12af69a06ef752d93f7fa;p=DailyTxT.git login with httpOnly-cookie working --- diff --git a/backend/server/routers/users.py b/backend/server/routers/users.py index 6e2a62f..6f214b6 100644 --- a/backend/server/routers/users.py +++ b/backend/server/routers/users.py @@ -1,10 +1,14 @@ +import datetime import json import secrets -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Response from pydantic import BaseModel from ..utils import fileHandling from ..utils import security +from ..utils.settings import settings import logging +import base64 +import jwt logger = logging.getLogger("dailytxtLogger") @@ -15,9 +19,36 @@ class Login(BaseModel): password: str @router.post("/users/login") -async def login(login: Login): - print(login.username, login.password) - return {"message": "Login"} +async def login(login: Login, respose: Response): + # check if user exists + content:dict = fileHandling.getUsers() + if len(content) == 0 or "users" not in content.keys() or len(content["users"]) == 0 or not any(user["username"] == login.username for user in content["users"]): + logger.error(f"Login failed. User '{login.username}' not found") + raise HTTPException(status_code=404, detail="User not found") + + # get user data + user = next(user for user in content["users"] if user["username"] == login.username) + if not security.verify_password(login.password, user["password"]): + logger.error(f"Login failed. Password for user '{login.username}' is incorrect") + raise HTTPException(status_code=400, detail="Password is incorrect") + + # get intermediate key + derived_key = base64.b64encode(security.derive_key_from_password(login.password, user["salt"])).decode() + + + # build jwt + jwt = create_jwt(user["user_id"], user["username"], derived_key) + respose.set_cookie(key="jwt", value=jwt, httponly=True) + return {"username": user["username"]} + +def create_jwt(user_id, username, derived_key): + return jwt.encode({"iat": datetime.datetime.now() + datetime.timedelta(days=settings.logout_after_days), "user_id": user_id, "name": username, "derived_key": derived_key}, settings.secret_token, algorithm="HS256") + + +@router.get("/users/logout") +def logout(response: Response): + response.delete_cookie("jwt") + return {"success": True} class Register(BaseModel): @@ -26,10 +57,7 @@ class Register(BaseModel): @router.post("/users/register") async def register(register: Register): - content = fileHandling.getUsers() - if isinstance(content, Exception): - raise HTTPException(status_code=500, detail="Internal Server Error when trying to open users.json") - + content:dict = fileHandling.getUsers() # check if username already exists if len(content) > 0: @@ -46,14 +74,14 @@ async def register(register: Register): username = register.username password = security.hash_password(register.password) salt = secrets.token_urlsafe(16) - enc_enc_key = security.create_new_enc_enc_key(register.password, salt.encode()).decode() + enc_enc_key = security.create_new_enc_enc_key(register.password, salt).decode() if len(content) == 0: content = {"id_counter": 1, "users": [ { "user_id": 1, - "dailytxt-version": 2, + "dailytxt_version": 2, "username": username, "password": password, "salt": salt, @@ -67,7 +95,7 @@ async def register(register: Register): content["users"].append( { "user_id": content["id_counter"], - "dailytxt-version": 2, + "dailytxt_version": 2, "username": username, "password": password, "salt": salt, diff --git a/backend/server/utils/fileHandling.py b/backend/server/utils/fileHandling.py index 77fa593..b1b0121 100644 --- a/backend/server/utils/fileHandling.py +++ b/backend/server/utils/fileHandling.py @@ -1,6 +1,8 @@ import json import os import logging + +from fastapi import HTTPException from .settings import settings logger = logging.getLogger("dailytxtLogger") @@ -10,13 +12,16 @@ def getUsers(): f = open(os.path.join(settings.data_path, "users.json"), "r") except FileNotFoundError: logger.info("users.json - File not found") - return "" + return {} except Exception as e: logger.exception(e) - return e + raise HTTPException(status_code=500, detail="Internal Server Error when trying to open users.json") else: with f: - return f.read() + s = f.read() + if s == "": + return {} + return json.loads(s) def writeUsers(content): # print working directory diff --git a/backend/server/utils/security.py b/backend/server/utils/security.py index 3cee90b..cf5af99 100644 --- a/backend/server/utils/security.py +++ b/backend/server/utils/security.py @@ -9,13 +9,11 @@ def hash_password(password: str) -> str: def verify_password(password: str, hash: str) -> bool: return argon2.verify(password, hash) -def derive_key_from_password(password: str, salt: bytes) -> bytes: - return hash_secret_raw(secret=password.encode(), salt=salt, time_cost=2, memory_cost=2**15, parallelism=1, hash_len=32, type=Type.ID) +def derive_key_from_password(password: str, salt: str) -> bytes: + return hash_secret_raw(secret=password.encode(), salt=salt.encode(), time_cost=2, memory_cost=2**15, parallelism=1, hash_len=32, type=Type.ID) -def create_new_enc_enc_key(password: str, salt: bytes) -> bytes: +def create_new_enc_enc_key(password: str, salt: str) -> bytes: derived_key = derive_key_from_password(password, salt) # password derived key only to encrypt the actual encryption key key = Fernet.generate_key() # actual encryption key f = Fernet(base64.urlsafe_b64encode(derived_key)) return f.encrypt(key) - - \ No newline at end of file diff --git a/backend/server/utils/settings.py b/backend/server/utils/settings.py index f4c7a7c..1781186 100644 --- a/backend/server/utils/settings.py +++ b/backend/server/utils/settings.py @@ -1,6 +1,9 @@ +import secrets from pydantic_settings import BaseSettings class Settings(BaseSettings): data_path: str = "/data" + secret_token: str = secrets.token_urlsafe(32) + logout_after_days: int = 30 settings = Settings() \ No newline at end of file diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index d091f91..3adc9d1 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,9 +1,27 @@
@@ -11,6 +29,20 @@
Navbar Navbar +
diff --git a/frontend/src/routes/+page.js b/frontend/src/routes/+page.js new file mode 100644 index 0000000..d48ea95 --- /dev/null +++ b/frontend/src/routes/+page.js @@ -0,0 +1,8 @@ +import {redirect} from '@sveltejs/kit' + +export const load = () => { + const user = JSON.parse(localStorage.getItem('user')); + if (!user) { + throw redirect(307, '/login'); + } +} \ No newline at end of file diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index fccb823..b22af18 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -2,6 +2,7 @@ import '../scss/styles.scss'; //import * as bootstrap from 'bootstrap'; import { Tooltip } from 'bootstrap'; + import { goto } from '$app/navigation'; //on mount import { onMount } from 'svelte'; diff --git a/frontend/src/routes/login/+page.js b/frontend/src/routes/login/+page.js new file mode 100644 index 0000000..17491da --- /dev/null +++ b/frontend/src/routes/login/+page.js @@ -0,0 +1,8 @@ +import {redirect} from '@sveltejs/kit' + +export const load = () => { + const user = JSON.parse(localStorage.getItem('user')); + if (user) { + throw redirect(307, '/'); + } +} \ No newline at end of file diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index c415332..ed6dd23 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -3,6 +3,7 @@ import { onMount } from 'svelte'; import axios from 'axios'; import { dev } from '$app/environment'; + import { goto } from '$app/navigation'; let show_warning_empty_fields = $state(false); let show_warning_passwords_do_not_match = $state(false); @@ -22,7 +23,8 @@ axios .post(API_URL + '/users/login', { username, password }) .then((response) => { - console.log(response); + localStorage.setItem('user', JSON.stringify(response.data.username)); + goto('/'); }) .catch((error) => { console.error(error);