.NET 8 MAUIでSMSのOTPを自動入力する方法|iOS/Androidの制約と実装例

.NET 8 MAUIでログイン/本人確認画面を作るとき、SMSで届くワンタイムコード(OTP)をEntryに自動入力できれば、入力ミスや離脱を大きく減らせます。ただしiOSとAndroidでは「できること」が根本的に違い、同じ発想で実装すると詰みます。両OSの制約を整理し、MAUIで現実的に運用できる実装パターンをまとめます。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

.NET 8 MAUIでSMSのOTP自動入力を検討する前提

まず押さえるべきは、「アプリがSMS本文を読んでOTPを取り出す」という発想が、OSや配布形態によっては成立しない(または非常にリスクが高い)という点です。特にiOSはプライバシー設計上、アプリがユーザーのSMS本文へアクセスする仕組みが用意されていません。

観点iOS(MAUI)Android(MAUI)
アプリがSMS本文を直接読む不可(OS仕様・プライバシー制約)可能(ただし強い権限が必要/審査・ポリシー面の難易度が高い)
「OSのOTPオートフィル」を使う可能(ワンタイムコード提案→タップで入力)可能(端末・キーボード・設定に依存)
ユーザー同意のもとで1通だけ取得(API)SMS自体を読むAPIは基本なし可能(SMS User Consent API 等)
権限なしでOTPを受け取る(API)SMS本文は読めない(別アプローチが必要)可能(SMS Retriever API 等)

結論:iOSは「SMS本文の読み取り」はできない。狙うべきはOSオートフィル

iOSでできる最適解は、アプリ側でSMS本文を解析して自動入力するのではなく、iOSの「セキュリティコード(ワンタイムコード)自動入力」が働くように画面(Entry)とメッセージ文面を整えることです。ユーザー体験としては「キーボード上部にコードが提案され、タップするとEntryに入る」挙動になります。

iOS側で狙うUX:キーボード上の「ワンタイムコード」提案

iOSの自動入力を引き出すために、アプリ側ではネイティブのテキストフィールドに対して「これはワンタイムコード入力だ」と伝える必要があります。ネイティブ(UITextField)ではtextContentTypeoneTimeCodeに設定するのが定番です。

MAUIでiOSのone-time-codeを有効にする実装例(Handlerで設定)

MAUIはハンドラー(Handler)でプラットフォームのネイティブビューにアクセスできます。OTP専用のEntryを作って、iOSだけネイティブ側へ設定を流す形が分かりやすいです。

1) 共有プロジェクトにOTP専用Entryを用意

using Microsoft.Maui.Controls;

namespace YourApp.Controls;

public class OtpEntry : Entry
{
// 必要なら桁数や正規表現などのプロパティを追加してもOK
}

2) MauiProgramでHandlerマッピングを追加(iOS向け oneTimeCode)

using Microsoft.Maui.Handlers;
using YourApp.Controls;

#if IOS
using UIKit;
#endif

namespace YourApp;

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp();


    // OtpEntryに対するプラットフォーム設定
    EntryHandler.Mapper.AppendToMapping("OtpEntryMapping", (handler, view) =>
    {
        if (view is not OtpEntry)
            return;


#if IOS
// iOS:ワンタイムコード用のコンテンツタイプを設定
handler.PlatformView.TextContentType = UITextContentType.OneTimeCode;


        // できれば数字キーボード(見た目・入力ミス低減)
        handler.PlatformView.KeyboardType = UIKeyboardType.NumberPad;


#endif
});


    return builder.Build();
}


}

3) XAMLでOtpEntryを使う

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:controls="clr-namespace:YourApp.Controls"
    x:Class="YourApp.Views.VerifyPage">


<VerticalStackLayout Padding="24" Spacing="12">
    <Label Text="SMSに届いた認証コードを入力してください" />

    <controls:OtpEntry
        x:Name="OtpEntry"
        Keyboard="Numeric"
        MaxLength="6"
        Placeholder="例:123456" />
</VerticalStackLayout>


iOSのオートフィルを出しやすくするSMS文面のコツ

サーバー側(SMS送信側)の文面が雑だと、iOSがワンタイムコードとして認識しにくくなります。以下は「認識されやすい形」を目指すための実務的な観点です(厳密なルールというより成功率を上げるコツです)。

