diff --git a/multi_llm_chatbot_backend/app/api/routes/auth.py b/multi_llm_chatbot_backend/app/api/routes/auth.py index 37c00f8..6b45d33 100644 --- a/multi_llm_chatbot_backend/app/api/routes/auth.py +++ b/multi_llm_chatbot_backend/app/api/routes/auth.py @@ -33,7 +33,7 @@ async def signup(user_data: UserCreate): ) # Create new user - hashed_password = get_password_hash(user_data.password) + hashed_password = get_password_hash(user_data.password_hash) user = User( firstName=user_data.firstName, lastName=user_data.lastName, @@ -76,7 +76,7 @@ async def login(user_credentials: UserLogin): """Login with email and password""" try: # Authenticate user - user = await authenticate_user(user_credentials.email, user_credentials.password) + user = await authenticate_user(user_credentials.email, user_credentials.password_hash) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/multi_llm_chatbot_backend/app/core/auth.py b/multi_llm_chatbot_backend/app/core/auth.py index 4fa0377..b66d1e2 100644 --- a/multi_llm_chatbot_backend/app/core/auth.py +++ b/multi_llm_chatbot_backend/app/core/auth.py @@ -22,13 +22,13 @@ # Security scheme security = HTTPBearer() -def verify_password(plain_password: str, hashed_password: str) -> bool: - """Verify a password against its hash""" - return pwd_context.verify(plain_password, hashed_password) +def verify_password(password_hash: str, hashed_password: str) -> bool: + """Verify a client-provided SHA-256 password hash against the stored bcrypt hash""" + return pwd_context.verify(password_hash, hashed_password) -def get_password_hash(password: str) -> str: - """Hash a password""" - return pwd_context.hash(password) +def get_password_hash(password_hash: str) -> str: + """Bcrypt-hash a client-provided SHA-256 password hash for storage""" + return pwd_context.hash(password_hash) def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): """Create a JWT access token""" @@ -61,12 +61,12 @@ async def get_user_by_id(user_id: str) -> Optional[User]: except Exception: return None -async def authenticate_user(email: str, password: str) -> Optional[User]: - """Authenticate user with email and password""" +async def authenticate_user(email: str, password_hash: str) -> Optional[User]: + """Authenticate user with email and client-provided SHA-256 password hash""" user = await get_user_by_email(email) if not user: return None - if not verify_password(password, user.hashed_password): + if not verify_password(password_hash, user.hashed_password): return None return user diff --git a/multi_llm_chatbot_backend/app/models/user.py b/multi_llm_chatbot_backend/app/models/user.py index 95d1067..8df903c 100644 --- a/multi_llm_chatbot_backend/app/models/user.py +++ b/multi_llm_chatbot_backend/app/models/user.py @@ -25,13 +25,13 @@ class UserCreate(BaseModel): firstName: str lastName: str email: EmailStr - password: str + password_hash: str academicStage: Optional[str] = None researchArea: Optional[str] = None class UserLogin(BaseModel): email: EmailStr - password: str + password_hash: str class User(BaseModel): model_config = ConfigDict( diff --git a/phd-advisor-frontend/src/components/Login.js b/phd-advisor-frontend/src/components/Login.js index 5e17a27..cb13f96 100644 --- a/phd-advisor-frontend/src/components/Login.js +++ b/phd-advisor-frontend/src/components/Login.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { Eye, EyeOff, Mail, Lock, ArrowRight, BookOpen, Phone } from 'lucide-react'; import { useAppConfig } from '../contexts/AppConfigContext'; +import { hashPassword } from '../utils/hashPassword'; import '../styles/Login.css'; const Login = ({ onNavigateToSignup, onNavigateToHome }) => { @@ -55,6 +56,7 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => { setIsLoading(true); try { + const hashedPassword = await hashPassword(formData.password); const response = await fetch(`${process.env.REACT_APP_API_URL}/auth/login`, { method: 'POST', headers: { @@ -62,7 +64,7 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => { }, body: JSON.stringify({ email: formData.email, - password: formData.password + password_hash: hashedPassword }), }); diff --git a/phd-advisor-frontend/src/components/Signup.js b/phd-advisor-frontend/src/components/Signup.js index 4a54242..d953ba4 100644 --- a/phd-advisor-frontend/src/components/Signup.js +++ b/phd-advisor-frontend/src/components/Signup.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { Eye, EyeOff, Mail, Lock, User, ArrowRight, BookOpen, Phone, GraduationCap } from 'lucide-react'; import { useAppConfig } from '../contexts/AppConfigContext'; +import { hashPassword } from '../utils/hashPassword'; import '../styles/Signup.css'; const Signup = ({ onNavigateToLogin, onNavigateToHome }) => { @@ -90,6 +91,7 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => { setIsLoading(true); try { + const hashedPassword = await hashPassword(formData.password); const response = await fetch(`${process.env.REACT_APP_API_URL}/auth/signup`, { method: 'POST', headers: { @@ -99,7 +101,7 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => { firstName: formData.firstName, lastName: formData.lastName, email: formData.email, - password: formData.password, + password_hash: hashedPassword, academicStage: formData.academicStage, researchArea: formData.researchArea }), diff --git a/phd-advisor-frontend/src/utils/hashPassword.js b/phd-advisor-frontend/src/utils/hashPassword.js new file mode 100644 index 0000000..9e26dd5 --- /dev/null +++ b/phd-advisor-frontend/src/utils/hashPassword.js @@ -0,0 +1,14 @@ +/** + * Hash a password client-side before sending to the backend, + * so the server never receives plaintext credentials. + * + * @param {string} password - The user's plaintext password + * @returns {Promise} 64-character lowercase SHA-256 hex digest + */ +export async function hashPassword(password) { + const encoder = new TextEncoder(); + const data = encoder.encode(password); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +}