Add google authentication

This commit is contained in:
dvirlabs 2025-12-14 13:44:00 +02:00
parent ba7d0c9121
commit 1da5dc0a30
12 changed files with 147 additions and 5 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules/
my-recipes/
my-recipes-chart/

View File

@ -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
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

1
backend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__/

View File

@ -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])

View File

@ -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'
}
)

View File

@ -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);

View File

@ -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();
}

View File

@ -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 (
<div className="auth-container">
<div className="auth-card">
@ -121,6 +126,28 @@ function Login({ onSuccess, onSwitchToRegister }) {
המשך עם Google
</button>
<button
type="button"
onClick={handleAzureLogin}
className="btn ghost full-width"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "0.5rem",
border: "1px solid var(--border-subtle)",
marginTop: "0.5rem"
}}
>
<svg width="18" height="18" viewBox="0 0 23 23">
<path fill="#f25022" d="M1 1h10v10H1z"/>
<path fill="#00a4ef" d="M12 1h10v10H12z"/>
<path fill="#7fba00" d="M1 12h10v10H1z"/>
<path fill="#ffb900" d="M12 12h10v10H12z"/>
</svg>
המשך עם Microsoft
</button>
<div className="auth-footer">
<p>
עדיין אין לך חשבון?{" "}