Azure AD B2C / Entra External ID で Graph API 事前作成フェデレーションユーザーが自動的に無効化される原因と対策

Azure AD B2C / Microsoft Entra External ID で外部 IdP とフェデレーションしている環境で、「Graph API で事前作成したソーシャルユーザーが急にサインインできなくなった」「勝手に無効ユーザーになっている」というケースがここ最近一気に増えています。本記事では、この現象の原因となっている 2024 年末以降の仕様変更と、その影響を避けるためのカスタムポリシー/実装パターンを詳しく整理します。

目次

現象の整理:Graph API で事前作成したフェデレーションユーザーが自動的に無効化される

まず問題のシナリオを整理します。

  • Azure AD B2C(Entra External ID for customers)テナントで、外部 IdP(ソーシャル / SAML / OIDC など)とフェデレーションしている
  • ユーザーが初回サインインする前に、バックエンドから Microsoft Graph API を使って B2C テナントにユーザーを事前作成している
  • 事前作成するユーザーは identities[].signInType = "federated" のフェデレーションユーザー
  • これまでは accountEnabled : true を指定すれば、有効ユーザーとしてサインインできていた
  • しかし 2024 年末ごろから、同じ実装のままでもユーザーが自動的に「無効(accountEnabled = false)」で作成されるようになった
  • カスタムポリシーで accountEnabled をチェックしているため、サインインがブロックされる

特に Microsoft Q&A などでも「2024 年末までは動いていたが、最近になって急に事前作成したソーシャルユーザーが Disabled になる」という報告が複数上がっており、個別のバグではなくプラットフォーム仕様の変更であることが明言されています。

原因:2024 年末以降の B2C プラットフォーム仕様変更

フェデレーションユーザー作成時の accountEnabled が強制的に無視される

2025 年時点で公開されている Microsoft Q&A では、次のような仕様変更が案内されています。

  • 対象:identities[].signInType = "federated" のユーザーを Microsoft Graph API で作成したケース
  • 変更内容:
    • ユーザー作成リクエストで accountEnabled を true にして送っても作成時には完全に無視される
    • フェデレーションユーザーは必ず Disabled(accountEnabled = false)として作成される
  • 目的:セキュリティ強化のため、「実際に外部 IdP からのフェデレーションフローでサインインを完了したユーザーだけを有効化する」というモデルに統一するため

公式回答の中でも次のポイントが強調されています。

  • Graph API で事前作成されたフェデレーションユーザーは、たとえ accountEnabled : true を指定しても、B2C 上では Disabled として表示される
  • カスタムポリシーで <EnforceAccountIsEnabled> 相当のチェックを行っている場合、初回のフェデレーションサインインを完了するまでサインインがブロックされる
  • 作成時点で有効化するための API ベースの回避策はサポートされない

ローカルアカウントへの影響はない

この仕様変更は signInType=federated のユーザーのみが対象であり、ユーザー名+パスワードで B2C が直接認証するローカルアカウント(signInType=emailAddress など)には影響しません。ローカルアカウントはこれまでどおり、Graph API で accountEnabled の値を指定して作成・更新できます。

「以前」と「今」の挙動の比較

ユーザー種別作成方法〜2024 年末頃までの典型的な挙動2024 年末以降の挙動
ローカルアカウントGraph APIaccountEnabled の指定どおりに作成される変更なし(引き続き指定どおり)
フェデレーションアカウントGraph API による事前作成accountEnabled : true を指定すれば有効として作成されるケースが多かった常に accountEnabled = false として作成(指定は無視)
フェデレーションアカウント初回サインイン時に自動作成サインイン成功とともに有効ユーザーが作成される仕様変更後も基本的には同様。
「フェデレーションフローを完了したユーザーだけを有効とみなす」モデルに統一

結果として、「Graph API で事前作成したフェデレーションユーザーだけが、カスタムポリシーの accountEnabled チェックで引っかかる」という状態になりやすくなりました。

accountEnabled とフェデレーションユーザーの関係

なぜフェデレーションユーザーの accountEnabled は B2C 側で管理されるのか

