Azure AD B2Cのメール検証をACS SMTPで送る方法|カスタムポリシー実装手順

Azure AD B2C のメール検証(OTP/認証コード送信)を、SendGrid や Mailjet などのサードパーティではなく Azure Communication Services(ACS)の SMTP(Azure SMTP)で実現したい――という要望は多いです。結論として、B2C は SMTP を直接設定できませんが、カスタムポリシーの REST 連携を使えば「B2C → 自作REST API → ACS SMTP」で検証メール送信を構築できます。

目次

やりたいことを整理すると何が難しいのか

Azure AD B2C のメール検証は、ユーザーがメールアドレスを入力し、認証コード(OTP)を受け取り、画面に入力して検証する流れです。問題は「その認証コードメールをどこから送るか」です。

Azure AD B2C(特にカスタムポリシー)には、メール送信の拡張ポイントとして REST API 連携が用意されています。一方で、B2C そのものに「SMTP サーバーを指定して送信する」設定項目はありません。つまり、SMTP を使いたい場合は、B2C から直接 SMTP ではなく、いったん HTTP(REST)で自分のエンドポイントに渡し、そのバックエンドから SMTP で送る必要があります。

結論:推奨アーキテクチャは「B2C → REST API → ACS SMTP」

構成を一言で表すと、次の 1 本線です。

B2C カスタムポリシー
  └─(REST 呼び出し:email / otp などのクレームを送る)
      └─ 自作の送信API(Azure Functions / App Service など)
           └─(SMTP:smtp.azurecomm.net:587 + TLS)
                └─ 受信者のメールボックス

ACS の SMTP 機能は 2024 年春以降に一般提供(GA)となり、既存の SMTP クライアントから送信できる選択肢が増えました。

この構成が現実的な理由

  • B2C は REST API 連携(RESTful Technical Profile)が標準機能として提供されている
  • ACS は SMTP(認証付き送信)に対応しており、送信基盤を Azure で統一できる
  • メール本文のテンプレート、ブランド、言語、ログなどを自作 API 側で柔軟に制御できる

選択肢の比較

選択肢実装難易度ブランド/テンプレ自由度送信基盤の統一向いているケース
B2C 既定メール低低〜中不可最短で動かしたい、こだわりが少ない
SendGrid/Mailjet 等(公式サンプル)中高部分的メール基盤が既に外部にある、テンプレ重視
ACS SMTP(B2C→自作API→SMTP)中〜高高高送信基盤を Azure に寄せたい、運用も Azure で完結したい

ACS 側の準備:Email リソースとドメインを整える

ACS のメール送信は、ざっくり言うと「Email Communication Services 側でドメインと MailFrom を作る」→「Communication Services 側にドメインを接続する」→「SMTP 認証情報を作る」という 3 段階です。

登場する Azure リソースを先に整理

リソースAzure ポータルでの名称役割ここでやること
Email の管理リソースEmail Communication Servicesドメイン検証、SPF/DKIM、MailFrom 管理カスタムドメインの検証、送信元(MailFrom)作成
ACS 本体Communication Servicesメール送信(SMTP/SDK)に使う接続先Email ドメインの接続、SMTP Username 作成

Email Communication Services を作成する

  1. Azure ポータルで「Email Communication Services」を検索し、リソースを作成します。
  2. リージョンやデータロケーションの指定は、後で ACS に接続する際に「同一の地理」である必要があります。環境が複数ある場合は、この時点で命名とタグをきちんと揃えると運用が楽です。

カスタムドメインを検証し、SPF/DKIM を設定する

検証メールは到達率が最重要です。ACS でもカスタムドメインの場合、TXT レコードによる所有確認と、SPF/DKIM(場合によっては DKIM2)の DNS レコード設定が必要になります。

作業目的ポイント
TXT レコードでドメイン所有確認あなたのドメインで送ってよいことを証明DNS 反映に時間がかかることがある(焦って削除しない)
SPF レコード追加なりすまし対策、到達率向上ドメイン/サブドメインのゾーンの違いに注意
DKIM / DKIM2 CNAME 追加署名で改ざん検知、迷惑判定の低減ポータルが提示するレコードを DNS 側に正確に反映

