From a8ccb2db5ee917ba98d0a3a26a423f806c97bb7c Mon Sep 17 00:00:00 2001 From: Varkha Sharma Date: Thu, 13 Nov 2025 10:05:31 +0000 Subject: [PATCH 1/2] initial commit for oauth implementation --- docker-compose-server.yml | 10 ++ env-example | 33 ++++ server/app/auth_config.py | 47 +++++ server/app/auth_middleware.py | 77 ++++++++ server/app/auth_routes.py | 168 ++++++++++++++++++ server/app/auth_utils.py | 164 +++++++++++++++++ server/app/main.py | 21 +++ server/requirements.txt | 3 + ui/package.json | 3 +- ui/src/App.js | 69 ++++++- ui/src/client.js | 9 +- .../components/ExamplePageWithUserProfile.js | 37 ++++ ui/src/components/ProtectedRoute.js | 44 +++++ ui/src/components/UserProfile.css | 131 ++++++++++++++ ui/src/components/UserProfile.js | 79 ++++++++ ui/src/contexts/AuthContext.js | 91 ++++++++++ ui/src/pages/login/index.js | 103 +++++++++++ ui/src/pages/login/login.css | 129 ++++++++++++++ 18 files changed, 1207 insertions(+), 11 deletions(-) create mode 100644 server/app/auth_config.py create mode 100644 server/app/auth_middleware.py create mode 100644 server/app/auth_routes.py create mode 100644 server/app/auth_utils.py create mode 100644 ui/src/components/ExamplePageWithUserProfile.js create mode 100644 ui/src/components/ProtectedRoute.js create mode 100644 ui/src/components/UserProfile.css create mode 100644 ui/src/components/UserProfile.js create mode 100644 ui/src/contexts/AuthContext.js create mode 100644 ui/src/pages/login/index.js create mode 100644 ui/src/pages/login/login.css diff --git a/docker-compose-server.yml b/docker-compose-server.yml index 313563a1..65714cd7 100644 --- a/docker-compose-server.yml +++ b/docker-compose-server.yml @@ -65,6 +65,16 @@ services: POSTGRES_PORT: ${POSTGRES_PORT:-5432} POSTGRES_DB: ${POSTGRES_DB:-mlmd} REACT_APP_CMF_API_URL: ${REACT_APP_CMF_API_URL} + # OAuth Configuration + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} + GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI} + SECRET_KEY: ${SECRET_KEY} + ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-60} + FRONTEND_URL: ${FRONTEND_URL} + AUTH_ENABLED: ${AUTH_ENABLED:-true} + COOKIE_SECURE: ${COOKIE_SECURE:-false} + ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-} healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080 || exit 1"] interval: 15s diff --git a/env-example b/env-example index 592985ab..7f5e631b 100644 --- a/env-example +++ b/env-example @@ -18,6 +18,39 @@ POSTGRES_DB=mlmd # Directory for persistent data storage CMF_DATA_DIR=/home/kulkashr/sk/ +# ======================================== +# OAuth2 Authentication Configuration +# ======================================== + +# Google OAuth2 Credentials (REQUIRED for Google Login) +# Get these from: https://console.cloud.google.com/apis/credentials +GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=your-google-client-secret +GOOGLE_REDIRECT_URI=http://localhost:8080/api/auth/google/callback + +# JWT Secret Key (REQUIRED - Generate a secure random string) +# Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))" +SECRET_KEY=your-secret-key-change-this-in-production + +# Session Configuration (OPTIONAL - defaults provided) +ACCESS_TOKEN_EXPIRE_MINUTES=60 +REFRESH_TOKEN_EXPIRE_DAYS=30 + +# Frontend URL (REQUIRED for OAuth callback) +FRONTEND_URL=http://localhost:3000 + +# Authentication Toggle (OPTIONAL - set to false to disable auth) +AUTH_ENABLED=true + +# Email Domain Whitelist (OPTIONAL - comma separated, leave empty to allow all) +# Example: ALLOWED_EMAIL_DOMAINS=company.com,partner.com +ALLOWED_EMAIL_DOMAINS= + +# Cookie Security (OPTIONAL - set to true in production with HTTPS) +COOKIE_SECURE=false + +# ======================================== + #USER=frist #UID=123456789 diff --git a/server/app/auth_config.py b/server/app/auth_config.py new file mode 100644 index 00000000..25aff68d --- /dev/null +++ b/server/app/auth_config.py @@ -0,0 +1,47 @@ +""" +OAuth2 Authentication Configuration for CMF Server +""" +import os +from datetime import timedelta +from dotenv import load_dotenv + +load_dotenv() + +# Google OAuth2 Settings (REQUIRED) +GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID", "") +GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET", "") +GOOGLE_REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI", "http://localhost:8080/api/auth/google/callback") + +# JWT Settings for Session Management (REQUIRED) +SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-this-in-production") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60")) # 1 hour +REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "30")) # 30 days + +# Session Cookie Settings +COOKIE_NAME = "cmf_session" +COOKIE_MAX_AGE = ACCESS_TOKEN_EXPIRE_MINUTES * 60 # in seconds +COOKIE_SECURE = os.getenv("COOKIE_SECURE", "false").lower() == "true" # True for HTTPS only +COOKIE_HTTPONLY = True # Prevents JavaScript access +COOKIE_SAMESITE = "lax" # CSRF protection + +# Frontend URL (REQUIRED) +FRONTEND_URL = os.getenv("REACT_APP_CMF_API_URL", "http://localhost:3000") + +# Optional: Enable/Disable Authentication +AUTH_ENABLED = os.getenv("AUTH_ENABLED", "true").lower() == "true" + +# Optional: Whitelist of allowed email domains (e.g., only allow company emails) +ALLOWED_EMAIL_DOMAINS = os.getenv("ALLOWED_EMAIL_DOMAINS", "").split(",") if os.getenv("ALLOWED_EMAIL_DOMAINS") else [] + +# OAuth2 Configuration +GOOGLE_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_ACCESS_TOKEN_URL = "https://oauth2.googleapis.com/token" +GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo" + +# Scopes +GOOGLE_SCOPES = [ + "openid", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile" +] diff --git a/server/app/auth_middleware.py b/server/app/auth_middleware.py new file mode 100644 index 00000000..c60cb3ac --- /dev/null +++ b/server/app/auth_middleware.py @@ -0,0 +1,77 @@ +""" +Authentication middleware for protecting API routes +""" +from fastapi import Request, HTTPException, status +from starlette.middleware.base import BaseHTTPMiddleware +from server.app.auth_utils import verify_token +from server.app.auth_config import COOKIE_NAME, AUTH_ENABLED + +# Routes that don't require authentication +PUBLIC_ROUTES = [ + "/api/", + "/api/auth/login/google", + "/api/auth/google/callback", + "/api/auth/status", + "/api/auth/me", + "/cmf-server/data/static" +] + + +class AuthenticationMiddleware(BaseHTTPMiddleware): + """ + Middleware to enforce authentication on protected routes + """ + + async def dispatch(self, request: Request, call_next): + # Skip authentication if disabled + if not AUTH_ENABLED: + return await call_next(request) + + # Check if route is public + path = request.url.path + if any(path.startswith(route) for route in PUBLIC_ROUTES): + return await call_next(request) + + # Check for authentication cookie + token = request.cookies.get(COOKIE_NAME) + + if not token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authentication required" + ) + + # Verify token + try: + payload = verify_token(token) + # Add user info to request state + request.state.user = payload + except HTTPException: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired session" + ) + + response = await call_next(request) + return response + + +def get_authenticated_user(request: Request) -> dict: + """ + Dependency to get authenticated user from request + + Args: + request: FastAPI Request object + + Returns: + User information dictionary + + Raises: + HTTPException: If user is not authenticated + """ + if not hasattr(request.state, "user"): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authentication required" + ) + return request.state.user diff --git a/server/app/auth_routes.py b/server/app/auth_routes.py new file mode 100644 index 00000000..36029402 --- /dev/null +++ b/server/app/auth_routes.py @@ -0,0 +1,168 @@ +""" +OAuth2 Authentication Routes for Google Login +""" +from fastapi import APIRouter, Request, HTTPException, Depends +from fastapi.responses import RedirectResponse, JSONResponse +from authlib.integrations.starlette_client import OAuth +from starlette.config import Config +from server.app.auth_config import ( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + GOOGLE_SCOPES, + FRONTEND_URL, + AUTH_ENABLED +) +from server.app.auth_utils import ( + create_access_token, + set_auth_cookie, + clear_auth_cookie, + get_current_user_from_cookie, + validate_email_domain, + create_user_session_data +) +from typing import Dict, Any + +# Create router +router = APIRouter(prefix="/auth", tags=["authentication"]) + +# Configure OAuth +config = Config(environ={ + "GOOGLE_CLIENT_ID": GOOGLE_CLIENT_ID, + "GOOGLE_CLIENT_SECRET": GOOGLE_CLIENT_SECRET +}) + +oauth = OAuth(config) + +# Register Google OAuth provider +oauth.register( + name='google', + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={ + 'scope': ' '.join(GOOGLE_SCOPES) + } +) + + +@router.get("/login/google") +async def login_google(request: Request): + """ + Initiate Google OAuth login flow + + Returns: + Redirect to Google's authorization page + """ + if not AUTH_ENABLED: + raise HTTPException(status_code=403, detail="Authentication is disabled") + + if not GOOGLE_CLIENT_ID or not GOOGLE_CLIENT_SECRET: + raise HTTPException( + status_code=500, + detail="Google OAuth not configured properly" + ) + + redirect_uri = GOOGLE_REDIRECT_URI + return await oauth.google.authorize_redirect(request, redirect_uri) + + +@router.get("/google/callback") +async def google_callback(request: Request): + """ + Handle Google OAuth callback + + Returns: + Redirect to frontend with authentication cookie set + """ + try: + # Get access token from Google + token = await oauth.google.authorize_access_token(request) + + # Get user info from Google + user_info = token.get('userinfo') + if not user_info: + # If userinfo not in token, fetch it + resp = await oauth.google.get('https://www.googleapis.com/oauth2/v3/userinfo', token=token) + user_info = resp.json() + + # Validate email domain if configured + email = user_info.get('email') + if not validate_email_domain(email): + return RedirectResponse( + url=f"{FRONTEND_URL}/?error=unauthorized_domain", + status_code=302 + ) + + # Create user session data + session_data = create_user_session_data(user_info) + + # Create JWT token + access_token = create_access_token(data=session_data) + + # Redirect to frontend with cookie + response = RedirectResponse(url=f"{FRONTEND_URL}/?auth=success", status_code=302) + set_auth_cookie(response, access_token) + + return response + + except Exception as e: + print(f"OAuth callback error: {str(e)}") + return RedirectResponse( + url=f"{FRONTEND_URL}/?error=auth_failed", + status_code=302 + ) + + +@router.post("/logout") +async def logout(request: Request): + """ + Logout user and clear session cookie + + Returns: + Success response with cleared cookie + """ + response = JSONResponse(content={"message": "Logged out successfully"}) + clear_auth_cookie(response) + return response + + +@router.get("/me") +async def get_current_user(request: Request): + """ + Get current authenticated user information + + Returns: + User information from session + + Raises: + HTTPException: If user is not authenticated + """ + try: + user = get_current_user_from_cookie(request) + return { + "authenticated": True, + "user": { + "email": user.get("email"), + "name": user.get("name"), + "picture": user.get("picture"), + "email_verified": user.get("email_verified", False) + } + } + except HTTPException: + return { + "authenticated": False, + "user": None + } + + +@router.get("/status") +async def auth_status(): + """ + Check if authentication is enabled + + Returns: + Authentication configuration status + """ + return { + "auth_enabled": AUTH_ENABLED, + "google_oauth_configured": bool(GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET) + } diff --git a/server/app/auth_utils.py b/server/app/auth_utils.py new file mode 100644 index 00000000..4e21fccf --- /dev/null +++ b/server/app/auth_utils.py @@ -0,0 +1,164 @@ +""" +Authentication utilities for JWT token management and user sessions +""" +from datetime import datetime, timedelta +from typing import Optional, Dict, Any +from jose import JWTError, jwt +from fastapi import HTTPException, status, Request, Response +from server.app.auth_config import ( + SECRET_KEY, + ALGORITHM, + ACCESS_TOKEN_EXPIRE_MINUTES, + COOKIE_NAME, + COOKIE_MAX_AGE, + COOKIE_SECURE, + COOKIE_HTTPONLY, + COOKIE_SAMESITE, + ALLOWED_EMAIL_DOMAINS +) + + +def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + """ + Create JWT access token with user information + + Args: + data: Dictionary containing user information (email, name, picture, etc.) + expires_delta: Optional expiration time delta + + Returns: + Encoded JWT token string + """ + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({ + "exp": expire, + "iat": datetime.utcnow() + }) + + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def verify_token(token: str) -> Dict[str, Any]: + """ + Verify and decode JWT token + + Args: + token: JWT token string + + Returns: + Decoded token payload + + Raises: + HTTPException: If token is invalid or expired + """ + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload + except JWTError: + raise credentials_exception + + +def get_current_user_from_cookie(request: Request) -> Dict[str, Any]: + """ + Extract and verify user from session cookie + + Args: + request: FastAPI Request object + + Returns: + User information dictionary + + Raises: + HTTPException: If no valid session found + """ + token = request.cookies.get(COOKIE_NAME) + + if not token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated" + ) + + return verify_token(token) + + +def set_auth_cookie(response: Response, token: str) -> None: + """ + Set authentication cookie in response + + Args: + response: FastAPI Response object + token: JWT token to store in cookie + """ + response.set_cookie( + key=COOKIE_NAME, + value=token, + max_age=COOKIE_MAX_AGE, + httponly=COOKIE_HTTPONLY, + secure=COOKIE_SECURE, + samesite=COOKIE_SAMESITE + ) + + +def clear_auth_cookie(response: Response) -> None: + """ + Clear authentication cookie (for logout) + + Args: + response: FastAPI Response object + """ + response.delete_cookie( + key=COOKIE_NAME, + httponly=COOKIE_HTTPONLY, + secure=COOKIE_SECURE, + samesite=COOKIE_SAMESITE + ) + + +def validate_email_domain(email: str) -> bool: + """ + Validate if email domain is in allowed list (if configured) + + Args: + email: User email address + + Returns: + True if email is allowed, False otherwise + """ + if not ALLOWED_EMAIL_DOMAINS or not ALLOWED_EMAIL_DOMAINS[0]: + # If no domain restrictions, allow all + return True + + domain = email.split("@")[-1] + return domain in ALLOWED_EMAIL_DOMAINS + + +def create_user_session_data(google_user_info: Dict[str, Any]) -> Dict[str, Any]: + """ + Extract and format user session data from Google user info + + Args: + google_user_info: User information from Google OAuth + + Returns: + Formatted user session dictionary + """ + return { + "email": google_user_info.get("email"), + "name": google_user_info.get("name"), + "picture": google_user_info.get("picture"), + "email_verified": google_user_info.get("email_verified", False), + "sub": google_user_info.get("sub") # Google user ID + } diff --git a/server/app/main.py b/server/app/main.py index e646325b..31918ae3 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -6,6 +6,7 @@ from fastapi.responses import HTMLResponse, PlainTextResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware from contextlib import asynccontextmanager import pandas as pd from typing import List, Dict, Any, Optional @@ -53,6 +54,10 @@ import dotenv from jsonpath_ng.ext import parse from cmflib.cmf_federation import update_mlmd +# Import authentication routes +from server.app.auth_routes import router as auth_router +# Optional: Import authentication middleware (uncomment to enable) +from server.app.auth_middleware import AuthenticationMiddleware dotenv.load_dotenv() @@ -84,6 +89,16 @@ async def lifespan(app: FastAPI): app = FastAPI(title="cmf-server", lifespan=lifespan, root_path="/api") +# Add Session Middleware (REQUIRED for OAuth) +# This must be added BEFORE CORS middleware +app.add_middleware( + SessionMiddleware, + secret_key=os.getenv("SECRET_KEY", "your-secret-key-change-this-in-production"), + max_age=3600, # 1 hour + same_site="lax", + https_only=False # Set to True in production with HTTPS +) + # Add CORS middleware app.add_middleware( CORSMiddleware, @@ -93,6 +108,12 @@ async def lifespan(app: FastAPI): allow_headers=["*"], # Allows all headers ) +# Optional: Add authentication middleware (uncomment to enable route protection) +# app.add_middleware(AuthenticationMiddleware) + +# Include authentication routes +app.include_router(auth_router) + BASE_PATH = Path(__file__).resolve().parent app.mount("/cmf-server/data/static", StaticFiles(directory="/cmf-server/data/static"), name="static") diff --git a/server/requirements.txt b/server/requirements.txt index c671165a..eb644a28 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -10,3 +10,6 @@ python-dotenv psycopg[binary] httpx jsonpath_ng +authlib>=1.3.0 +python-jose[cryptography]>=3.3.0 +itsdangerous>=2.1.0 diff --git a/ui/package.json b/ui/package.json index 2a965fe2..1b13230d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,7 +21,8 @@ "react-table": "^7.8.0", "web-vitals": "^2.1.4", "react-data-table-component": "^7.7.0", - "papaparse": "^5.5.2" + "papaparse": "^5.5.2", + "js-cookie": "^3.0.5" }, "scripts": { "start": "react-scripts start", diff --git a/ui/src/App.js b/ui/src/App.js index 8b6ef6d4..a3c0a9da 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -16,7 +16,10 @@ import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { AuthProvider } from "./contexts/AuthContext"; +import ProtectedRoute from "./components/ProtectedRoute"; import Home from "./pages/home"; +import Login from "./pages/login"; import Lineage from "./pages/lineage"; import TensorBoard from "./pages/tensorboard"; import Metahub from "./pages/metahub"; @@ -28,14 +31,64 @@ function App() { return (
- - } /> - } /> - } /> - } /> - } /> - } /> - + + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + +
); diff --git a/ui/src/client.js b/ui/src/client.js index ae744a6d..b045d04f 100644 --- a/ui/src/client.js +++ b/ui/src/client.js @@ -33,6 +33,7 @@ class FastAPIClient { getApiClient(config) { const initialConfig = { baseURL: `${config.apiBasePath}/`, + withCredentials: true, // Important: include cookies in all requests }; const client = axios.create(initialConfig); @@ -41,8 +42,12 @@ class FastAPIClient { (response) => response, (error) => { console.error('API Error:', error); - // Show simple error message to user - if (error.response?.status >= 500) { + + // Handle authentication errors + if (error.response?.status === 401) { + // Redirect to login page + window.location.href = '/login'; + } else if (error.response?.status >= 500) { alert('Server error. Please try again later.'); } else if (error.request && !error.response) { alert('Unable to connect to server. Please check your connection.'); diff --git a/ui/src/components/ExamplePageWithUserProfile.js b/ui/src/components/ExamplePageWithUserProfile.js new file mode 100644 index 00000000..90e68577 --- /dev/null +++ b/ui/src/components/ExamplePageWithUserProfile.js @@ -0,0 +1,37 @@ +/*** + * Example of how to add UserProfile component to your page headers + * + * You can integrate this into any of your existing page components + * by adding the UserProfile component to your navigation or header area + ***/ + +import React from "react"; +import UserProfile from "../components/UserProfile"; + +const ExamplePageWithUserProfile = () => { + return ( +
+ {/* Your page header/navigation */} +
+

CMF Server

+ + {/* Add UserProfile component here */} + +
+ + {/* Your page content */} +
+

Your page content goes here

+
+
+ ); +}; + +export default ExamplePageWithUserProfile; diff --git a/ui/src/components/ProtectedRoute.js b/ui/src/components/ProtectedRoute.js new file mode 100644 index 00000000..1401f91e --- /dev/null +++ b/ui/src/components/ProtectedRoute.js @@ -0,0 +1,44 @@ +/*** + * Copyright (2023) Hewlett Packard Enterprise Development LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***/ + +import React from "react"; +import { Navigate } from "react-router-dom"; +import { useAuth } from "../contexts/AuthContext"; + +const ProtectedRoute = ({ children }) => { + const { authenticated, loading } = useAuth(); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + if (!authenticated) { + return ; + } + + return children; +}; + +export default ProtectedRoute; diff --git a/ui/src/components/UserProfile.css b/ui/src/components/UserProfile.css new file mode 100644 index 00000000..2d210ec3 --- /dev/null +++ b/ui/src/components/UserProfile.css @@ -0,0 +1,131 @@ +.user-profile { + position: relative; +} + +.user-profile-trigger { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + cursor: pointer; + border-radius: 8px; + transition: background-color 0.2s; +} + +.user-profile-trigger:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.user-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + object-fit: cover; + border: 2px solid #e0e0e0; +} + +.user-name { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.dropdown-arrow { + transition: transform 0.2s; +} + +.dropdown-arrow.open { + transform: rotate(180deg); +} + +.user-profile-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 998; +} + +.user-profile-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + background: white; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + min-width: 280px; + z-index: 999; + overflow: hidden; +} + +.user-info { + padding: 20px; + display: flex; + align-items: center; + gap: 12px; +} + +.user-avatar-large { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + border: 2px solid #e0e0e0; +} + +.user-details { + flex: 1; +} + +.user-name-large { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 4px; +} + +.user-email { + font-size: 13px; + color: #666; +} + +.menu-divider { + height: 1px; + background: #e0e0e0; + margin: 0 12px; +} + +.logout-button { + width: 100%; + padding: 14px 20px; + display: flex; + align-items: center; + gap: 10px; + border: none; + background: white; + color: #d32f2f; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.logout-button:hover { + background-color: #f5f5f5; +} + +.logout-button svg { + flex-shrink: 0; +} + +/* Responsive */ +@media (max-width: 768px) { + .user-name { + display: none; + } + + .user-profile-menu { + right: -12px; + } +} diff --git a/ui/src/components/UserProfile.js b/ui/src/components/UserProfile.js new file mode 100644 index 00000000..4e6059d9 --- /dev/null +++ b/ui/src/components/UserProfile.js @@ -0,0 +1,79 @@ +/*** + * Copyright (2023) Hewlett Packard Enterprise Development LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***/ + +import React, { useState } from "react"; +import { useAuth } from "../contexts/AuthContext"; +import "./UserProfile.css"; + +const UserProfile = () => { + const { user, logout } = useAuth(); + const [showMenu, setShowMenu] = useState(false); + + if (!user) return null; + + const toggleMenu = () => setShowMenu(!showMenu); + + return ( +
+
+ {user.name} + {user.name} + + + +
+ + {showMenu && ( + <> +
+
+
+ {user.name} +
+
{user.name}
+
{user.email}
+
+
+ +
+ + +
+ + )} +
+ ); +}; + +export default UserProfile; diff --git a/ui/src/contexts/AuthContext.js b/ui/src/contexts/AuthContext.js new file mode 100644 index 00000000..35769cd9 --- /dev/null +++ b/ui/src/contexts/AuthContext.js @@ -0,0 +1,91 @@ +/*** + * Copyright (2023) Hewlett Packard Enterprise Development LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***/ + +import React, { createContext, useContext, useState, useEffect } from "react"; +import axios from "axios"; +import config from "../config"; + +const AuthContext = createContext(null); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within AuthProvider"); + } + return context; +}; + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [authenticated, setAuthenticated] = useState(false); + + // Check authentication status on mount + useEffect(() => { + checkAuthStatus(); + }, []); + + const checkAuthStatus = async () => { + try { + const response = await axios.get(`${config.apiBasePath}/auth/me`, { + withCredentials: true, // Important: include cookies + }); + + if (response.data.authenticated) { + setUser(response.data.user); + setAuthenticated(true); + } else { + setUser(null); + setAuthenticated(false); + } + } catch (error) { + console.error("Error checking auth status:", error); + setUser(null); + setAuthenticated(false); + } finally { + setLoading(false); + } + }; + + const login = () => { + // Redirect to Google OAuth login + window.location.href = `${config.apiBasePath}/auth/login/google`; + }; + + const logout = async () => { + try { + await axios.post(`${config.apiBasePath}/auth/logout`, {}, { + withCredentials: true, + }); + setUser(null); + setAuthenticated(false); + window.location.href = "/"; // Redirect to home + } catch (error) { + console.error("Error logging out:", error); + } + }; + + const value = { + user, + loading, + authenticated, + login, + logout, + checkAuthStatus, + }; + + return {children}; +}; diff --git a/ui/src/pages/login/index.js b/ui/src/pages/login/index.js new file mode 100644 index 00000000..f9066632 --- /dev/null +++ b/ui/src/pages/login/index.js @@ -0,0 +1,103 @@ +/*** + * Copyright (2023) Hewlett Packard Enterprise Development LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***/ + +import React from "react"; +import { useAuth } from "../../contexts/AuthContext"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import "./login.css"; + +const Login = () => { + const { login, authenticated } = useAuth(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + // Handle successful OAuth callback + React.useEffect(() => { + if (searchParams.get("auth") === "success") { + // Redirect to home after successful login + navigate("/"); + } + + // Handle authentication errors + const error = searchParams.get("error"); + if (error === "unauthorized_domain") { + alert("Your email domain is not authorized to access this application."); + } else if (error === "auth_failed") { + alert("Authentication failed. Please try again."); + } + }, [searchParams, navigate]); + + // Redirect if already authenticated + React.useEffect(() => { + if (authenticated) { + navigate("/"); + } + }, [authenticated, navigate]); + + const handleGoogleLogin = () => { + login(); + }; + + return ( +
+
+
+

