login with httpOnly-cookie working
authorPhiTux <redacted>
Wed, 18 Dec 2024 21:17:58 +0000 (22:17 +0100)
committerPhiTux <redacted>
Wed, 18 Dec 2024 21:17:58 +0000 (22:17 +0100)
backend/server/routers/users.py
backend/server/utils/fileHandling.py
backend/server/utils/security.py
backend/server/utils/settings.py
frontend/src/routes/+layout.svelte
frontend/src/routes/+page.js [new file with mode: 0644]
frontend/src/routes/+page.svelte
frontend/src/routes/login/+page.js [new file with mode: 0644]
frontend/src/routes/login/+page.svelte

index 6e2a62f92f142245e023c82ac0bd51c47c4feedb..6f214b693b35f883cc8118714cc0411c90b04516 100644 (file)
@@ -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, 
index 77fa5932e473ea451afad2cae23e753d15aa24de..b1b0121c8266b9f9053f8838c52838a16cb2a2c9 100644 (file)
@@ -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
index 3cee90b393c6e83cb73119c27514a2c10bcba26e..cf5af99eeb1bee41311f7c1ef966c23b04e3608d 100644 (file)
@@ -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
index f4c7a7c7818598de7cf0809b95adc14edca704cc..17811860fe3dd5c7e13221295303973b8bf3db41 100644 (file)
@@ -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
index d091f9181e522f07a3014e744eb5fcdad4f11933..3adc9d110cd2bb3093b0a1c1f423066e9fa77d17 100644 (file)
@@ -1,9 +1,27 @@
 <script>
        import { fade, blur, slide } from 'svelte/transition';
+       import axios from 'axios';
+       import { dev } from '$app/environment';
+       import { goto } from '$app/navigation';
+       import { onMount } from 'svelte';
 
        export let data;
        let inDuration = 150;
        let outDuration = 150;
+
+       let API_URL = dev ? 'http://localhost:8000' : window.location.pathname.replace(/\/+$/, '');
+
+       function logout() {
+               axios
+                       .get(API_URL + '/users/logout')
+                       .then((response) => {
+                               localStorage.removeItem('user');
+                               goto('/login');
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                       });
+       }
 </script>
 
 <main class="d-flex flex-column">
                <div class="container-fluid">
                        <a class="nav-item" href="/">Navbar</a>
                        <a class="nav-item" href="/login">Navbar</a>
+                       <div class="dropdown">
+                               <button
+                                       class="btn btn-outline-secondary dropdown-toggle"
+                                       type="button"
+                                       data-bs-toggle="dropdown"
+                                       aria-expanded="false"
+                               >
+                                       Dropdown button
+                               </button>
+                               <ul class="dropdown-menu dropdown-menu-end">
+                                       <li><button class="dropdown-item">Settings</button></li>
+                                       <li><button class="dropdown-item" onclick={logout}>Logout</button></li>
+                               </ul>
+                       </div>
                </div>
        </nav>
 
diff --git a/frontend/src/routes/+page.js b/frontend/src/routes/+page.js
new file mode 100644 (file)
index 0000000..d48ea95
--- /dev/null
@@ -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
index fccb823d62cd222f7bfce917c6803589a8edda9b..b22af18f463ece2861c2893e1a0cb43383c3d265 100644 (file)
@@ -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 (file)
index 0000000..17491da
--- /dev/null
@@ -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
index c415332e13674abb9352d4270e72407cd5be583d..ed6dd230e8de3b2d11d54191aa2b20d8bc495ff6 100644 (file)
@@ -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);
git clone https://git.99rst.org/PROJECT