Entra ID SCIM連携でグループメンバー追加がreplaceになる原因と対処法【Azure AD】

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\" }
  • 結果として、SCIM 側のグループ A のメンバーは U1〜U4 の 4 名になる

実際に起きている挙動

しかし、トラブルが発生している環境では次のようになります。

  • Entra からは PATCH が 4 回飛んでくるが、すべて
    • op: "replace"
    • path: "members"
    • value: [{ "value": "ユーザーX" }]
  • SCIM サーバは仕様どおり replace を「全置換」として処理
  • 結果として、最後に処理した 1 ユーザー(例: U4)だけがメンバーとして残る
項目期待される挙動実際の挙動(問題発生時)
PATCH の opaddreplace
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=membersmembers を含まないグループ情報
メンバー追加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 の内部ロジックはざっくり次のようなイメージになっていると考えられます(擬似的なイメージです)。

  1. グループを GET(本来は members なしで返ってくる前提)
  2. Entra 側の「あるべきメンバー構成」と比較して差分を計算
    • GET レスポンスに members が 含まれない → 「サービス側の現在メンバーは知らない」とみなし、個別に add / remove を送る
    • GET レスポンスに members が 含まれる → 「サービス側の現在メンバーはこれだ」とみなし、全体を replace で置き換えようとする

このとき、Entra が「今この PATCH で送りたいメンバー」を 1 人ずつしか含めていない場合、SCIM サーバ側では members が毎回「1 人だけの配列」で上書きされてしまい、最終的に 1 人しか残らない、というわけです。

実際のやり取りを時系列で見る

よくあるパターンを簡略化して追ってみます。

  1. Entra がグループ A を取得 GET /scim/v2/Groups/abc123?excludedAttributes=members しかし、SCIM サーバは誤って次のようなレスポンスを返してしまう: { "id": "abc123", "displayName": "Sales", "members": [{ "value": "U0" }] }
  2. Entra では「現在の members = {U0}」と認識してしまう
  3. Entra で U1〜U4 をメンバーに追加
  4. Entra が差分計算を行い、4 回の PATCH を発行(ただし全て replace) { "op": "replace", "path": "members", "value": [{ "value": "U1" }] } { "op": "replace", "path": "members", "value": [{ "value": "U2" }] } ...
  5. 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/Groups
  • GET /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 が飛んでくる可能性を考慮し、一時的な安全策として「ユニオン更新」を入れておくのも一案です。

例として、次のようなロジックです:

  1. 一時的な設定フラグ(例: featureFlag.scimGroupReplaceIsAddLike = true)を用意
  2. 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 応答の修正が完了したら、次の手順で動作確認を行います。

  1. Entra 側で対象グループを選び、オンデマンドプロビジョニング(またはフル同期)を実行
  2. SCIM サーバ側のログで、次の点を確認
    • GET /Groups に対して、レスポンスに members が含まれなくなっている
    • グループメンバー追加時の PATCH が op: "add" になっている
  3. 最終的に、SCIM 側のグループメンバーが Entra 側と一致していることを確認

Microsoft Q&A の事例でも、GET レスポンスから members を除外しただけで replace 問題が解消されたと報告されています。

ステップ 5: それでも改善しない場合は Microsoft サポートへ

もし、

  • GET 応答で members を返さないようにした
  • HTTP ログでも間違いがない
  • それでも Entra から replace が飛んでくる

という状況であれば、Entra 側のバグやテナント固有の問題の可能性があります。その場合は、

  • ログ(リクエスト・レスポンスの抜粋)
  • 問題が再現する具体的な手順
  • SCIM サーバの実装概要

を添えて、Microsoft サポートにチケットを起票することをおすすめします。Microsoft 自身も Q&A で「この挙動はドキュメントと合っていないので、サポートチケットで調査させてほしい」と案内しています。

ログから原因を読み解くためのポイント

実際に原因を突き止めるには、SCIM ログを「時間軸」で眺めるのが有効です。例えば次のような視点でログを整理してみてください。

時刻HTTP メソッド / パス主な内容チェックポイント
T0GET /Groups/abc123?excludedAttributes=membersグループ情報取得レスポンス JSON に members が含まれていないか?
T1PATCH /Groups/abc1231 回目のメンバー追加op が add か replace か?
T2PATCH /Groups/abc1232 回目のメンバー追加value 内のメンバー数は?(1 人か複数か)
T3………

特に次の 2 点を押さえると、原因究明が一気に進みます。

  1. GET 応答に members が含まれていないか
  2. 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 つです。

  1. GET /Groups / GET /Groups/{id} のレスポンスに members が含まれていないか(特に excludedAttributes=members が付いている場合)
  2. attributes / excludedAttributes の処理が RFC 7644 の仕様どおりに実装されているか
  3. 修正後に Entra からの PATCH が add / remove 中心の挙動に変わっているか

根本解決の順番としては、

  • ① SCIM サーバ側の GET 実装を修正(members を返さない)
  • ② 再同期して PATCH が add になっていることを確認
  • ③ それでもダメなら Microsoft サポートにログ付きで問い合わせ

という流れがもっとも現実的で、かつ安全です。

Entra との SCIM 連携は、仕様やサンプルどおりに実装すれば非常に強力な自動プロビジョニング基盤になります。今回紹介したポイントを押さえておけば、「メンバーが勝手に消える」といった事故を防ぎつつ、安心して運用できるはずです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次