CMF Server

+

Common Metadata Framework

+
+ +
+

Sign in to continue

+ + + +

+ Sign in with your Google account to access the CMF Server dashboard. +

+
+ +
+

© 2023 Hewlett Packard Enterprise Development LP

+
+
+
+ ); +}; + +export default Login; diff --git a/ui/src/pages/login/login.css b/ui/src/pages/login/login.css new file mode 100644 index 00000000..6c777892 --- /dev/null +++ b/ui/src/pages/login/login.css @@ -0,0 +1,129 @@ +.login-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; +} + +.login-card { + background: white; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); + max-width: 450px; + width: 100%; + overflow: hidden; +} + +.login-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 40px 30px; + text-align: center; +} + +.login-header h1 { + margin: 0; + font-size: 32px; + font-weight: 700; + letter-spacing: -0.5px; +} + +.login-header p { + margin: 10px 0 0 0; + font-size: 16px; + opacity: 0.9; +} + +.login-content { + padding: 40px 30px; +} + +.login-content h2 { + margin: 0 0 30px 0; + font-size: 24px; + font-weight: 600; + color: #333; + text-align: center; +} + +.google-login-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 14px 24px; + font-size: 16px; + font-weight: 500; + color: #333; + background: white; + border: 2px solid #ddd; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.google-login-button:hover { + background: #f8f9fa; + border-color: #bbb; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + transform: translateY(-1px); +} + +.google-login-button:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.google-icon { + width: 24px; + height: 24px; +} + +.login-note { + margin: 25px 0 0 0; + font-size: 14px; + color: #666; + text-align: center; + line-height: 1.5; +} + +.login-footer { + background: #f8f9fa; + padding: 20px 30px; + text-align: center; + border-top: 1px solid #eee; +} + +.login-footer p { + margin: 0; + font-size: 13px; + color: #888; +} + +/* Responsive design */ +@media (max-width: 480px) { + .login-card { + border-radius: 0; + } + + .login-header { + padding: 30px 20px; + } + + .login-header h1 { + font-size: 28px; + } + + .login-content { + padding: 30px 20px; + } + + .google-login-button { + padding: 12px 20px; + font-size: 15px; + } +} From 8349b86b3a0deb42e8d50aa775f87acfac183a55 Mon Sep 17 00:00:00 2001 From: Varkha Sharma Date: Thu, 13 Nov 2025 10:44:13 +0000 Subject: [PATCH 2/2] commenting not needed code --- env-example | 2 +- server/app/auth_middleware.py | 126 ++++++++-------- server/app/auth_routes.py | 2 +- server/app/main.py | 2 +- .../components/ExamplePageWithUserProfile.js | 64 ++++---- ui/src/components/UserProfile.css | 8 +- ui/src/components/UserProfile.js | 140 +++++++++--------- 7 files changed, 172 insertions(+), 172 deletions(-) diff --git a/env-example b/env-example index 7f5e631b..3dc91e6d 100644 --- a/env-example +++ b/env-example @@ -37,7 +37,7 @@ ACCESS_TOKEN_EXPIRE_MINUTES=60 REFRESH_TOKEN_EXPIRE_DAYS=30 # Frontend URL (REQUIRED for OAuth callback) -FRONTEND_URL=http://localhost:3000 +#FRONTEND_URL=http://localhost:3000 # Authentication Toggle (OPTIONAL - set to false to disable auth) AUTH_ENABLED=true diff --git a/server/app/auth_middleware.py b/server/app/auth_middleware.py index c60cb3ac..5b4e219a 100644 --- a/server/app/auth_middleware.py +++ b/server/app/auth_middleware.py @@ -1,77 +1,77 @@ -""" -Authentication middleware for protecting API routes -""" -from fastapi import Request, HTTPException, status -from starlette.middleware.base import BaseHTTPMiddleware -from server.app.auth_utils import verify_token -from server.app.auth_config import COOKIE_NAME, AUTH_ENABLED +# """ +# Authentication middleware for protecting API routes +# """ +# from fastapi import Request, HTTPException, status +# from starlette.middleware.base import BaseHTTPMiddleware +# from server.app.auth_utils import verify_token +# from server.app.auth_config import COOKIE_NAME, AUTH_ENABLED -# Routes that don't require authentication -PUBLIC_ROUTES = [ - "/api/", - "/api/auth/login/google", - "/api/auth/google/callback", - "/api/auth/status", - "/api/auth/me", - "/cmf-server/data/static" -] +# # Routes that don't require authentication +# PUBLIC_ROUTES = [ +# "/api/", +# "/api/auth/login/google", +# "/api/auth/google/callback", +# "/api/auth/status", +# "/api/auth/me", +# "/cmf-server/data/static" +# ] -class AuthenticationMiddleware(BaseHTTPMiddleware): - """ - Middleware to enforce authentication on protected routes - """ +# class AuthenticationMiddleware(BaseHTTPMiddleware): +# """ +# Middleware to enforce authentication on protected routes +# """ - async def dispatch(self, request: Request, call_next): - # Skip authentication if disabled - if not AUTH_ENABLED: - return await call_next(request) +# async def dispatch(self, request: Request, call_next): +# # Skip authentication if disabled +# if not AUTH_ENABLED: +# return await call_next(request) - # Check if route is public - path = request.url.path - if any(path.startswith(route) for route in PUBLIC_ROUTES): - return await call_next(request) +# # Check if route is public +# path = request.url.path +# if any(path.startswith(route) for route in PUBLIC_ROUTES): +# return await call_next(request) - # Check for authentication cookie - token = request.cookies.get(COOKIE_NAME) +# # Check for authentication cookie +# token = request.cookies.get(COOKIE_NAME) - if not token: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Authentication required" - ) +# if not token: +# raise HTTPException( +# status_code=status.HTTP_401_UNAUTHORIZED, +# detail="Authentication required" +# ) - # Verify token - try: - payload = verify_token(token) - # Add user info to request state - request.state.user = payload - except HTTPException: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid or expired session" - ) +# # Verify token +# try: +# payload = verify_token(token) +# # Add user info to request state +# request.state.user = payload +# except HTTPException: +# raise HTTPException( +# status_code=status.HTTP_401_UNAUTHORIZED, +# detail="Invalid or expired session" +# ) - response = await call_next(request) - return response +# response = await call_next(request) +# return response -def get_authenticated_user(request: Request) -> dict: - """ - Dependency to get authenticated user from request +# def get_authenticated_user(request: Request) -> dict: +# """ +# Dependency to get authenticated user from request - Args: - request: FastAPI Request object +# Args: +# request: FastAPI Request object - Returns: - User information dictionary +# Returns: +# User information dictionary - Raises: - HTTPException: If user is not authenticated - """ - if not hasattr(request.state, "user"): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Authentication required" - ) - return request.state.user +# Raises: +# HTTPException: If user is not authenticated +# """ +# if not hasattr(request.state, "user"): +# raise HTTPException( +# status_code=status.HTTP_401_UNAUTHORIZED, +# detail="Authentication required" +# ) +# return request.state.user diff --git a/server/app/auth_routes.py b/server/app/auth_routes.py index 36029402..8486abe1 100644 --- a/server/app/auth_routes.py +++ b/server/app/auth_routes.py @@ -1,7 +1,7 @@ """ OAuth2 Authentication Routes for Google Login """ -from fastapi import APIRouter, Request, HTTPException, Depends +from fastapi import APIRouter, Request, HTTPException from fastapi.responses import RedirectResponse, JSONResponse from authlib.integrations.starlette_client import OAuth from starlette.config import Config diff --git a/server/app/main.py b/server/app/main.py index 31918ae3..cb30d4c8 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -57,7 +57,7 @@ # Import authentication routes from server.app.auth_routes import router as auth_router # Optional: Import authentication middleware (uncomment to enable) -from server.app.auth_middleware import AuthenticationMiddleware +#from server.app.auth_middleware import AuthenticationMiddleware dotenv.load_dotenv() diff --git a/ui/src/components/ExamplePageWithUserProfile.js b/ui/src/components/ExamplePageWithUserProfile.js index 90e68577..869d5eb5 100644 --- a/ui/src/components/ExamplePageWithUserProfile.js +++ b/ui/src/components/ExamplePageWithUserProfile.js @@ -1,37 +1,37 @@ -/*** - * Example of how to add UserProfile component to your page headers - * - * You can integrate this into any of your existing page components - * by adding the UserProfile component to your navigation or header area - ***/ +// /*** +// * Example of how to add UserProfile component to your page headers +// * +// * You can integrate this into any of your existing page components +// * by adding the UserProfile component to your navigation or header area +// ***/ -import React from "react"; -import UserProfile from "../components/UserProfile"; +// import React from "react"; +// import UserProfile from "../components/UserProfile"; -const ExamplePageWithUserProfile = () => { - return ( -
- {/* Your page header/navigation */} -
-

CMF Server

+// const ExamplePageWithUserProfile = () => { +// return ( +//
+// {/* Your page header/navigation */} +//
+//

CMF Server

- {/* Add UserProfile component here */} - -
+// {/* Add UserProfile component here */} +// +//
- {/* Your page content */} -
-

Your page content goes here

-
-
- ); -}; +// {/* Your page content */} +//
+//

