Microsoft Graph(Java SDK)で「特定ユーザーが特定グループのメンバーか」を確認したいとき、/groups/{id}/members を全件取得してループする実装は、ページングやスロットリングの原因になりやすく、レスポンスも重くなりがちです。この記事では1リクエストで判定する方法と、owners(オーナー)を含める場合の考え方を整理します。
まず押さえる:グループの「members」と「owners」は別の関係
Microsoft Entra ID(旧 Azure AD)のグループは、メンバー(members)とオーナー(owners)を別の関係として持ちます。UIでは「オーナー=所属者」と見えることもありますが、API上は分かれているため、仕様を決めずに実装すると「想定外に判定できない/一覧に出ない」が起きます。
| やりたいこと | 代表エンドポイント(v1.0) | 返るもの | 推移(ネスト) | メモ |
|---|---|---|---|---|
| メンバー一覧 | /groups/{groupId}/members | 直接メンバー(directoryObjectの集合) | 非推移 | 「直接」だけ。ネストは別API |
| ネスト含むメンバー一覧 | /groups/{groupId}/transitiveMembers | 推移メンバー(フラットな一覧) | 推移 | 入れ子グループまで展開 |
| オーナー一覧 | /groups/{groupId}/owners | オーナー(ユーザー/サービスプリンシパル) | 概念なし | オーナーは権限者。グループによっては取得不可の注意あり |
| ユーザーが候補グループ群に属するか判定 | /users/{userId}/checkMemberGroups | 「候補のうち所属しているグループID」だけ | 推移 | 最大20グループを一括判定 |
| ユーザーが属する全グループIDを取得 | /users/{userId}/getMemberGroups | ユーザーが属する全グループID | 推移 | 最大11,000件。超えるとエラーになり得る |
「members()」で取れるのはメンバーだけ?オーナーも含む?
Java SDKの members() は /groups/{id}/members に対応し、基本的にグループの直接メンバーを返します。この操作は非推移で、入れ子グループの中のユーザーまで自動で展開はされません。
一方でオーナーは /groups/{id}/owners の別関係です。オーナーが常にメンバーにも入っているとは限らないため、members だけで「所属者全員」を表現するのは危険です。
入れ子(推移)メンバーシップがあると、判定結果が食い違う例
典型例として、グループAのメンバーにグループBが追加され、ユーザーUはグループBのメンバー、という構造を考えます。
/groups/A/members:返るのは「B(グループ)」で、Uは直接メンバーではないので出てきません/groups/A/transitiveMembers:B配下のUまで展開され、Uが返ります/users/U/checkMemberGroups(候補にAを渡す):推移判定なのでAが返り、UはAに「所属している」と判定されます
この差は「どの所属を判定したいか(直接 or 推移)」を明確にしないと、バグに見える挙動になります。
1リクエストでメンバー判定するなら checkMemberGroups が手早い
「特定ユーザーが特定グループのメンバーか」を1回の呼び出しで判定したいなら、まず検討すべきは checkMemberGroups です。ユーザー(DirectoryObject)に対して、判定したいグループIDを最大20個まで渡すと、そのユーザーがメンバーであるグループIDだけが返ります。
返り値が「一致したIDだけ」なので、contains で判定でき、members一覧を全件取得して突合するより転送量と処理が小さくなります。
HTTPで見る checkMemberGroups(ユーザーに対して判定)
POST https://graph.microsoft.com/v1.0/users/{userId}/checkMemberGroups
Authorization: Bearer {access_token}
Content-Type: application/json
{
"groupIds": [
"{groupId}"
]
}
レスポンスは「一致したグループIDの配列」です。{groupId} が含まれていればメンバー、含まれていなければ非メンバーです。
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(Edm.String)",
"value": [
"{groupId}"
]
}
Java SDK(6.x系)の実装イメージ
SDKのバージョンによりクラス名や戻り値の型が変わることがありますが、6.x系(Kiotaベース)では概ね次の形です。ここでは認証(requestAdapter作成)は済んでいる前提にしています。
GraphServiceClient graphClient = new GraphServiceClient(requestAdapter);
List<String> groupIds = List.of(groupId);
CheckMemberGroupsPostRequestBody body = new CheckMemberGroupsPostRequestBody();
body.setGroupIds(groupIds);
// userId に対して判定(/users/{id}/checkMemberGroups)
var result = graphClient.users()
.byUserId(userId)
.checkMemberGroups()
.post(body);
// 返却は「文字列IDの配列(value)」になるので、SDKの型に合わせて取り出す
List<String> matched = result.getValue();
boolean isMember = matched != null && matched.contains(groupId);
最大20グループ制限を超えるときの分割(Java例)
checkMemberGroups は1回で最大20グループまでという制約があります。チェック対象が多い場合は、20件ずつに分割して呼び出し、結果を結合します。
List<String> allCandidateGroupIds = ...; // 判定したい候補(例:ロールに紐づくグループID群)
Set<String> memberGroupIds = new HashSet<>();
for (int i = 0; i < allCandidateGroupIds.size(); i += 20) {
List<String> chunk = allCandidateGroupIds.subList(i, Math.min(i + 20, allCandidateGroupIds.size()));
CheckMemberGroupsPostRequestBody body = new CheckMemberGroupsPostRequestBody();
body.setGroupIds(chunk);
var res = graphClient.users().byUserId(userId).checkMemberGroups().post(body);
if (res != null && res.getValue() != null) {
memberGroupIds.addAll(res.getValue());
}
}
// 目的のグループに所属しているか
boolean isMember = memberGroupIds.contains(targetGroupId);
「候補が多すぎる」なら getMemberGroups を先に取る手もある
候補グループIDが何百・何千とある場合、checkMemberGroups を分割して呼ぶより、getMemberGroups で「ユーザーが属するグループIDを一括取得」してから、アプリ側で突合する方が速い場面もあります。とはいえ getMemberGroups には返却上限(最大11,000件)があり、超える場合は別APIが推奨される点に注意してください。
重要:checkMemberGroups は owners(オーナー)を判定しない
ここが一番つまずきやすいポイントです。checkMemberGroups は「メンバー(members)」の判定であり、オーナー(owners)という関係は見ません。ユーザーが「オーナーとして設定されているだけ」で「メンバーではない」場合、チェックしてもヒットしない可能性があります。
また、オーナーはグループによってはGraphで取得できない制約があることや、サービスプリンシパルが一覧に出ない時期があることも、仕様として押さえておくと安全です。
運用で解決するか、ロジックで吸収するか
「所属=メンバーだけ」と割り切れるなら判定がシンプルになります。一方で「所属=メンバー+オーナー」という要件なら、実装も二段構えにしておくのが堅実です。
| 方針 | メリット | 注意点 | おすすめ場面 |
|---|---|---|---|
| オーナーは必ずメンバーにも追加(運用/自動化) | 判定ロジックが単純(members判定だけで済む) | 例外が出ると破綻。動的グループでは運用が難しいことがある | 権限管理をメンバーシップに一本化したい |
| メンバー判定とオーナー判定を分ける(実装で吸収) | データモデルに忠実で例外に強い | API呼び出しが増える。キャッシュやバッチ化が必要 | 「所属=メンバー+オーナー」が要件として明確 |
オーナーも含めた「所属判定」テンプレ
「所属=members または owners」と定義するなら、次のように組み立てると実装が読みやすくなります。
- メンバー判定:
checkMemberGroupsで groupId が返るかを見る - オーナー判定:
/groups/{groupId}/ownersを取得して userId が含まれるかを見る(グループのオーナーは最大100)
「HTTP呼び出し自体を1回にしたい」場合は、GraphのJSONバッチ($batch)に「メンバー判定用のPOST」と「owners取得用のGET」をまとめると、ネットワーク往復を1回にできます。
「所属している人(オーナーも含めた全員)」を取得する方法
結論としては、members と owners をそれぞれ取得し、IDで突合して統合(重複排除)するのが最も分かりやすく、仕様にも忠実です。同一人物が両方に現れるケースがあるため、重複排除が前提になります。
取得に使うAPI(まずは最小フィールドで)
- メンバー一覧:
GET /groups/{groupId}/members - オーナー一覧:
GET /groups/{groupId}/owners
一覧系APIは対象が directoryObject の集合になりやすいので、実務では $select と OData cast(ユーザーだけに絞る)を組み合わせると扱いやすくなります。
GET https://graph.microsoft.com/v1.0/groups/{groupId}/members/microsoft.graph.user?$select=id,displayName,userPrincipalName
GET https://graph.microsoft.com/v1.0/groups/{groupId}/owners/microsoft.graph.user?$select=id,displayName,userPrincipalName
$filter や OData cast を使う場合、ConsistencyLevel: eventual と $count が必要になるケースがあるため、必要になったタイミングで付ける方針にするとハマりにくいです。
統合の考え方(重複排除)
// 疑似コード(DirectoryObject / User の扱いはSDKに合わせて読み替え)
Map<String, Object> peopleById = new HashMap<>();
for (Object m : members) {
peopleById.put(getId(m), m);
}
for (Object o : owners) {
peopleById.put(getId(o), o); // 既に存在すれば上書きされるだけ(重複排除)
}
// peopleById.values() が「メンバー+オーナー」の統合結果
入れ子グループも含めた「全メンバー」を取りたい場合
セキュリティグループで「グループがグループを含む」構造がある場合、/groups/{id}/members だけではネスト内のユーザーまで出てきません。その場合は /groups/{id}/transitiveMembers を使うと、推移的に展開されたフラットな一覧を取得できます。
権限(Permission)設計のポイント
Graphは「動くまでに時間がかかった」の原因が、コードよりも権限不足(403)であることが珍しくありません。最初は最小権限で構成し、必要に応じて段階的に強い権限へ広げるのが安全です。
| API | 最小権限の例(代表) | 補足 |
|---|---|---|
| checkMemberGroups(他ユーザー) | User.ReadBasic.All + GroupMember.Read.All | より強い権限として Directory.Read.All なども選択肢 |
| /groups/{id}/members | GroupMember.Read.All | Hidden membership の読み取りでは追加権限が必要 |
| /groups/{id}/owners | GroupMember.Read.All | Exchange作成/配布リスト/オンプレ同期のグループでは取得できない場合がある |
また、Graphのベストプラクティスとしても「最小権限(least privilege)」が推奨されています。アプリの監査・運用まで見据えるなら、ドキュメントの permissions セクションをベースに設計するのが近道です。
よくある落とし穴と対策
403 Forbidden(権限不足)
- アプリ登録で必要スコープが付与されていない
- 管理者同意(Admin consent)が未実施
- 委任権限の場合、サインインユーザー側のロールや可視性制約(Hidden membership)に引っかかっている
Graph公式のトラブルシュートでは、トークン取得フローや権限・同意の確認が推奨されています。
429 Too Many Requests(スロットリング)
members一覧を全件取得してループする実装は、リクエスト数と転送量が増えやすく、スロットリングの温床です。checkMemberGroupsで「判定」を寄せると、API回数と転送量が大きく減ることが多いです。429が出た場合は、Retry-After を尊重しつつ指数バックオフで再試行します。
404 Not Found が直後だけ出る
ユーザーやグループを作成した直後に参照すると、複製(レプリケーション)遅延で一時的に404になることがあります。時間を置いてリトライし、改善しない場合は待ち時間を伸ばします。
まとめ:メンバー判定は checkMemberGroups、所属者一覧は members+owners の統合
- 「特定ユーザーが特定グループのメンバーか」を1回で判定したいなら checkMemberGroups
- checkMemberGroups は推移(入れ子)判定。直接だけを見たい場合は
/groups/{id}/membersを検討 - owners は members と別管理。所属に含めたいなら owners も別途取得して統合する
- 大量アクセスではキャッシュ・リトライ(429対策)・最小権限の設計が効く

コメント