Microsoft Graph APIでユーザー情報を一括取得しようとして、/v1.0/users?$select=… に多くのプロパティを詰め込むと、403 Forbidden・404 Not Found・501 Not Implementedが混在して原因が見えづらくなります。本記事では、/users(一覧)と/users/{id}(単一)の仕様差、Intune系プロパティの権限設計、そして大量項目取得の実装パターンを整理します。
現象:/users?$select=… で 403 / 404 / 501 が混在する
Azure AD(Microsoft Entra ID)連携済みのアプリから、次のように ユーザー一覧(/users) を取得しつつ、$select に「ほぼ全プロパティ」を指定すると、指定した項目の組み合わせによってエラーが変化することがあります。
GET https://graph.microsoft.com/v1.0/users?$select=id,displayName,mail,aboutMe,birthday,skills,deviceEnrollmentLimit,...
よく見られるパターンは次の3つです。
| ステータス | 今回の典型的なトリガー | 実務上の見立て |
|---|---|---|
| 404 Not Found | aboutMe / birthday / hireDate / interests / mySite / pastProjects / preferredName / responsibilities / schools / skills などを $select に含める | そのプロパティは「/users(一覧)」のレスポンスとしてはサポート外(エンドポイント上“存在しない”扱い) |
| 403 Forbidden | deviceEnrollmentLimit を $select に含める(Intune系の UnknownError を伴うことが多い) | Intune / デバイス管理領域の権限不足、またはトークン種別(委任/アプリケーション)の不一致 |
| 501 Not Implemented | 上記のような「一覧で返せない項目」を大量に混ぜて $select する | 一覧取得ではその操作が実装されていない(サポート外プロパティが混ざり内部処理パスが破綻) |
結論から言うと、「/users(一覧)で取得できる項目」と「/users/{id}(単一ユーザー)でしか取得できない項目」を分けて設計することが最大の近道です。さらに、deviceEnrollmentLimit のように 別サービス領域(Intune)と結び付いたプロパティは、Graph のユーザー読み取り権限だけでは足りないケースが多く、権限設計も分けて考える必要があります。
/users(一覧)と /users/{id}(単一)の違いを押さえる
Microsoft Graph API の user エンティティはプロパティ数が多い一方で、エンドポイントによって「返せる(選べる)項目」が同一ではありません。特に、一覧系(コレクション)は最適化のため、複雑な属性や裏側で別システム参照が必要な属性を返さない(返せない)ことがあります。
| 呼び出し | 目的 | $select の考え方 | 向いている場面 |
|---|---|---|---|
GET /v1.0/users | ユーザー一覧の取得 | 「list users で返せる」と明記されている一般属性に絞る | アドレス帳、同期、一覧表示、検索インデックス作成 |
GET /v1.0/users/{id} | 単一ユーザーの詳細取得 | 一覧では取れない詳細プロパティを必要に応じて $select | ユーザー詳細画面、個別プロファイル参照、設定画面 |
GET /v1.0/me | サインイン中ユーザーの取得 | 委任権限の範囲で、ユーザー体験寄りの項目取得に強い | フロントエンドでのマイページ、自己情報の表示 |
「全部ほしい」気持ちは分かりますが、Graph の設計思想としては “必要な時に必要なものだけ取る”が前提です。/users で無理に総取りしようとすると、今回のような 404/501 が出たり、レスポンスが肥大化してスロットリング(429)やタイムアウトの温床になりがちです。
404 Not Found の正体:一覧(/users)でサポートされないプロパティを $select している
aboutMe や birthday などの「プロフィール詳細」系プロパティは、user リソースのプロパティとしては存在しますが、/users(一覧)での返却対象としてはサポートされていない扱いになることがあります。その結果、一覧取得に対して $select してしまうと、エンドポイント上は “存在しないプロパティ” のように扱われ、404 が返るケースが出ます。
今回 404 になっていた代表例は次のとおりです。
- aboutMe
- birthday
- hireDate
- interests
- mySite
- pastProjects
- preferredName
- responsibilities
- schools
- skills
- mailboxSettings(環境によっては一覧で取得不可になりやすい)
これらが必要な場合は、単一ユーザー取得(/users/{id})で明示的に $select するのが基本です。
GET https://graph.microsoft.com/v1.0/users/{user-id}?$select=aboutMe,birthday,skills,preferredName
ここでのポイントは、「一覧」→「個別」へ段階的に取得する設計にすることです。ユーザー全員分の詳細が本当に必要なのか、ビジネス要件として再確認し、必要なユーザーにだけ詳細 API を打つようにすると、パフォーマンスと安定性が一気に上がります。
/users で取りやすい項目と、分離すべき項目の目安
実装で迷ったら、まずは次のように整理すると判断しやすくなります。
| 分類 | 例 | /users(一覧) | 推奨する取得方法 |
|---|---|---|---|
| ディレクトリ基本属性 | id, userPrincipalName, displayName, mail, givenName, surname, department, jobTitle, accountEnabled など | 取得しやすい | GET /users?$select=...(必要最低限に絞る) |
| プロフィール詳細 | aboutMe, birthday, hireDate, interests, pastProjects, responsibilities, schools, skills など | 取得不可/不安定になりやすい | GET /users/{id}?$select=...(個別取得) |
| メールボックス/設定系 | mailboxSettings, automaticRepliesSetting など | 一覧では避ける | GET /users/{id}/mailboxSettings や単一取得で必要時に |
| 関係(ナビゲーション) | manager, memberOf, directReports など | 一覧では直接返さない | GET /users/{id}/manager など専用エンドポイント |
| 別サービス領域に紐づく属性 | deviceEnrollmentLimit(Intune)など | 権限不足で 403 の原因になりやすい | 権限を分離し、別リクエストで取得(または Intune 側 API を活用) |
「/users で返らないものは、Graph が持っていない」のではなく、一覧レスポンスとしてサポートしていない(または最適化上返さない)というニュアンスです。ここを誤解すると、権限をいくら追加しても 404 が消えない、という状態に陥ります。
501 Not Implemented の正体:一覧(/users)に “一覧で返せない項目” を詰め込みすぎている
501 Not Implemented は「その操作は API として実装されていない(サポート外)」を意味します。今回の文脈では、/users の一覧取得で実装されていないプロパティ取得を強制していることが原因になりやすいです。
特に、次のような状態になると 501 が出やすくなります。
- /users に対して、プロフィール詳細やメールボックス設定、Intune 系など “別系統のデータ参照” が必要な項目を大量に混在させる
- $select の項目数が増えすぎて、内部的に最適化ルートが外れたり、バックエンド連携の制約に当たる
- 一部はサポート外(本来は 404/400 相当)でも、まとめて評価され 501 として返ってくる
「403 と 404 が混ざった結果 501 になっているのでは?」という疑問は自然ですが、実務上は “一覧で返せないものが混ざった時点で、一覧取得のやり方が間違っている”と捉えるのが正解です。501 を回避する最短ルートは、/users の $select を「一覧で返せる一般属性」だけに絞ることです。
501 を回避するための $select 設計例(一覧用)
まずは一覧で必要な“骨格”だけを取るのが安定します。例えば、組織のユーザー台帳・検索・同期でよく使うセットは次のようになります。
GET https://graph.microsoft.com/v1.0/users?$select=
id,userPrincipalName,displayName,mail,givenName,surname,
department,jobTitle,officeLocation,mobilePhone,businessPhones,
accountEnabled,employeeId,companyName,usageLocation,preferredLanguage
ここに aboutMe や skills を混ぜないのがポイントです。詳細が必要な場面でだけ、後述の個別取得やバッチで補完します。
deviceEnrollmentLimit の 403 Forbidden:Intune 領域の権限とトークン種別を疑う
deviceEnrollmentLimit はユーザー属性のように見えますが、実務的には Intune のデバイス登録制限(デバイス管理ポリシー)と関係が深いプロパティです。そのため、User.Read.All など “ディレクトリ読み取り系” の権限だけでは取得できず、Intune / deviceManagement 系の権限が必要になることがあります。
また、ここが重要なのですが、Graph の権限は 委任権限(Delegated) と アプリケーション権限(Application)で別物です。バックエンドでクライアント資格情報フロー(client credentials)を使っているのに、委任権限だけ付与して「付けたつもり」になっていると、トークンに必要な権限が載らず 403 が出ます。
委任権限とアプリケーション権限の違い(トラブルの温床)
| 観点 | 委任権限(Delegated) | アプリケーション権限(Application) |
|---|---|---|
| 主な利用シーン | ユーザーがサインインして操作する(UI/フロント) | デーモン/サーバー間、バッチ処理、バックエンド同期 |
| トークンの特徴 | ユーザーの権限 + アプリの許可の合成 | アプリ単体の許可(ユーザー不在) |
| トークンで見るべきクレーム | scp(スコープ) | roles(アプリロール) |
| よくある事故 | “管理者同意が必要な権限” を付け忘れる | 委任権限だけ付けて、アプリケーション権限が空のまま呼ぶ |
deviceEnrollmentLimit をアプリケーション トークンで取得する場合は、一般に次のような Graph 権限(アプリケーション権限)が必要になります。
- DeviceManagementConfiguration.Read.All または DeviceManagementConfiguration.ReadWrite.All
そして、管理者による同意(Admin consent)が必須です。同意が入っていないと、Azure ポータル上で権限が見えていても、実際のトークンには載りません。
403 を解消するための実務手順(最短コース)
- どのフローでトークンを取っているかを確認する(Authorization Code / On-Behalf-Of / Client Credentials など)。
- クライアント資格情報フローなら、アプリケーション権限として
DeviceManagementConfiguration.Read.All(必要なら ReadWrite)を追加する。 - Azure ポータルの「API のアクセス許可」で 管理者による同意を実行する。
- 同意後に新しいトークンを取得し、トークン内の
rolesに deviceManagement 系の権限が入っていることを確認する。 - それでも 403 が出る場合、Intune が未有効化/未契約、MDM 権限の委任状態、条件付きアクセス、テナント制限など “環境側” の要因も疑う。
トークン確認は、開発者ツールや JWT デコーダ(例:jwt.ms)で scp/roles を見ると一発です。「アプリ登録に権限を足した」だけでは足りず、「同意が入った状態で取り直したトークン」で呼ぶ必要があります。
deviceEnrollmentLimit を取るなら、ユーザー一覧取得と分離するのが安全
deviceEnrollmentLimit を /users の一覧と一緒に取りに行くと、権限不足で一覧自体が 403 になる、という“巻き添え事故”が起こりがちです。現場では次のように 取得ルートを分離するのが安定します。
| 目的 | おすすめの取り方 | 理由 |
|---|---|---|
| ユーザー一覧を表示/同期したい | /users は一般属性だけで取得(deviceEnrollmentLimit は除外) | 一覧が 403/501 で落ちるのを防ぐ |
| 特定ユーザーの登録上限だけ見たい | /users/{id} を別リクエストで取得(必要時にだけ) | 必要ユーザーだけに負荷と権限を適用できる |
| ポリシーや制限の全体像を把握したい | Intune の deviceManagement 配下のエンドポイントを利用 | ユーザー属性ではなく“設定/ポリシー”として扱う方が自然 |
「一覧の1回で全て取れる設計」は、便利に見えても長期運用で壊れやすいです。Graph はバックエンドが複数サービスに跨るため、権限やライセンス、最適化の都合で“一覧に混ぜられない属性”が一定数存在すると割り切ると、設計がスムーズになります。
ステータスコード別の意味と、今回の当てはめ
エラー解析を早くするために、今回のケースを “ステータスコード→疑うべきこと” で整理します。
| ステータス | 何が起きているか | 真っ先に確認すること | 打ち手 |
|---|---|---|---|
| 404 | そのエンドポイントでは、そのプロパティを返せない/存在しない扱い | /users(一覧)に “単一でのみ取れる項目” を混ぜていないか | /users の $select を絞る。必要なら /users/{id} で個別取得 |
| 403 | リソースはあるが、権限・同意・条件によりアクセス拒否 | 委任/アプリケーションの取り違え、admin consent、トークン内の scp/roles | 必要な権限を「正しい種別」で付与し、同意してトークンを取り直す |
| 501 | その操作は一覧としてサポートされていない/実装されていない | $select に “一覧で返せない項目” を大量に含めていないか | 一覧は一般属性のみ。詳細は個別・バッチ・専用エンドポイントに分割 |
できるだけ多くのユーザー項目を取るベストプラクティス
“できるだけ多く” を “1回で全部” と同義にすると失敗します。ここでは、運用で破綻しにくい「多く取るための分割戦略」を具体例つきで紹介します。
一覧(/users)で取る項目は「検索・一覧表示・同期待ち受け」に必要なものだけ
/users は、いわばユーザー台帳のインデックスです。ここで扱うのは、検索・表示・紐付けに使うキー情報が中心になります。例えば次のように “よく使う属性” に絞ると、エラーが減り、レスポンスも安定します。
GET https://graph.microsoft.com/v1.0/users?$select=
id,userPrincipalName,displayName,mail,
department,jobTitle,officeLocation,
accountEnabled,employeeId,createdDateTime
さらに大規模テナントでは、取得件数が増えるほど障害時の影響が大きくなります。次の点も合わせて徹底すると運用が楽になります。
- ページング(@odata.nextLink)を前提に実装する($top で固定しない)
- レスポンスをキャッシュし、同じ一覧を短時間に何度も引かない
- 可能なら delta クエリで変更分だけ追う(全件フルスキャンを減らす)
一覧で取れない項目は「必要なユーザーだけ」単一取得で補完する
aboutMe や skills のような詳細項目は、ユーザー詳細画面や、プロフィール充実度の表示など、“閲覧された時だけ必要”であることが多いです。その場合、一覧で無理に取らず、表示時に個別取得します。
GET https://graph.microsoft.com/v1.0/users/{id}?$select=
aboutMe,birthday,hireDate,interests,pastProjects,skills
全ユーザー分の詳細がどうしても必要(例:夜間バッチでプロファイルを収集して社内検索に載せる)なら、次の「バッチ API」と併用すると効率が上がります。
$batch を使って “個別取得の束ね” を現実的な回数に抑える
Graph の $batch を使うと、複数の GET を 1 回の HTTP リクエストにまとめられます。/users で id 一覧を取ったあと、必要なユーザーに対して /users/{id} を束ねるのが定番です。
{
"requests": [
{
"id": "u1",
"method": "GET",
"url": "/users/00000000-0000-0000-0000-000000000000?$select=aboutMe,birthday,skills"
},
{
"id": "u2",
"method": "GET",
"url": "/users/11111111-1111-1111-1111-111111111111?$select=aboutMe,birthday,skills"
}
]
}
バッチ化するときは、次の実務ポイントを押さえるとトラブルが減ります。
- 1 バッチに詰め込みすぎない(失敗時のリトライ範囲が大きくなる)
- 429(スロットリング)と 503 を想定し、Retry-After を尊重してリトライする
- バッチ内で 1 件だけ 404/403 が出ても、他の成功分は活かせるように設計する
“別領域” の情報は専用エンドポイントへ寄せる
Graph は “1つの API” に見えて、裏側は Azure AD / Exchange / SharePoint / Intune など複数サービスの統合です。次のような情報は、ユーザー一覧に混ぜるより、専用エンドポイントに寄せた方がトラブルが少なくなります。
| 欲しい情報 | 混ぜがちなプロパティ/関係 | おすすめの取得ルート |
|---|---|---|
| 上司情報 | manager | GET /users/{id}/manager |
| 所属グループ | memberOf | GET /users/{id}/memberOf |
| ユーザー写真 | photo | GET /users/{id}/photo/$value |
| メールボックス設定 | mailboxSettings | GET /users/{id}/mailboxSettings |
| Intune/デバイス管理 | deviceEnrollmentLimit など | /deviceManagement 配下の API + deviceManagement 系権限 |
「ユーザーを取る」ではなく「ユーザーに紐づく何を取るか」で API を切り分けると、権限も疎結合になり、監査・運用・セキュリティ面でもメリットが出ます。
トラブルシューティングのチェックリスト
403/404/501 が出たときに、現場で最短で潰すための確認ポイントをまとめます。
| チェック項目 | 見るべき場所 | ありがちな原因 | 対処 |
|---|---|---|---|
| $select に “一覧で返せないプロパティ” が混ざっていないか | リクエスト URL | aboutMe/skills などを /users に混ぜている | /users の $select を絞り、詳細は /users/{id} に分離 |
| 呼び出しが v1.0 / beta どちらか | エンドポイント | beta 前提の仕様を v1.0 で使っている | 本番は v1.0 を基本にし、必要なら beta を限定利用 |
| トークンが委任かアプリケーションか | JWT(scp/roles) | フローと権限種別の不一致 | 用途に合わせて権限を揃え、不要な混在を避ける |
| admin consent が実行されているか | Azure ポータル(API のアクセス許可) | 追加した権限が “未同意” のまま | 管理者が同意し、トークンを取り直す |
| Intune 系の権限が “アプリケーション権限” に付いているか | アプリ登録の権限一覧 | 委任にはあるがアプリケーションには無い | DeviceManagementConfiguration.Read.All などを Application に追加 |
| 条件付きアクセスやテナント制限でブロックされていないか | Entra 管理センター/サインインログ | ポリシーで Graph 呼び出しが制限 | 対象アプリの除外や、適切な条件付きアクセス設計 |
まとめ:403/404/501 を回避して“必要なユーザー情報”を安全に集める
- 404/501 は、/users(一覧)でサポートされないプロパティを $select している設計ミスが原因になりやすい。一覧は一般属性に絞る。
- deviceEnrollmentLimit の 403 は、Intune 領域の権限不足や、委任/アプリケーション権限の取り違えが典型。DeviceManagementConfiguration 系の アプリケーション権限 + 管理者同意 を確認する。
- “できるだけ多く取る” ためには、一覧(/users)→個別(/users/{id})→専用エンドポイントの三段構えで分割し、必要に応じて $batch や並列処理で効率化する。
この設計に切り替えるだけで、エラーの再現性が上がり、原因切り分けも容易になります。さらに、権限を用途別に分離できるため、セキュリティレビューや運用監査でも説明しやすい構成になります。

コメント