Skip to content

[Week 04] 최선규 - MSA에서의 SSO 서버 전환 #19

Description

@luke0408

1. 작업 내용

이번 주에는 분리된 서비스가 각자 가진 로그인과
인가 경계를, 하나의 로그인 경험을 제공하는 SSO 서버로 전환하는 MSA SSO 전환
설계
를 정리했다. 목표는 인증을 중앙화하되 각 서비스의 사용자, 테넌트, 업무 권한을
중앙 서비스가 대신 소유하지 않게 만드는 것이다.

처음에는 "Keycloak으로 로그인만 통합하면 되는 것 아닌가?"라는 생각에서 출발했다.
하지만 로그인 성공은 사용자를 확인했다는 뜻일 뿐, 그 사용자가 특정 기능을 쓰거나 권한을 관리할 수 있다는 근거는 아니다. 그래서 인증과
인가의 정본을 한 곳에 몰지 않고 다음처럼 책임을 나눴다.

경계 정본 맡는 책임
Keycloak 계정, credential, 로그인 세션, token 발급 iss, sub, sid, realm/client role
SSO 권한 서버 서비스 목록, 권한 키, 범위, 계정 연결, 감사 여러 모듈에 공통인 접근 조건 판정
각 서비스 서비스 사용자, tenant membership, 업무 데이터 실제 업무의 최종 허용 또는 거부

목표는 한 번 로그인한 사용자가 서비스마다 비밀번호를 다시 입력하지 않는 것이다. 반면
MSA의 서비스끼리 cookie나 access token을 복사해 쓰는 방식은 채택하지 않았다. 각
서비스는 자신의 OIDC client와 API audience를 가지고 Keycloak에 로그인하며, API는
자신을 대상으로 발급된 access token만 받는다.

출발점: A/B/C 서비스가 각자 인증과 인가를 가진 구조

아래는 실제 서비스명, 서버 주소, DB 이름을 제거한 개념도다. 출발점에는 A, B, C
서비스가 각자 서버와 DB를 갖고, 서로 다른 로그인, session/token, 사용자 역할과
업무 권한을 처리한다. 이 상태에서는 A의 인증 결과가 B나 C에서 곧바로 통하는 계약이
없으며, 서비스별 계정 연결과 회수 정책도 따로 존재한다.

flowchart LR
    U[사용자 브라우저] -->|A 서비스 로그인·업무 요청| AS[A 서비스 서버]
    U -->|B 서비스 로그인·업무 요청| BS[B 서비스 서버]
    U -->|C 서비스 로그인·업무 요청| CS[C 서비스 서버]

    AS --> AD[(A 서비스 DB<br/>계정·권한·업무 데이터)]
    BS --> BD[(B 서비스 DB<br/>계정·권한·업무 데이터)]
    CS --> CD[(C 서비스 DB<br/>계정·권한·업무 데이터)]
Loading
구성 전환 전 책임
A/B/C 서비스 서버 자기 로그인, token 또는 session, API 인증, 로컬 권한 검사
A/B/C 서비스 DB 서비스별 사용자, 역할, tenant membership, 업무 데이터
서비스 간 연계 서비스마다 별도 credential 또는 직접 연계 계약을 관리

문제의 핵심은 A/B/C의 인증을 무조건 하나로 바꾸는 것이 아니라, 서로 다른 인증
결과를 안전한 신뢰 경계 안에서 조합해야 한다
는 점이다. 기존 서비스의 인증 방식을
모른 채 JWT나 cookie로 단정하거나, 브라우저에 각 서비스의 자격증명을 전달하는 방식은
전환안에서 제외했다.

전환 후: SSO 중심 영역과 기존 인증 유지 영역을 나눈 구조

