Skip to content

Commit 039854d

Browse files
committed
added usage of httpOnly cookies for jwt token storage (instead of localStorage)
1 parent a0e5873 commit 039854d

File tree

6 files changed

+139
-47
lines changed

6 files changed

+139
-47
lines changed

backend/app/api/routes/auth.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from app.core.security import security_service
77
from app.db.repositories.user_repository import UserRepository, get_user_repository
88
from app.schemas.user import UserCreate, UserInDB, UserResponse
9-
from fastapi import APIRouter, Depends, HTTPException, Request
9+
from fastapi import APIRouter, Depends, HTTPException, Request, Response
1010
from fastapi.security import OAuth2PasswordRequestForm
1111
from slowapi import Limiter
1212
from slowapi.util import get_remote_address
@@ -18,6 +18,7 @@
1818
@router.post("/login")
1919
async def login(
2020
request: Request,
21+
response: Response,
2122
form_data: OAuth2PasswordRequestForm = Depends(),
2223
user_repo: UserRepository = Depends(get_user_repository),
2324
) -> Dict[str, str]:
@@ -79,7 +80,18 @@ async def login(
7980
},
8081
)
8182

82-
return {"access_token": access_token, "token_type": "bearer"}
83+
# Set httpOnly cookie for secure token storage
84+
response.set_cookie(
85+
key="access_token",
86+
value=access_token,
87+
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, # Convert to seconds
88+
httponly=True,
89+
secure=True, # HTTPS only
90+
samesite="strict", # CSRF protection
91+
path="/",
92+
)
93+
94+
return {"message": "Login successful", "username": user.username}
8395

8496

8597
@router.post("/register", response_model=UserResponse)
@@ -181,3 +193,37 @@ async def verify_token(
181193
detail="Invalid token",
182194
headers={"WWW-Authenticate": "Bearer"},
183195
) from e
196+
197+
198+
@router.post("/logout")
199+
async def logout(
200+
request: Request,
201+
response: Response,
202+
) -> Dict[str, str]:
203+
logger.info(
204+
"Logout attempt",
205+
extra={
206+
"client_ip": get_remote_address(request),
207+
"endpoint": "/logout",
208+
"user_agent": request.headers.get("user-agent"),
209+
},
210+
)
211+
212+
# Clear the httpOnly cookie
213+
response.delete_cookie(
214+
key="access_token",
215+
path="/",
216+
secure=True,
217+
httponly=True,
218+
samesite="strict",
219+
)
220+
221+
logger.info(
222+
"Logout successful",
223+
extra={
224+
"client_ip": get_remote_address(request),
225+
"user_agent": request.headers.get("user-agent"),
226+
},
227+
)
228+
229+
return {"message": "Logout successful"}

backend/app/core/security.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,24 @@
55
from app.config import get_settings
66
from app.db.repositories.user_repository import UserRepository, get_user_repository
77
from app.schemas.user import UserInDB
8-
from fastapi import Depends, HTTPException, status
8+
from fastapi import Depends, HTTPException, Request, status
99
from fastapi.security import OAuth2PasswordBearer
1010
from passlib.context import CryptContext
1111

1212
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login")
1313

1414

