들어가며 — SSO는 절반의 해답입니다
엔터프라이즈 IAM을 이야기할 때 대부분의 논의는 SSO(Single Sign-On)에 집중됩니다. SAML이냐 OIDC냐, IdP는 무엇을 쓰느냐 같은 주제입니다. 하지만 실무에서 SSO를 도입해 본 분이라면 곧바로 다음 질문에 부딪히게 됩니다.
"로그인은 되는데, 계정은 누가 만들어 주나요? 퇴사자는 누가 지우나요?"
이것이 바로 **프로비저닝(provisioning)** 의 문제이고, 2026년 현재 이 문제의 사실상 표준 해답이 **SCIM 2.0(System for Cross-domain Identity Management)** 입니다. Zero Trust가 "identity-first"를 외치는 시대에, 계정 수명주기 자동화는 더 이상 선택이 아니라 보안의 기본 전제입니다. AI 에이전트 같은 non-human identity까지 관리 대상에 들어오면서, 수작업 계정 관리는 운영적으로도 보안적으로도 유지가 불가능해졌습니다.
이 글에서는 SCIM 2.0의 스펙 구조부터 실제 HTTP 예제, 주요 IdP(Okta, Microsoft Entra ID, Keycloak)의 지원 현황, 그리고 SCIM 서버를 직접 구현할 때 빠지기 쉬운 함정까지 실무 관점에서 정리합니다.
왜 프로비저닝이 SSO만큼 중요한가 — JML 수명주기
계정 수명주기는 흔히 **JML(Joiner / Mover / Leaver)** 모델로 설명합니다.
| 단계 | 이벤트 | 필요한 작업 | 실패 시 리스크 |
| --- | --- | --- | --- |
| Joiner | 입사, 신규 채용 | 계정 생성, 그룹/권한 부여 | 온보딩 지연, 생산성 손실 |
| Mover | 부서 이동, 직무 변경 | 권한 재조정, 그룹 변경 | 권한 누적(privilege creep) |
| Leaver | 퇴사, 계약 종료 | 계정 비활성화, 세션/토큰 회수 | 퇴사자 접근, 정보 유출 |
SSO는 "인증의 순간"만 다룹니다. 반면 JML은 "계정의 일생"을 다룹니다. SSO만 있고 프로비저닝이 없으면 다음과 같은 일이 벌어집니다.
- 신규 입사자가 SaaS 앱마다 관리자에게 계정 생성을 요청하며 며칠을 허비합니다.
- 부서를 옮긴 직원이 이전 부서의 권한을 그대로 들고 다닙니다. 감사(audit)에서 가장 많이 지적되는 항목입니다.
- 퇴사자의 IdP 계정은 막혔지만, JIT(Just-in-Time)로 생성된 SaaS 로컬 계정과 API 토큰은 살아 있습니다. 실제 침해 사고의 단골 시나리오입니다.
특히 Leaver 처리는 보안 사고와 직결됩니다. SSO 차단은 "새 로그인"만 막을 뿐, 이미 발급된 세션·리프레시 토큰·앱 로컬 계정은 별도로 회수해야 합니다. 이 회수를 자동화하는 표준 통로가 SCIM의 deprovisioning입니다.
SCIM 2.0 스펙 구조 — RFC 세 개로 이루어진 표준
SCIM 2.0은 2015년 IETF에서 세 개의 RFC로 표준화되었습니다.
| RFC | 제목 | 역할 |
| --- | --- | --- |
| [RFC 7642](https://datatracker.ietf.org/doc/html/rfc7642) | Definitions, Overview, Concepts, and Requirements | 용어 정의, 유스케이스, 요구사항 |
| [RFC 7643](https://datatracker.ietf.org/doc/html/rfc7643) | Core Schema | User/Group 리소스 스키마, 확장 모델 |
| [RFC 7644](https://datatracker.ietf.org/doc/html/rfc7644) | Protocol | REST API, 필터, PATCH, 벌크, 페이지네이션 |
구조를 그림으로 보면 다음과 같습니다.
+----------------------------+ +----------------------------+
| IdP / HR 시스템 | SCIM | SP (SaaS 앱) |
| (SCIM 클라이언트) | ------> | (SCIM 서버) |
| | HTTPS | |
| - Okta | | - POST /Users |
| - Entra ID | | - GET /Users?filter= |
| - Keycloak + 커스텀 | | - PATCH /Users/id |
| - Workday 등 HR | | - DELETE /Users/id |
+----------------------------+ +----------------------------+
RFC 7642: 개념 RFC 7643: 스키마 RFC 7644: 프로토콜
핵심 포인트는 역할 분담입니다. 일반적으로 **IdP가 SCIM 클라이언트**, **SaaS 앱(SP)이 SCIM 서버**입니다. "우리 서비스가 SCIM을 지원한다"는 말은 곧 "우리가 SCIM 서버를 구현했다"는 뜻입니다.
User / Group 스키마와 확장 모델
Core User 스키마
RFC 7643이 정의하는 User 리소스의 대표 속성은 다음과 같습니다.
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "2819c223-7f76-453a-919d-413861904646",
"externalId": "emp-10042",
"userName": "yjkim@example.com",
"name": {
"familyName": "Kim",
"givenName": "Youngju"
},
"displayName": "Youngju Kim",
"emails": [
{
"value": "yjkim@example.com",
"type": "work",
"primary": true
}
],
"active": true,
"groups": [
{
"value": "e9e30dba-f08f-4109-8486-d5c6a331660a",
"display": "platform-team"
}
],
"meta": {
"resourceType": "User",
"created": "2026-06-12T09:00:00Z",
"lastModified": "2026-06-12T09:00:00Z",
"version": "W/\"3694e05e9dff590\"",
"location": "https://api.example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646"
}
}
주의 깊게 봐야 할 속성들입니다.
- **id** — SCIM 서버(SP)가 발급하는 불변 식별자입니다. 클라이언트가 정하지 않습니다.
- **externalId** — 클라이언트(IdP/HR) 쪽 식별자입니다. 상호 매핑의 핵심이며, 직원 번호처럼 불변 값을 쓰는 것이 좋습니다.
- **userName** — 서버 내에서 유일해야 합니다. 이메일을 쓰는 경우가 많지만, 이메일은 변경될 수 있다는 점이 함정입니다.
- **active** — deprovisioning의 핵심입니다. DELETE 대신 active를 false로 바꾸는 soft-delete 패턴이 일반적입니다.
- **meta.version** — ETag 값으로, 동시성 제어에 사용됩니다.
Group 스키마
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "e9e30dba-f08f-4109-8486-d5c6a331660a",
"displayName": "platform-team",
"members": [
{
"value": "2819c223-7f76-453a-919d-413861904646",
"display": "yjkim@example.com",
"type": "User"
}
]
}
Group은 단순해 보이지만 실무에서 가장 골치 아픈 리소스입니다. 수천 명이 속한 그룹의 members를 통째로 PUT으로 교체하는 IdP가 있는가 하면, PATCH로 한 명씩 add/remove하는 IdP도 있어서 서버 구현이 양쪽을 모두 견뎌야 합니다.
Enterprise User 확장과 커스텀 확장
HR 속성(부서, 사번, 매니저 등)은 Enterprise User 확장 스키마로 표현합니다.
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"userName": "yjkim@example.com",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "10042",
"department": "Platform Engineering",
"costCenter": "CC-3120",
"manager": {
"value": "26118915-6090-4610-87e4-49d8ca9f808d",
"displayName": "Jane Doe"
}
}
}
자체 속성이 필요하면 자신의 URN 네임스페이스로 커스텀 확장 스키마를 정의할 수 있고, 서버는 /Schemas 엔드포인트로 이를 광고합니다. 다만 커스텀 확장은 IdP 쪽 속성 매핑 UI가 지원해야 실제로 쓸 수 있으므로, 가능하면 Enterprise 확장 범위 안에서 해결하는 것이 호환성에 유리합니다.
프로토콜 — REST 엔드포인트와 HTTP 예제
RFC 7644가 정의하는 엔드포인트 전체 목록입니다.
| 메서드 | 경로 | 용도 |
| --- | --- | --- |
| POST | /Users | 사용자 생성 |
| GET | /Users | 목록 조회 + filter 검색 |
| GET | /Users/id | 단건 조회 |
| PUT | /Users/id | 전체 교체 |
| PATCH | /Users/id | 부분 수정 |
| DELETE | /Users/id | 삭제 |
| GET | /ServiceProviderConfig | 서버 기능 광고 |
| GET | /Schemas | 스키마 광고 |
| GET | /ResourceTypes | 리소스 타입 광고 |
| POST | /Bulk | 벌크 연산(선택) |
사용자 생성 — POST
POST /scim/v2/Users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"externalId": "emp-10042",
"userName": "yjkim@example.com",
"name": { "givenName": "Youngju", "familyName": "Kim" },
"emails": [{ "value": "yjkim@example.com", "type": "work", "primary": true }],
"active": true
}
성공 시 서버는 201 Created와 함께 id, meta가 채워진 전체 리소스를 반환합니다. userName이 이미 존재하면 409 Conflict에 SCIM 에러 바디를 담아 응답해야 합니다.
HTTP/1.1 409 Conflict
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"scimType": "uniqueness",
"detail": "userName yjkim@example.com already exists",
"status": "409"
}
필터 검색 — GET과 filter 쿼리
IdP는 푸시 전에 "이 사용자가 이미 있는가"를 filter로 확인합니다. 가장 빈번하게 호출되는 패턴입니다.
GET /scim/v2/Users?filter=userName%20eq%20%22yjkim%40example.com%22 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
디코딩하면 다음 필터입니다.
filter=userName eq "yjkim@example.com"
RFC 7644의 필터 문법은 eq, ne, co(contains), sw(starts with), gt, ge, lt, le, pr(present) 연산자와 and/or/not 결합, 괄호, 복합 속성 경로까지 지원합니다.
filter=emails[type eq "work" and value co "@example.com"]
filter=meta.lastModified gt "2026-06-01T00:00:00Z"
filter=userName sw "yj" and active eq true
다만 실제 IdP들이 쓰는 필터는 거의 "userName eq ..." 와 "externalId eq ..." 두 가지입니다. 서버 구현 시 전체 필터 문법을 완벽 지원하기보다, 최소 이 두 패턴을 정확히 지원하고 나머지는 점진 확장하는 전략이 현실적입니다.
부분 수정 — PATCH
PATCH는 SCIM에서 가장 복잡한 부분입니다. add / remove / replace 세 가지 op를 가진 연산 목록을 전달합니다.
PATCH /scim/v2/Users/2819c223-7f76-453a-919d-413861904646 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "replace", "path": "active", "value": false },
{ "op": "replace", "path": "name.familyName", "value": "Lee" },
{
"op": "add",
"path": "emails",
"value": [{ "value": "yj.lee@example.com", "type": "work" }]
}
]
}
그룹 멤버십 조작은 value filter가 들어간 path를 사용합니다.
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
"path": "members[value eq \"2819c223-7f76-453a-919d-413861904646\"]"
},
{
"op": "add",
"path": "members",
"value": [{ "value": "08b0fe34-0ec4-4857-b8e2-58dbccca2f48" }]
}
]
}
전체 교체 — PUT
PUT은 리소스 전체를 교체합니다. 단순하지만 위험합니다. 클라이언트가 모르는 속성(서버가 자체 관리하는 속성)까지 날려버릴 수 있기 때문에, 서버는 readOnly/immutable 속성을 PUT 바디와 무관하게 보존해야 합니다.
푸시 모델 vs 풀 모델
| 구분 | 푸시(IdP → SP) | 풀(SP → IdP/HR) |
| --- | --- | --- |
| 방향 | IdP가 변경 발생 시 SP의 SCIM API 호출 | SP가 주기적으로 원천을 폴링 |
| 지연 | 거의 실시간(이벤트 기반) | 폴링 주기에 의존(수십 분~수 시간) |
| 대표 사례 | Okta, Entra ID의 SaaS 프로비저닝 | Entra ID의 온프레미스 HR 연동 일부 |
| SP 구현 부담 | SCIM 서버 구현 필요 | SCIM 클라이언트 구현 필요 |
| 장애 처리 | IdP의 재시도 큐에 의존 | 다음 폴링에서 자연 복구 |
업계 주류는 **푸시 모델**입니다. Okta와 Entra ID 모두 자사 디렉터리의 변경 이벤트를 감지해 SP의 SCIM 엔드포인트로 푸시합니다. 흥미로운 점은 Entra ID의 동작 방식인데, 순수 이벤트 푸시가 아니라 약 40분 주기의 동기화 사이클 안에서 변경분을 모아 푸시합니다. "Entra 프로비저닝이 즉시 반영되지 않아요"라는 문의의 대부분이 이 주기 때문입니다.
푸시 모델에서 SP가 반드시 고려해야 할 것은 **멱등성**입니다. IdP는 네트워크 오류 시 같은 요청을 재시도하므로, 같은 externalId의 POST가 두 번 와도 중복 계정이 생기면 안 됩니다.
주요 IdP의 SCIM 지원 현황 (2026)
Okta
- **클라이언트 역할**: 수천 개의 OIN(Okta Integration Network) 앱에 SCIM 푸시를 지원합니다. 커스텀 앱도 SCIM 2.0 템플릿으로 연동 가능합니다.
- **서버 역할**: Okta 자체도 SCIM 서버 API를 제공하여, 외부 시스템이 Okta 사용자를 SCIM으로 관리할 수 있습니다.
- 그룹 푸시(Group Push)는 별도 기능으로 분리되어 있고, 그룹 멤버십 동기화는 PATCH 기반입니다.
Microsoft Entra ID (구 Azure AD)
- 엔터프라이즈 앱의 프로비저닝 기능이 SCIM 2.0 기반입니다. 갤러리 앱뿐 아니라 "비갤러리 앱 + SCIM 엔드포인트" 조합도 지원합니다.
- 동기화 주기(약 40분)와 초기 동기화/증분 동기화 구분이 있으며, On-demand provisioning으로 특정 사용자를 즉시 푸시해 디버깅할 수 있습니다.
- Entra의 SCIM 클라이언트는 스펙과 미묘하게 다른 부분이 있었던 것으로 유명합니다(예: 과거의 PATCH 형식 이슈). 서버 구현 시 Entra 호환성 테스트는 필수입니다.
Keycloak
- **Keycloak 26.6.x(2026-05 기준 26.6.2)까지도 SCIM은 코어 기능이 아닙니다.** 이 점을 모르고 "Keycloak 쓰면 SCIM 되겠지"라고 가정하면 곤란해집니다.
- 선택지는 세 가지입니다. (1) 커뮤니티 확장(scim-for-keycloak 등)으로 Keycloak을 SCIM 서버화, (2) Keycloak 이벤트 리스너 SPI로 변경 이벤트를 받아 자체 SCIM 클라이언트 구현, (3) Keycloak 26.6의 Workflows 기능으로 realm 내 수명주기 자동화를 일부 대체.
- Keycloak 26.6의 [Workflows](https://www.keycloak.org/docs/latest/release_notes/index.html)는 "비활성 사용자 자동 비활성화" 같은 realm 내부 자동화에는 유용하지만, 외부 SP로의 프로비저닝은 여전히 별도 구현이 필요합니다.
이벤트 리스너 SPI로 푸시형 연동을 구현하는 골격은 다음과 같습니다.
public class ScimPushEventListener implements EventListenerProvider {
private final ScimClient scimClient;
@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
if (event.getResourceType() != ResourceType.USER) {
return;
}
switch (event.getOperationType()) {
case CREATE -> scimClient.createUser(toScimUser(event));
case UPDATE -> scimClient.patchUser(toScimPatch(event));
case DELETE -> scimClient.deactivateUser(extractUserId(event));
}
}
@Override
public void close() {
// no-op
}
}
실제로는 이벤트 유실에 대비한 아웃박스(outbox) 테이블과 재시도 큐를 함께 두는 것이 안전합니다.
SCIM 서버 구현 시 함정들
함정 1 — PATCH 연산의 복잡성
PATCH는 SCIM 구현 난이도의 8할을 차지합니다.
- **path 없는 PATCH**: op에 path가 생략되면 value가 "부분 리소스"가 되어 속성 단위로 병합해야 합니다. Entra ID가 즐겨 쓰는 패턴이었습니다.
- **value filter path**: members[value eq "..."] 같은 경로는 사실상 미니 쿼리 언어 파서가 필요합니다.
- **다중 값 속성의 의미론**: emails에 add하면 추가인가 교체인가, primary가 두 개가 되면 어떻게 할 것인가를 스펙(RFC 7643 2.4절)에 따라 정확히 처리해야 합니다.
- **대소문자**: SCIM 속성 이름은 대소문자 비구분(case-insensitive)입니다. "userName"과 "username"을 같게 처리해야 합니다. 의외로 많은 구현이 이걸 놓칩니다.
직접 파서를 만들기보다 검증된 라이브러리(Java라면 UnboundID SCIM 2 SDK 등)를 쓰는 것을 권합니다.
함정 2 — ETag와 동시성
HR 시스템과 IdP 관리자가 동시에 같은 사용자를 수정하면 마지막 쓰기가 이깁니다(lost update). SCIM은 이를 위해 ETag 기반 낙관적 잠금을 정의합니다.
PUT /scim/v2/Users/2819c223-7f76-453a-919d-413861904646 HTTP/1.1
If-Match: W/"3694e05e9dff590"
Content-Type: application/scim+json
버전이 다르면 서버는 412 Precondition Failed를 반환합니다. 다만 현실에서는 대부분의 IdP 클라이언트가 If-Match를 보내지 않으므로, 서버는 ETag를 "지원하되 강제하지 않는" 수준으로 구현하고 ServiceProviderConfig에 정직하게 광고하는 것이 맞습니다.
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"patch": { "supported": true },
"bulk": { "supported": false, "maxOperations": 0, "maxPayloadSize": 0 },
"filter": { "supported": true, "maxResults": 200 },
"etag": { "supported": true },
"changePassword": { "supported": false },
"sort": { "supported": false },
"authenticationSchemes": [
{
"type": "oauthbearertoken",
"name": "OAuth Bearer Token",
"description": "Authorization header with Bearer token"
}
]
}
함정 3 — 페이지네이션
SCIM의 기본 페이지네이션은 1부터 시작하는 startIndex 기반입니다.
GET /scim/v2/Users?startIndex=101&count=100 HTTP/1.1
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 5042,
"startIndex": 101,
"itemsPerPage": 100,
"Resources": []
}
함정은 다음과 같습니다.
- startIndex는 0이 아니라 **1부터** 시작합니다. off-by-one 버그의 단골입니다.
- 오프셋 기반이므로 페이지를 넘기는 동안 데이터가 변하면 누락/중복이 발생합니다. 대량 동기화 중 발생하는 미묘한 불일치의 원인입니다. 커서 기반 페이지네이션을 위한 확장 드래프트가 논의되어 왔지만, 클라이언트 호환성 때문에 startIndex는 반드시 지원해야 합니다.
- totalResults를 정확히 계산하는 count 쿼리가 대규모 테이블에서 비싸질 수 있습니다. DB 인덱스 설계에 반영해야 합니다.
함정 4 — 에러 응답 형식
SCIM 클라이언트는 에러 바디의 scimType을 보고 동작을 분기합니다. 401/403/404/409/412를 정확한 SCIM 에러 형식으로 반환하지 않으면, IdP가 "엔드포인트 비정상"으로 판단해 프로비저닝을 격리(quarantine)시키는 경우가 있습니다. Entra ID의 quarantine 상태가 대표적입니다.
Deprovisioning과 보안
Leaver 처리는 보안 관점에서 가장 중요한 흐름입니다. 권장 설계는 다음과 같습니다.
[HR: 퇴사 처리]
|
v
[IdP: 계정 비활성화] ----> SCIM PATCH active=false ----> [SP: soft-delete]
| |
v v
[IdP 세션 전부 폐기] [SP 세션/리프레시 토큰 폐기]
[API 키, PAT 무효화]
[보존 기간 후 hard-delete]
핵심 원칙입니다.
1. **DELETE보다 active=false 우선** — 감사 추적과 데이터 보존(법적 요구) 때문에 즉시 hard-delete는 피합니다. 다수 IdP도 기본 동작이 "비활성화"입니다.
2. **SCIM은 세션을 죽이지 않습니다** — active=false는 "새 인증"을 막을 뿐입니다. SP는 이 신호를 받으면 해당 사용자의 활성 세션과 리프레시 토큰을 능동적으로 폐기해야 합니다. 이 간극을 메우는 보완 표준이 OpenID의 [Shared Signals Framework / CAEP](https://openid.net/wg/sharedsignals/)이며, 2026년 현재 주요 벤더들이 속속 채택 중입니다.
3. **non-human identity도 잊지 말 것** — 퇴사자가 만든 서비스 계정, 봇 토큰, AI 에이전트 위임 권한은 SCIM 범위 밖인 경우가 많습니다. 소유권 이전 프로세스를 별도로 설계해야 합니다.
HR-driven Provisioning 아키텍처
성숙한 조직의 최종 형태는 HR 시스템을 단일 진실 원천(source of truth)으로 삼는 구조입니다.
+-----------+ +---------------------+ +----------------------+
| HR 시스템 | --> | IdP | --> | SaaS 앱 1 (SCIM 서버) |
| (Workday | | (Okta / Entra / | --> | SaaS 앱 2 (SCIM 서버) |
| 등) | | Keycloak) | --> | 사내 앱 (SCIM 서버) |
+-----------+ +---------------------+ +----------------------+
입사/이동/퇴사 그룹/권한 매핑 규칙 계정/권한 반영
이벤트 발행 (부서 -> 그룹 -> 앱 롤) 세션/토큰 회수
설계 포인트입니다.
- **속성 매핑 규칙의 중앙화**: "부서 코드 3120은 platform-team 그룹, platform-team은 앱 X의 admin 롤"처럼 매핑을 IdP 한 곳에서 관리합니다.
- **개시일/종료일 선반영**: HR 데이터에는 미래의 입사일/퇴사일이 있습니다. 입사일 전에 계정을 만들되 비활성으로 두고, 퇴사일 0시에 자동 비활성화하는 스케줄링이 이상적입니다.
- **예외 흐름**: 계약직, 파트너, 인수합병으로 들어온 조직 등 HR 시스템에 없는 신원에 대한 별도 등록 경로와 만료 정책이 필요합니다.
테스트 전략
SCIM 서버를 출시하기 전 점검할 테스트 매트릭스입니다.
| 카테고리 | 테스트 항목 |
| --- | --- |
| 생성 | POST 정상, 중복 userName 409, 필수 속성 누락 400 |
| 조회 | userName/externalId filter, 대소문자 비구분, URL 인코딩 |
| 수정 | PATCH add/remove/replace, path 없는 PATCH, value filter path |
| 교체 | PUT 시 readOnly 속성 보존, 미포함 속성 처리 |
| 비활성화 | active=false 시 세션/토큰 회수 연동 |
| 페이지네이션 | startIndex=1 기준, 경계값, 빈 결과 |
| 동시성 | ETag 불일치 412, 동시 PATCH |
| 인증 | 만료 토큰 401, 권한 부족 403 |
| 멱등성 | 동일 POST 재시도, 동일 PATCH 재적용 |
도구 측면에서는 다음을 활용할 수 있습니다.
- **Microsoft SCIM Validator**: Entra 호환성을 자동 점검해 줍니다.
- **Okta SCIM 테스트(Runscope 컬렉션 기반)**: OIN 등록 전 필수 통과 항목입니다.
- **curl 스모크 테스트**: CI에 넣기 좋은 최소 검증 예시입니다.
#!/usr/bin/env bash
set -euo pipefail
BASE="https://api.example.com/scim/v2"
TOKEN="$SCIM_TEST_TOKEN"
1. 생성
USER_ID=$(curl -sf -X POST "$BASE/Users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/scim+json" \
-d @fixtures/user-create.json | jq -r '.id')
2. filter 조회
curl -sf "$BASE/Users?filter=userName%20eq%20%22scim-test%40example.com%22" \
-H "Authorization: Bearer $TOKEN" | jq -e '.totalResults == 1'
3. 비활성화
curl -sf -X PATCH "$BASE/Users/$USER_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations":[{"op":"replace","path":"active","value":false}]}' \
| jq -e '.active == false'
4. 정리
curl -sf -X DELETE "$BASE/Users/$USER_ID" -H "Authorization: Bearer $TOKEN"
echo "SCIM smoke test passed"
스테이징 환경에 실제 IdP 테넌트(Okta developer org, Entra 테스트 테넌트)를 연결해 두고, 릴리스마다 실 IdP 발 트래픽으로 회귀 테스트를 돌리는 것이 가장 확실합니다. IdP별 구현 편차는 문서만으로는 절대 다 잡히지 않습니다.
운영 베스트 프랙티스
- **인증은 OAuth Bearer 토큰으로, 토큰은 회전 가능하게**: 장수명 고정 토큰 하나로 운영하다 유출되면 전체 디렉터리가 노출됩니다. 토큰 회전 절차와 만료 모니터링을 갖춥니다.
- **SCIM 엔드포인트는 별도 rate limit과 감사 로그**: 누가(어느 IdP 테넌트가) 언제 무엇을 바꿨는지 모두 남깁니다. SOC 2 / ISO 27001 감사의 단골 증적입니다.
- **프로비저닝 드리프트 감지**: IdP의 사용자 목록과 SP의 사용자 목록을 주기적으로 대사(reconcile)하는 배치를 둡니다. 푸시 이벤트 유실은 반드시 발생한다고 가정해야 합니다.
- **스키마 변경은 하위 호환으로**: 이미 연동된 고객사의 속성 매핑을 깨지 않도록, 속성 제거/의미 변경은 신규 버전 경로로 분리합니다.
마치며
SCIM 2.0은 화려한 기술이 아닙니다. 그러나 SSO가 "들어오는 문"이라면 SCIM은 "명부 관리"이고, 보안 사고의 다수는 문이 아니라 명부에서 터집니다. 2026년의 엔터프라이즈 B2B 시장에서 SCIM 지원은 SSO와 함께 엔터프라이즈 딜의 체크리스트 항목이 된 지 오래입니다.
정리하면 다음과 같습니다.
1. SSO와 프로비저닝은 별개의 문제이며, JML 수명주기 전체를 자동화해야 비로소 identity-first 보안이 성립합니다.
2. SCIM 2.0은 RFC 7642(개념), 7643(스키마), 7644(프로토콜) 세 축으로 이해하면 됩니다.
3. 서버 구현의 난소는 PATCH 의미론, 페이지네이션, 에러 형식, 멱등성입니다. 검증된 SDK와 IdP 실연동 테스트로 잡으십시오.
4. deprovisioning은 active=false로 끝나지 않습니다. 세션·토큰 회수까지 설계해야 하며, 장기적으로는 Shared Signals/CAEP 같은 이벤트 표준과의 결합을 바라봐야 합니다.
다음 글에서는 멀티테넌트 SaaS에서 고객사별 SSO를 어떻게 설계하는지, 그리고 그 위에서 SCIM 온보딩을 어떻게 셀프서비스화하는지 다루겠습니다.
참고 자료
- [RFC 7642 — SCIM: Definitions, Overview, Concepts, and Requirements](https://datatracker.ietf.org/doc/html/rfc7642)
- [RFC 7643 — SCIM: Core Schema](https://datatracker.ietf.org/doc/html/rfc7643)
- [RFC 7644 — SCIM: Protocol](https://datatracker.ietf.org/doc/html/rfc7644)
- [RFC 6749 — The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)
- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)
- [Okta — SCIM 프로토콜 문서](https://developer.okta.com/docs/reference/scim/)
- [Microsoft Entra ID — SCIM 엔드포인트 개발 가이드](https://learn.microsoft.com/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups)
- [Keycloak Documentation](https://www.keycloak.org/documentation)
- [Keycloak 릴리스 노트 (26.6 Workflows 등)](https://www.keycloak.org/docs/latest/release_notes/index.html)
- [OpenID Shared Signals Framework / CAEP](https://openid.net/wg/sharedsignals/)
- [UnboundID SCIM 2 SDK for Java](https://github.com/pingidentity/scim2)
현재 단락 (1/342)
엔터프라이즈 IAM을 이야기할 때 대부분의 논의는 SSO(Single Sign-On)에 집중됩니다. SAML이냐 OIDC냐, IdP는 무엇을 쓰느냐 같은 주제입니다. 하지만 실무에...