Step 5: Authentication System & Environment Variables

- Implemented JWT authentication in Console backend
- Added .env file for environment variable management
- Updated docker-compose to use .env variables
- Created authentication endpoints (login/logout/me)
- Added protected route middleware
- Created ARCHITECTURE.md with Kafka as main messaging platform
- Defined Kafka for both events and task queues
- Redis dedicated for caching and session management

Test credentials:
- admin/admin123
- user/user123

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2025-09-10 16:36:47 +09:00
parent 4b2be7ccaf
commit 315eeea2ae
6 changed files with 378 additions and 20 deletions

65
console/backend/auth.py Normal file
View File

@ -0,0 +1,65 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
import os
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
class UserLogin(BaseModel):
username: str
password: str
class UserInDB(BaseModel):
username: str
hashed_password: str
email: str
full_name: Optional[str] = None
is_active: bool = True
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
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])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
return token_data

View File

@ -1,10 +1,17 @@
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi import FastAPI, HTTPException, Request, Response, Depends, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordRequestForm
import uvicorn
from datetime import datetime
from datetime import datetime, timedelta
import httpx
import os
from typing import Any
from auth import (
Token, UserLogin, UserInDB,
verify_password, get_password_hash,
create_access_token, get_current_user,
ACCESS_TOKEN_EXPIRE_MINUTES
)
app = FastAPI(
title="Console API Gateway",
@ -40,6 +47,58 @@ async def health_check():
"timestamp": datetime.now().isoformat()
}
# Authentication endpoints
@app.post("/api/auth/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""Login endpoint for authentication"""
# For demo purposes - in production, check against database
# This is temporary until we integrate with Users service
demo_users = {
"admin": {
"username": "admin",
"hashed_password": get_password_hash("admin123"),
"email": "admin@site11.com",
"full_name": "Administrator",
"is_active": True
},
"user": {
"username": "user",
"hashed_password": get_password_hash("user123"),
"email": "user@site11.com",
"full_name": "Test User",
"is_active": True
}
}
user = demo_users.get(form_data.username)
if not user or not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"]}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/api/auth/me")
async def get_me(current_user = Depends(get_current_user)):
"""Get current user information"""
return {
"username": current_user.username,
"email": f"{current_user.username}@site11.com",
"is_active": True
}
@app.post("/api/auth/logout")
async def logout(current_user = Depends(get_current_user)):
"""Logout endpoint"""
# In a real application, you might want to blacklist the token
return {"message": "Successfully logged out"}
@app.get("/api/status")
async def system_status():
services_status = {}
@ -65,10 +124,19 @@ async def system_status():
"timestamp": datetime.now().isoformat()
}
# Protected endpoint example
@app.get("/api/protected")
async def protected_route(current_user = Depends(get_current_user)):
"""Example of a protected route"""
return {
"message": "This is a protected route",
"user": current_user.username
}
# API Gateway - Route to Users service
@app.api_route("/api/users/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def proxy_to_users(path: str, request: Request):
"""Proxy requests to Users service"""
async def proxy_to_users(path: str, request: Request, current_user = Depends(get_current_user)):
"""Proxy requests to Users service (protected)"""
try:
async with httpx.AsyncClient() as client:
# Build the target URL

View File

@ -2,4 +2,7 @@ fastapi==0.109.0
uvicorn[standard]==0.27.0
python-dotenv==1.0.0
pydantic==2.5.3
httpx==0.26.0
httpx==0.26.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6