diff --git a/.gitignore b/.gitignore index 2cf626f..ff404f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ +my-recipes/ my-recipes-chart/ \ No newline at end of file diff --git a/backend/.env b/backend/.env index de1b663..9748b1b 100644 --- a/backend/.env +++ b/backend/.env @@ -16,4 +16,10 @@ SMTP_FROM=dvirlabs@gmail.com GOOGLE_CLIENT_ID=143092846986-hsi59m0on2c9rb5qrdoejfceieao2ioc.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=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 GOOGLE_REDIRECT_URI=http://localhost:8001/auth/google/callback -FRONTEND_URL=http://localhost:5174 \ No newline at end of file +FRONTEND_URL=http://localhost:5174 + +# Microsoft Entra ID (Azure AD) OAuth +AZURE_CLIENT_ID=db244cf5-eb11-4738-a2ea-5b0716c9ec0a +AZURE_CLIENT_SECRET=Zad8Q~qRBxaQq8up0lLXAq4pHzrVM2JFGFJhHaDp +AZURE_TENANT_ID=consumers +AZURE_REDIRECT_URI=http://localhost:8001/auth/azure/callback \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/backend/__pycache__/email_utils.cpython-312.pyc b/backend/__pycache__/email_utils.cpython-312.pyc index 519a5fc..6524825 100644 Binary files a/backend/__pycache__/email_utils.cpython-312.pyc and b/backend/__pycache__/email_utils.cpython-312.pyc differ diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index ac62b4a..783ca7e 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/__pycache__/oauth_utils.cpython-312.pyc b/backend/__pycache__/oauth_utils.cpython-312.pyc index 6ba3367..5a69a27 100644 Binary files a/backend/__pycache__/oauth_utils.cpython-312.pyc and b/backend/__pycache__/oauth_utils.cpython-312.pyc differ diff --git a/backend/__pycache__/user_db_utils.cpython-312.pyc b/backend/__pycache__/user_db_utils.cpython-312.pyc index 2b2f41f..0a43b6f 100644 Binary files a/backend/__pycache__/user_db_utils.cpython-312.pyc and b/backend/__pycache__/user_db_utils.cpython-312.pyc differ diff --git a/backend/main.py b/backend/main.py index 1c6b64d..2216857 100644 --- a/backend/main.py +++ b/backend/main.py @@ -205,6 +205,8 @@ app.add_middleware( SessionMiddleware, secret_key=os.getenv("SECRET_KEY", "your-secret-key-change-in-production-min-32-chars"), max_age=3600, # 1 hour + same_site="lax", # Prevent session issues on frequent refreshes + https_only=False, # Set to True in production with HTTPS ) # Allow CORS from frontend domains @@ -638,6 +640,89 @@ async def google_callback(request: Request): raise HTTPException(status_code=400, detail=f"Google authentication failed: {str(e)}") +# ============= Microsoft Entra ID (Azure AD) OAuth Endpoints ============= + +@app.get("/auth/azure/login") +async def azure_login(request: Request): + """Redirect to Microsoft Entra ID OAuth login""" + redirect_uri = os.getenv("AZURE_REDIRECT_URI", "http://localhost:8001/auth/azure/callback") + return await oauth.azure.authorize_redirect(request, redirect_uri) + + +@app.get("/auth/azure/callback") +async def azure_callback(request: Request): + """Handle Microsoft Entra ID OAuth callback""" + try: + # Get token from Azure + token = await oauth.azure.authorize_access_token(request) + + # Get user info from Azure + user_info = token.get('userinfo') + if not user_info: + raise HTTPException(status_code=400, detail="Failed to get user info from Microsoft") + + email = user_info.get('email') or user_info.get('preferred_username') + azure_id = user_info.get('oid') or user_info.get('sub') + name = user_info.get('name', '') + given_name = user_info.get('given_name', '') + family_name = user_info.get('family_name', '') + + if not email: + raise HTTPException(status_code=400, detail="Email not provided by Microsoft") + + # Check if user exists + existing_user = get_user_by_email(email) + + if existing_user: + # User exists, log them in + user_id = existing_user["id"] + username = existing_user["username"] + else: + # Create new user + # Generate username from email or name + username = email.split('@')[0] + + # Check if username exists, add number if needed + base_username = username + counter = 1 + while get_user_by_username(username): + username = f"{base_username}{counter}" + counter += 1 + + # Create user with random password (they'll use Azure login) + import secrets + random_password = secrets.token_urlsafe(32) + password_hash = hash_password(random_password) + + new_user = create_user( + username=username, + email=email, + password_hash=password_hash, + first_name=given_name if given_name else None, + last_name=family_name if family_name else None, + display_name=name if name else username, + is_admin=False + ) + user_id = new_user["id"] + + # Create JWT token + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": str(user_id), "username": username}, + expires_delta=access_token_expires + ) + + # Redirect to frontend with token + frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5174") + return Response( + status_code=302, + headers={"Location": f"{frontend_url}?token={access_token}"} + ) + + except Exception as e: + raise HTTPException(status_code=400, detail=f"Microsoft authentication failed: {str(e)}") + + # ============= Grocery Lists Endpoints ============= @app.get("/grocery-lists", response_model=List[GroceryList]) diff --git a/backend/oauth_utils.py b/backend/oauth_utils.py index 78547fc..da38e46 100644 --- a/backend/oauth_utils.py +++ b/backend/oauth_utils.py @@ -18,3 +18,16 @@ oauth.register( 'scope': 'openid email profile' } ) + +# Register Microsoft Entra ID (Azure AD) OAuth +# Use 'common' for multi-tenant + personal accounts, or 'consumers' for personal accounts only +tenant_id = os.getenv('AZURE_TENANT_ID', 'common') +oauth.register( + name='azure', + client_id=os.getenv('AZURE_CLIENT_ID'), + client_secret=os.getenv('AZURE_CLIENT_SECRET'), + server_metadata_url=f'https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration', + client_kwargs={ + 'scope': 'openid email profile' + } +) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 405eb50..6cc3749 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -72,9 +72,16 @@ function App() { setUser(userData); setIsAuthenticated(true); } catch (err) { - // Token invalid or expired - removeToken(); - setIsAuthenticated(false); + // Only remove token on authentication errors (401), not network errors + if (err.status === 401) { + console.log("Token invalid or expired, logging out"); + removeToken(); + setIsAuthenticated(false); + } else { + // Network error or server error - keep user logged in + console.warn("Auth check failed but keeping session:", err.message); + setIsAuthenticated(true); // Assume still authenticated + } } } setLoadingAuth(false); diff --git a/frontend/src/authApi.js b/frontend/src/authApi.js index c947259..6aa0919 100644 --- a/frontend/src/authApi.js +++ b/frontend/src/authApi.js @@ -48,7 +48,9 @@ export async function getMe(token) { }, }); if (!res.ok) { - throw new Error("Failed to get user info"); + const error = new Error("Failed to get user info"); + error.status = res.status; + throw error; } return res.json(); } diff --git a/frontend/src/components/Login.jsx b/frontend/src/components/Login.jsx index 1882b24..2074b94 100644 --- a/frontend/src/components/Login.jsx +++ b/frontend/src/components/Login.jsx @@ -40,6 +40,11 @@ function Login({ onSuccess, onSwitchToRegister }) { window.location.href = `${apiBase}/auth/google/login`; }; + const handleAzureLogin = () => { + const apiBase = window.__ENV__?.API_BASE || "http://localhost:8001"; + window.location.href = `${apiBase}/auth/azure/login`; + }; + return (
@@ -121,6 +126,28 @@ function Login({ onSuccess, onSwitchToRegister }) { המשך עם Google + +

עדיין אין לך חשבון?{" "}