ポイント推奨避けたい
コードの表記4〜8桁程度の数字を単独に近い形で文中に複数の数字(注文番号/電話番号/日付)が混在
説明文「認証コード」「確認コード」など用途が明確「番号」「ID」など曖昧な表現
アプリ名送信元・本文にアプリ名/サービス名が含まれるどのアプリ向けか分からない
URL基本は最小限URLや短縮URL、追跡パラメータが多い

iOSは「アプリがSMSを読む」ことはできませんが、OSの提案→タップで入力の導線を整えるだけでも体感はかなり良くなります。iOS対応をこの方向に寄せることで、プライバシーや審査の地雷を踏みにくくなります。

Androidは実装可能。ただし最初に「どの方式を選ぶか」が最重要

Androidでは技術的にはSMSを受信して本文からOTPを抽出できます。しかし、READ/RECEIVE_SMSは非常に強い(危険)権限で、ユーザーの心理的ハードルも高く、配布ストアのポリシーや審査で問題になりやすいです。

そのため実務では「権限で読む」以外の選択肢(SMS Retriever / User Consent)を先に検討し、どうしても必要な場合のみ権限方式に寄せるのが安定します。

方式SMS権限ユーザー操作実装難易度向いているケース
SMS Retriever API(推奨)不要基本不要(自動で受信→抽出)一般向けアプリ、審査・運用の安定重視
SMS User Consent API不要「このSMSを読み取りますか?」に同意が必要自動取得よりも透明性を優先したい
SMS_RECEIVED + 解析(RECEIVE/READ_SMS)必要(強い)権限許可が必須低〜中企業内配布、端末管理下、ストア制約がない環境

Android推奨:SMS Retriever APIでOTPを受け取る(権限なし)

権限なしでOTPの自動入力を成立させる代表例がSMS Retriever APIです。ポイントは、アプリ固有の署名ハッシュ(アプリハッシュ)をSMS本文に含めること。端末側が「このSMSはこのアプリ宛だ」と判断できるため、アプリはSMS権限なしで対象メッセージだけ受け取れます。

実装の全体像

手順やることMAUIでの置き場所
依存関係Google Play services(Auth API Phone系)を利用できるようにするPlatforms/Android(NuGet追加)
開始リスナー開始(通常はOTP入力画面表示時)MainActivity / Androidサービス
受信Retriever用BroadcastReceiverでメッセージを受け取るPlatforms/Android
抽出本文からOTP(数字)を正規表現で抽出Platforms/Android
反映共有コードへ通知し、Entry.Textを更新Views / ViewModel

SMS Retrieverの受信例(BroadcastReceiver)

以下は「受け取ったSMS文字列からOTPを抜いて共有側へ通知する」骨組みです。依存パッケージや名前空間は環境で差が出るため、基本構造として捉えてください(概念が合っていれば移植できます)。

using Android.App;
using Android.Content;
using Android.Gms.Auth.Api.Phone;
using Android.Gms.Common;
using Android.Gms.Common.Apis;
using CommunityToolkit.Mvvm.Messaging;
using System.Text.RegularExpressions;

namespace YourApp.Platforms.Android;

public sealed record OtpReceivedMessage(string Code);

[BroadcastReceiver(Enabled = true, Exported = true)]
[IntentFilter(new[] { SmsRetriever.SmsRetrievedAction })]
public class SmsRetrieverReceiver : BroadcastReceiver
{
    public override void OnReceive(Context context, Intent intent)
    {
        if (intent.Action != SmsRetriever.SmsRetrievedAction)
            return;

        var extras = intent.Extras;
        if (extras is null) return;

        var status = extras.Get(SmsRetriever.ExtraStatus) as Status;
        if (status is null) return;

        if (status.StatusCode == CommonStatusCodes.Success)
        {
            var message = extras.GetString(SmsRetriever.ExtraSmsMessage);
            if (string.IsNullOrWhiteSpace(message)) return;

            // 例:4〜8桁をOTPとして抽出(要件に合わせて調整)
            var m = Regex.Match(message, @"\b(\d{4,8})\b");
            if (!m.Success) return;

            var code = m.Groups[1].Value;
            WeakReferenceMessenger.Default.Send(new OtpReceivedMessage(code));
        }
        else if (status.StatusCode == CommonStatusCodes.Timeout)
        {
            // タイムアウト(一定時間で待受が切れる)
            // 必要なら再開やユーザーへの案内を行う
        }
    }
}