MailFrom アドレスを決める

ACS の Email では、既定の MailFrom(例:donotreply@…azurecomm.net)があります。カスタムドメインの場合は MailFrom を追加・管理でき、受信者から見て自然な送信元(例:[email protected])に寄せられます。

ドメインを ACS(Communication Services)に接続する

Email 側で検証したドメインは、ACS リソース側に「Connect domain」で接続してはじめて送信に使える状態になります。接続できるドメイン数や、地理の一致などの制約もあるため、設計時点で「dev/test/prod でどのドメインを使うか」を決めておくと事故が減ります。

ACS SMTP の認証情報を作る

ACS の SMTP は、一般的な「SMTP ユーザー名/パスワードを払い出す」形に見えますが、中身は Microsoft Entra アプリ(サービスプリンシパル)を使った認証の仕組みです。つまり、パスワード相当は「アプリのクライアントシークレット」になります。

必要な作業の流れ

  1. Microsoft Entra ID でアプリ登録(クライアント ID / テナント ID を取得)
  2. クライアントシークレットを作成(これが SMTP のパスワード相当)
  3. ACS リソース(Communication Services)に対して、アプリにロールを割り当てる(最初は組み込みロールでも可、最小権限にしたいならカスタムロール)
  4. ACS リソースの「SMTP Usernames」で SMTP Username を作成し、上記アプリと紐付ける

SMTP 接続情報の要点

項目値の例補足
SMTP サーバーsmtp.azurecomm.netDNS 名で指定(IP 直指定は避ける)
ポート587(推奨)25 も選べるが、ネットワーク/実行環境で塞がれることがある
TLS有効(StartTLS)TLS 1.2 以上が前提
ユーザー名作成した SMTP Usernameメール形式/任意文字列のどちらも可能(メール形式の場合はドメイン制約あり)
パスワードEntra アプリのクライアントシークレットシークレットローテーション設計が重要

実務メモ:シークレット管理とローテーション

  • シークレットはアプリに直書きせず、Azure Key Vault に格納し、Functions/App Service はマネージド ID で参照するのが安全です。
  • ローテーション時は「新シークレット作成 → Key Vault を更新 → アプリを再起動/設定更新 → 旧シークレット削除」という順序にすると、メール送信が途切れにくいです。

送信用 REST API を作る

B2C から呼ばれる REST API は「メールを送るだけ」の薄いサービスにするのがコツです。B2C は認証コードの生成・検証の仕組みを持てるため、API は送信だけに責務を絞ると保守が簡単になります。

API 仕様の例

項目例
メソッドPOST
パス/send-otp
ボディ{“email”:”[email protected]”,”otp”:”123456″,”locale”:”ja-JP”}
レスポンス200 OK(または 202 Accepted)
エラー400(入力不正)/ 401・403(認証不正)/ 500(送信失敗)

Node.js(nodemailer)で ACS SMTP を使う例

Azure Functions(Node.js)や App Service(Node.js)で動かす場合のイメージです。SMTP の「ユーザー名=SMTP Username」「パスワード=クライアントシークレット」にして、smtp.azurecomm.net:587 を StartTLS で叩きます。

import nodemailer from "nodemailer";

export async function sendOtpEmail({ email, otp }) {
  const transporter = nodemailer.createTransport({
    host: "smtp.azurecomm.net",
    port: 587,
    secure: false, // STARTTLS を使う
    auth: {
      user: process.env.ACS_SMTP_USERNAME, // ACS の「SMTP Username」
      pass: process.env.ACS_SMTP_PASSWORD, // Entra アプリの Client Secret
    },
    tls: {
      minVersion: "TLSv1.2",
    },
  });

  const fromAddress = process.env.ACS_MAIL_FROM; // 例: [email protected]
  const subject = "認証コードのお知らせ";
  const text = `あなたの認証コードは ${otp} です。\n有効期限内に入力してください。`;

  await transporter.sendMail({
    from: fromAddress,
    to: email,
    subject,
    text,
  });
}