15+
def get_token_from_cookie(request: Request) -> str:
16+
token = request.cookies.get("access_token")
17+
if not token:
18+
raise HTTPException(
19+
status_code=status.HTTP_401_UNAUTHORIZED,
20+
detail="Authentication token not found",
21+
headers={"WWW-Authenticate": "Bearer"},
22+
)
23+
return token
24+
25+
1526
class SecurityService:
1627
def __init__(self) -> None:
1728
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@@ -39,7 +50,7 @@ def create_access_token(
3950

4051
async def get_current_user(
4152
self,
42-
token: str = Depends(oauth2_scheme),
53+
token: str = Depends(get_token_from_cookie),
4354
user_repo: UserRepository = Depends(get_user_repository),
4455
) -> UserInDB:
4556
credentials_exception = HTTPException(

frontend/src/components/Header.svelte

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script>
22
import { Link, navigate } from "svelte-routing";
3-
import { authToken, username, logout as authLogout } from "../stores/auth.js";
3+
import { isAuthenticated, username, logout as authLogout } from "../stores/auth.js";
44
import { theme, toggleTheme } from "../stores/theme.js";
55
import { fade } from 'svelte/transition';
66
import { onMount, onDestroy } from 'svelte';
@@ -49,8 +49,8 @@
4949
isMenuActive = false;
5050
}
5151
52-
function handleLogout() {
53-
authLogout();
52+
async function handleLogout() {
53+
await authLogout();
5454
closeMenu();
5555
navigate('/login');
5656
}
@@ -98,7 +98,7 @@
9898
</button>
9999

100100
<div class="hidden lg:flex items-center space-x-3">
101-
{#if $authToken}
101+
{#if $isAuthenticated}
102102
<span class="text-sm text-fg-muted dark:text-dark-fg-muted hidden xl:inline">Welcome, <span class="font-medium text-fg-default dark:text-dark-fg-default">{$username}!</span></span>
103103
<button on:click={handleLogout} class="btn btn-secondary-outline btn-sm">
104104
Logout
@@ -128,7 +128,7 @@
128128
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
129129

130130
<div class="pt-3 mt-2 border-t border-border-default dark:border-dark-border-default">
131-
{#if $authToken}
131+
{#if $isAuthenticated}
132132
<div class="px-3 py-2">
133133
<div class="text-sm font-medium text-fg-default dark:text-dark-fg-default">{$username}</div>
134134
<div class="text-xs text-fg-muted dark:text-dark-fg-muted">Logged in</div>

frontend/src/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import App from './App.svelte';
22
import './app.css';
3+
import axios from 'axios';
4+
5+
axios.defaults.withCredentials = true;
36

47
const app = new App({
58
target: document.body,

frontend/src/routes/Editor.svelte

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import {fade, fly, slide} from "svelte/transition";
44
import {get, writable} from "svelte/store";
55
import axios from "axios";
6-
import {authToken, logout as authLogout} from "../stores/auth.js";
6+
import {isAuthenticated, logout as authLogout, verifyAuth} from "../stores/auth.js";
77
import {addNotification} from "../stores/notifications.js";
88
import Spinner from "../components/Spinner.svelte";
99
import {navigate} from "svelte-routing";
@@ -78,7 +78,7 @@
7878
let showOptions = false;
7979
let showSavedScripts = false;
8080
81-
let isAuthenticated = false;
81+
let authenticated = false;
8282
let savedScripts = [];
8383
let scriptName = createPersistentStore("scriptName", "");
8484
let currentScriptId = createPersistentStore("currentScriptId", null);
@@ -126,12 +126,15 @@
126126
}
127127
128128
onMount(async () => {
129-
unsubscribeAuth = authToken.subscribe(token => {
130-
const wasAuthenticated = isAuthenticated;
131-
isAuthenticated = !!token;
132-
if (!wasAuthenticated && isAuthenticated && editorView) {
129+
// Verify authentication status on startup
130+
await verifyAuth();
131+
132+
unsubscribeAuth = isAuthenticated.subscribe(authStatus => {
133+
const wasAuthenticated = authenticated;
134+
authenticated = authStatus;
135+
if (!wasAuthenticated && authenticated && editorView) {
133136
loadSavedScripts();
134-
} else if (wasAuthenticated && !isAuthenticated) {
137+
} else if (wasAuthenticated && !authenticated) {
135138
savedScripts = [];
136139
showSavedScripts = false;
137140
currentScriptId.set(null);
@@ -184,7 +187,7 @@
184187
}
185188
});
186189
187-
if (isAuthenticated) {
190+
if (authenticated) {
188191
await loadSavedScripts();
189192
}
190193
});
@@ -334,11 +337,10 @@
334337
}
335338
336339
async function loadSavedScripts() {
337-
if (!isAuthenticated) return;
338-
const authTokenValue = get(authToken);
340+
if (!authenticated) return;
339341
try {
340342
const response = await axios.get(`/api/v1/scripts`, {
341-
headers: {Authorization: `Bearer ${authTokenValue}`},
343+
withCredentials: true, // Use cookies for authentication
342344
});
343345
savedScripts = response.data || [];
344346
} catch (err) {
@@ -371,7 +373,7 @@
371373
}
372374
373375
async function saveScript() {
374-
if (!isAuthenticated) {
376+
if (!authenticated) {
375377
addNotification("Please log in to save scripts.", "warning");
376378
return;
377379
}
@@ -381,7 +383,6 @@
381383
return;
382384
}
383385
const scriptValue = get(script);
384-
const authTokenValue = get(authToken);
385386
const currentIdValue = get(currentScriptId);
386387
let operation = currentIdValue ? 'update' : 'create';
387388
@@ -391,14 +392,14 @@
391392
response = await axios.put(
392393
`/api/v1/scripts/${currentIdValue}`,
393394
{name: nameValue, script: scriptValue},
394-
{headers: {Authorization: `Bearer ${authTokenValue}`}}
395+
{withCredentials: true}
395396
);
396397
addNotification("Script updated successfully.", "success");
397398
} else {
398399
response = await axios.post(
399400
`/api/v1/scripts`,
400401
{name: nameValue, script: scriptValue},
401-
{headers: {Authorization: `Bearer ${authTokenValue}`}}
402+
{withCredentials: true}
402403
);
403404
currentScriptId.set(response.data.id);
404405
addNotification("Script saved successfully.", "success");
@@ -414,18 +415,17 @@
414415
}
415416
416417
async function deleteScript(scriptIdToDelete) {
417-
if (!isAuthenticated) return;
418+
if (!authenticated) return;
418419
const scriptToDelete = savedScripts.find(s => s.id === scriptIdToDelete);
419420
const confirmMessage = scriptToDelete
420421
? `Are you sure you want to delete "${scriptToDelete.name}"?`
421422
: "Are you sure you want to delete this script?";
422423
423424
if (!confirm(confirmMessage)) return;
424425
425-
const authTokenValue = get(authToken);
426426
try {
427427
await axios.delete(`/api/v1/scripts/${scriptIdToDelete}`, {
428-
headers: {Authorization: `Bearer ${authTokenValue}`},
428+
withCredentials: true,
429429
});
430430
addNotification("Script deleted successfully.", "success");
431431
if (get(currentScriptId) === scriptIdToDelete) {
@@ -517,7 +517,7 @@
517517
518518
function toggleSavedScripts() {
519519
showSavedScripts = !showSavedScripts;
520-
if (showSavedScripts && isAuthenticated) {
520+
if (showSavedScripts && authenticated) {
521521
loadSavedScripts();
522522
}
523523
}
@@ -893,7 +893,7 @@
893893
894894
<!-- Right Column: Saved Scripts -->
895895
<div class="w-1/2 space-y-3">
896-
{#if isAuthenticated}
896+
{#if authenticated}
897897
<h4 class="text-xs font-medium text-fg-muted dark:text-dark-fg-muted uppercase tracking-wider">
898898
Saved Scripts
899899
</h4>

frontend/src/stores/auth.js

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
11
import { writable } from 'svelte/store';
22
import {backendUrl} from "../config.js";
33

4-
function createPersistentStore(key, startValue) {
5-
const storedValue = localStorage.getItem(key);
6-
const store = writable(storedValue ? JSON.parse(storedValue) : startValue);
7-
8-
store.subscribe(value => {
9-
localStorage.setItem(key, JSON.stringify(value));
10-
});
11-
12-
return store;
13-
}
14-
15-
export const authToken = createPersistentStore("auth_token", null);
16-
export const username = createPersistentStore("username", null);
4+
export const isAuthenticated = writable(false);
5+
export const username = writable(null);
176

187
export async function login(email, password) {
198
try {
@@ -26,6 +15,7 @@ export async function login(email, password) {
2615
headers: {
2716
'Content-Type': 'application/x-www-form-urlencoded',
2817
},
18+
credentials: 'include',
2919
body: formData
3020
});
3121

@@ -35,16 +25,58 @@ export async function login(email, password) {
3525
}
3626

3727
const data = await response.json();
38-
authToken.set(data.access_token);
39-
username.set(email);
28+
// Token is now stored in httpOnly cookie, just update auth state
29+
isAuthenticated.set(true);
30+
username.set(data.username || email);
4031
return true;
4132
} catch (error) {
4233
console.error("Login failed:", error);
4334
throw error;
4435
}
4536
}
4637

47-
export function logout() {
48-
authToken.set(null);
49-
username.set(null);
38+
export async function logout() {
39+
try {
40+
const response = await fetch('/api/v1/logout', {
41+
method: 'POST',
42+
credentials: 'include',
43+
});
44+
45+
// Clear auth state regardless of response (cookie might be expired)
46+
isAuthenticated.set(false);
47+
username.set(null);
48+
49+
if (!response.ok) {
50+
console.warn('Logout request failed, but cleared local auth state');
51+
}
52+
} catch (error) {
53+
console.error('Logout error:', error);
54+
isAuthenticated.set(false);
55+
username.set(null);
56+
}
57+
}
58+
59+
export async function verifyAuth() {
60+
try {
61+
const response = await fetch('/api/v1/verify-token', {
62+
method: 'GET',
63+
credentials: 'include',
64+
});
65+
66+
if (response.ok) {
67+
const data = await response.json();
68+
isAuthenticated.set(data.valid);
69+
username.set(data.username);
70+
return data.valid;
71+
} else {
72+
isAuthenticated.set(false);
73+
username.set(null);
74+
return false;
75+
}
76+
} catch (error) {
77+
console.error('Auth verification failed:', error);
78+
isAuthenticated.set(false);
79+
username.set(null);
80+
return false;
81+
}
5082
}

0 commit comments

Comments
 (0)