전환 후에도 모든 서비스를 한 번에 SSO로 바꾸지 않는다. 브라우저는 SSO로 로그인하고
어드민 서버라는 단일 업무 API 진입점과 통신한다. 새로 통합하는 A/B 서비스 Pod는 SSO
token과 공통 권한 계약을 직접 사용한다. 반면 기존 C 서비스는 기존 인증을 유지하고,
어드민 서버 안의 서버 전용 인증 어댑터를 통해서만 호출한다.

여기서 SSO 서버는 통합 계정, 로그인 session, 권한, 모듈 사용자 ID 연결을 맡는다.
기존 C 서비스의 세부 업무 권한까지 즉시 대체하지 않으며, C 서버가 자기 정책으로
최종 인가를 계속 수행한다.

이 그림에서 A/B는 처음부터 SSO 계약을 사용하는 통합 영역의 서비스이고, C는 기존
인증을 유지하는 모듈의 대표 표기다. 실제로 C와 같은 기존 모듈이 여러 개여도 각각의
인증 어댑터와 로컬 권한 정본을 분리해 같은 원칙으로 연결한다.

flowchart LR
    U[사용자 브라우저] -->|Authorization Code + PKCE| S[SSO 서버]
    S --> SD[(SSO 저장소<br/>계정·session·권한·ID 연결)]
    U -->|어드민 API audience token| A[어드민 서버<br/>단일 업무 API 진입점]

    A -->|audience 제한 token 또는 token exchange| AP[A 서비스 Pod]
    A -->|audience 제한 token 또는 token exchange| BP[B 서비스 Pod]
    AP --> D[(A/B 공용 물리 DB<br/>DB Role·논리 소유권 분리)]
    BP --> D

    A -->|서버 전용 인증 어댑터| CA[C 서비스 인증 어댑터]
    CA -->|기존 인증 규약으로 호출| CS[C 서비스 서버]
    CS --> CD[(C 서비스 DB<br/>기존 계정·권한·업무 데이터)]
Loading
구성 전환 후 책임 보관하지 않는 것
SSO 서버/저장소 통합 계정, session, 권한, ModuleIdentityLink, token 발급·회수 A/B/C의 업무 데이터와 C의 세부 업무 권한
어드민 서버 브라우저의 단일 업무 API, SSO 인가, A/B 라우팅, C 인증 어댑터 호출과 감사 SSO 계정·권한 정본, A/B 업무 로직, 브라우저에 C 자격증명 노출
A/B 서비스 Pod SSO token 검증, 각자 소유한 업무 규칙과 테이블 접근 C의 기존 인증에 새로 의존하는 것
A/B 공용 물리 DB 같은 DB 인스턴스를 쓰되 DB Role과 테이블/스키마 소유권 분리 A/B의 배포·확장·장애 경계 통합
C 서비스/DB 기존 인증·권한·업무 규칙의 최종 판정 전환 초기에 강제 SSO migration

이 구조에서 중요한 신뢰 경계는 두 가지다. 브라우저는 C 서비스를 직접 호출하거나 C의
token/refresh token을 보관하지 않는다. 어드민 서버만 C의 서버 간 인증을 처리하며,
C는 받은 요청에 대해 자신의 로컬 권한을 다시 판정한다. 따라서 SSO 전환은 DB 통합이나
사용자 데이터 복제가 아니다. SSO 저장소에는 (issuer, sub)와 서비스별 불변 사용자
ID의 검증된 연결만 보관하고, 실제 사용자 상태와 업무 데이터는 각 서비스 DB에 남긴다.

현재 SSO 서버에는 Keycloak issuer 기반 JWT Resource Server와 로컬 Keycloak
개발 환경이 있지만, 권한 저장소, 모듈별 client, audience 검증, token exchange,
업무 API는 아직 구현하지 않았다. 따라서 이번 작업은 구현 완료 보고가 아니라,
나중에 각 모듈을 안전하게 연결하기 위한 목표 설계와 시작 조건을 정한 작업이다.

2. Deep Dive 해본 것

인증 성공 뒤에도 세 번 더 확인해야 한다

