Azure AD B2Cでemails配列をemail文字列で返す方法|ユーザーフローの制約とカスタムポリシー対応

Azure AD B2Cのユーザーフローでサインインすると、トークンのメールアドレスが「emails」配列で返り、OIDCの「email」文字列クレームを期待するアプリで扱いづらいことがあります。本記事では挙動の理由と制約、現実的な回避策を整理します。

目次

Azure AD B2C サインイン後に「emails」配列しか返らない現象

Azure AD B2C(ビルトインのユーザーフロー。例:B2C_1_SignIn)でサインインすると、アプリが受け取る ID トークン(またはアクセストークン)にメールアドレスが次のように出力されるケースがあります。

{
  "emails": ["[email protected]"]
}

一方で、既存アプリや外部サービス(SaaS、API ゲートウェイ、認可サーバー、社内共通基盤など)が OIDC の慣習に合わせた「email」クレーム(単一文字列) を前提としていると、次のような形式を求められることがあります。

{
  "email": "[email protected]"
}

ここで重要なのは、「emails」を「email」に置き換えたいのか、「emails」を残したまま追加で email を出したいのかです。要件として多いのは後者(互換性のために email を追加)ですが、どちらであってもビルトイン ユーザーフローだけで“型”と“構造”を変えるのは難しいのが実情です。


結論:ビルトイン ユーザーフローだけでは「emails(配列)」を「email(文字列)」に変更できない

Azure AD B2C のビルトイン ユーザーフローでは、標準クレームの多くが固定のクレーム名・固定のデータ型として扱われます。今回のポイントはここです。

  • emails は StringCollection(文字列配列) として固定で扱われる
  • ユーザーフローの画面設定(アプリケーションクレームの選択等)では、配列→単一文字列への型変換や、emails→email という別名の追加を実現できない

つまり、「Azure AD B2C 側の設定やユーザーフローの設定だけで email 文字列クレームを追加できるか?」という問いに対しては、基本的にできないという回答になります。


なぜ「emails」は配列なのか:設計意図を知ると判断が早くなる

「1つしかメールがないのに、なぜ配列?」と感じますが、Azure AD B2C の emails は、単に“サインインIDのメール”を返すだけのクレームとして設計されていません。

多くの構成では、次のような複数ソースのメール情報をまとめて返すために “配列” になっています。

項目意味結果として起きること
サインインに使うメール(サインインID)ユーザーがログイン時に入力する識別子最優先で含まれる(多くの場合 1要素目)
ユーザーに紐づく追加のメール(例:otherMails など)連絡先・予備メール等として登録される可能性がある将来的に複数要素になり得る

この設計を踏まえると、emails を単一文字列として扱うことには注意点もあります。例えば将来的に「連絡先メールを追加したい」「購読通知の送付先を増やしたい」などの要件が出た場合、emails が複数要素になるのは自然な流れです。


選択肢は大きく2つ:アプリ側で吸収するか、カスタムポリシーへ移行するか

「email 文字列クレームがどうしても必要」という状況で、現実的な打ち手は次の2つに集約されます。

選択肢できることメリットデメリット向いているケース
アプリ側で emails[0] を email として扱うトークンの受け取り後に値を取り出して内部表現を統一最短で導入できる/B2C設定変更が最小トークンそのものに email は増えない/サービス間連携で不便が残る自社アプリ内で完結、または短期での解決が最優先
カスタムポリシー(IEF)で email を発行emails → email の変換、独自クレーム名・型・出力制御トークンの設計を思い通りにできる/将来拡張に強い導入コストが高い/ポリシー運用・テストが必要複雑な要件(独自属性、条件分岐、外部API連携)が増えていく

「ユーザーフローは維持したいが、外部システムが email を要求する」という要件はよくあります。その場合はまずアプリ側で吸収し、要件が増えてきた時点でカスタムポリシーへ移行する、という段階的アプローチが現場では採られがちです。


ワークアラウンド:ビルトイン ユーザーフローを維持してアプリ側で email を作る

ユーザーフロー(B2C_1_SignIn など)を変えずに済ませるには、受け取ったトークンをアプリ側で解釈し、emails の先頭要素(emails[0] 相当)を email として扱うのが定石です。

.NET(ASP.NET Core)での実装例