そもそもフェデレーションユーザーは、認証自体は外部 IdP(Google、Azure AD、LINE、ADFS など)が行い、B2C はトークンの検証とユーザーオブジェクトの紐付けだけを担当します。そのため、「このユーザーをサインインさせてよいか」という一次判断は本来 IdP 側の責務です。

Microsoft Q&A では、B2C の federated アカウントに対する accountEnabled は B2C プラットフォーム側が管理し、Graph API で直接このフラグを使ってブロック制御することは想定されていないと明示されています。

そのため、フェデレーションユーザーの「アカウント停止」や「一時的なブロック」を行いたい場合は、カスタム属性(例:extension_canSignIn)など別の属性で制御するのが公式に推奨されるパターンになっています。

Graph API での事前作成が問題化しやすい理由

Graph API でフェデレーションユーザーを事前作成する場合、多くの実装で次のような JSON を送っていると思います。

{
  "displayName": "Taro Example",
  "mailNickname": "taro.example",
  "identities": [
    {
      "signInType": "federated",
      "issuer": "MyIdP",
      "issuerAssignedId": "1234567890"
    }
  ],
  "userType": "member",
  "accountEnabled": true,      // &lt;= これを付けている
  "passwordProfile": {         // &lt;= ローカルアカウントと同じノリで設定
    "password": "xWwvJ]6NMw+bWH-d",
    "forceChangePasswordNextSignIn": false
  }
}

しかし 2024 年末以降は、フェデレーションユーザーに対して

  • accountEnabled は完全に無視されて Disabled で作成される
  • フェデレーションユーザーにパスワードを設定しても、サインインには基本的に使われない(むしろ混乱の元)

という挙動に統一されたため、「ローカルアカウントと同じノリでユーザーを作る」コードが一斉に壊れた、というのが今回の本質です。

対応策 1:カスタムポリシーの accountEnabled チェックをローカルアカウント限定にする(推奨)

公式の回答でも第一に案内されているのが、カスタムポリシー側のロジックを修正する方法です。

具体的には:

  • ローカルアカウント:
    • これまで通り accountEnabled をチェックし、「アカウントが無効ならサインインをブロック」する
  • フェデレーションアカウント:
    • accountEnabled のチェックをスキップする
    • 必要に応じて、後述の extension_canSignIn のようなカスタム属性で制御する

ローカルかフェデレーションかを判定するための代表的な claim

カスタムポリシーでは、次のような claim を使ってサインイン経路を判定できます。

claim 名例用途
authenticationSourceローカル:localAccountAuthentication ソーシャル/外部 IdP:socialIdpAuthentication などどの種類の認証(ローカル/ソーシャル)でサインインしてきたかを判定する
identityProviderローカル:localAccount(サンプルに依存) Google:google.com Azure AD:https://sts.windows.net/{tenantId}/ などどの IdP からのトークンかを判定する(issuer / iss をマッピング)

どの claim を使うかは既存ポリシーや Starter Pack によって異なるため、実際の値は自テナントのポリシー XML を確認しながら決めてください。

authenticationSource を使って accountEnabled チェックをスキップする例

もっともシンプルな考え方は「authenticationSource がローカルのときだけ EnforceAccountIsEnabled ステップを通す」という分岐です。

以下は概念的なオーケストレーションステップ例です(実際の TechnicalProfile 名や順序は環境に合わせて修正してください)。

&lt;OrchestrationStep Order="6" Type="ClaimsExchange"&gt;
  &lt;Preconditions&gt;
    <!-- ローカルアカウントでサインインしている場合だけ、このステップを実行 -->
    &lt;Precondition Type="ClaimEquals" ExecuteActionsIf="true"&gt;
      &lt;Value&gt;authenticationSource&lt;/Value&gt;
      &lt;Value&gt;localAccountAuthentication&lt;/Value&gt;
      &lt;Action&gt;Continue&lt;/Action&gt;
    &lt;/Precondition&gt;

    <!-- ソーシャル / フェデレーションの場合は accountEnabled チェックをスキップ -->
    &lt;Precondition Type="ClaimEquals" ExecuteActionsIf="true"&gt;
      &lt;Value&gt;authenticationSource&lt;/Value&gt;
      &lt;Value&gt;socialIdpAuthentication&lt;/Value&gt;
      &lt;Action&gt;SkipThisOrchestrationStep&lt;/Action&gt;
    &lt;/Precondition&gt;
  &lt;/Preconditions&gt;

  &lt;ClaimsExchanges&gt;
    &lt;ClaimsExchange Id="EnforceAccountIsEnabled"
        TechnicalProfileReferenceId="AAD-UserReadUsingObjectId-AccountEnabled" /&gt;
  &lt;/ClaimsExchanges&gt;