.NET(System.Net.Mail)で送る例

Microsoft Learn の SMTP クイックスタートでも、System.Net.Mail の SmtpClient を用いた送信例が示されています。

using System.Net;
using System.Net.Mail;

public static void SendOtp(string to, string otp)
{
    var smtpHost = "smtp.azurecomm.net";
    var smtpUser = Environment.GetEnvironmentVariable("ACS_SMTP_USERNAME");
    var smtpPass = Environment.GetEnvironmentVariable("ACS_SMTP_PASSWORD"); // Client Secret

    var from = Environment.GetEnvironmentVariable("ACS_MAIL_FROM"); // 例: [email protected]
    var subject = "認証コードのお知らせ";
    var body = $"あなたの認証コードは {otp} です。";

    var client = new SmtpClient(smtpHost)
    {
        Port = 587,
        EnableSsl = true,
        Credentials = new NetworkCredential(smtpUser, smtpPass),
    };

    client.Send(new MailMessage(from, to, subject, body));
}

API 実装で必ず入れたい実務機能

  • 入力バリデーション:email の形式、otp の桁数、想定外の文字の排除
  • 送信回数制限:同一メールへの短時間連投を防ぐ(後述)
  • ログ(相関ID):B2C 側の相関IDや request-id をログに残すと、障害解析が速い
  • テンプレート分離:件名・本文はテンプレートとして外だしし、ブランド変更時にコード改修を減らす

API のセキュリティは「B2C からの正当な呼び出し」を証明する

この API はインターネット経由で呼び出されるため、無防備にすると第三者が勝手にメール送信できてしまいます。B2C の RESTful Technical Profile では、複数の認証方式が選べます。

B2C が対応している主な認証方式

方式設定のしやすさセキュリティ向いているケース
ApiKeyHeader高中Azure Functions(x-functions-key)や単純な API キーで守る
Basic中中APIM などで Basic を受ける、早く作りたい
ClientCertificate中〜低高最も堅牢に「B2C だけ」を通したい(推奨)
Bearer中高(設計次第)Bearer トークン運用を既に持っている

ポイントとして、B2C の ApiKeyHeader は「ヘッダーを 1 つだけ」しか扱えない制約があります。複数ヘッダーが必要な方式を採るなら、APIM などで一度受けて、内部で必要なヘッダーに変換する設計にします。

最短で安全にするなら

  • Azure Functions の場合:ApiKeyHeader(例:x-functions-key)で保護し、さらに APIM を前段に置いてレート制限
  • より堅牢にするなら:ClientCertificate で B2C からの呼び出しだけを許可

B2C カスタムポリシー側の実装:メール検証の送信部分だけを差し替える

「カスタムポリシーでメール検証を ACS に置き換える」場合、最もハマりにくいのは、Microsoft Learn のカスタムメール検証(SendGrid)サンプルと同じ発想で、VerificationControl の SendCode アクション内にある送信 Technical Profile を置き換える方法です。これなら、OTP の生成・検証は B2C の仕組みを使い、送信だけ自分の API に差し替えられます。

全体像

  • GenerateOtp:B2C が OTP を生成(有効期限、桁数などを設定可能)
  • SendOtp:REST API を呼び、API が ACS SMTP でメール送信(ここを自作に置換)
  • VerifyOtp:ユーザーが入力したコードを検証

ClaimType の例

すでにスターターパック(SocialAndLocalAccounts)を使っている前提で、追加が必要になりやすいクレームの例です。名前は環境に合わせて調整してください。

<BuildingBlocks>
  <ClaimsSchema>
    <ClaimType Id="otp">
      <DisplayName>One-time password</DisplayName>
      <DataType>string</DataType>
    </ClaimType>

    <ClaimType Id="verificationCode">
      <DisplayName>Verification Code</DisplayName>
      <DataType>string</DataType>
      <UserHelpText>メールで届いた認証コードを入力してください</UserHelpText>
      <UserInputType>TextBox</UserInputType>
    </ClaimType>
  </ClaimsSchema>
</BuildingBlocks>

VerificationControl の例