Your page content goes here

+//
+//
+// ); +// }; -export default ExamplePageWithUserProfile; +// export default ExamplePageWithUserProfile; diff --git a/ui/src/components/UserProfile.css b/ui/src/components/UserProfile.css index 2d210ec3..3d660412 100644 --- a/ui/src/components/UserProfile.css +++ b/ui/src/components/UserProfile.css @@ -1,4 +1,4 @@ -.user-profile { +/* .user-profile { position: relative; } @@ -117,10 +117,10 @@ .logout-button svg { flex-shrink: 0; -} +} */ /* Responsive */ -@media (max-width: 768px) { +/* @media (max-width: 768px) { .user-name { display: none; } @@ -128,4 +128,4 @@ .user-profile-menu { right: -12px; } -} +} */ diff --git a/ui/src/components/UserProfile.js b/ui/src/components/UserProfile.js index 4e6059d9..a376fb62 100644 --- a/ui/src/components/UserProfile.js +++ b/ui/src/components/UserProfile.js @@ -1,79 +1,79 @@ -/*** - * Copyright (2023) Hewlett Packard Enterprise Development LP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ***/ +// /*** +// * Copyright (2023) Hewlett Packard Enterprise Development LP +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * You may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// ***/ -import React, { useState } from "react"; -import { useAuth } from "../contexts/AuthContext"; -import "./UserProfile.css"; +// import React, { useState } from "react"; +// import { useAuth } from "../contexts/AuthContext"; +// import "./UserProfile.css"; -const UserProfile = () => { - const { user, logout } = useAuth(); - const [showMenu, setShowMenu] = useState(false); +// const UserProfile = () => { +// const { user, logout } = useAuth(); +// const [showMenu, setShowMenu] = useState(false); - if (!user) return null; +// if (!user) return null; - const toggleMenu = () => setShowMenu(!showMenu); +// const toggleMenu = () => setShowMenu(!showMenu); - return ( -
-
- {user.name} - {user.name} - - - -
+// return ( +//
+//
+// {user.name} +// {user.name} +// +// +// +//
- {showMenu && ( - <> -
-
-
- {user.name} -
-
{user.name}
-
{user.email}
-
-
+// {showMenu && ( +// <> +//
+//
+//
+// {user.name} +//
+//
{user.name}
+//
{user.email}
+//
+//
-
+//
- -
- - )} -
- ); -}; +// +//
+// +// )} +//
+// ); +// }; -export default UserProfile; +// export default UserProfile;