JWT の配列クレームは、ライブラリやミドルウェアによって「配列のまま取得できる」場合と「同名クレームが複数に展開される」場合があります。まずは claims の実際の並びを確認し、取り出し方を決めるのが安全です。

// 例:同名クレームが複数になっているケースを想定
var emails = User.Claims
    .Where(c => c.Type == "emails")
    .Select(c => c.Value)
    .ToList();

var email = emails.FirstOrDefault(); // emails[0] 相当(null の可能性あり)

アプリ内で「email」というキーに揃えたい場合は、認証後の処理(ClaimsTransformation 等)で追加してしまうのが実務的です。

using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;

public class EmailClaimTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var identity = principal.Identity as ClaimsIdentity;
        if (identity == null) return Task.FromResult(principal);

        // 既に email があれば何もしない
        if (identity.HasClaim(c => c.Type == "email"))
            return Task.FromResult(principal);

        var firstEmail = identity.Claims
            .Where(c => c.Type == "emails")
            .Select(c => c.Value)
            .FirstOrDefault();

        if (!string.IsNullOrEmpty(firstEmail))
        {
            identity.AddClaim(new Claim("email", firstEmail));
            // 互換用に ClaimTypes.Email も追加したい場合
            identity.AddClaim(new Claim(ClaimTypes.Email, firstEmail));
        }

        return Task.FromResult(principal);
    }
}

この方法なら、コントローラーやビジネスロジック側は常に email クレームを参照できるようになり、影響範囲を局所化できます。

Node.js での実装例(概念)

Node.js でも同様に、検証済みのトークンから emails の先頭を取り出して email として扱うだけです。重要なのは「デコードしただけ」で信じないこと(署名検証と issuer/audience の検証は必須)で、実装自体はシンプルです。

// req.user などに検証済みのクレームが乗っている想定
const emails = req.user?.emails;
const email = Array.isArray(emails) ? emails[0] : undefined;

// 以降は email を正として扱う

このワークアラウンドを採るときの注意点

注意点理由実務での推奨
emails[0] が常に「連絡先メール」とは限らないサインインID、予備メール等が混在し得る「何に使う email か」(表示用/連絡先/ID)を明確化する
外部IdP(Google 等)では “メール未提供” のことがあるプロバイダー設定やユーザー同意次第email が無い場合の挙動(必須/任意)を設計しておく
将来的に複数メールを扱う要件が出る通知先追加などで自然に発生内部では emails を保持しつつ、互換用に email を作る

本命の解決策:カスタムポリシー(Identity Experience Framework)で email 文字列クレームを発行する

トークンに email: “[email protected]” を確実に含めたい、またはクレーム設計を自由にコントロールしたい場合は、カスタムポリシー(Identity Experience Framework のカスタムポリシー)へ移行するのが王道です。

カスタムポリシーに移行すると、次のことが可能になります。

  • 独自のクレームスキーマ(名前・型)を定義できる
  • トークンに出すクレームを、条件付き・変換付きで制御できる
  • 「emails(配列)」の先頭要素を取り出して「email(文字列)」にするなど、変換ロジックを持てる

全体像:どこで何をするのか

作業目的編集対象の例
スターターパックをベースにポリシーを用意カスタムポリシーの土台を作るTrustFrameworkBase.xml / TrustFrameworkExtensions.xml
email(文字列)クレームを定義トークン出力用の入れ物を作るClaimsSchema(Extensions側)
emails → email への変換を定義配列から先頭要素を抽出ClaimsTransformations
JWT 出力に email を含めるIDトークンの最終形を決めるRelyingParty の TechnicalProfile

実装例:emails の先頭を email にコピーする(概念)

ここでは「既に emails(StringCollection)がどこかのステップで取得できている」前提で、先頭要素を email(string)に作る例を示します。

1) email(文字列)クレームを定義

<ClaimType Id="email">
  <DisplayName>Email</DisplayName>
  <DataType>string</DataType>
  <UserHelpText>Primary email address</UserHelpText>
</ClaimType>

2) emails(配列)から先頭要素を取り出す ClaimsTransformation を定義

<ClaimsTransformation Id="CreateEmailFromEmails"
                      TransformationMethod="GetSingleItemFromStringCollection">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="emails" TransformationClaimType="inputClaim" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="item" DataType="int" Value="0" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="email" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>

3) トークン発行(RelyingParty)で email を出力
(例:RelyingParty の TechnicalProfile で OutputClaims と OutputClaimsTransformations を設定)

