킨텍스 AI 전시·행사시스템 — Open SSO · 인사(조직) API 연동 아키텍처
산출: 시스템 아키텍트(SA) · 작성일: 2026-07-12 · 대상: 소유자 지시("직원 인사(조직)정보는 API 방식, Open SSO 도입")
정합 근거: system.md(SA §5·§6 인증/연동), app.md(AA 인증 계약), network.md(NA 존/아웃바운드), data.md(DA 마스터/소프트참조), ../PLANNING.md §2(6역할)·§5B(공통·시스템관리)·§8, ../DEVELOPMENT_GUIDE.md §4·§5
본 문서는 설계·계약·이행계획만 정본. src·기존 인증코드 미수정(파괴적 구현 금지). 실제 구현은 인계 백로그(§9)로 각 dev 트랙에 위임.
확정 스택(불변): React 18/19(Vite·TS) + Spring Boot 3.x(Java 17)+MyBatis + PostgreSQL(PostGIS) + Redis. 신규 IdP(Keycloak)는 온프레미스 인프라 구성요소로 추가(스택 위반 아님).
0. 요지 (Executive Summary)
| 항목 |
결정 |
근거 |
| 인증 프로토콜 |
OIDC(우선) + SAML 2.0(옵션) — Authorization Code + PKCE |
표준·웹/모바일 공통·JWKS 검증. SAML은 고객사 레거시 IdP 대비 폴백 |
| IdP |
Keycloak(자체 호스팅, 온프레미스) |
오픈소스·OIDC/SAML/그룹·클레임 매핑·2FA·back-channel 로그아웃 내장. 외부 인터넷 API 아님(내부망 IdP) |
| 기존 JWT+2FA 관계 |
삭제·교체 금지. 공존(coexist) — SSO를 1차 인증, 앱 JWT를 세션토큰으로 브로커 발급. 로컬 로그인은 폴백으로 유지 |
W12 자산(app_user·event_member·TotpService) 보존, 무중단 이행 |
| 2FA |
IdP가 OTP 제공 시 위임(앱 OTP 강제 해제), 미제공 시 앱 TOTP 유지 |
중복 2FA 방지·기존 TwoFactorService 재사용 |
| 인사·조직 |
로컬 마스터 유지가 아니라 HrDirectoryClient 어댑터로 외부 HR API 조달 + 로컬 읽기 캐시 스냅샷(HR 미가용 폴백) |
HR 정본화, dept/sys_user는 캐시로 전환(멱등 upsert 배치) |
| 3자 매핑 |
SSO subject ↔ HR 사번(empNo) ↔ 로컬 user(app_user.id) 매핑 테이블 신설 |
계정·조직·권한 정합 단일 키 체계 |
| 이행 |
coexist → pilot(파일럿 부서) → cutover(SSO 우선) → 로컬로그인 축소 4단계 + 롤백 게이트 |
파괴 없는 점진 전환 |
보안 정합(불변): Open SSO(자체 IdP)·HR API(고객 내부망 인사시스템)는 온프레미스/내부 통합 → 외부 인터넷 API 금지 규칙에 해당하지 않음(Claude/Gemini 예외와 별개 범주). 단, 토큰·클라이언트 시크릿·HR 자격증명은 env/시크릿 스토어 only, 응답·로그·커밋 미노출, 마스킹 유지.
1. 현행(As-Is) 인증·조직 기준선
설계는 기존 코드를 읽어 확인한 실제 계약 위에 얹는다(교체 없음).
| 자산 |
실체 |
본 설계에서의 처리 |
app_user(V2) |
id·email·display_name·password_hash(BCrypt)·hall_manager·otp_secret·failed_login_count·locked_until·status |
보존. 로컬 계정 정본 → 이행 후 "SSO 매핑 대상 + 로컬 폴백" |
event_member(V2) |
행사 RBAC(ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER, booth/company 스코프) |
보존. IdP는 행사 역할을 모름 → 행사 역할은 계속 로컬 권위 |
app_user.role_code(V31) |
전역 역할(rc 클레임) |
IdP 그룹/클레임 → 전역 역할 매핑 소스로 확장 |
dept(V37) |
부서 트리(tenant_id·id·parent_id·sort_order·use_yn) |
HR 정본 시 읽기 캐시로 전환(스냅샷 upsert) |
company(V2/V37) |
등록업체(M7 응찰 게이트) |
불변(HR 무관 — 외부 파트너) |
JwtService |
HS256, 클레임 sub·name·roles(eventId→role)·hm·tid·rc |
불변. SSO 성공 후 이 서비스로 동일 형태 앱 JWT를 브로커 발급(다운스트림 RBAC 무변경) |
TwoFactorService/TotpService/LoginAttemptService |
로컬 2FA·잠금·챌린지 |
IdP 2FA 위임 시 우회, 로컬 폴백 시 유지 |
JwtAuthenticationFilter |
Bearer 앱JWT → SecurityContext |
불변. SSO 도입해도 백엔드 보호경로는 계속 앱 JWT 검증(핵심 무중단 포인트) |
설계 핵심 원칙: SSO는 로그인 게이트웨이만 교체한다. 로그인 성공의 산출물은 여전히 "기존 형태의 앱 JWT"이므로, 84개 화면·전 RBAC 가드·행사 스코프 로직은 한 줄도 바뀌지 않는다. HR API는 조직 마스터의 데이터 출처만 교체한다(dept 트리 소비자 무변경).
2. 설계 A — Open SSO 도입 (OIDC / Keycloak)
2-1. 채택 결정 (ADR)
| # |
결정 |
이유 |
대안·트레이드오프 |
| A1 |
OIDC 우선(SAML 옵션) |
웹+모바일(expo-auth-session) 동일 표준, JSON/JWKS, PKCE로 공개 클라이언트 안전 |
SAML-only(모바일·SPA 부적합) 배제. 고객 레거시가 SAML뿐이면 Keycloak가 SAML IdP↔OIDC 브로커로 흡수 |
| A2 |
Keycloak 자체 호스팅 |
오픈소스·온프레미스·그룹/클레임 매핑·OTP·Identity Brokering(외부 AD/LDAP/SAML 연합)·back-channel 로그아웃 |
상용 IdP(비용·외부 SaaS=보안 위반) 배제. 경량 대안(자체 OIDC 구현)은 표준 준수·유지비 열위 |
| A3 |
브로커드 세션토큰(IdP 토큰 → 앱 JWT 교환) |
다운스트림 RBAC·행사 스코프·tid/rc 클레임 무변경, 무상태 유지 |
IdP 액세스토큰 직접 신뢰(리소스서버 모드)는 행사 RBAC 클레임 부재로 전 가드 재작성 필요 → 이행기엔 배제(§2-6 장기 옵션으로만) |
| A4 |
로컬 로그인 폴백 유지 |
IdP 장애 시 관리자·핵심 운영 지속(가용성 NFR) |
IdP 단일 의존(장애 시 전면 로그인 불가) 배제 |
IdP 후보 근거: Keycloak(1순위, 위 A2) / 대안 Authentik·ZITADEL(경량·최신 UX이나 조직 내 운영 실적·AD 연합 성숙도에서 Keycloak 우위) / Gluu·Ory Hydra(각각 무거움·인증 파트 분리 필요). 킨텍스 온프레미스·AD 연합·SAML 흡수 요건에서 Keycloak 채택.
2-2. 토폴로지 (존 배치)
Keycloak는 내부 애플리케이션 존에 배치하고, 브라우저 리다이렉트를 위해 리버스 프록시(nginx)의 별도 vhost(auth.<kintex-domain>)로만 외부 노출한다. HR API·AD 연합은 IdP↔내부망 구간.
graph TB
subgraph EDGE["엣지 / DMZ (nginx TLS 종단)"]
RP[리버스 프록시<br/>app.· auth.· api. vhost]
end
subgraph APPZ["애플리케이션 존 (내부망)"]
FE[역할별 프론트 SPA + 공개/관람객]
BE[공유 백엔드 Spring Boot × N<br/>OIDC RP + 앱JWT 브로커]
KC[Keycloak IdP<br/>realm=KINTEX · OIDC/SAML · OTP]
end
subgraph DATAZ["데이터 존 (최심부)"]
PG[(PostgreSQL+PostGIS<br/>app_user·매핑·조직 캐시)]
KCDB[(Keycloak DB<br/>별도 스키마/DB)]
RED[(Redis<br/>state·nonce·세션상관)]
end
subgraph HRZONE["고객 내부망 (연동 대상)"]
AD[AD / LDAP<br/>직원 계정]
HRAPI[HR 인사시스템 API<br/>조직·직원 마스터]
end
FE -->|1 로그인 리다이렉트| RP --> KC
KC -.연합(선택).-> AD
FE -->|2 code+PKCE 콜백| RP --> BE
BE -->|3 code→token 교환·JWKS 검증| KC
BE -->|4 앱 JWT 발급| FE
BE --> PG
BE -.HrDirectoryClient(배치·조회).-> HRAPI
KC --> KCDB
BE --> RED
배치 규칙
- Keycloak DB는 앱 PG와 별도 데이터베이스/스키마(계정 데이터 격리·백업 주기 분리).
auth.<kintex-domain> vhost는 IdP UI/토큰 엔드포인트만 프록시. 백오피스(admin.)와 동일하게 접근 제한 정책은 NA 확정.
- 아웃바운드: Keycloak→AD/LDAP, 백엔드→HR API는 내부망 구간(인터넷 egress 아님). NA egress 화이트리스트에 인터넷 목적지 추가 없음.
2-3. 로그인 시퀀스 (Authorization Code + PKCE → 앱 JWT 브로커)
sequenceDiagram
participant U as 사용자(브라우저/앱)
participant FE as 프론트(SPA/Expo)
participant BE as 백엔드(OIDC RP)
participant KC as Keycloak(IdP)
U->>FE: 로그인 클릭
FE->>FE: PKCE code_verifier/challenge, state, nonce 생성
FE->>KC: /authorize (code, PKCE challenge, redirect_uri)
KC->>U: 로그인 폼(+IdP OTP if 위임)
U->>KC: 자격증명(+OTP)
KC->>FE: redirect_uri?code=...&state=...
FE->>BE: POST /api/auth/oidc/callback (code, code_verifier, state)
BE->>KC: token 교환(code + code_verifier + client_secret)
KC->>BE: id_token + access_token (JWT)
BE->>KC: JWKS 공개키(캐시) — id_token 서명·iss·aud·nonce·exp 검증
BE->>BE: subject(sub)로 매핑 조회 → 로컬 user 해석/JIT 프로비저닝(§2-5)
BE->>BE: event_member 행사역할·hall_manager·tid·rc 조립
BE->>FE: 앱 JWT(JwtService.issue) + 워크스페이스(기존 LoginResponse 형태)
FE->>FE: 기존 저장키에 앱 JWT 저장 — 이후 전 API는 기존 그대로
핵심: 5~7단계 산출물 = 현행 LoginResponse와 100% 동일. /api/auth/oidc/callback만 신설, 나머지 전 경로 불변.
2-4. 토큰 갱신·로그아웃 시퀀스
| 흐름 |
설계 |
| 앱 JWT 갱신 |
현행 TTL 정책 유지. 만료 시 프론트가 silent OIDC 재인증(prompt=none, IdP 세션 유효 시 무마찰) → 백엔드가 앱 JWT 재브로커. 리프레시 토큰은 백엔드가 보관(HttpOnly·서버측), 프론트 미노출 |
| IdP 세션 만료 |
silent 재인증 실패 → 로그인 화면. 앱 JWT 짧게, IdP 세션이 마스터 수명 |
| 로그아웃 |
프론트 로컬 토큰 파기 + /api/auth/oidc/logout → Keycloak end-session(id_token_hint). back-channel logout: Keycloak → 백엔드 /api/auth/oidc/backchannel-logout(로그아웃 토큰 검증) → Redis 세션상관 무효화(다중 인스턴스 팬아웃) |
| 단일 로그아웃(SLO) |
Keycloak realm SSO 로그아웃으로 연동 클라이언트 전파(웹·모바일·타 GUARDiA 연계 시) |
2-5. 계정 매핑 · JIT 프로비저닝 (SSO subject ↔ 로컬 user)
graph LR
SUB[OIDC sub<br/>=IdP subject] --> MAP[user_identity 매핑]
EMP[HR empNo<br/>=사번] --> MAP
LU[app_user.id<br/>=로컬 user] --> MAP
MAP --> RESOLVE{로컬 user 존재?}
RESOLVE -->|Yes| LOGIN[역할 조립 → 앱 JWT]
RESOLVE -->|No| JIT[JIT 프로비저닝<br/>app_user upsert + 매핑 생성]
JIT --> LOGIN
- 매핑 키: IdP
sub(불변 식별자) 우선. 보조 매칭키 = 이메일 + HR 사번(empNo, IdP 커스텀 클레임). 이메일 단독 매칭 금지(재사용·변경 위험) — sub↔empNo 확정 후 email은 표시용.
- JIT 프로비저닝: 첫 SSO 로그인 시 매핑 없으면
app_user를 password_hash 없이(SSO 전용 플래그) 생성 + user_identity 매핑 생성. 로컬 폴백 비번은 미설정(SSO 전용 계정).
- 충돌 규칙: 동일 이메일에 기존 로컬 계정 존재 → 자동 병합 금지, 관리자 수동 링크(계정 탈취 방지).
link_status(UNLINKED/LINKED/CONFLICT).
- 역할 소스: 전역 역할(rc)·hall_manager = IdP 그룹/클레임 매핑(§2-7). 행사 역할(event_member)은 로컬 권위 유지(IdP는 행사를 모름).
2-6. 기존 JWT+2FA 공존안 (핵심)
| 관심사 |
공존 규칙 |
| 1차 인증 |
Keycloak(OIDC). 성공 후 백엔드가 기존 JwtService.issue()로 앱 JWT 브로커 — 다운스트림 무변경 |
| 로컬 로그인 |
/api/auth/login·/login/secure 유지(폴백). 관리자·핵심 운영은 IdP 장애 시 로컬 폴백 허용. 일반 사용자는 이행 단계별로 SSO 우선/로컬 차단(§8) |
| 2FA |
IdP realm이 OTP 요구 시 → 앱 requiresOtp 위임(앱 OTP 강제 해제, 이중 2FA 방지). IdP OTP 미구성 계정·로컬 폴백 로그인 → 기존 TwoFactorService 그대로 |
| admin 비번 |
ADMIN_PASSWORD_ENC env 시드 정책 유지(IdP 장애 시 최후 관리자 접근) |
| 토큰 신뢰 모드 |
이행기 = 브로커 모드(앱 JWT). 장기 옵션 = 리소스서버 모드(IdP 토큰 직접, 커스텀 클레임 매퍼로 행사역할 주입) — 행사 RBAC 재검증 부담으로 이행 완료 후 별도 과제(A3) |
공존 상태 판정 로직(개념):
로그인 요청
├─ SSO 경로(/oidc/callback): IdP 검증 성공 → 매핑 해석 → 앱 JWT (2FA는 IdP 위임)
└─ 로컬 경로(/login, /login/secure): 기존 TwoFactorService (단계별 허용범위 §8)
다운스트림(전 API): 기존 JwtAuthenticationFilter가 앱 JWT만 검증 — 경로 무관 동일
2-7. 멀티테넌트·역할 매핑
- tenant: realm=단일(
KINTEX) 또는 realm 그룹 클레임 tenant. 앱 tid 클레임 = IdP tenant 클레임(부재 시 기준 테넌트 KINTEX 폴백, 현행 normalizeTenant 규칙 준수).
- 역할 매핑표(IdP 그룹/클레임 → 앱 역할):
| IdP 그룹/클레임 |
앱 전역 역할(rc) |
hall_manager |
트랙 |
/kintex/admin |
ADMIN |
- |
내부 백오피스 |
/kintex/manager |
MANAGER |
- |
내부 운영 |
/kintex/hall-manager |
(rc null) |
true |
내부 운영(홀) |
/kintex/staff |
STAFF |
- |
내부 직원 |
| (매핑 없음·외부) |
null |
false |
행사역할은 event_member로 |
- 6역할 정합: 주최자·참가업체·장치/공사업체는 대개 외부(HR 대상 아님) → SSO 계정이라도 행사 역할은
event_member 초대로만 부여. 관람객/일반대중은 셀프서비스(SSO 대상 아님·별도 트랙 §8 note).
2-8. 웹 + 모바일 플로우
| 클라이언트 |
라이브러리 |
리다이렉트 URI |
특이사항 |
| 웹 SPA(역할별 6종) |
oidc-client-ts 또는 백엔드 BFF 콜백 |
https://<role>.<kintex-domain>/auth/callback |
PKCE. 역할별 번들마다 클라이언트 등록(또는 와일드카드 redirect + 역할 라우팅) |
| 모바일(Expo) |
expo-auth-session(PKCE 기본) |
kintexai://auth/callback (앱스킴) + https://<domain>/auth/native-callback(유니버설 링크 폴백) |
공개 클라이언트(시크릿 없음)=PKCE 필수. B2B 앱=SSO+IdP OTP, B2C 관람객 앱=SSO 대상 아님(셀프서비스 유지) |
- 클라이언트 등록(Keycloak):
kintex-web(confidential, BFF 콜백) / kintex-mobile(public, PKCE). redirect_uri·web_origins(CORS)·post_logout_redirect 화이트리스트 등록.
- 모바일 레퍼런스: WISE 모바일(
workspace/guardia-messenger/app/uiws) 2FA 로그인 화면 컨벤션 위에 expo-auth-session 브라우저 플로우 삽입(디자인은 Stitch 경유 — MEMORY 준수).
3. 설계 B — 인사(조직) API 연동 (HrDirectoryClient)
3-1. 채택 결정 (ADR)
| # |
결정 |
이유 |
| B1 |
어댑터 패턴 HrDirectoryClient 인터페이스 + 고객 HR 구현 |
고객사별 HR API 상이 → 인터페이스로 격리, 구현만 교체. Mock 구현으로 개발·폴백 |
| B2 |
로컬 읽기 캐시 스냅샷 유지(HR 미가용 폴백) |
HR 장애·야간 배치 실패에도 조직도·직원조회 지속(가용성). HR=정본, 로컬 dept/직원=파생 캐시 |
| B3 |
증분 동기화(배치) + 실시간 조회 혼합 |
조직 트리·직원 마스터는 배치 스냅샷(멱등 upsert), 로그인 순간 신원 확인은 실시간 조회(선택) |
| B4 |
PII 최소 수집·마스킹 |
직원 개인정보 경계(SA §5-3). 필요 필드만 수집, 연락처·주민식별 미수집/마스킹 |
3-2. 어댑터 계약 (인터페이스 · DTO)
인터페이스(개념 시그니처 — backend-dev 구현):
| 메서드 |
목적 |
동기성 |
List<HrDept> fetchOrgTree(tenantId, sinceRev?) |
조직(부서) 트리 — 전체 또는 변경분 |
배치 |
Page<HrEmployee> fetchEmployees(tenantId, sinceRev?, page) |
직원 마스터 — 전체/증분 페이지 |
배치 |
Optional<HrEmployee> findByEmpNo(tenantId, empNo) |
로그인·매핑 시 단건 신원 확인 |
실시간(선택) |
SyncCursor currentRevision(tenantId) |
증분 동기화 커서(변경 기준점) |
배치 |
표준 DTO(계약·PII 최소):
HrDept { empDeptCode, deptName, parentDeptCode, sortOrder, useYn, revision }
HrEmployee {
empNo, // 사번 = 매핑 정본 키
name, // 표시명
email, // SSO sub 보조 매칭·표시
deptCode, // 소속 부서(HrDept.empDeptCode 참조)
positionCode, // 직급(공통코드 POSITION 매핑)
employmentStatus, // 재직상태 ACTIVE|LEAVE|RETIRED
revision // 증분 동기화 기준
// 미수집: 주민번호·개인 연락처·급여 등 (PII 최소)
}
- 원격/캐시 이원화: 서비스 계층은 항상
HrDirectoryClient를 부르되, HR 실패 시 HrSnapshotRepository(로컬 캐시)로 폴백(degraded=true 표기). 소비자(조직도·직원선택 컴포넌트)는 출처를 모름.
3-3. 동기화 전략
sequenceDiagram
participant SCH as 스케줄러(배치)
participant HC as HrDirectoryClient
participant HR as 고객 HR API
participant SNAP as 로컬 스냅샷(hr_dept_snapshot·hr_emp_snapshot)
participant DEPT as dept(파생 캐시)
SCH->>HC: fetchOrgTree(sinceRev) / fetchEmployees(sinceRev)
HC->>HR: GET 조직·직원 변경분
alt HR 정상
HR->>HC: 변경 레코드
HC->>SNAP: 멱등 upsert(revision 기준)
SNAP->>DEPT: dept 트리 파생 반영(use_yn·parent 매핑)
else HR 미가용
HC->>SNAP: 기존 스냅샷 유지(degraded)
Note over DEPT: 조직도·직원조회는 마지막 스냅샷으로 계속 서비스
end
- 주기: 조직 트리·직원 = 야간 전체 리컨실 + 주간(수시) 증분. 로그인 시점 신원은
findByEmpNo 실시간(선택, 실패 시 스냅샷).
- 멱등:
revision/empNo/empDeptCode 기준 upsert. 삭제는 소프트(useYn='N', employmentStatus=RETIRED) — 물리삭제 금지(감사·과거 참조 보존).
- 정본 전환: HR 정본화 후
dept·직원정보는 읽기 전용 캐시. 백오피스 조직 편집 UI는 "HR 마스터 편집 안내"로 전환(직접 수정 차단), 단 로컬 전용 부서(외부 파트너 그룹핑 등)는 source='LOCAL' 플래그로 공존 허용.
3-4. 기존 dept/sys_user 매핑
| 현행 |
HR 연동 후 |
dept(로컬 편집) |
source 컬럼 추가 → HR(캐시, 읽기전용) / LOCAL(로컬 전용, 편집가능) 공존. HR 파생행은 배치만 갱신 |
sys_user(=app_user 조회) |
직원 신원(name·dept·position)은 HR 스냅샷 조인으로 표시, app_user는 로그인/권한 최소필드만 |
| 조직도 화면(WISE 트리) |
데이터 출처만 HR 스냅샷 → 컴포넌트 무변경 |
4. 3자 매핑 통합 규칙 (SSO subject ↔ HR 사번 ↔ 로컬 user)
| 키 |
소유 |
역할 |
sub(OIDC subject) |
Keycloak |
로그인 신원 불변 식별자 |
empNo(사번) |
HR API |
직원·조직 정본 키(직급·부서·재직상태) |
app_user.id |
로컬 |
앱 계정·권한(event_member·hall_manager·tid) 앵커 |
- 연결 테이블
user_identity(§5): (tenant_id, app_user_id) ↔ (idp_sub, emp_no) + link_status.
- 로그인 해석 순서:
sub → user_identity 조회 → app_user 해석 → (선택) empNo로 HR 스냅샷 조인(부서·직급 최신화) → 역할 조립 → 앱 JWT.
- 불일치 처리: sub 있으나 empNo 없음(외부 SSO 사용자) 허용 / empNo 있으나 sub 없음(미로그인 직원) 허용(사전 프로비저닝) / 이메일 충돌 = CONFLICT(관리자 수동 링크).
5. 데이터 모델 (신설 — DA 정합, 소프트참조 표준)
신규 테이블만. 기존 테이블 변경 최소(dept에 source 컬럼 순증만). 테넌트 표준(V31: tenant_id DEFAULT 'KINTEX' 복합 PK 선두)·소프트참조(FK 남발 금지, 애플리케이션 조인) 준수. 실제 DDL은 db-engineer 인계(§9).
| 테이블 |
목적 |
핵심 컬럼(개념) |
user_identity |
3자 매핑 |
(tenant_id, app_user_id) PK, idp_sub UNIQUE, emp_no, link_status, provider, linked_at |
hr_dept_snapshot |
HR 조직 캐시 |
(tenant_id, emp_dept_code) PK, dept_name, parent_dept_code, sort_order, use_yn, revision, synced_at |
hr_emp_snapshot |
HR 직원 캐시 |
(tenant_id, emp_no) PK, name, email, dept_code, position_code, employment_status, revision, synced_at |
hr_sync_log |
배치 감사 |
(tenant_id, id) PK, kind(DEPT/EMP), rows_upserted, result, degraded, started_at, finished_at |
dept(순증) |
출처 구분 |
source varchar(10) DEFAULT 'LOCAL'('HR' |
- PII 경계: hr_emp_snapshot는 name·email·dept·position·status만. 연락처·식별번호 미보관. email은 표시·매칭용(마스킹은 로그·감사 계층).
- 소프트참조: emp_no·emp_dept_code·idp_sub는 소프트 참조(FK 미설정) — HR/IdP 외부 키를 물리 FK로 묶지 않음(배치 순서·정합 유연성). 공통코드(POSITION·EMPLOYMENT_STATUS)는 common_code 순증.
6. NFR 정합 (SA system.md §3)
| NFR |
SSO/HR 반영 |
| 확장성 |
백엔드 무상태 유지(OIDC state/nonce는 Redis 외부화). Keycloak는 클러스터 가능. HR 배치는 백엔드와 분리 스케줄(응답성 무영향) |
| 가용성/HA |
로컬 로그인 폴백(IdP 장애)·HR 스냅샷 폴백(HR 장애) 이중 degraded 모드로 코어 기능 유지. Keycloak 최소 2노드 + DB HA |
| 성능 |
JWKS 공개키 캐시(매 요청 IdP 미호출). 로그인만 IdP 왕복, 이후 앱 JWT 로컬 검증(현행 성능 무변경). HR은 배치 오프라인 |
| 보안영역 |
Keycloak=내부 애플리케이션 존, auth. vhost만 노출. HR/AD=내부망 구간(인터넷 egress 무증가). 토큰·시크릿 env only |
| 용량 |
user_identity·hr_*_snapshot는 직원 규모(수백~수천) 소량. hr_sync_log 보존정책(N개월) |
7. 보안 · 개인정보 (불변 준수)
- 온프레미스 판정: Keycloak(자체 IdP)·HR API(고객 내부 인사시스템)는 내부망 통합 → 외부 인터넷 API 금지 규칙 비해당. NA egress 화이트리스트에 인터넷 목적지 신규 추가 없음.
- 시크릿: OIDC client_secret·HR API 자격증명·리프레시 토큰 = env/시크릿 스토어 only(systemd EnvironmentFile drop-in,
ADMIN_PASSWORD_ENC 방식 준용). DB·코드·로그·커밋·API응답 미기록.
- 토큰 노출 금지: id_token/access_token/refresh_token은 응답 본문·로그 미노출. 프론트에는 앱 JWT만(리프레시는 서버 보관).
- 감사: 계정 링크/언링크·JIT 프로비저닝·HR 배치·권한 매핑 변경 =
TB_AUDIT_LOG 전수. 로그인 성공/실패는 기존 login_history(이메일/IP 마스킹) 재사용.
- PII 최소화: HR 수집 필드 최소, 삭제권·재직상태 반영(RETIRED 소프트), 보존정책 DA 확정.
- 검증: id_token 서명(JWKS)·iss·aud·exp·nonce, code 교환 시 code_verifier(PKCE), state(CSRF), back-channel logout token 검증 필수.
8. 이행 계획 (coexist → pilot → cutover → 롤백)
| 단계 |
범위 |
로컬 로그인 |
완료 기준(게이트) |
롤백 |
| 0. 준비 |
Keycloak 배치(realm/클라이언트)·매핑 테이블·HrDirectoryClient(Mock)·백엔드 /oidc/* 엔드포인트(피처플래그 sso.enabled=false) |
전면 허용(현행) |
staging OIDC 왕복·앱 JWT 브로커·회귀 0 |
플래그 off = 완전 현행 |
| 1. coexist |
SSO 로그인 옵션 노출(로그인 화면 "SSO로 로그인" 병행). JIT 프로비저닝·수동 링크 UI |
전면 허용 |
파일럿 계정 SSO 로그인·역할 정합·2FA 위임 검증 |
SSO 버튼 숨김 |
| 2. pilot |
1개 부서(내부 직원) SSO 우선 + HR 배치 실연동(스냅샷→dept). 로컬은 폴백만 |
파일럿 외 허용, 파일럿은 폴백 |
HR 스냅샷 정합·조직도 HR 출처·degraded 폴백 동작 |
파일럿 부서 로컬 복귀 |
| 3. cutover |
전 내부 직원 SSO 필수(로컬은 관리자·비상만). HR 정본화(dept 읽기전용) |
관리자·비상만 |
전 직원 SSO·HR 정본·이중 2FA 없음·감사 완비 |
IdP 장애 시 로컬 폴백 자동 |
| 4. 정착 |
로컬 비번 로그인 축소(SSO 전용 계정 비번 미발급). 리소스서버 모드 전환은 별도 과제(A3) |
최소(비상 관리자) |
운영 안정·SLO 충족 |
— |
- 무중단 원칙: 각 단계는 피처플래그로 제어, 언제든 이전 단계 복귀. 다운스트림(앱 JWT 소비)은 전 단계 불변 → 화면·API 회귀 0이 롤백 안전판.
- 관람객/외부(B2C): SSO 이행 대상 아님 — 셀프서비스(간편가입·게스트) 트랙 유지. 본 이행은 내부 직원·조직 한정.
9. 인계 백로그 (트랙별)
본 문서는 설계 정본. 아래는 각 dev 트랙 착수 항목. 모두 피처플래그 뒤에서 순증 구현(기존 인증코드 미파괴).
| 트랙 |
항목 |
산출 |
| backend-dev |
① OIDC RP: /api/auth/oidc/authorize-info·callback·logout·backchannel-logout, JWKS 캐시·id_token 검증 ② 콜백 성공 후 기존 JwtService.issue() 재사용한 앱 JWT 브로커 ③ HrDirectoryClient 인터페이스 + Mock 구현 + 고객 어댑터 스텁 ④ 매핑 해석·JIT 프로비저닝·수동 링크 서비스 |
신규 패키지 auth/oidc, hr/ (기존 auth·security 불변) |
| common-dev(인증 레이어) |
① 로그인 화면 SSO 버튼(피처플래그)·expo-auth-session(모바일) ② TwoFactorService.requiresOtp IdP 위임 분기(설정형) ③ 계정 링크/언링크 마이페이지·관리자 링크 UI |
인증 공통 레이어 |
| db-engineer |
① user_identity·hr_dept_snapshot·hr_emp_snapshot·hr_sync_log DDL(V38+, 멱등·테넌트 표준·소프트참조) ② dept.source 순증 ALTER ③ common_code POSITION·EMPLOYMENT_STATUS 순증 |
Flyway 마이그레이션 |
| mobile |
expo-auth-session PKCE 플로우 + B2B 앱 SSO/IdP OTP, WISE 모바일 로그인 컨벤션 위 삽입, 디자인 Stitch 경유 |
mobile/ 로그인 |
| devops |
① Keycloak 배치(systemd/컨테이너·별도 DB·realm import) ② auth.<domain> nginx vhost·TLS·접근제한 ③ client_secret·HR 자격증명 env drop-in(시크릿 스토어) ④ HR 배치 스케줄 |
인프라(G2 연계) |
| planner(인계) |
PLANNING §5B/§8에 Open SSO·HR API 연동 반영(6역할·인증 스택·조직 마스터 출처 갱신). 관람객 SSO 제외 명시 |
PLANNING 개정 |
| NA/DA(정합) |
NA: auth. vhost·IdP↔AD/HR 내부망 구간 존 반영 / DA: hr_*_snapshot·user_identity ERD·PII 보존정책 편입 |
network.md·data.md 갱신 |
10. 리스크
| # |
리스크 |
완화 |
| R1 |
IdP 단일 장애 → 전면 로그인 불가 |
로컬 로그인 폴백 유지(§2-6)·Keycloak HA·앱 JWT 수명으로 순간 장애 흡수 |
| R2 |
이메일 재사용·계정 병합 오류 → 권한 탈취 |
sub↔empNo 확정 매핑, 이메일 단독 병합 금지, CONFLICT 수동 링크·감사 |
| R3 |
HR API 스펙 상이·미제공 |
HrDirectoryClient 어댑터로 격리, Mock·스냅샷 폴백으로 개발·운영 지속 |
| R4 |
이중 2FA(IdP+앱) 마찰 |
IdP OTP 위임 시 앱 OTP 강제 해제(설정형 분기) |
| R5 |
행사 RBAC 클레임 누락(리소스서버 직접 신뢰 시) |
이행기 브로커 모드 고정(앱 JWT에 event_member 조립). 직접 신뢰는 정착 후 별도 과제 |
| R6 |
HR 정본 전환 후 로컬 조직 편집 상실 |
dept.source=LOCAL 공존 허용(외부 파트너 그룹핑 등 로컬 전용 유지) |
| R7 |
토큰/시크릿 노출 |
env only·서버측 리프레시 보관·응답/로그 미노출·JWKS 검증 |
11. 변경 이력
| 버전 |
일자 |
작성자 |
내용 |
| v1.0 |
2026-07-12 |
SA |
최초 작성 — Open SSO(OIDC/Keycloak, PKCE, 브로커드 앱JWT 공존, 로컬 폴백, 2FA 위임, 6역할/멀티테넌트 매핑, 웹+모바일) + 인사·조직 API 연동(HrDirectoryClient 어댑터·스냅샷 폴백·증분 동기화·PII 최소) + 3자 매핑(sub↔empNo↔app_user) + 데이터 모델·NFR·보안·이행(coexist→pilot→cutover→롤백)·인계 백로그·리스크. 기존 W12 인증(app_user·event_member·TotpService·JwtService) 미수정 공존 설계. system.md/app.md/network.md/data.md 교차참조 |