프로젝트를 불러오는 중…
프로젝트를 불러오는 중…
사내 도구·내부 API·AI 에이전트가 늘면서 인증 표면이 빠르게 파편화됐다. 각 서비스가 자체적으로 ERP 자격증명을 받아 처리하거나, 서비스 간 호출에 long-lived API key가 코드에 박혀 돌았고, 새로 만들 MCP 기반 AI 에이전트는 표준 OAuth 흐름이 필요한데 사내에는 그걸 발급해 줄 IdP가 없었다.
| 한계 | 영향 |
|---|---|
| 서비스마다 ERP 자격증명 직접 처리 | 비밀번호가 N개 코드베이스에 분산 — 회수·로테이션 사실상 불가 |
| M2M 호출은 환경변수 API key | 만료·revocation 없음, 누가 언제 만들었는지 추적 불가 |
| MCP 에이전트용 IdP 부재 | RFC 표준 흐름(DCR · PKCE · introspection) 없이는 외부 클라이언트 연동 불가 |
이 세 가지를 하나의 인증 백엔드로 수렴시키되, 각 클라이언트의 정체성을 가짜로 통일하지 않는 게 목표였다. 사람(브라우저·SSO), 사내 서비스(M2M), AI 에이전트(MCP/OAuth)는 보안 요구가 다르고 — 같은 토큰 모양으로 강제하면 어느 한 쪽이 반드시 깨진다.
목표는 세 가지로 좁혔다.
같은 FastAPI 앱이 세 진입점을 노출한다. 코어(JWT 서명·토큰 저장소·rate limit)는 공유하지만 정체성은 분리된다.
| 페르소나 | 진입점 | 인증 출처 | 토큰 |
|---|---|---|---|
| 사람 | POST /auth/login | ERPClient(httpx → ERP API) | HS256 JWT access + refresh |
| 사내 서비스 (M2M) | /auth/service-tokens CRUD | 사전 발급, svc_ 접두어 | 장수명 service token (DB 추적) |
| AI 에이전트 / 외부 | OAuth 2.1 endpoints + Google Workspace OIDC | DCR + PKCE | OAuth access token (introspect 지원) |
ERPClient는 사용자 id/pw를 ERP에 그대로 던지고 결과로 받은 사용자 프로파일에 HS256 JWT를 서명해 돌려준다 — auth-service는 자격증명을 저장하지 않는다. 패스워드 책임은 끝까지 ERP에 있고, 우리는 그 검증 결과를 토큰화할 뿐이다.
리프레시 토큰을 한 번 쓰고 버리는 단발 로테이션이 아니라, 패밀리(family) 단위로 묶어 도난 탐지를 내장했다.
family_id + jti 포함./auth/refresh 호출 시 사용된 토큰을 used 마크하고 같은 family로 새 토큰 발급.used 표시된 토큰이 다시 들어오면 패밀리 전체 revoke — 탈취된 토큰이 한 번이라도 재사용되면 그 사용자의 모든 세션이 끊긴다.이 패턴 덕에 "현재 활성 세션 N개"를 사용자별로 조회·강제 종료할 수 있고, 보안 사건이 나면 family_id 한 줄 update로 즉시 차단된다.
사내 서비스 간 호출은 OAuth로 가기엔 무겁고, 평문 API key는 추적이 안 된다. 절충안으로 svc_<random> 접두어 토큰을 도입했다.
/auth/verify가 토큰 첫 4자만 봐도 JWT vs service token 분기. JWT 디코딩 시도 → 실패 → svc 조회의 fallback 비용 없음.MCP Spec 2025-06-18은 AI 클라이언트 인증을 OAuth 2.1 + DCR(Dynamic Client Registration)로 명확히 정의한다. 자체 프로토콜을 발명하는 대신 그대로 따랐다 — 6일 안에 끝낸 가장 큰 이유.
| 엔드포인트 | 역할 | 주의점 |
|---|---|---|
/oauth/authorize | PKCE code 발급 | Jinja 템플릿으로 동의 페이지 ("Institutional Precision" 리디자인) |
/oauth/token | code → access_token 교환 | code_verifier 검증, refresh 회전 |
/oauth/register | DCR | rate limit 10/min (스팸 등록 차단) |
/oauth/introspect | RS용 토큰 유효성 조회 | 인증된 RS만 호출 가능 (Basic auth) |
/oidc/google | Google Workspace 로그인 | hd=사내 도메인 강제로 외부 도메인 차단 |
Google Workspace OIDC를 IdP로 묶었기 때문에 사내 직원은 OAuth 동의 페이지에서 워크스페이스 계정으로 로그인하면 그대로 통과한다 — 별도 패스워드 입력 불필요.
/auth/login 실패 경로에만 묶어 정상 인증 거부(잘못된 비밀번호)는 노이즈로 잡지 않는다.전 구간을 일주일 안에 단계적으로 풀었다. 각 단계는 이전 단계의 표면을 깨지 않도록 추가 전용(additive) 으로 묶였다.
| 시기 | 변경 | 의미 |
|---|---|---|
| 04-10 | ERP 인증 공통 백엔드 서비스 초안 추가 | 코어(JWT · refresh · ERPClient · M2M) 완성 |
| 04-13 | Cloud Run prod 배포 설정 / Dockerfile → Containerfile | 배포 표준화 (Podman/Docker 양립) |
| 04-16 | OAuth 2.1 AS + MCP RS + Google Workspace OIDC | AI 에이전트 페르소나 추가 (코어 무수정) |
| 04-16 | DCR rate limit 10/min · Jinja 템플릿 git 추적 · 로그인 페이지 리디자인 | 운영 안정화 |
| 04-17 | "MCP × OAuth 2.1 구축기" 가이드 추가 | 사내 공유 — 다른 팀이 같은 패턴으로 IdP 묶을 수 있게 |
.env.example에 누락됐던 OAUTH_SESSION_SECRET·introspect 자격 3개를 추가 커밋한 것은 작은 일이지만 중요했다 — Cloud Run 환경에서 시작 직후 500이 떠야 알게 되는 종류의 실수를, 새 배포자가 같은 함정에 빠지지 않도록 막아두는 작업이다.
| 항목 | 결과 |
|---|---|
| 인증 표면 통합 | 3페르소나 (사람·M2M·OAuth) 단일 서비스 수렴 |
| 집중 개발 | 6일 (04-10 ~ 04-17, 코어 + OAuth AS + OIDC + 가이드) |
| 준수 스펙 | OAuth 2.1 / RFC 7591 DCR / MCP Spec 2025-06-18 |
| 자격증명 저장 | 0 — ERP 패스워드는 통과만 시킴, JWT만 서명 |
| Refresh 도난 탐지 | family rotation으로 사용 가능 (재사용 시 family revoke) |
| 사내 IdP | Google Workspace OIDC (hd=사내 도메인 강제) |
| 배포 | Cloud Run · Supabase Pooler 6543 · Sentry on_error |
기술적·운영적으로 가져간 학습은 네 가지다.
if used:)로 잡고 그 순간 패밀리 전체를 끊는 단순함은, 사후 사고 대응 절차를 통째로 없앤다.