From 14b237a73458ada4573fd48964f448bac9eb4600 Mon Sep 17 00:00:00 2001 From: devmrko Date: Thu, 23 Jul 2026 14:03:20 +0900 Subject: [PATCH] refs #710: remove POC4 URL remember tokens --- ai-web-agent-console/app.py | 53 ++++++++++++++----- .../README.md | 27 ++++++++++ 2 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 docs/design/710-smilegate-poc4-url-token-security/README.md diff --git a/ai-web-agent-console/app.py b/ai-web-agent-console/app.py index 578aac1..5a6ccd5 100644 --- a/ai-web-agent-console/app.py +++ b/ai-web-agent-console/app.py @@ -93,8 +93,6 @@ VPD_OPERATIONS_URL = "https://smilegate-backoffice.cloud-handson.com/" PORTAL_AUTHENTICATED_KEY = "poc4_portal_authenticated" PORTAL_AUTH_USER_KEY = "poc4_portal_auth_user" PORTAL_LOGIN_FAILURE_KEY = "poc4_portal_login_failed" -PORTAL_REMEMBER_TOKEN_PARAM = "poc4_remember" -PORTAL_REMEMBER_MAX_AGE_SECONDS = 7 * 24 * 60 * 60 AUDIT_SCHEMA = "SGMP_POC" REFERENCE_EVIDENCE_ENABLED = ( os.environ.get("POC4_REFERENCE_EVIDENCE_ENABLED", "").strip().lower() @@ -763,27 +761,57 @@ def _restore_portal_proxy_session() -> None: and expires_at > int(datetime.now(timezone.utc).timestamp()) and hmac.compare_digest(username, expected_username) ) - if not authenticated: - st.session_state.pop(PORTAL_AUTHENTICATED_KEY, None) - st.session_state.pop(PORTAL_AUTH_USER_KEY, None) - return - st.session_state[PORTAL_AUTHENTICATED_KEY] = True - st.session_state[PORTAL_AUTH_USER_KEY] = username + + +def _portal_credentials_are_valid(username: str, password: str) -> bool: + expected_username = _portal_auth_value("POC4_LOGIN_USER") + encoded_password = _portal_auth_value("POC4_LOGIN_PASSWORD_PBKDF2") + username_matches = hmac.compare_digest(username.strip(), expected_username) + password_matches = _portal_password_matches(password, encoded_password) + return username_matches and password_matches def _render_portal_login(profile: AppProfile) -> None: with st.container(key="console_login_container"): render_login_brand(st, profile) - st.error( - "인증 게이트웨이의 사용자 확인 정보가 없습니다. " - "공식 포털 주소로 다시 접속해 주세요." - ) + if not _portal_auth_configured(): + st.info("데모 계정 설정 중입니다. 운영 담당자에게 계정 발급을 요청해 주세요.") + return + with st.form("poc4_portal_login_form", clear_on_submit=True): + username = st.text_input( + "사용자 ID", + max_chars=80, + placeholder="사용자 ID를 입력하세요.", + ) + password = st.text_input( + "비밀번호", + type="password", + max_chars=200, + placeholder="비밀번호를 입력하세요.", + ) + submitted = st.form_submit_button("로그인", use_container_width=True) + if submitted: + if _portal_credentials_are_valid(username, password): + st.session_state[PORTAL_AUTHENTICATED_KEY] = True + st.session_state[PORTAL_AUTH_USER_KEY] = username.strip() + st.session_state[PORTAL_LOGIN_FAILURE_KEY] = False + st.rerun() + st.session_state[PORTAL_LOGIN_FAILURE_KEY] = True + if st.session_state.get(PORTAL_LOGIN_FAILURE_KEY, False): + st.error("사용자 ID 또는 비밀번호를 확인해 주세요.") st.markdown( f'

{html.escape(profile.login_footer)}

', unsafe_allow_html=True, ) +def _logout_portal() -> None: + st.session_state.pop(PORTAL_AUTHENTICATED_KEY, None) + st.session_state.pop(PORTAL_AUTH_USER_KEY, None) + st.session_state.pop(PORTAL_LOGIN_FAILURE_KEY, None) + st.rerun() + + def _render_app_header(profile: AppProfile) -> None: render_console_header(st, profile) @@ -5718,7 +5746,6 @@ def main() -> None: page_title=profile.page_title, page_icon=profile.page_icon, layout="wide" ) _apply_console_theme(profile) - _restore_portal_proxy_session() if not st.session_state.get(PORTAL_AUTHENTICATED_KEY, False): _render_portal_login(profile) return diff --git a/docs/design/710-smilegate-poc4-url-token-security/README.md b/docs/design/710-smilegate-poc4-url-token-security/README.md new file mode 100644 index 0000000..bdce291 --- /dev/null +++ b/docs/design/710-smilegate-poc4-url-token-security/README.md @@ -0,0 +1,27 @@ +# #710 POC4 URL 로그인 토큰 제거 + +## 프로젝트 개요 + +Smilegate DATA & AI PoC의 POC4 Streamlit 콘솔은 게임 데이터 MCP와 Select AI Text2SQL 데모를 제공한다. 포털 로그인은 콘솔 접근을 보호한다. + +## 문제 + +로그인 유지용 서명 토큰이 `poc4_remember` query parameter로 URL에 포함됐다. URL은 브라우저 기록, 프록시 로그, 공유 링크, Referrer에 남을 수 있으므로 인증 정보를 전달하는 경로로 사용하면 안 된다. + +## 조치 설계 + +1. Streamlit 코드에서 URL 토큰 생성·검증·삭제를 모두 제거한다. +2. 로그인 상태는 현재 Streamlit 브라우저 세션에서만 유지한다. 서버가 `HttpOnly`, `Secure`, `SameSite` cookie를 발급하는 전용 인증 경로가 마련되기 전에는 영구 로그인 기능을 제공하지 않는다. +3. `POC4_LOGIN_REMEMBER_SECRET`을 교체해 기존 서명 링크를 무효화한다. +4. Caddy가 기존 `poc4_remember` query 요청을 애플리케이션으로 전달하지 않고 `https://smilegate.cloud-handson.com/`으로 303 redirect한다. + +## 검증 기준 + +- `mcp_discovery_ui.py`에 `poc4_remember` 또는 `st.query_params` 로그인 토큰 코드가 없다. +- 기존 query URL 요청은 query가 없는 루트 URL로 303 응답한다. +- `smilegate-poc4-console.service`가 정상 기동한다. +- 토큰, password hash, signing key는 Git·Redmine·명령 출력에 기록하지 않는다. + +## 후속 개선 + +영구 로그인 요구가 다시 생기면 POC4 자체가 아닌 서버 인증 endpoint가 `HttpOnly; Secure; SameSite=Lax` cookie를 발급하고, Streamlit은 요청 cookie의 서버 검증 결과만 읽는 구조로 구현한다.