<RelyingParty>
  <DefaultUserJourney ReferenceId="SignUpOrSignIn" />
  <TechnicalProfile Id="PolicyProfile">
    <DisplayName>PolicyProfile</DisplayName>
    <Protocol Name="OpenIdConnect" />

    <OutputClaimsTransformations>
      <OutputClaimsTransformation ReferenceId="CreateEmailFromEmails" />
    </OutputClaimsTransformations>

    <OutputClaims>
      <OutputClaim ClaimTypeReferenceId="displayName" />
      <OutputClaim ClaimTypeReferenceId="emails" />
      <OutputClaim ClaimTypeReferenceId="email" />
      <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
    </OutputClaims>

    <SubjectNamingInfo ClaimType="sub" />
  </TechnicalProfile>
</RelyingParty>

この形にすると、アプリが受け取るトークンには emails(配列)とemail(文字列)の両方を含められます。既存連携を壊さずに email の互換クレームを追加できるため、移行時の事故を減らせます。

実運用でのポイント:email の「正」をどれにするか決める

カスタムポリシーに移行すると自由度が上がる反面、「どのメールを email として出すか」を決めないと、あとで仕様がブレます。設計の整理として次のような方針が現場で多いです。

用途推奨される考え方理由
認証の識別子(ユーザーIDとしてのメール)サインインID(ログインに使った値)を優先同一性の基準がブレにくい
連絡先(通知先メール)ユーザーが設定した連絡先を別クレームで持つ(例:contactEmail)サインインIDと連絡先は将来分離されやすい
外部IdP連携(Google/Microsoft等)IdP由来の email を正としつつ、未提供ケースのフォールバックを用意同意設定次第で email が空になることがある

「ユーザーフローのまま」やりたい人がハマるポイント

ビルトイン ユーザーフローに固執すると、次のような場面で詰まりやすくなります。

  • クレーム名の互換性問題:下流システムが email 固定で、emails を受け付けない
  • 型の不一致:配列が来る前提でない実装が落ちる(スキーマ検証、型付きマッピング等)
  • 将来要件の増加:独自属性追加、条件分岐、複数IdP統合、外部REST連携が必要になる

逆に言えば、これらが当面起きないなら「アプリ側で emails[0] を email として扱う」だけで十分なことも多いです。


よくある質問(実務のつまずきポイント)

emails が配列なら、常に 0 番目を使って大丈夫?

多くの構成では問題になりにくい一方で、将来的に複数メールが混在すると「0番目=常に正しい」とは限りません。短期は emails[0] を email として扱い、要件が増えたら「サインインID」「連絡先」など用途別にクレームを分けるのが安全です。

ビルトイン ユーザーフローで “email という名前のクレーム” を追加できない理由は?

ユーザーフローでは、ポータルで「どのクレームを出すか」は選べても、標準クレームの型変換や別名(エイリアス)の追加のような “クレーム設計そのもの” まで制御できません。トークン設計を制御したい要件は、カスタムポリシー領域です。

カスタムポリシーは重そう。どのタイミングで移行すべき?

目安として、次のどれかが当てはまったら移行を検討する価値があります。

  • トークンに出すクレーム名・型をサービス間で統一したい(互換層をなくしたい)
  • 外部IdPを複数扱い、クレーム正規化が必要
  • サインイン前後で外部API連携・条件分岐・高度なUX制御が必要
  • 監査やセキュリティ要件で、クレームの最小化や明確なルール化が必要

まとめ:最短で前に進めるための実務的な落としどころ

Azure AD B2C のビルトイン ユーザーフローでは、emails(配列)を email(単一文字列)に“変更”することはできません。そのため、現場の落としどころは次のいずれかになります。

  • 短期解決:アプリ側で emails[0] を email として扱い、互換レイヤーを実装する
  • 中長期の本命:カスタムポリシー(IEF)に移行し、emails → email の変換を含めたトークン設計を制御する

「今はユーザーフローで十分」でも、クレーム整形や拡張要件はあとから必ず出がちです。emails を前提に設計しつつ、必要に応じて email を追加できる構成(アプリ側→カスタムポリシーへ段階移行)にしておくと、運用コストと将来の変更コストのバランスが取りやすくなります。

この記事を書いた人

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

コメント

コメントする

目次