Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docker-compose-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions env-example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions server/app/auth_config.py
Original file line number Diff line number Diff line change
@@ -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"
]
77 changes: 77 additions & 0 deletions server/app/auth_middleware.py
Original file line number Diff line number Diff line change
@@ -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
168 changes: 168 additions & 0 deletions server/app/auth_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
OAuth2 Authentication Routes for Google Login
"""
from fastapi import APIRouter, Request, HTTPException
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)
}
Loading