7.9 KiB
7.9 KiB
로그인 인증 시스템 보안 개선 필요사항
작성일: 2025-08-15
작성자: Claude & happybell80
상태: 아이디어 → 긴급 개선 필요
관련: auth-server, robing-gateway, frontend
개요
현재 로빙 프로젝트의 인증 시스템에서 JWT 토큰은 발급되지만 실제 API 호출 시 검증되지 않는 심각한 보안 취약점이 발견되었습니다. 누구나 X-User-Id 헤더를 조작하여 다른 사용자로 위장할 수 있는 상태입니다.
현재 인증 흐름
1. Frontend → auth-server로 리다이렉트 (/auth/gmail/login)
↓
2. Auth-server → Google OAuth → 콜백 받음
↓
3. Auth-server → Redis에 임시 코드 저장 → Frontend로 리다이렉트 (#code=xxx)
↓
4. Frontend → 코드로 토큰 교환 (/auth/verify) → JWT 토큰 받음
↓
5. Frontend → localStorage에 토큰 저장
↓
6. Frontend → robing-gateway 호출 시 X-User-Id 헤더만 전송 ⚠️
↓
7. robing-gateway → 토큰 검증 없이 X-User-Id 신뢰 ⚠️
발견된 보안 취약점
1. 인증 토큰 미전달
- 현상: Frontend가 JWT 토큰을 저장하지만 API 호출 시 전송하지 않음
- 코드 위치: frontend의 API 클라이언트
- 영향: 인증 메커니즘이 무의미함
2. 토큰 검증 부재
- 현상: robing-gateway가 X-User-Id 헤더만 확인하고 JWT 검증 없음
- 코드 예시:
# 현재 (취약함) user_id = request.headers.get("X-User-Id", "default") # 필요한 것 token = request.headers.get("Authorization", "").replace("Bearer ", "") user_id = verify_jwt_token(token) # 검증 필요! - 영향: 누구나 헤더 조작으로 다른 사용자 행세 가능
3. 시스템 간 연결 끊김
- auth-server: JWT 발급 기능 있음 (51123 서버, 포트 9000)
- robing-gateway: JWT 검증 기능 없음 (51124 서버)
- frontend: 토큰 보유하지만 전송 안 함
- 영향: 인증 시스템이 형식적으로만 존재
4. 기본값 폴백 문제
- 현상: X-User-Id 없으면 "default"로 처리
- 영향: 모든 미인증 사용자가 동일한 로빙/데이터 공유
긴급 수정 필요사항
Frontend 수정
robing-api.ts 수정 필요
// 현재 문제: 토큰 전송 안 함, 하드코딩된 userId
export async function sendMessage(text: string, userId: string = 'test_user'): Promise<MessageResponse> {
const response = await fetch(`${ROBING_API_URL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-User-Id': userId // ❌ 토큰 없음, 조작 가능
},
body: JSON.stringify({
message: text,
user_id: userId
})
});
}
// 수정 필요
export async function sendMessage(text: string, token: string, userId: string): Promise<MessageResponse> {
const response = await fetch(`${ROBING_API_URL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`, // ✅ JWT 토큰 추가
'X-User-Id': userId // 호환성 유지 (임시)
},
body: JSON.stringify({
message: text,
user_id: userId
})
});
}
chat-interface.tsx 수정 필요
// 현재: useAuth 사용 안 함
import { sendMessage } from '@/services/robing-api';
// 수정 필요
import { useAuth } from '@/contexts/auth-context';
const ChatInterface = () => {
const { user, isAuthenticated } = useAuth();
const handleSend = async (input: string) => {
if (!isAuthenticated || !user) {
// 로그인 유도
return;
}
const token = localStorage.getItem('auth_token');
const response = await sendMessage(input, token, user.id);
};
};
robing-gateway 수정
# JWT 검증 미들웨어 추가
from jose import jwt, JWTError
async def verify_token(request: Request):
token = request.headers.get("Authorization", "").replace("Bearer ", "")
if not token:
raise HTTPException(status_code=401, detail="Token missing")
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
request.state.user_id = payload.get("sub", "default")
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
return request.state.user_id
@app.post("/api/chat")
async def chat(request: Request):
user_id = await verify_token(request) # 검증 필수!
# ...
시스템 간 연동
- JWT 시크릿 공유: auth-server와 robing-gateway가 동일한 JWT_SECRET 사용
- 환경변수 설정:
# .env 파일 JWT_SECRET=동일한_시크릿_키_사용 JWT_ALGORITHM=HS256 JWT_EXPIRY_HOURS=24
에러 처리
# 인증 실패 시 명확한 에러
@app.exception_handler(401)
async def unauthorized_handler(request, exc):
return JSONResponse(
status_code=401,
content={"error": "Authentication required", "detail": str(exc)}
)
추가 개선사항
1. Refresh Token 구현
# 장기 세션 유지를 위한 refresh token
def create_tokens(user_id: str):
access_token = create_jwt(user_id, expires_in=timedelta(hours=1))
refresh_token = create_jwt(user_id, expires_in=timedelta(days=30))
return access_token, refresh_token
2. 토큰 블랙리스트
# Redis를 활용한 로그아웃 토큰 관리
async def logout(token: str):
await redis.setex(f"blacklist:{token}", JWT_EXPIRY_HOURS * 3600, "1")
async def is_token_blacklisted(token: str):
return await redis.exists(f"blacklist:{token}")
3. Role-Based Access Control (RBAC)
# JWT에 역할 정보 포함
payload = {
"sub": user_id,
"email": user_email,
"role": "admin", # or "user", "guest"
"permissions": ["read", "write", "delete"]
}
서버별 현황 및 설정 관리 문제
51123 서버
- auth-server 실행 중 (포트 9000)
- JWT 발급 기능 정상
- Redis 임시 코드 시스템 작동
- ✅ pydantic BaseSettings 사용
51124 서버
- robing-gateway 실행 중
- JWT 검증 로직 없음 ⚠️
- 모든 로빙 서비스 운영
- ❌ config.py 없음 - os.getenv() 분산 사용 (database.py에서 직접 호출)
로컬 개발
- frontend 개발
- 토큰 저장은 하지만 전송 안 함 ⚠️
- API URL 문제:
https://ro-being.com/rb10508대신 Gateway URL 사용 필요
설정 관리 표준화 필요
| 서비스 | Config 위치 | 상태 |
|---|---|---|
| rb10508_micro | app/config.py | ✅ 표준 |
| rb8001 | app/core/config.py | ✅ 표준 |
| skill-slack | app/core/config.py | ✅ 표준 |
| robing-gateway | 없음 | ❌ 비표준 |
robing-gateway도 BaseSettings 패턴으로 전환 필요
보안 위험 수준
🔴 Critical (긴급)
현재 상태에서는:
- 누구나 다른 사용자로 위장 가능
- 개인 데이터 접근 가능
- 로빙 설정 변경 가능
- 실질적인 인증 보호 없음
구현 우선순위
-
즉시 (1일 내)
- robing-gateway에 JWT 검증 추가
- Frontend API 호출 시 토큰 헤더 추가
-
긴급 (1주 내)
- JWT 시크릿 환경변수 통일
- 에러 처리 및 로깅 추가
-
중요 (2주 내)
- Refresh token 구현
- 토큰 블랙리스트 관리
- RBAC 권한 시스템
테스트 체크리스트
- Frontend에서 Authorization 헤더 전송 확인
- robing-gateway JWT 검증 동작 확인
- 잘못된 토큰 시 401 에러 반환
- 토큰 없을 시 적절한 에러 메시지
- 토큰 만료 시 재로그인 유도
- 로그아웃 시 토큰 무효화
참고사항
- JWT 라이브러리: python-jose (이미 사용 중)
- Redis: 임시 코드 및 블랙리스트 관리
- 환경변수: JWT_SECRET 반드시 동일하게 설정
"인증은 시스템의 문지기입니다. 문이 열려있다면 의미가 없습니다."
이 문제는 즉시 수정이 필요한 보안 취약점입니다.