&lt;/OrchestrationStep&gt;

ポイントは以下の通りです。

  • 既存の EnforceAccountIsEnabled 相当のステップに対して Precondition を追加するだけでよい
  • ローカルアカウントのサインインフローには影響を与えない
  • フェデレーションユーザーは accountEnabled チェックを素通りするため、「事前作成時に Disabled であること」が問題にならなくなる

identityProvider を使って特定の IdP のみスキップする例

外部 IdP が複数ある場合や、「この IdP からのユーザーだけは特別扱いしたい」というケースでは identityProvider claim で分岐するのが便利です。

&lt;OrchestrationStep Order="6" Type="ClaimsExchange"&gt;
  &lt;Preconditions&gt;
    <!-- ローカルアカウントのみ accountEnabled をチェック -->
    &lt;Precondition Type="ClaimEquals" ExecuteActionsIf="true"&gt;
      &lt;Value&gt;identityProvider&lt;/Value&gt;
      &lt;Value&gt;localAccount&lt;/Value&gt;
      &lt;Action&gt;Continue&lt;/Action&gt;
    &lt;/Precondition&gt;

    <!-- Google / Facebook / Azure AD などの外部 IdP はスキップ -->
    &lt;Precondition Type="ClaimEquals" ExecuteActionsIf="true"&gt;
      &lt;Value&gt;identityProvider&lt;/Value&gt;
      &lt;Value&gt;google.com&lt;/Value&gt;
      &lt;Action&gt;SkipThisOrchestrationStep&lt;/Action&gt;
    &lt;/Precondition&gt;
    &lt;Precondition Type="ClaimEquals" ExecuteActionsIf="true"&gt;
      &lt;Value&gt;identityProvider&lt;/Value&gt;
      &lt;Value&gt;facebook.com&lt;/Value&gt;
      &lt;Action&gt;SkipThisOrchestrationStep&lt;/Action&gt;
    &lt;/Precondition&gt;
    <!-- 必要に応じて他の IdP も追加 -->
  &lt;/Preconditions&gt;

  &lt;ClaimsExchanges&gt;
    &lt;ClaimsExchange Id="EnforceAccountIsEnabled"
        TechnicalProfileReferenceId="AAD-UserReadUsingObjectId-AccountEnabled" /&gt;
  &lt;/ClaimsExchanges&gt;
&lt;/OrchestrationStep&gt;

このパターンは「一部の外部 IdP はポリシー外で厳格に管理しているので B2C 側ではチェックしない」といった要件に向いています。

対応策 2:カスタム属性(extension_canSignIn)で有効/無効を制御する

フェデレーションユーザーについても「運用側で明示的に有効/無効を切り替えたい」場合は、accountEnabled ではなくカスタム属性を使うのが王道です。これは Microsoft Q&A の回答でも紹介されている公式パターンです。

拡張属性の設計例

B2C のユーザープロファイルには、ディレクトリ拡張属性を最大 100 個まで追加できます。

  • 属性名(論理):extension_canSignIn
  • データ型:boolean
  • 意味:
    • true:サインイン許可
    • false:サインイン禁止(ブロック)
  • 初期値:true(特別な制限がないユーザー)

実際の Graph API 上では、B2C 拡張アプリの AppId をもとにした物理名(例:extension_1234567890abcdef1234567890abcdef_canSignIn)になりますが、ポリシー側では論理名で扱えます。

Graph API から extension_canSignIn を制御する例

ユーザー作成時に、ローカル/フェデレーションを問わず次のように設定できます。

POST https://graph.microsoft.com/v1.0/users
Content-Type: application/json

{
  "displayName": "Taro Example",
  "mailNickname": "taro.example",
  "identities": [
    {
      "signInType": "federated",
      "issuer": "MyIdP",
      "issuerAssignedId": "1234567890"
    }
  ],
  "userType": "member",
  "extension_1234567890abcdef1234567890abcdef_canSignIn": true
}