OTP入力画面を表示したタイミングでRetrieverを開始

待ち受けを「常時ON」にするのではなく、OTP入力画面(確認画面)を開いたときに開始すると無駄がなく、挙動も追いやすくなります。

// Platforms/Android/MainActivity.cs の例(概念)
using Android.Gms.Auth.Api.Phone;

protected override void OnCreate(Bundle savedInstanceState)
{
    base.OnCreate(savedInstanceState);

    // 必要になったタイミングで StartSmsRetriever を呼ぶのが理想。
    // ここでは例としてOnCreateに書いているが、画面遷移時に呼ぶ設計がおすすめ。
    SmsRetriever.GetClient(this).StartSmsRetriever();
}

共有側(Page / ViewModel)で受信してEntryへ反映

CommunityToolkit.MvvmのWeakReferenceMessengerを使うと、プラットフォーム側→共有側の通知が素直になります(依存を増やしたくなければイベントやDIでもOKです)。

using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Maui.Dispatching;
using YourApp.Platforms.Android; // OtpReceivedMessage の参照

public partial class VerifyPage : ContentPage, IRecipient<OtpReceivedMessage>
{
    public VerifyPage()
    {
        InitializeComponent();
        WeakReferenceMessenger.Default.Register(this);
    }

    public void Receive(OtpReceivedMessage message)
    {
        // UIスレッドで更新
        MainThread.BeginInvokeOnMainThread(() =>
        {
            OtpEntry.Text = message.Code;
            // ここで自動送信するかどうかはUX次第
            // 例:6桁そろったら自動検証する、など
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        WeakReferenceMessenger.Default.Unregister<OtpReceivedMessage>(this);
    }
}

SMS Retrieverで成功率を上げるためのSMS文面設計

Retrieverは「このアプリ宛のSMS」だと分かる情報を本文に入れる必要があります。実運用ではSMS送信基盤側でテンプレートを用意し、OTPだけ差し替える方式が管理しやすいです。

項目意識すること
OTPの抽出本文中の数字が複数あると誤抽出するため、OTP以外の数字を極力減らす
アプリハッシュRetrieverが拾える形式にする(末尾付近に固定で入れる運用が多い)
メッセージ長長すぎると端末側で扱いが不安定になるため、短く簡潔に

SMS送信側が自由にいじれない(外部ベンダー固定など)場合は、次に紹介する「権限方式」や「User Consent」へ寄せた方が早いケースもあります。

Android実装例:SMS受信(BroadcastReceiver)でOTPを抽出してEntryへ自動入力

ここからは、質問で挙がりやすい「SMS受信イベント(SMS_RECEIVED)を拾って本文を解析する」方式を、MAUI向けに噛み砕いて解説します。技術的には分かりやすい一方で、強い権限を伴うため、配布形態や審査要件に注意してください。

必要な権限と役割

権限目的備考
android.permission.RECEIVE_SMSSMS受信ブロードキャストを受け取る本文は受信インテントから取得できることが多い
android.permission.READ_SMSSMS受信箱(content://sms)を読み取る「最新SMSをクエリして読む」方式で必要。権限負担が増える

設計としては、まずRECEIVE_SMSだけで足りる構成を目指し、どうしても必要な場合だけREAD_SMSを追加するのが無難です。

AndroidManifest.xmlに権限を追加

MAUIのAndroidマニフェストは通常Platforms/Android/AndroidManifest.xmlを編集します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

  <uses-permission android:name="android.permission.RECEIVE_SMS" />
  <!-- Inboxをクエリするなら必要(可能なら避ける) -->
  <uses-permission android:name="android.permission.READ_SMS" />

  <application ... />
</manifest>

実行時権限の許可要求(危険権限)

Android 6.0(API 23)以降、危険権限は実行時にユーザー許可が必要です。MAUIでもMainActivity側で要求できます。

using Android.Content.PM;
using Android.OS;
using AndroidX.Core.App;
using AndroidX.Core.Content;

namespace YourApp;

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true)]
public class MainActivity : MauiAppCompatActivity
{
    const int RequestSmsPermissionsId = 1001;

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        RequestSmsPermissionsIfNeeded();
    }

