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 を追加できる構成(アプリ側→カスタムポリシーへ段階移行)にしておくと、運用コストと将来の変更コストのバランスが取りやすくなります。

コメント