サインイン禁止にしたくなった場合は、単純に PATCH で false に更新します。

PATCH https://graph.microsoft.com/v1.0/users/{userId}
Content-Type: application/json

{
  "extension_1234567890abcdef1234567890abcdef_canSignIn": false
}

この更新はフェデレーションユーザーにも問題なく適用できます。

カスタムポリシー側で extension_canSignIn を評価する

次に、サインインユーザージャーニーの中で extension_canSignIn を読み、false ならトークンを発行せずカスタムエラーページへ遷移するようにします。

① ClaimType の定義

&lt;ClaimType Id="extension_canSignIn"&gt;
  &lt;DisplayName&gt;Can Sign In&lt;/DisplayName&gt;
  &lt;DataType&gt;boolean&lt;/DataType&gt;
  &lt;UserHelpText&gt;Indicates whether the user is allowed to sign in.&lt;/UserHelpText&gt;
&lt;/ClaimType&gt;

② AAD-UserReadUsingObjectId への出力追加

既存の AAD-UserReadUsingObjectId を継承した TechnicalProfile を作り、拡張属性を OutputClaims に追加します。

&lt;TechnicalProfile Id="AAD-UserReadUsingObjectId-WithExtensions"&gt;
  &lt;IncludeTechnicalProfile ReferenceId="AAD-UserReadUsingObjectId" /&gt;
  &lt;OutputClaims&gt;
    &lt;OutputClaim ClaimTypeReferenceId="extension_canSignIn"
                 PartnerClaimType="extension_1234567890abcdef1234567890abcdef_canSignIn"
                 DefaultValue="true" /&gt;
  &lt;/OutputClaims&gt;
&lt;/TechnicalProfile&gt;

③ extension_canSignIn をチェックするステップ

評価用の SelfAsserted TechnicalProfile(または REST TechnicalProfile)を作り、extension_canSignIn = false のときにエラーコードを返すようにします。ここでは概念だけ示します。

&lt;TechnicalProfile Id="EnforceCanSignIn"&gt;
  &lt;DisplayName&gt;Enforce extension_canSignIn&lt;/DisplayName&gt;
  &lt;Protocol Name="Proprietary"
            Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine" /&gt;
  &lt;InputClaims&gt;
    &lt;InputClaim ClaimTypeReferenceId="extension_canSignIn" /&gt;
  &lt;/InputClaims&gt;
  &lt;Metadata&gt;
    &lt;Item Key="ContentDefinitionReferenceId"&gt;api.selfasserted&lt;/Item&gt;
  &lt;/Metadata&gt;
  &lt;OutputClaims&gt;
    &lt;OutputClaim ClaimTypeReferenceId="extension_canSignIn" /&gt;
  &lt;/OutputClaims&gt;
&lt;/TechnicalProfile&gt;

そして、オーケストレーションステップで extension_canSignIn が false のときだけこの TechnicalProfile へ飛ばす、といった分岐を入れます。

&lt;OrchestrationStep Order="7" Type="ClaimsExchange"&gt;
  &lt;Preconditions&gt;
    &lt;Precondition Type="ClaimEquals" ExecuteActionsIf="true"&gt;
      &lt;Value&gt;extension_canSignIn&lt;/Value&gt;
      &lt;Value&gt;false&lt;/Value&gt;
      &lt;Action&gt;Continue&lt;/Action&gt;
    &lt;/Precondition&gt;

    <!-- true の場合はステップをスキップしてそのままトークン発行へ -->
    &lt;Precondition Type="ClaimEquals" ExecuteActionsIf="true"&gt;
      &lt;Value&gt;extension_canSignIn&lt;/Value&gt;
      &lt;Value&gt;true&lt;/Value&gt;
      &lt;Action&gt;SkipThisOrchestrationStep&lt;/Action&gt;
    &lt;/Precondition&gt;
  &lt;/Preconditions&gt;

  &lt;ClaimsExchanges&gt;
    &lt;ClaimsExchange Id="BlockUserByCanSignIn"
                    TechnicalProfileReferenceId="EnforceCanSignIn" /&gt;
  &lt;/ClaimsExchanges&gt;
