kintex/docs/architecture/sso-hr-integration.md

374 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 킨텍스 AI 전시·행사시스템 — Open SSO · 인사(조직) API 연동 아키텍처
> 산출: 시스템 아키텍트(SA) · 작성일: 2026-07-12 · 대상: 소유자 지시("직원 인사(조직)정보는 API 방식, Open SSO 도입")
> 정합 근거: [`system.md`](system.md)(SA §5·§6 인증/연동), [`app.md`](app.md)(AA 인증 계약), [`network.md`](network.md)(NA 존/아웃바운드), [`data.md`](data.md)(DA 마스터/소프트참조), [`../PLANNING.md`](../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↔내부망 구간.
```mermaid
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 브로커)
```mermaid
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)
```mermaid
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. 동기화 전략
```mermaid
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'|'LOCAL') 컬럼 추가(멱등 ALTER) |
- **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 교차참조 |