최종 허용 조건을 단일 role 검사로 끝내지 않고 다음 네 조건의 AND로 정의했다.

인증 성공
AND SSO permission/scope 허용
AND VERIFIED ModuleIdentityLink
AND 대상 모듈의 로컬 업무 인가 허용

예를 들어 Keycloak token에 ADMIN 역할이 있어도, CRM의 crm.project.update
권한과 대상 프로젝트 범위가 없거나 CRM DB에서 담당자 배정이 해제됐다면 수정은
거부된다. 반대로 CRM의 로컬 역할만 남아 있고 SSO 권한이 회수됐다면 역시 거부한다.
판정할 수 없는 장애나 누락은 편의상 통과시키지 않고 fail closed로 처리한다.

전환기에 기존 C 서비스를 호출하는 요청은 아래처럼 어드민 서버를 경유한다. A/B 서비스는
같은 SSO token 검증과 권한 계약을 쓰지만, C 인증 어댑터는 거치지 않는다.

sequenceDiagram
    participant U as 사용자
    participant A as 어드민 서버
    participant S as SSO 서버
    participant C as C 인증 어댑터/C 서버
    participant D as C 서비스 DB

    U->>A: 업무 요청 + 어드민 API audience token
    A->>S: issuer, 서명, audience, 만료 검증
    S-->>A: 인증된 subject와 SSO 권한 판정
    A->>S: C 사용자 ID 연결과 대상 범위 조회
    S-->>A: VERIFIED 연결 또는 deny
    A->>C: 최소 작업·범위·수행자·request_id + 서버 인증
    C->>D: C 로컬 권한과 업무 상태 확인
    D-->>C: allow 또는 deny
    C-->>A: 업무 결과 또는 오류
    A-->>U: 모든 gate가 allow일 때만 응답
Loading

연결의 기준은 이메일이 아니라 불변 ID다

같은 이메일 주소가 CRM과 전시에 존재한다고 해서 동일 사용자를 자동으로 연결하면,
재가입, 계정 통합, 이메일 변경, 공유 이메일에서 잘못된 권한이 이어질 수 있다. 그래서
SSO의 Keycloak (issuer, sub)와 모듈이 발급한 불변 opaque 사용자 ID를 별도
ModuleIdentityLink로 연결하도록 했다.

연결은 CANDIDATE, VERIFIED, REJECTED, REVOKED 상태를 갖고 실제 접근에는
VERIFIED만 쓴다. 초대 코드, 기존 계정 재인증, 운영자 검토처럼 모듈별로 정한 증거가
검증된 뒤에만 상태를 바꾼다. 이 덕분에 SSO 서버는 기존 모듈의 개인정보나 password를
복제하지 않고도 계정 연결의 승인 이력과 회수를 관리할 수 있다.

역할, 범위, token은 서로 섞지 않는다

플랫폼 전반의 역할과 특정 모듈 안에서만 의미 있는 역할은 이름이 같아도 다른
namespace로 유지한다.

구분 예시 의미
PLATFORM ADMIN, ACCOUNT_OWNER, EVENT_MANAGER, AGENT 여러 모듈을 걸치는 운영 역할
MODULE:EXHIBITION USER 전시 모듈에서만 쓰는 역할
permission crm.project.update 실행하려는 구체 행위
scope PLATFORM, ORGANIZATION, PROJECT, TENANT, MODULE, PAGE 그 행위가 허용된 데이터 범위

전시의 기존 ROLE_USER를 전역 USER로 승격하지 않는 것도 같은 이유다. 역할은
후보 집합일 뿐이며, 실제 요청에서는 permission과 대상 scope를 함께 확인한다.
URL이나 요청 body에 들어온 projectCd, siteId도 그대로 믿지 않고, 서버가
인증된 사용자의 tenant/소유권과 대상 업무 데이터를 다시 대조해야 한다.

