Add google authentication
This commit is contained in:
parent
ba7d0c9121
commit
1da5dc0a30
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
my-recipes/
|
||||||
my-recipes-chart/
|
my-recipes-chart/
|
||||||
@ -17,3 +17,9 @@ GOOGLE_CLIENT_ID=143092846986-hsi59m0on2c9rb5qrdoejfceieao2ioc.apps.googleuserco
|
|||||||
GOOGLE_CLIENT_SECRET=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
|
GOOGLE_CLIENT_SECRET=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
|
||||||
GOOGLE_REDIRECT_URI=http://localhost:8001/auth/google/callback
|
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
1
backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -205,6 +205,8 @@ app.add_middleware(
|
|||||||
SessionMiddleware,
|
SessionMiddleware,
|
||||||
secret_key=os.getenv("SECRET_KEY", "your-secret-key-change-in-production-min-32-chars"),
|
secret_key=os.getenv("SECRET_KEY", "your-secret-key-change-in-production-min-32-chars"),
|
||||||
max_age=3600, # 1 hour
|
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
|
# 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)}")
|
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 =============
|
# ============= Grocery Lists Endpoints =============
|
||||||
|
|
||||||
@app.get("/grocery-lists", response_model=List[GroceryList])
|
@app.get("/grocery-lists", response_model=List[GroceryList])
|
||||||
|
|||||||
@ -18,3 +18,16 @@ oauth.register(
|
|||||||
'scope': 'openid email profile'
|
'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'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@ -72,9 +72,16 @@ function App() {
|
|||||||
setUser(userData);
|
setUser(userData);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Token invalid or expired
|
// Only remove token on authentication errors (401), not network errors
|
||||||
|
if (err.status === 401) {
|
||||||
|
console.log("Token invalid or expired, logging out");
|
||||||
removeToken();
|
removeToken();
|
||||||
setIsAuthenticated(false);
|
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);
|
setLoadingAuth(false);
|
||||||
|
|||||||
@ -48,7 +48,9 @@ export async function getMe(token) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
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();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,11 @@ function Login({ onSuccess, onSwitchToRegister }) {
|
|||||||
window.location.href = `${apiBase}/auth/google/login`;
|
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 (
|
return (
|
||||||
<div className="auth-container">
|
<div className="auth-container">
|
||||||
<div className="auth-card">
|
<div className="auth-card">
|
||||||
@ -121,6 +126,28 @@ function Login({ onSuccess, onSwitchToRegister }) {
|
|||||||
המשך עם Google
|
המשך עם Google
|
||||||
</button>
|
</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">
|
<div className="auth-footer">
|
||||||
<p>
|
<p>
|
||||||
עדיין אין לך חשבון?{" "}
|
עדיין אין לך חשבון?{" "}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user