Entra ID(旧 Azure AD)のSCIMグループ連携で、メンバーを複数人追加したはずなのに最後の1人しか残らない──そんな「メンバー全置換トラブル」の原因は、多くの場合 Entra 側ではなく、こちらの SCIM サーバ実装にあります。この記事では、その仕組みと根本原因、そして安全に解消するための実践的な対処方法を詳しく解説します。
Entra SCIM 連携で起きる「replace 問題」とは
まずは現場で実際によく起きている症状を整理します。
想定している挙動
- SCIM 対応アプリに グループ A が存在
- Entra ID 側でグループ A にユーザーを 4 名(U1〜U4)追加
- Entra から SCIM サーバへは、以下のような PATCH が 4 回 飛んでくる想定
- op:
add - path:
members - value: それぞれ 1 ユーザー分の
{ \"value\": \"user_id\" }
- op:
- 結果として、SCIM 側のグループ A のメンバーは U1〜U4 の 4 名になる
実際に起きている挙動
しかし、トラブルが発生している環境では次のようになります。
- Entra からは PATCH が 4 回飛んでくるが、すべて
op: "replace"path: "members"value: [{ "value": "ユーザーX" }]
- SCIM サーバは仕様どおり
replaceを「全置換」として処理 - 結果として、最後に処理した 1 ユーザー(例: U4)だけがメンバーとして残る
| 項目 | 期待される挙動 | 実際の挙動(問題発生時) |
|---|---|---|
| PATCH の op | add | replace |
| PATCH 1 回分の value | 追加したい 1 ユーザー | 1 ユーザー(ただし全置換) |
| 最終的なメンバー | 4 ユーザー(U1〜U4) | 最後の 1 ユーザーのみ(例: U4) |
この「すべて replace で飛んでくる」という挙動は、Entra の仕様そのものというより、SCIM サーバの GET /Groups 実装が引き金になっているケースが多いです。
SCIM と Entra ID のグループ同期仕様をおさらい
Entra ID の SCIM 実装のポイント
Microsoft 公式ドキュメントでは、Entra の SCIM 実装において、グループ連携では次のような前提が示されています。
- SCIM 2.0 準拠の API(特に
/Usersと/Groups)を実装する - グループ取得時には
excludedAttributes=membersを付けてリクエストを送る - つまり、Entra は「グループのメンバー一覧は基本的に読まない」前提で動く
ドキュメント中でも、SCIM エンドポイントが満たすべき要件の一つとして、グループ取得で excludedAttributes=members をサポートすることが明記されています。
SCIM 2.0 の attributes / excludedAttributes 仕様
SCIM 2.0(RFC 7644)では、attributes と excludedAttributes のクエリパラメータは、レスポンスに含める/含めない属性を制御するために使われます。
attributes: 指定した属性だけを返すexcludedAttributes: デフォルトで返す属性セットから、指定した属性を除外する- サービスプロバイダ(SCIM サーバ)は、これらの指定を必ず尊重しなければならない(MUST)
つまり、Entra から次のようなリクエストが来た場合:
GET /scim/v2/Groups/abc123?excludedAttributes=members
SCIM サーバは、レスポンスから members 属性を除外する必要があります。
Entra のグループ同期の考え方
公式ドキュメントの「Group provisioning and deprovisioning」のセクションでは、Entra がグループを扱う際の特徴として次の点が挙げられています。
- グループ取得リクエストでは
membersを除外して送る - メンバーの追加・削除は PATCH で個別に行う
- Entra 側は「どのメンバーを増減させるか」の差分を計算し、SCIM に対して
add/removeを発行する
ここまでをまとめると、本来の姿はこうです。
| フェーズ | Entra のリクエスト | SCIM サーバの正しい応答 |
|---|---|---|
| グループ情報の取得 | GET /Groups/{id}?excludedAttributes=members | members を含まないグループ情報 |
| メンバー追加 | PATCH op: "add" path: "members" | 既存 members に対して「追加」処理 |
| メンバー削除 | PATCH op: "remove" path: "members[value eq \"...\"]" | 該当メンバーを削除 |
ところが、SCIM サーバ側が excludedAttributes=members を無視してしまうと、この期待が崩れます。
なぜ PATCH が「add」ではなく「replace」になるのか
原因は GET レスポンスに members を含めていたこと
Microsoft Q&A では、今回とまったく同じ現象(4 人追加したのに 1 人だけになる)が報告されており、投稿者が次のように原因を特定しています。
- Entra は、グループの GET 応答に
membersが含まれている場合、メンバー更新にreplaceを使ってしまう - しかし、その GET リクエストには
excludedAttributes=membersが付いている - 本来は
membersを返してはいけないが、SCIM サーバが返してしまっていた - → SCIM サーバを修正し、
membersを返さないようにすると、Entra からの PATCH がaddに戻り、問題が解消した
つまり、Entra の内部ロジックはざっくり次のようなイメージになっていると考えられます(擬似的なイメージです)。
- グループを GET(本来は
membersなしで返ってくる前提) - Entra 側の「あるべきメンバー構成」と比較して差分を計算
- GET レスポンスに
membersが 含まれない → 「サービス側の現在メンバーは知らない」とみなし、個別にadd/removeを送る - GET レスポンスに
membersが 含まれる → 「サービス側の現在メンバーはこれだ」とみなし、全体をreplaceで置き換えようとする
- GET レスポンスに
このとき、Entra が「今この PATCH で送りたいメンバー」を 1 人ずつしか含めていない場合、SCIM サーバ側では members が毎回「1 人だけの配列」で上書きされてしまい、最終的に 1 人しか残らない、というわけです。
実際のやり取りを時系列で見る
よくあるパターンを簡略化して追ってみます。
- Entra がグループ A を取得
GET /scim/v2/Groups/abc123?excludedAttributes=membersしかし、SCIM サーバは誤って次のようなレスポンスを返してしまう:{ "id": "abc123", "displayName": "Sales", "members": [{ "value": "U0" }] } - Entra では「現在の members = {U0}」と認識してしまう
- Entra で U1〜U4 をメンバーに追加
- Entra が差分計算を行い、4 回の PATCH を発行(ただし全て
replace){ "op": "replace", "path": "members", "value": [{ "value": "U1" }] } { "op": "replace", "path": "members", "value": [{ "value": "U2" }] } ... - SCIM サーバは RFC に従って「全置換」として処理
- PATCH1 後: members = {U1}
- PATCH2 後: members = {U2}
- …
- PATCH4 後: members = {U4}
こうして、「最後の 1 人だけが残る」状態が再現されます。
これは仕様なのか? Microsoft の見解
SCIM 仕様上の意味づけ
SCIM 2.0 仕様では、PATCH の op は次のような意味を持ちます(要約)。
| op | 意味 | 多値属性(members)の場合 |
|---|---|---|
add | 属性に値を追加 | 既存の members に要素を追加 |
remove | 属性から値を削除 | 条件に合う members を削除 |
replace | 属性全体を置き換え | members 全部が置き換わる |
ですので、「replace が飛んでくる → メンバーが全置換される」のは SCIM 的には正しい挙動です。ただし、グループメンバー追加のユースケースにおいては望ましい挙動とは言えません。
Microsoft ドキュメント・Q&A での立場
Microsoft の公式ドキュメントのサンプルでは、「Update Group [Add Members]」の例として、op: "add" を使った PATCH が示されています。
また、Microsoft Q&A では、SCIM Validator がグループの members に対して replace を送っているケースに対して、Microsoft 社員が次のように回答しています。
members属性に対するreplaceを送っているのは SCIM Validator 側の誤り- Entra の本番の SCIM プロビジョニングサービスは、グループメンバーの追加・削除に対しては
add/removeのみを送るべき
さらに、先ほど引用した「Unexpected SCIM Group Membership Updates in Azure」のスレッドでは、Microsoft サポートがこのシナリオを「ドキュメントの期待と異なる挙動」と認識しており、調査のためのサポートチケットを推奨しています。
これらを総合すると、
- 仕様(RFC)的には
replaceは合法だが、 - Microsoft の設計意図としては「グループメンバー追加時は
addを使うべき」 - 今回のように
replaceが連発されるのは、「SCIM サーバの GET 応答が不正」+「Entra 側のロジックの弱さ」が合わさった 望ましくない挙動
という整理になります。
根本解決策:GET 応答で attributes / excludedAttributes を厳守する
もっとも効果的で安全な対処は、SCIM サーバ側の GET 実装を修正することです。ここが直れば、多くの環境で Entra からの PATCH は自然と add 中心の挙動に戻ります。
ステップ 1: Entra からの GET リクエストを確認する
まずは HTTP ログを取得し、Entra からどのようなリクエストが飛んできているか確認します。
GET /scim/v2/GroupsGET /scim/v2/Groups/{id}
このとき、クエリパラメータに次のようなものが付いていないかを確認します。
?excludedAttributes=members- もしくは
?attributes=id,displayNameのようなattributes指定
ほとんどのケースで、Entra は excludedAttributes=members を付けてグループを取得しているはずです。
ステップ 2: GET /Groups のレスポンスを修正(最重要)
次に、SCIM サーバの GET /Groups / GET /Groups/{id} 実装を見直します。
NG 例:excludedAttributes を無視して members を返している
GET /scim/v2/Groups/abc123?excludedAttributes=members
{
"id": "abc123",
"displayName": "Sales",
"members": [
{ "value": "u1" },
{ "value": "u2" }
]
}
これは RFC 的にも Entra 的にも NG です。excludedAttributes=members が指定されているのに members を返してしまっています。
OK 例:members を返さない
GET /scim/v2/Groups/abc123?excludedAttributes=members
{
"id": "abc123",
"displayName": "Sales",
"meta": { "resourceType": "Group" }
}
また、attributes が指定されている場合も同様に、その属性だけを返す必要があります。
GET /scim/v2/Groups/abc123?attributes=id,displayName
{
"id": "abc123",
"displayName": "Sales"
}
実装上は、例えば次のようなイメージでフィルタリングします(疑似コード)。
function filterAttributes(resource, attributes, excludedAttributes) {
let result = {};
if (attributes) {
// attributes 指定がある場合は、指定された属性だけを返す
for (const name of attributes) {
if (resource.hasOwnProperty(name)) {
result[name] = resource[name];
}
}
} else {
// デフォルトセットから excludedAttributes を除外
result = { ...resource };
for (const name of excludedAttributes) {
delete result[name];
}
}
return result;
}
このフィルタリング処理を /Groups だけでなく、/Users を含む全体の実装で共通化しておくと、今後のトラブル防止にもつながります。
ステップ 3: PATCH の「replace」に対する暫定セーフガード
根本原因は GET 側ですが、過去に Entra から replace が飛んでくる可能性を考慮し、一時的な安全策として「ユニオン更新」を入れておくのも一案です。
例として、次のようなロジックです:
- 一時的な設定フラグ(例:
featureFlag.scimGroupReplaceIsAddLike = true)を用意 - PATCH の
op: "replace"かつpath: "members"のとき、- フラグが ON なら → 現在の members と value のユニオンを取り、「実質 add」として処理
- フラグが OFF なら → 仕様どおり「全置換」として処理
擬似コードのイメージ:
if (op === "replace" && path === "members") {
if (featureFlag.scimGroupReplaceIsAddLike) {
// 暫定運用:ユニオン更新
const current = loadCurrentMembers(groupId);
const incoming = extractMembersFromValue(value);
const merged = union(current, incoming);
saveMembers(groupId, merged);
} else {
// 本来の replace (全置換)
const incoming = extractMembersFromValue(value);
saveMembers(groupId, incoming);
}
}
重要なのは、これはあくまで暫定策であり、標準仕様の振る舞いではないという点です。本番環境で常時 ON にするのではなく、
- 検証環境や一部テナントのみ
- トラブルシューティング期間だけ
といった用途に絞ることをおすすめします。
ステップ 4: 実装修正後に再同期して挙動を確認
GET 応答の修正が完了したら、次の手順で動作確認を行います。
- Entra 側で対象グループを選び、オンデマンドプロビジョニング(またはフル同期)を実行
- SCIM サーバ側のログで、次の点を確認
GET /Groupsに対して、レスポンスにmembersが含まれなくなっている- グループメンバー追加時の PATCH が
op: "add"になっている
- 最終的に、SCIM 側のグループメンバーが Entra 側と一致していることを確認
Microsoft Q&A の事例でも、GET レスポンスから members を除外しただけで replace 問題が解消されたと報告されています。
ステップ 5: それでも改善しない場合は Microsoft サポートへ
もし、
- GET 応答で
membersを返さないようにした - HTTP ログでも間違いがない
- それでも Entra から
replaceが飛んでくる
という状況であれば、Entra 側のバグやテナント固有の問題の可能性があります。その場合は、
- ログ(リクエスト・レスポンスの抜粋)
- 問題が再現する具体的な手順
- SCIM サーバの実装概要
を添えて、Microsoft サポートにチケットを起票することをおすすめします。Microsoft 自身も Q&A で「この挙動はドキュメントと合っていないので、サポートチケットで調査させてほしい」と案内しています。
ログから原因を読み解くためのポイント
実際に原因を突き止めるには、SCIM ログを「時間軸」で眺めるのが有効です。例えば次のような視点でログを整理してみてください。
| 時刻 | HTTP メソッド / パス | 主な内容 | チェックポイント |
|---|---|---|---|
| T0 | GET /Groups/abc123?excludedAttributes=members | グループ情報取得 | レスポンス JSON に members が含まれていないか? |
| T1 | PATCH /Groups/abc123 | 1 回目のメンバー追加 | op が add か replace か? |
| T2 | PATCH /Groups/abc123 | 2 回目のメンバー追加 | value 内のメンバー数は?(1 人か複数か) |
| T3 | … | … | … |
特に次の 2 点を押さえると、原因究明が一気に進みます。
- GET 応答に
membersが含まれていないか - PATCH の
opとvalueの中身(1 回の PATCH で何人分送っているか)
これらを確認することで、「Entra がそもそも replace を投げているのか」「SCIM 側でパースを間違えていないか」を切り分けられます。
SCIM 実装で同じトラブルを防ぐためのベストプラクティス
今回の問題は、一言で言えば「SCIM 仕様どおりに attributes / excludedAttributes を扱っていない」ことから始まっています。ここでは、今後のトラブルを避けるための実践的なポイントをまとめます。
1. attributes / excludedAttributes の共通実装を作る
/Usersと/Groupsの両方で使える共通のフィルタリング関数を用意する- 新しいリソースタイプ(例: /Entitlements など)を追加する際も、その共通処理を利用する
- 単体テストを用意し、次のケースを必ず通す
?attributes=id,displayName?excludedAttributes=members?attributes=id,members.valueなどの複雑な指定
2. グループの GET では members を「重い属性」として扱う
多くのサービスでは、グループのメンバー数が何千、何万と膨らむ可能性があります。そのため、
- 基本方針として、GET /Groups では members を返さない(Entra 以外のクライアント向けにも同様)
- どうしても必要な場合にだけ、
?attributes=membersのような明示指定を要求する
こうしておくと、今回のような「想定外の attributes 返却」によるトラブルのリスクも下げられます。
3. 「SCIM Validator の挙動」と「本番サービスの挙動」を区別する
Microsoft の SCIM Validator や各種テストツールは便利ですが、ときどきツール側の挙動が実際の Entra 本番サービスと異なることがあります。先述の Q&A でも、
- SCIM Validator が
membersに対してreplaceを送るのはツール側の誤りであり、本番サービスの想定挙動ではない
と明言されています。
したがって、
- Validator でエラーや違和感のある挙動を見つけたら、「本番サービスでも同じか?」を切り分ける
- 本番の Entra テナントを使った小規模な検証環境で必ず再現確認する
といった運用が重要です。
4. ログに「相手(Entra)側の意図」が分かる情報を残す
トラブルシューティングのしやすさという観点では、次のようなログ出力もおすすめです。
- Entra からのリクエストヘッダ(特に
SCIM-SchemaやCorrelation-IDのようなもの) - 各 PATCH ごとに「before → after」のメンバー数、差分
- attributes / excludedAttributes の生の値
これらを残しておくと、「どの PATCH から挙動がおかしくなったのか」「Entra のどの操作と対応しているのか」を後から追いやすくなります。
まとめ:まずは GET の members を消すのが最短ルート
Entra(旧 Azure AD)と SCIM 連携したグループで、
- 複数メンバーを追加したのに最後の 1 人しか残らない
- PATCH の
opがすべてreplaceになっている
という症状が出ている場合、ほぼ確実にチェックすべきポイントは次の 3 つです。
GET /Groups/GET /Groups/{id}のレスポンスにmembersが含まれていないか(特にexcludedAttributes=membersが付いている場合)- attributes / excludedAttributes の処理が RFC 7644 の仕様どおりに実装されているか
- 修正後に Entra からの PATCH が
add/remove中心の挙動に変わっているか
根本解決の順番としては、
- ① SCIM サーバ側の GET 実装を修正(members を返さない)
- ② 再同期して PATCH が
addになっていることを確認 - ③ それでもダメなら Microsoft サポートにログ付きで問い合わせ
という流れがもっとも現実的で、かつ安全です。
Entra との SCIM 連携は、仕様やサンプルどおりに実装すれば非常に強力な自動プロビジョニング基盤になります。今回紹介したポイントを押さえておけば、「メンバーが勝手に消える」といった事故を防ぎつつ、安心して運用できるはずです。

コメント