VerificationControl は、送信(SendCode)と検証(VerifyCode)を UI と一体で扱えるため、メール検証の実装がすっきりします。SendCode の中で GenerateOtp → SendOtp を呼ぶのがポイントです。

<BuildingBlocks>
  <DisplayControls>
    <DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
      <DisplayClaims>
        <DisplayClaim ClaimTypeReferenceId="email" Required="true" />
        <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
      </DisplayClaims>

      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="email" />
      </OutputClaims>

      <Actions>
        <Action Id="SendCode">
          <ValidationClaimsExchange>
            <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" />
            <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendOtpUsingAcsSmtp" />
          </ValidationClaimsExchange>
        </Action>

        <Action Id="VerifyCode">
          <ValidationClaimsExchange>
            <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
          </ValidationClaimsExchange>
        </Action>
      </Actions>
    </DisplayControl>
  </DisplayControls>
</BuildingBlocks>

OTP 生成・検証の Technical Profile(ほぼテンプレ)

OTP を生成・検証する Technical Profile は、SendGrid サンプルと同様に OneTimePasswordProtocolProvider を使います。期限や桁数、再試行回数などをここで制御できます。なお、この OTP はブラウザーセッションに紐づく挙動があるため、複数セッションでの再送などの仕様も理解して設計してください。

<ClaimsProvider>
  <DisplayName>One time password technical profiles</DisplayName>
  <TechnicalProfiles>

    <TechnicalProfile Id="GenerateOtp">
      <DisplayName>Generate one time password</DisplayName>
      <Protocol Name="Proprietary"
        Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
      <Metadata>
        <Item Key="Operation">GenerateCode</Item>
        <Item Key="CodeExpirationInSeconds">600</Item>
        <Item Key="CodeLength">6</Item>
        <Item Key="CharacterSet">0-9</Item>
        <Item Key="NumRetryAttempts">5</Item>
        <Item Key="NumCodeGenerationAttempts">10</Item>
        <Item Key="ReuseSameCode">false</Item>
      </Metadata>
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
      </InputClaims>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="otp" PartnerClaimType="otpGenerated" />
      </OutputClaims>
    </TechnicalProfile>

    <TechnicalProfile Id="VerifyOtp">
      <DisplayName>Verify one time password</DisplayName>
      <Protocol Name="Proprietary"
        Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
      <Metadata>
        <Item Key="Operation">VerifyCode</Item>
      </Metadata>
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
        <InputClaim ClaimTypeReferenceId="verificationCode" PartnerClaimType="otpToVerify" />
      </InputClaims>
    </TechnicalProfile>

  </TechnicalProfiles>
</ClaimsProvider>

送信用 REST Technical Profile(ここが ACS 置換の本体)

SendGrid では SendGrid API に直接投げるために複雑な JSON を GenerateJson で組み立てますが、ACS SMTP を使う場合は「自作 API」に投げればよいので、構造をシンプルにできます。B2C からは email と otp を送って、API 側でテンプレートを組み立てて送信します。

<ClaimsProvider>
  <DisplayName>RestfulProvider</DisplayName>
  <TechnicalProfiles>

    <TechnicalProfile Id="SendOtpUsingAcsSmtp">
      <DisplayName>Send OTP via Custom API (ACS SMTP)</DisplayName>
      <Protocol Name="Proprietary"
        Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

      <Metadata>
        <Item Key="ServiceUrl">https://your-api.example.com/send-otp</Item>
        <Item Key="SendClaimsIn">Body</Item>

        <!-- 例:Functions の x-functions-key を使う -->
        <Item Key="AuthenticationType">ApiKeyHeader</Item>

        <Item Key="DefaultUserMessageIfRequestFailed">現在メールを送信できません。しばらくしてから再度お試しください。</Item>
        <Item Key="AllowInsecureAuthInProduction">false</Item>
      </Metadata>

      <CryptographicKeys>
        <Key Id="x-functions-key" StorageReferenceId="B2C_1A_SendOtpApiKey" />
      </CryptographicKeys>

      <InputClaims>
        <InputClaim ClaimTypeReferenceId="email" />
        <InputClaim ClaimTypeReferenceId="otp" />
      </InputClaims>
    </TechnicalProfile>

  </TechnicalProfiles>