&lt;/OrchestrationStep&gt;

EnforceCanSignIn の中では、カスタムエラーメッセージを出してフローを終了するようにしておけば、「accountEnabled に依存せず、ローカル/フェデレーション共通のブロックポリシー」を実現できます。

対応策 3:フェデレーションユーザーの「事前プロビジョニング戦略」を見直す

ここまでの対策に加えて、そもそもの「事前プロビジョニングの粒度」を見直すことで、将来の仕様変更に強い設計にしておくことも重要です。

フェデレーションユーザーは「最小限のスタブ」として作成する

フェデレーションユーザーを Graph API で事前作成する場合、次のような割り切りが実務的にはおすすめです。

  • 必須なのは identities だけ(issuer / issuerAssignedId)
  • accountEnabled は指定しても意味がないので送らない
  • パスワード(passwordProfile)はフェデレーションには不要なので設定しない
    • 誤って forceChangePasswordNextSignIn = true などを設定すると「Password has expired」エラーの原因にもなります

概念的な最小例は次の通りです。

{
  "displayName": "Taro Example",
  "mailNickname": "taro.example",
  "identities": [
    {
      "signInType": "federated",
      "issuer": "MyExternalIdP",
      "issuerAssignedId": "external-user-id-123"
    }
  ],
  "userType": "member"
  // accountEnabled や passwordProfile は指定しない
}

このスタイルであれば、将来 B2C / External ID 側で accountEnabled の扱いが再度変わったとしても、アプリ側のコード変更は最小限で済みます。

初回サインイン後にプロファイルを更新するフローに寄せる

前述のとおり、仕様変更のポイントは「フェデレーションフローをちゃんと完了したユーザーだけ有効とみなす」ことにあります。

そのため、設計方針としては次の流れに寄せると矛盾が少なくなります。

  1. Graph API でユーザーを「スタブ」として事前作成(identities だけ持つ)
  2. ユーザーが外部 IdP 経由で初回サインインを実行(または招待メールからサインイン)
  3. B2C がフェデレーションフローを完了したタイミングでユーザーを有効化(プラットフォーム挙動)
  4. アプリケーション側で必要に応じて追加属性を Graph API 経由で更新

「事前作成時点で完全なプロファイルを作らなければいけない」という要件が本当に必要かを見直し、初回サインイン後に補完しても問題ない情報は後追い更新に寄せることで、B2C 側のセキュリティモデルに沿った実装にできます。

既存環境で注意すべきポイント

デバッグ環境では再現しない原因

Microsoft Q&A の質問にもある通り、「本番では再現するが、デバッグ時には再現しない」という報告がよく見られます。

主な原因としては次のようなものが考えられます。

  • デバッグ用のポリシーでは EnforceAccountIsEnabled 相当のステップが無効になっている、またはローカルアカウントにしか適用されていない
  • テストユーザーは Graph API で事前作成せず、ポータルまたはサインアップフローから作成しているため、仕様変更の影響を受けていない
  • Application Insights などのログ設定が本番と異なり、どの OrchestrationStep で止まっているのか追えていない

トラブルシュート時は、Application Insights に IEF ログ(IdentityExperienceFramework)を出力し、どのステップでエラーになっているかを確認するのが近道です。

すでに「Disabled」で大量に存在するフェデレーションユーザーの扱い

既に本番環境に「Disabled のフェデレーションユーザー」が大量に存在している場合、次のような選択肢があります。

  • ポリシー側を修正し、フェデレーションユーザーに対しては accountEnabled を見ないようにする
    • もっとも安全かつ推奨されるアプローチ
  • extension_canSignIn を導入し、今後の制御はすべてそちらに寄せる
    • 既存ユーザーに対して一括で extension_canSignIn = true を設定するバッチを流す
  • どうしても一部ユーザーだけ例外的に扱いたい場合
    • ポータルで手動有効化することはできますが、長期的には推奨されません
    • API による強制有効化でプラットフォームのセキュリティモデルを迂回しようとするのは避けるべきです(Microsoft も「サポートされない」と明言)

メールアドレス重複とアカウント連携の落とし穴

