싱글 사인온
싱글 사인온을 사용하면 외부 ID 공급자(IdP)가 내 Control Center 서버에 로그인할 수 있는 사람을 결정합니다. SAML 2.0과 OpenID Connect, 두 가지 프로토콜을 지원하며, 둘 다 같은 지점에 도착합니다. 즉 이후의 모든 경로에서 초대나 QR 페어링과 구분할 수 없는, 새로 발급된 기기 자격 증명입니다.
로그인의 형태
섹션 제목: “로그인의 형태”SAML 로그인은 브라우저를 매개로 이루어집니다. IdP는 절대 서버에 연결을 열지 않으며, 사용자의 브라우저가 모든 것을 나릅니다:
browser ──> /saml/login ──> IdP ──POST SAMLResponse──> /saml/acs │ verify ▼ provision user (maybe) │ ▼ browser handoff: device PSK in the URL fragment (never a query parameter)브라우저가 전달자 역할을 하므로 로그인에는 NAT 뒤의 서버도 문제없이 동작합니다. localhost, LAN, Tailscale, VPN, 터널이 모두 괜찮으며, 사용자의 브라우저만 ACS URL에 도달하면 됩니다. 휴대폰 원격 제어에 사용되는 WebSocket 릴레이는 HTTP 왕복을 나를 수 없어서, SSO 진입점은 항상 서버의 HTTP 오리진을 대상으로 합니다.
OpenID Connect는 권한 부여 코드 + PKCE 왕복이라는 같은 형태를 따릅니다. 클레임은 TLS를 통해 발급자의 토큰 엔드포인트에서 읽으므로, 브라우저는 신원 데이터를 전혀 제공하지 않습니다.
어댑터
섹션 제목: “어댑터”Control Center는 책임을 의도적으로 분리합니다:
| 구성 요소 | 위치 | 담당 |
|---|---|---|
cc_saml 네이티브 |
packages/cc_natives/native/saml/ |
XML-DSig 검증, 정규화, SAML 프로필 검증 — 무상태 C ABI 뒤에 고정된 순수 Rust saml 크레이트에 위임 |
SamlService / OidcService |
cc_server_core |
로그인 상태(대기 중인 요청 추적기, 재전송 중복 제거), 검증 정책 설정값, 공유 프로비저닝 호출 |
SsoProvisioner |
cc_server_core |
사용자 조회 또는 생성, 제공자 주체 고정, 자동 멤버 정책에 따른 멤버십 부여 |
ScimService |
cc_server_core |
SCIM 2.0 푸시 표면(프로비저닝/디프로비저닝 수명주기) |
SsoSettingsService |
cc_server_core |
저장된 연결 행, 실시간 재적용, SCIM 토큰 |
순수 Dart SAML 암호화 폴백은 의도적으로 두지 않습니다. 직접 구현한
XML-DSig/정규화는 바로 서명 래핑 취약점이 사는 곳이므로, cc_saml
라이브러리가 없으면 SAML 로그인과 서버 부팅을 모두 거부합니다. 네이티브는
설계상 무상태이며, 요청 간 상태는 Dart 서비스에 존재하고 일회용이며
수명이 짧습니다.
로그인이 검증하는 것
섹션 제목: “로그인이 검증하는 것”검증된 네이티브와 서비스 내부에서 다음 순서로 확인합니다:
- 어설션의 서명(항상 필수이며, 응답 봉투의 서명은 구성에 따라 선택) — 검증된 페이로드가 검증 대상 요소에 묶여 있으므로 구조적으로 서명 래핑에 강합니다,
- 발급자가 구성된 IdP와 일치하며,
- 대상이 이 서비스 공급자를 지칭하며,
- 목적지가 우리 ACS URL이며,
InResponseTo가 우리가 발행한 대기 중 AuthnRequest와 일치하고 (요청하지 않은 IdP 시작 응답은 기본적으로 꺼져 있음),- 수명 구간이 클록 오차(기본 90s) 범위 내에 있으며,
- 어설션 id를 이전에 본 적이 없습니다(재전송 캐시).
실패는 유형이 지정된 코드 — signature, expired, audience, destination, issuer, request-match — 로 반환되며, 엔드포인트는 일반적인 오류로 응답하므로 공격자는 위조가 왜 실패했는지 아무것도 알 수 없습니다.
계정과 주체 고정
섹션 제목: “계정과 주체 고정”첫 SSO 로그인은 제공자의 변경할 수 없는 주체(issuer와 SAML NameID,
또는 OIDC sub, 또는 SCIM externalId)를 계정에 고정합니다. 이후에는
이메일이 아니라 주체가 로그인을 식별합니다. 나중에 다른 사람이 같은
IdP 또는 다른 IdP에서 같은 이메일을 제시하면, 고정된 계정은 그 사람을
거부합니다. 이렇게 해서 제공자에서 재활용된 이메일이 계정 탈취로
이어지는 것을 막습니다.
프로비저닝과 디프로비저닝
섹션 제목: “프로비저닝과 디프로비저닝”JIT 프로비저닝은 로그인 시점에 일어납니다(SsoProvisioner). SCIM은
별도의 푸시 표면(ScimService)으로, 사용자 행에 같은 주체를 고정하며
워크스페이스 멤버십을 부여하지는 않습니다. 디프로비저닝 — SCIM
active: false 또는 DELETE — 은 계정이 워크스페이스 소유자면
거부합니다(먼저 이전해야 함). 그렇지 않으면 먼저 모든 기기 자격 증명을
취소하고(기기 감시를 통해 세션이 몇 초 안에 끊김), 모든 워크스페이스
멤버십을 제거한 뒤 계정을 비활성화로 표시합니다. 사용자 행은 남습니다.
즉 귀속 정보는 영구적입니다. 재활성화하면 로그인은 복원되지만 멤버십은
절대 다시 부여되지 않습니다.
제외되는 것
섹션 제목: “제외되는 것”- 그룹→역할 매핑은 로그인 시점에 SAML 속성 / OIDC 클레임에서 이루어집니다. 푸시된 SCIM 그룹은 저장되지 않습니다.
- 소유자 역할은 SSO로 도달할 수 없습니다 — 기본 역할로도, 그룹 매핑을 통해서도 안 됩니다.
- WebSocket 릴레이는 절대 SSO 채널이 아닙니다. 릴레이를 통해서만 서버에 도달하는 휴대폰은 서버를 상대로 브라우저 왕복을 완료할 수 없습니다.