</ClaimsProvider>

二重送信を防ぐ考え方

よくある失敗は「既定の検証メール送信の仕組みが残っているのに、別のステップで独自送信を追加してしまい、同じタイミングで 2 通送られる」ことです。

安全な方針は次のどちらかです。

  • 推奨:VerificationControl の SendCode 内にある送信 Technical Profile を置換する(送信が一箇所に集約され、二重送信が起きにくい)
  • どうしても別ステップで送る場合:既定の送信箇所を明確に無効化/置換して、送信箇所が 1 箇所だけになるように整理する

テストと監視:つまずきポイントを先回りする

メール送信は「動けば終わり」ではなく、環境差・ネットワーク・到達率で事故りやすい領域です。最低限、次の観点でテストします。

テスト観点

観点チェック内容おすすめの確認方法
送信成功サインアップ/パスワードリセットでメールが届くFunctions/App Service のログ、SMTP 例外の有無
認証コード一致届いた OTP を入力して通るOTP の有効期限と再送挙動を確認
再送・連打耐性短時間で SendCode を連打されたときの挙動API 側でレート制限、ログに残るか
到達率迷惑メールに入らないSPF/DKIM 設定、件名/本文の見直し

よくあるエラーと対処

症状原因候補対処
API は呼ばれるがメールが届かないMailFrom 未設定/誤り、ドメイン未接続、SPF/DKIM 未完了ドメイン検証と接続、MailFrom を再確認
SMTP 認証で失敗するSMTP Username 未 Ready、ロール未割当、Client Secret 誤り/期限切れSMTP Username の状態、IAM ロール、シークレットの有効期限を確認
ネットワークエラー587 がブロック、TLS 交渉失敗実行環境のアウトバウンド制御を確認、TLS 1.2 以上を強制
B2C 画面で「要求を処理できません」系REST Technical Profile が 4xx/5xx を返したDefaultUserMessageIfRequestFailed を設定しつつ、API 側のログで原因特定

運用で差がつくポイント:レート制限と不正対策

認証コード送信 API は攻撃者にとっても便利な「メール送信装置」になり得ます。B2C 側のフローだけに頼らず、API 側でも最低限の不正対策を入れておくと運用事故を大きく減らせます。

  • レート制限:同一メールアドレスに対する送信間隔を制限(例:60 秒に 1 回まで)
  • IP/地域制御:APIM や WAF で異常なリクエストを遮断
  • 監査ログ:メールアドレスはマスクしつつ、送信回数・失敗理由を記録
  • テンプレの安全設計:ユーザー入力を件名・HTML にそのまま埋め込まない(ヘッダーインジェクション等の対策)

補足:Azure AD B2C の今後と、新規導入の選択肢

Microsoft Learn のドキュメントでは、Azure AD B2C は 2025 年 5 月以降、新規購入に関する注意が明記されています。一方で、既存顧客向けのサポート継続方針も示されています。新規案件の場合は Microsoft Entra External ID(外部テナント)での設計も含め、将来性と要件を照らして選ぶのが安全です。

また、External ID(外部テナント)では「OTP 送信イベント(emailOtpSend)で REST API を呼び出して自前のメール基盤に流す」タイプの拡張も案内されています。B2C カスタムポリシーとは別物ですが、「OTP メールを ACS で送りたい」だけが目的なら、選択肢として知っておく価値はあります。

まとめ

  • Azure AD B2C は SMTP を直接設定できないため、「B2C → REST API → ACS SMTP」で実現するのが現実解
  • ACS SMTP は smtp.azurecomm.net:587 + TLS、認証は Entra アプリ(SMTP Username と Client Secret)で構成する
  • B2C 側は VerificationControl + GenerateOtp/VerifyOtpを使い、送信 Technical Profile(SendOtp)だけを差し替えると二重送信を避けやすい
  • 送信 API は必ず認証・レート制限・ログを入れ、運用事故(不正送信/大量送信)を防ぐ

この記事を書いた人

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

コメント

コメントする

目次