ローカルアカウントとソーシャルアカウントの両方を許可している場合、

  • 同じメールアドレスでローカルアカウントと Google アカウントを作成
  • それぞれ別ユーザーとして B2C に存在してしまう

といったことがよく起こります。B2C / External ID では「メールアドレスが同じだから自動でアカウントをリンクする」といった挙動は基本的に行われません。

そのため、

  • alternativeSecurityId やカスタム属性を使って、ローカルアカウントとフェデレーションアカウントを明示的にリンクする
  • または「ローカル禁止+フェデレーションのみ」など、認証パターンをシンプルに保つ

といった設計が重要になります。事前プロビジョニングを行う場合も、どの IDP から来たユーザーとどのユーザーオブジェクトを結び付けるのかを、ポリシー/アプリ側でしっかり決めておく必要があります。

よくある質問と回答(FAQ)

Q. Graph API で作成したフェデレーションユーザーを、後から PATCH で accountEnabled : true に変えればサインインできますか?

A. 仕様上は更新リクエスト自体はエラーにならない場合もありますが、フェデレーションユーザーの有効/無効制御を accountEnabled に依存すること自体が非推奨です。Microsoft Q&A でも、「フェデレーションユーザーを作成時点で API から強制有効化するためのサポートされたワークアラウンドは存在しない」と明言されています。

そのため、実務的には:

  • ローカルアカウントのみ accountEnabled を利用する
  • フェデレーションユーザーは extension_canSignIn など別属性で制御する

という使い分けを強くおすすめします。

Q. すべてのユーザーに対して「管理者が手動で有効化するまでサインインさせたくない」という要件がある場合は?

A. その場合も、フェデレーションユーザーに対しては accountEnabled ではなく、カスタム属性+カスタムポリシーで承認フローを作るのが現実的です。

  • ユーザー作成時には extension_canSignIn = false
  • 管理者画面から承認されたユーザーだけを true に更新
  • カスタムポリシー側で extension_canSignIn = true のユーザーだけトークンを発行

とすることで、「初回サインイン済み」かどうかに加え、「管理者承認済み」かどうかも組み込んだフローを構築できます。

Q. Azure AD B2C は 2025 年以降どうなるの? Entra External ID への移行は必要?

A. ドキュメント上は、Azure AD B2C は 2025 年 5 月 1 日以降、新規顧客向けの販売終了がアナウンスされており、新規採用には Entra External ID(external tenant)の利用が推奨されています。

既存テナントはすぐに停止されるわけではありませんが、中長期的には External ID への移行パスを検討しておくと安心です。本記事の内容(フェデレーションユーザーの扱い、カスタム属性による制御、カスタムポリシーの考え方)は External ID でも基本的な考え方は共通なので、将来を見据えた設計にもなります。

まとめ:仕様変更に合わせて「ローカル vs フェデレーション」を明確に分けた設計へ

最後に、本記事のポイントを整理します。

  • 2024 年末以降、Graph API で事前作成したフェデレーションユーザー(signInType=federated)は、accountEnabled の指定に関わらず必ず Disabled で作成されるようになりました
  • この仕様変更により、カスタムポリシーでローカル/フェデレーション問わず accountEnabled を一律チェックしている環境では、事前作成ユーザーのサインインがブロックされるという現象が発生します
  • 推奨される対応は次の 3 つです。
    1. カスタムポリシーを修正し、accountEnabled チェックをローカルアカウントに限定する
    2. カスタム属性(例:extension_canSignIn)を導入し、有効/無効の制御をそちらに寄せる
    3. フェデレーションユーザーの事前プロビジョニングは「identities だけを持つスタブ」にとどめ、初回サインイン後にプロファイルを補完する
  • API でフェデレーションユーザーを強制有効化することはサポートされておらず、Microsoft もカスタム属性による制御を推奨しています

「ローカルアカウント」と「フェデレーションアカウント」は本来性質が異なります。今回の仕様変更をきっかけに、カスタムポリシーやアプリケーションコード側でもその違いをはっきり分けた設計に見直しておくことで、今後の Entra External ID への移行やさらなるセキュリティ強化にも対応しやすくなるはずです。

この記事を書いた人

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

コメント

コメントする

目次