    void RequestSmsPermissionsIfNeeded()
    {
        var permissions = new[]
        {
            Android.Manifest.Permission.ReceiveSms,
            Android.Manifest.Permission.ReadSms, // 必要な場合のみ
        };

        var needRequest = permissions.Any(p =>
            ContextCompat.CheckSelfPermission(this, p) != Permission.Granted);

        if (needRequest)
            ActivityCompat.RequestPermissions(this, permissions, RequestSmsPermissionsId);
    }

    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
    {
        base.OnRequestPermissionsResult(requestCode, permissions, grantResults);

        // Essentials系を使っている場合はPlatformへも伝搬する(環境に応じて)
        // Microsoft.Maui.ApplicationModel.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

運用では、単に許可を求めるのではなく、なぜ必要なのか(OTP入力を省略してログインを簡単にする等)を事前に説明する画面を挟むと許可率が上がります。

SMS_RECEIVEDを受け取るBroadcastReceiver(本文からOTP抽出)

受信したSMS本文は、受信インテントのPDUから取得できます。MAUIではPlatforms/Android配下にReceiverクラスを置くと整理しやすいです。

using Android.App;
using Android.Content;
using Android.Provider;
using Android.Telephony;
using CommunityToolkit.Mvvm.Messaging;
using System.Text.RegularExpressions;

namespace YourApp.Platforms.Android;

public sealed record OtpReceivedMessage(string Code);

[BroadcastReceiver(Enabled = true, Exported = true, Permission = "android.permission.BROADCAST_SMS")]
[IntentFilter(new[] { Telephony.Sms.Intents.SmsReceivedAction })]
public class OtpSmsReceiver : BroadcastReceiver
{
    public override void OnReceive(Context context, Intent intent)
    {
        if (intent.Action != Telephony.Sms.Intents.SmsReceivedAction)
            return;

        var messages = Telephony.Sms.Intents.GetMessagesFromIntent(intent);
        if (messages is null || messages.Length == 0) return;

        // 分割SMSの可能性があるため連結
        var body = string.Concat(messages.Select(m => m.MessageBody ?? string.Empty));
        var from = messages[0].OriginatingAddress ?? string.Empty;

        // 送信元でフィルタ(任意:SMS送信元番号/アルファベット送信者IDなど)
        // if (!from.Contains("YourSender")) return;

        // OTP抽出(例:6桁固定。要件に応じて4〜8桁にする等)
        var match = Regex.Match(body, @"\b(\d{6})\b");
        if (!match.Success) return;

        var code = match.Groups[1].Value;

        // 共有側へ通知
        WeakReferenceMessenger.Default.Send(new OtpReceivedMessage(code));
    }
}

ポイントは次のとおりです。

  • 分割SMSを想定し、本文を連結してから抽出する
  • 送信元フィルタ(送信者ID/電話番号/キーワード)を入れて誤爆を防ぐ
  • 正規表現は「ゆるすぎる」と誤抽出し、「固すぎる」と取りこぼす。実SMSで検証して調整する

「最新SMSをinboxから拾う」方式(ContentResolver.Query)の注意点

質問の流れでよくあるのが、Receiverで受信通知だけ拾って、本文はcontent://sms/inboxから最新を引く実装です。これは実装としては可能ですが、次のデメリットがあります。

  • READ_SMSが必要になり、権限負担が増える
  • タイミング次第で「最新」が別のSMSになるなど、誤取得リスクがある
  • 端末やOEM実装差で、SMS Provider周りの挙動がブレることがある

それでも実装する場合の最小例は以下です(必ず送信元や本文パターンで絞り込む設計にしてください)。

using Android.Content;
using Android.Database;
using Android.Net;

public static class SmsInboxReader
{
    public static string? GetLatestSmsBody(Context context)
    {
        var uri = Uri.Parse("content://sms/inbox");
        var projection = new[] { "address", "body", "date" };

        using ICursor? cursor = context.ContentResolver?.Query(
            uri,
            projection,
            null,
            null,
            "date DESC"
        );

        if (cursor is null) return null;
        if (!cursor.MoveToFirst()) return null;

        var bodyIndex = cursor.GetColumnIndex("body");
        if (bodyIndex < 0) return null;

        return cursor.GetString(bodyIndex);
    }
}

実務では「受信インテントから本文を取る」方がシンプルかつ安全なため、まずはReceiver内で完結する設計をおすすめします。

Entryに自動入力する際の実装ポイント(MAUI共通)

SMSからOTPを得られても、Entry.Textにそのまま入れて終わりだと、UXが伸びません。OTP入力は失敗しやすい導線なので、以下をセットで入れると完成度が上がります。

ポイント理由実装のヒント
数字のみ入力誤入力・貼り付け時の混入を防ぐKeyboard="Numeric"+入力検証
最大桁数余計な文字を受け付けないMaxLengthを設定(例:6)
貼り付け対応自動入力できない端末でも救えるハイフンや空白を除去してから検証
自動送信の条件打鍵中に送信しない桁数が揃ったタイミングでのみ検証
タイムアウト/再送待ち続けるUXを避けるカウントダウンと再送ボタン

貼り付け・自動入力の共通整形(数字だけ取り出す)

using System.Text.RegularExpressions;

public static class OtpNormalizer
{
public static string Normalize(string? input, int maxLen = 6)
{
if (string.IsNullOrWhiteSpace(input))
return string.Empty;


    // 数字だけ抜く("123-456" や "コード: 123456" にも対応)
    var digits = Regex.Replace(input, @"\D", "");

    if (digits.Length > maxLen)
        digits = digits.Substring(0, maxLen);

    return digits;
}


}

受信側の正規表現が取りこぼしたときでも、最終的にこの正規化を通すことで「余計な文字がEntryに入る」事故を減らせます。

実運用での注意点(審査・セキュリティ・ログ)

OTPは本人確認に直結するため、実装が動けばOKではありません。特にAndroidのSMS権限方式は運用事故が起きやすいので、次の観点を強く意識してください。

  • SMS本文をログに出さない(デバッグログ、クラッシュレポート、解析基盤へ流さない)
  • OTPは保存しない(Preferences等へ永続化しない/必要なら短時間で破棄)
  • 送信元フィルタ本文パターンで誤抽出を防ぐ(他サービスのSMSを拾う事故を避ける)
  • 権限が拒否されたときの導線を必ず用意(手入力/貼り付けで完結できるようにする)
  • 公開ストア配布の場合、SMS権限はポリシーや審査で制約が強い。可能ならRetriever / Consentを優先する

よくあるハマりどころ(トラブルシューティング)

AndroidでReceiverが呼ばれない

  • 権限が許可されているか(設定アプリ→権限)を確認
  • Android 12以降をターゲットにしている場合、ReceiverのExported設定が不足していると受信できないことがある
  • インテントフィルタのアクション名が正しいか(Telephony.Sms.Intents.SmsReceivedAction
  • 端末の省電力設定でバックグラウンド制限が強い場合、想定と挙動が変わることがある(ただしOTP画面表示中に受け取る設計なら影響を減らせる)

OTPが誤って抽出される/抽出できない

  • SMS本文に数字が複数ある:送信テンプレを見直し、OTP以外の数字を減らす
  • 桁数が固定でない:抽出正規表現を\d{4,8}などへ調整し、最終的に桁数チェックで弾く
  • 送信元が変わる:電話番号の固定が難しい場合、本文中のキーワード(サービス名)を併用する

iOSでオートフィルが出ない

  • UITextField側にoneTimeCodeが設定されているか(Handlerでの設定漏れ)
  • SMS本文が冗長すぎる/数字が多すぎる:テンプレートを短くし、OTPが目立つようにする
  • 端末側の設定や学習状態にも左右される:手入力・貼り付けで完結できるUIを必ず残す

まとめ:iOSは「読まない」、Androidは「読めるが選び方が重要」

.NET 8 MAUIでSMSのOTP自動入力を狙うとき、iOSは「SMS本文を読む」のではなくOSのオートフィルが働く状態を作るのが王道です。一方Androidは複数の実装方式があり、配布形態・審査・ユーザー同意の観点で最適解が変わります。一般向けアプリで安定運用を目指すなら、まずは権限不要のRetriever/Consent系を検討し、最後の手段として権限方式を採用すると事故が減ります。

この記事を書いた人

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

コメント

コメントする

目次