브라우저 보관 방식도 모듈 구조에 맞춰 나눴다. 서버 렌더링 웹은 서버가 token을
처리하고 브라우저에는 HttpOnly opaque session cookie만 둔다. 순수 SPA는 PKCE를
사용하되 access token을 메모리에만 두며, 장수명 refresh token을 localStorage
sessionStorage에 저장하지 않는다.

전환은 교체가 아니라 병행 검증으로 시작한다

SSO 서버를 추가한다고 기존 로그인을 한 번에 삭제하지 않는다. 모듈마다 token/session
형식, 사용자 ID, tenant 경계, logout과 회수 방식이 다르기 때문이다. 전환 중에는 기존
인증과 새 OIDC 경로를 구분하고, 같은 업무 요청에서 SSO 판정과 모듈 로컬 guard가 모두
기대대로 거부하거나 허용하는지 확인해야 한다.

flowchart LR
    A[현행 인증·인가 inventory] --> B[OIDC client와 audience 분리]
    B --> C[identity link 검토·검증]
    C --> D[shadow 판정과 회귀 시험]
    D --> E[서비스별 SSO 기본 전환]
    E --> F[logout·회수·감사 확인]
    F --> G[기존 인증 종료 또는 rollback]
Loading

특히 병행 기간에는 기존 local token과 SSO token이 같은 API에서 혼동되지 않도록
issuer, audience, endpoint, cookie 이름을 명확히 분리한다. SSO 장애나 link 누락 시
기존 권한으로 조용히 우회시키지 않으며, 승인된 rollback 경로와 종료 조건을 따로
둔다.

아직 설계하지 않은 것을 남겨두는 것도 설계였다

등록 모듈은 현행 인증, 사용자 ID, tenant 구조, route guard가 확인되지 않았다. 그래서
CRM이나 전시의 방식을 복사하지 않고 BLOCK으로 남겼다. source와 runtime 근거가
확인되기 전에는 client 생성, DB migration, adapter 구현을 시작하지 않는 게이트를
정했다.

모듈 온보딩 때는 아래 항목을 먼저 확인한다.

  1. 로그인, refresh, logout, 회수의 실제 경로와 token/session 수명
  2. 이메일이 아닌 불변 사용자 ID와 tenant ID의 cardinality
  3. public/authenticated/role/operation route와 데이터 계층의 최종 guard
  4. SSO permission/scope와 모듈 로컬 권한의 행위별 매핑
  5. 기존 인증 병행, token 혼동 방지, rollback과 종료 조건

3. 알게된 것

  • SSO는 "한 번 로그인"을 제공하는 인증 통합이지, 여러 서비스의 업무 권한을 한
    role로 합치는 방법이 아니다.
  • API audience를 분리하지 않으면 한 모듈의 token이 다른 모듈 API에서도 통하는
    token confusion 문제가 생길 수 있다. ID token도 업무 API의 bearer token으로
    쓰면 안 된다.
  • 이메일 자동 연결은 사용자 경험을 조금 빠르게 만들 수 있지만 권한 오연결의 비용이
    더 크다. 사람의 확인 또는 재인증 근거를 가진 identity link가 필요하다.
  • 권한 설계는 role 목록보다 "누가 어떤 tenant의 어떤 상태인 데이터를 어떤 행위로
    바꿀 수 있는가"를 먼저 적을 때 빈틈이 드러난다.
  • 모르는 모듈의 설계를 억지로 완성하는 것보다, 구현을 막는 조건과 조사 산출물을
    명시하는 편이 더 안전한 설계가 될 수 있다.

이번 설계의 다음 단계는 Keycloak 설정을 늘리는 일이 아니라, 각 서비스의 route,
service, 데이터 guard를 근거로 인가 규칙표와 전환/rollback 조건을 확정하는 것이다.
그 뒤에야 권한 저장소와 OIDC 연동을 실제 코드로 옮길 수 있다.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions