.NET MAUIセキュリティ実践ガイド:SecureStorage・OAuth・証明書ピンニング・生体認証の実装手順

.NET MAUIアプリのセキュリティ対策を実践的に解説。SecureStorageでの機密データ管理、OAuth 2.0/PKCE認証、HTTPS証明書ピンニング、生体認証の実装からAPIキー管理まで、.NET 10対応のC#コード付きで紹介します。

.NET MAUIセキュリティ実装ガイド2026

はじめに:なぜ.NET MAUIアプリにセキュリティ対策が必要なのか

「セキュリティは後から対応すればいい」——開発の現場で、こういう声を聞いたことがある方は多いんじゃないでしょうか。正直なところ、自分も駆け出しの頃はそう思っていた時期がありました。でも実際には、その「後から」が来る頃にはもう手遅れになっていることがほとんどです。

ユーザーの個人情報漏洩、APIキーの流出、中間者攻撃によるデータ改ざん。こうしたセキュリティインシデントは、アプリの信頼を一瞬で崩壊させます。

.NET MAUI(Multi-platform App UI)は、Android、iOS、Windows、macOSを単一コードベースでカバーするクロスプラットフォームフレームワークです。複数プラットフォーム対応ということは、それぞれ固有のセキュリティメカニズムを理解して適切に使いこなす必要があるわけです。これが意外と大変なんですよね。

この記事では、.NET MAUIアプリにおけるセキュリティ対策を実践的に解説していきます。SecureStorageによる機密データ管理から、OAuth 2.0/OIDC認証フロー、HTTPS通信の証明書ピンニング、生体認証の実装まで。.NET 10(LTS)の最新機能も含めて、コード例を交えながら一つずつ見ていきましょう。

SecureStorageによる機密データの安全な保存

.NET MAUIのISecureStorageインターフェースは、暗号化されたキー/バリューストレージを提供してくれます。各プラットフォームのネイティブなセキュリティメカニズムを活用しているので、自前で暗号化処理を書く必要がありません。これだけでもかなりありがたい機能です。

プラットフォーム別の暗号化メカニズム

SecureStorageは、各プラットフォームで以下のネイティブAPIを利用してデータを暗号化しています。

  • AndroidEncryptedSharedPreferencesを使用。キーは決定論的に暗号化され、値はAES-256 GCMで非決定論的に暗号化されます
  • iOS:KeyChainを使用。SecRecordのServiceに[BundleId].microsoft.maui.essentials.preferencesが設定されます
  • WindowsDataProtectionProviderを使用。暗号化された値はApplicationDataContainerに保存されます

つまり、どのプラットフォームでもOSレベルの保護が効いているということです。

基本的な使い方:トークンの保存・取得・削除

まずはシンプルなところから。認証トークンの保存から取得、削除までの基本操作です。

// トークンの保存
await SecureStorage.Default.SetAsync("access_token", loginResult.AccessToken);
await SecureStorage.Default.SetAsync("refresh_token", loginResult.RefreshToken);
await SecureStorage.Default.SetAsync("id_token", loginResult.IdentityToken);

// トークンの取得
string accessToken = await SecureStorage.Default.GetAsync("access_token");
if (accessToken == null)
{
    // トークンが存在しない場合(初回起動やログアウト後)
    await NavigateToLoginPage();
    return;
}

// ログアウト時にすべてのトークンを削除
SecureStorage.Default.RemoveAll();

// 特定のキーのみ削除する場合
SecureStorage.Default.Remove("access_token");

見ての通り、APIは非常にシンプルです。SetAsync/GetAsync/Removeの3つを押さえておけば基本的な操作は十分カバーできます。

サービスクラスでの安全なトークン管理

実際のプロジェクトでは、トークン管理をサービスクラスに切り出してDIコンテナに登録するのが定番パターンです。直接SecureStorageを呼び出すよりもテストしやすくなりますし、トークンの有効期限管理なども一箇所にまとめられます。

public interface ITokenService
{
    Task<string?> GetAccessTokenAsync();
    Task SaveTokensAsync(string accessToken, string refreshToken);
    Task ClearTokensAsync();
    Task<bool> IsAuthenticatedAsync();
}

public class SecureTokenService : ITokenService
{
    private const string AccessTokenKey = "access_token";
    private const string RefreshTokenKey = "refresh_token";
    private const string TokenExpiryKey = "token_expiry";

    public async Task<string?> GetAccessTokenAsync()
    {
        var expiry = await SecureStorage.Default.GetAsync(TokenExpiryKey);
        if (expiry != null && DateTime.Parse(expiry) < DateTime.UtcNow)
        {
            // トークン期限切れの場合はリフレッシュを試行
            return null;
        }
        return await SecureStorage.Default.GetAsync(AccessTokenKey);
    }

    public async Task SaveTokensAsync(string accessToken, string refreshToken)
    {
        await SecureStorage.Default.SetAsync(AccessTokenKey, accessToken);
        await SecureStorage.Default.SetAsync(RefreshTokenKey, refreshToken);
        var expiry = DateTime.UtcNow.AddHours(1).ToString("O");
        await SecureStorage.Default.SetAsync(TokenExpiryKey, expiry);
    }

    public async Task ClearTokensAsync()
    {
        SecureStorage.Default.Remove(AccessTokenKey);
        SecureStorage.Default.Remove(RefreshTokenKey);
        SecureStorage.Default.Remove(TokenExpiryKey);
    }

    public async Task<bool> IsAuthenticatedAsync()
    {
        var token = await GetAccessTokenAsync();
        return token != null;
    }
}

Androidの自動バックアップに関する注意点

ここは意外と見落としがちなポイントです。Android 6.0以降にはアプリデータの自動バックアップ機能があり、SharedPreferencesのデータもバックアップ対象になります。

新しいデバイスに復元された場合、暗号化キーが異なるため復号化に失敗する可能性があります。.NET MAUIはこのケースを自動的に処理してくれますが、念のためAndroidManifest.xmlで自動バックアップの対象から除外しておくと安心です。

<!-- AndroidManifest.xml -->
<application android:allowBackup="true"
             android:fullBackupContent="@xml/backup_rules">
</application>

<!-- Resources/xml/backup_rules.xml -->
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
    <exclude domain="sharedpref"
             path="*.microsoft.maui.essentials.preferences.xml" />
</full-backup-content>

OAuth 2.0/OIDC認証フローの実装

モバイルアプリの認証で絶対にやってはいけないこと、それはアプリ内にクライアントシークレットを保存することです。モバイルアプリのコードやバイナリに含まれる秘密情報は、すべて安全ではないと考えてください。リバースエンジニアリングで簡単に抜かれます。

そこで推奨されるのが、OAuth 2.0のAuthorization Code Flow with PKCE(Proof Key for Code Exchange)です。

WebAuthenticatorを使ったOAuth認証

.NET MAUIのIWebAuthenticatorは、ブラウザベースの認証フローを開始し、アプリに登録されたコールバックURLでレスポンスを受け取る仕組みを提供します。実際のコードを見てみましょう。

public class AuthService
{
    private const string AuthorizeEndpoint = "https://your-idp.com/authorize";
    private const string TokenEndpoint = "https://your-idp.com/token";
    private const string ClientId = "your-maui-app-client-id";
    private const string RedirectUri = "myapp://callback";
    private readonly ITokenService _tokenService;

    public AuthService(ITokenService tokenService)
    {
        _tokenService = tokenService;
    }

    public async Task<bool> LoginAsync()
    {
        // PKCE用のコードベリファイヤとチャレンジを生成
        var codeVerifier = GenerateCodeVerifier();
        var codeChallenge = GenerateCodeChallenge(codeVerifier);

        var authUrl = $"{AuthorizeEndpoint}?" +
            $"client_id={ClientId}&" +
            $"response_type=code&" +
            $"redirect_uri={Uri.EscapeDataString(RedirectUri)}&" +
            $"scope=openid profile email offline_access&" +
            $"code_challenge={codeChallenge}&" +
            $"code_challenge_method=S256";

        var result = await WebAuthenticator.Default.AuthenticateAsync(
            new Uri(authUrl),
            new Uri(RedirectUri));

        var authCode = result.Properties["code"];

        // 認証コードをトークンに交換
        var tokenResponse = await ExchangeCodeForTokenAsync(
            authCode, codeVerifier);

        await _tokenService.SaveTokensAsync(
            tokenResponse.AccessToken,
            tokenResponse.RefreshToken);

        return true;
    }

    private string GenerateCodeVerifier()
    {
        var bytes = new byte[32];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(bytes);
        return Convert.ToBase64String(bytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }

    private string GenerateCodeChallenge(string verifier)
    {
        using var sha256 = SHA256.Create();
        var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(verifier));
        return Convert.ToBase64String(bytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }
}

PKCEの実装は一見複雑に見えますが、やっていることはシンプルです。ランダムな文字列(コードベリファイヤ)を生成して、そのハッシュ値を認証リクエストに添付する。トークン交換時に元の文字列を送って、正当なクライアントであることを証明する。それだけです。

MSAL.NETによるMicrosoft Entra ID認証

Microsoft Entra ID(旧Azure AD)を使う場合は、MSAL.NETが断然おすすめです。トークンキャッシュの自動管理やトークンの自動リフレッシュなど、自分で実装すると面倒な機能が標準で揃っています。

// MauiProgram.cs での設定
builder.Services.AddSingleton<IPublicClientApplication>(sp =>
{
    return PublicClientApplicationBuilder
        .Create("your-client-id")
        .WithAuthority(AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount)
        .WithRedirectUri($"msal{"your-client-id"}://auth")
        .WithIosKeychainSecurityGroup("com.microsoft.adalcache")
        .Build();
});

// 認証の実行
public class MsalAuthService
{
    private readonly IPublicClientApplication _pca;
    private readonly string[] _scopes = { "User.Read", "api://your-api/access" };

    public MsalAuthService(IPublicClientApplication pca)
    {
        _pca = pca;
    }

    public async Task<AuthenticationResult?> SignInAsync()
    {
        try
        {
            // まずサイレント認証を試行(キャッシュからトークン取得)
            var accounts = await _pca.GetAccountsAsync();
            return await _pca.AcquireTokenSilent(_scopes, accounts.FirstOrDefault())
                .ExecuteAsync();
        }
        catch (MsalUiRequiredException)
        {
            // UIが必要な場合はインタラクティブ認証にフォールバック
            return await _pca.AcquireTokenInteractive(_scopes)
#if ANDROID
                .WithParentActivityOrWindow(Platform.CurrentActivity)
#endif
                .ExecuteAsync();
        }
    }
}

サイレント認証を先に試して、ダメならインタラクティブ認証にフォールバックするこのパターンは、ユーザー体験の面でもかなり重要です。毎回ログイン画面を出されるのは誰だってストレスですからね。

プラットフォーム固有の設定

OAuth認証を正しく動作させるには、各プラットフォームでコールバックURLの処理を設定する必要があります。ここを忘れると認証後にアプリに戻ってこないので要注意です。

// Platforms/Android/MainActivity.cs
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    LaunchMode = LaunchMode.SingleTask)]
[IntentFilter(new[] { Intent.ActionView },
    Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
    DataScheme = "myapp", DataHost = "callback")]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnNewIntent(Intent? intent)
    {
        base.OnNewIntent(intent);
        Platform.OnNewIntent(intent);
    }
}

// Platforms/iOS/AppDelegate.cs
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

    public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
    {
        if (Platform.OpenUrl(app, url, options))
            return true;
        return base.OpenUrl(app, url, options);
    }
}

HTTPS通信のセキュリティ強化と証明書ピンニング

モバイルアプリとサーバー間の通信を保護するために、HTTPS通信のセキュリティ強化は欠かせません。特に中間者攻撃(MITM)を防ぐための証明書ピンニングは、金融系やヘルスケアアプリなど機密性の高いデータを扱う場面では必須の対策です。

HttpClientの安全な構成

まずはIHttpClientFactoryを使ったHttpClientの安全な構成から見ていきましょう。

// MauiProgram.cs
builder.Services.AddHttpClient("SecureApi", client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler
    {
        // TLS 1.2以上を強制
        SslProtocols = System.Security.Authentication.SslProtocols.Tls12
            | System.Security.Authentication.SslProtocols.Tls13,

        // サーバー証明書の検証を有効にする
        ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
        {
            // 本番環境では必ず適切な検証を行うこと
            if (errors == System.Net.Security.SslPolicyErrors.None)
                return true;

            // 証明書エラーがある場合は接続を拒否
            return false;
        }
    };
    return handler;
});

ポイントはTLS 1.2以上を強制しているところです。TLS 1.0/1.1はすでに非推奨なので、新規開発であれば1.2以上に限定して問題ありません。

証明書ピンニングの実装

証明書ピンニングでは、サーバーの公開鍵のハッシュ(SPKIフィンガープリント)をアプリに埋め込み、接続時にサーバー証明書の公開鍵と照合します。

ここで大事なのは、証明書全体ではなく公開鍵をピンニングすること。こうすることで、証明書の更新時にアプリを更新しなくても済むケースが増えます(公開鍵が同じなら証明書が変わってもOK)。

public class CertificatePinningHandler : HttpClientHandler
{
    // ピンニングする公開鍵のSHA-256ハッシュ(Base64エンコード)
    private static readonly HashSet<string> PinnedKeys = new()
    {
        "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", // プライマリ
        "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC="  // バックアップ
    };

    public CertificatePinningHandler()
    {
        ServerCertificateCustomValidationCallback = ValidateCertificate;
    }

    private static bool ValidateCertificate(
        HttpRequestMessage message,
        X509Certificate2? certificate,
        X509Chain? chain,
        SslPolicyErrors sslErrors)
    {
        if (certificate == null) return false;

        // 基本的なSSLエラーチェック
        if (sslErrors != SslPolicyErrors.None) return false;

        // 公開鍵のSHA-256ハッシュを計算
        var publicKey = certificate.GetPublicKey();
        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(publicKey);
        var base64Hash = Convert.ToBase64String(hash);

        // ピンニングされたキーと照合
        return PinnedKeys.Contains(base64Hash);
    }
}

// DIコンテナへの登録
builder.Services.AddHttpClient("PinnedApi")
    .ConfigurePrimaryHttpMessageHandler<CertificatePinningHandler>();

バックアップのピンを必ず用意しておくのも忘れずに。プライマリの証明書が突然失効した場合のフォールバック先がないと、アプリが完全に通信不能になってしまいます。

SPKIフィンガープリントの取得方法

サーバー証明書の公開鍵ハッシュは、OpenSSLで以下のコマンドを叩けば取得できます。

# サーバー証明書の公開鍵ハッシュを取得
openssl s_client -connect api.example.com:443 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary \
  | openssl enc -base64

認証ヘッダーの自動付与

APIリクエストのたびに手動で認証トークンを付けるのは面倒ですし、付け忘れのバグにもつながります。DelegatingHandlerを使って自動化してしまいましょう。

public class AuthorizationHandler : DelegatingHandler
{
    private readonly ITokenService _tokenService;

    public AuthorizationHandler(ITokenService tokenService)
    {
        _tokenService = tokenService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await _tokenService.GetAccessTokenAsync();

        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token);
        }

        var response = await base.SendAsync(request, cancellationToken);

        // 401の場合はトークンをクリアしてログイン画面へ遷移
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            await _tokenService.ClearTokensAsync();
            // ログイン画面への遷移処理をトリガー
            WeakReferenceMessenger.Default.Send(
                new SessionExpiredMessage());
        }

        return response;
    }
}

// DI登録
builder.Services.AddTransient<AuthorizationHandler>();
builder.Services.AddHttpClient("AuthApi")
    .ConfigurePrimaryHttpMessageHandler<CertificatePinningHandler>()
    .AddHttpMessageHandler<AuthorizationHandler>();

401レスポンスが返ってきたときにセッション切れのメッセージを飛ばす処理も入れておくと、ユーザーを自然にログイン画面へ誘導できます。

生体認証(指紋認証・Face ID)の実装

生体認証は、ユーザー体験を損なわずにセキュリティを強化できる機能です。個人的には、パスワード入力より生体認証のほうがユーザーの離脱率も下がると感じています。

.NET MAUIではPlugin.Maui.Biometricパッケージを使って、AndroidとiOSの両方で指紋認証やFace IDを実装できます。

セットアップ

まずはNuGetパッケージのインストールから。

dotnet add package Plugin.Maui.Biometric

AndroidではAndroidManifest.xmlにパーミッションを追加します。

<uses-permission android:name="android.permission.USE_BIOMETRIC" />

iOSではInfo.plistにFace IDの使用理由を記載します。これがないとApp Storeの審査でリジェクトされるので忘れないでください。

<key>NSFaceIDUsageDescription</key>
<string>セキュアな認証のためにFace IDを使用します</string>

生体認証サービスの実装

では、実際の生体認証サービスとViewModelでの使い方を見てみましょう。

// MauiProgram.cs
builder.Services.AddSingleton(
    BiometricAuthenticationService.Default);
builder.Services.AddSingleton<IBiometricService, BiometricService>();

// 生体認証サービス
public interface IBiometricService
{
    Task<bool> IsBiometricAvailableAsync();
    Task<BiometricResult> AuthenticateAsync(string reason);
}

public class BiometricService : IBiometricService
{
    private readonly IBiometricAuthentication _biometric;

    public BiometricService(IBiometricAuthentication biometric)
    {
        _biometric = biometric;
    }

    public async Task<bool> IsBiometricAvailableAsync()
    {
        var result = await _biometric.GetAuthenticationStatusAsync();
        return result == BiometricHwStatus.Available;
    }

    public async Task<BiometricResult> AuthenticateAsync(string reason)
    {
        var request = new AuthenticationRequest
        {
            Title = "本人確認",
            Subtitle = reason,
            NegativeText = "キャンセル",
            AllowPasswordAuth = true  // 生体認証失敗時にPIN入力へフォールバック
        };

        return await _biometric.AuthenticateAsync(
            request, CancellationToken.None);
    }
}

// ViewModelでの使用例
public partial class LoginViewModel : ObservableObject
{
    private readonly IBiometricService _biometricService;
    private readonly ITokenService _tokenService;

    [RelayCommand]
    private async Task BiometricLoginAsync()
    {
        if (!await _biometricService.IsBiometricAvailableAsync())
        {
            await Shell.Current.DisplayAlert(
                "エラー", "生体認証が利用できません", "OK");
            return;
        }

        var result = await _biometricService.AuthenticateAsync(
            "アプリにログインするために認証してください");

        if (result.Status == BiometricResponseStatus.Success)
        {
            // 保存済みトークンを使ってログイン状態を復元
            if (await _tokenService.IsAuthenticatedAsync())
            {
                await Shell.Current.GoToAsync("//main");
            }
        }
    }
}

AllowPasswordAuth = trueの設定は地味に大切です。すべてのユーザーが生体認証を使えるわけではないので、PINやパスワードへのフォールバックは必ず用意しておきましょう。

APIキーと機密情報の管理ベストプラクティス

モバイルアプリにおけるAPIキー管理は、正直いって最も厄介なセキュリティ課題の一つです。アプリバイナリに含まれるデータは、リバースエンジニアリングで抽出される可能性が常にあります。

絶対にやってはいけないこと

まず、NGパターンを明確にしておきましょう。

  • ソースコードにAPIキーをハードコーディングする
  • アプリバイナリにAPIキーを埋め込む
  • appsettings.jsonやリソースファイルにAPIキーを平文で保存する
  • クライアントサイドからサードパーティAPIに直接アクセスする(キーが丸見えになります)

「難読化すれば大丈夫」と思うかもしれませんが、難読化は時間稼ぎにしかなりません。本気で狙われたら抜かれると思って設計するべきです。

推奨パターン:バックエンドプロキシ

最も安全なアプローチは、APIキーをサーバーサイドに保持し、モバイルアプリからはバックエンドを経由してサードパーティAPIにアクセスするパターンです。

// モバイルアプリ側 - 自社APIを呼び出す(APIキー不要)
public class WeatherService
{
    private readonly HttpClient _httpClient;

    public WeatherService(IHttpClientFactory factory)
    {
        _httpClient = factory.CreateClient("AuthApi");
    }

    public async Task<WeatherData?> GetWeatherAsync(double lat, double lon)
    {
        // 自社バックエンドAPIを呼び出す
        // APIキーはサーバー側で管理されている
        var response = await _httpClient.GetAsync(
            $"/api/weather?lat={lat}&lon={lon}");
        response.EnsureSuccessStatusCode();
        return await response.Content
            .ReadFromJsonAsync<WeatherData>();
    }
}

// サーバー側(ASP.NET Core)- APIキーを安全に管理
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class WeatherController : ControllerBase
{
    private readonly IConfiguration _config;
    private readonly HttpClient _httpClient;

    public WeatherController(
        IConfiguration config,
        IHttpClientFactory factory)
    {
        _config = config;
        _httpClient = factory.CreateClient();
    }

    [HttpGet]
    public async Task<IActionResult> Get(double lat, double lon)
    {
        // APIキーは環境変数やAzure Key Vaultから取得
        var apiKey = _config["ExternalApi:WeatherApiKey"];
        var response = await _httpClient.GetAsync(
            $"https://api.weather.com/data?lat={lat}&lon={lon}&key={apiKey}");
        var data = await response.Content.ReadAsStringAsync();
        return Content(data, "application/json");
    }
}

モバイルアプリ側にはAPIキーが一切登場しないのがポイントです。ユーザー認証済みのリクエストだけをバックエンドが受け付け、サードパーティAPIへのアクセスはサーバーが代行します。

開発環境でのシークレット管理

開発中は.NET User Secretsを使うと、ソースコード管理の対象外でAPIキーを管理できて便利です。

# User Secretsの初期化
dotnet user-secrets init

# シークレットの設定
dotnet user-secrets set "ExternalApi:WeatherApiKey" "your-dev-api-key"

これならうっかりGitにAPIキーをコミットしてしまう事故も防げます。

.NET 10(LTS)におけるセキュリティ関連の改善

.NET 10は長期サポート(LTS)リリースとして3年間のサポートが提供されます。セキュリティ面でもいくつか注目すべき改善が入っています。

WebView リクエストインターセプション

.NET 10では、BlazorWebViewHybridWebViewのWebリクエストをインターセプトできるようになりました。認証ヘッダーの動的な追加や、不正なURLへのリダイレクト防止に使えます。Hybrid構成のアプリにとってはかなり嬉しいアップデートです。

MessagingCenterの廃止

MessagingCenterが内部クラスに変更され、CommunityToolkit.MvvmWeakReferenceMessengerが推奨されるようになりました。WeakReferenceMessengerは弱参照を使用するためメモリリークのリスクが減りますし、セキュリティの観点でも不要なオブジェクトがメモリに残り続けるのを防ぐ効果があります。

IQueryAttributableの推奨

ナビゲーションでのデータ受け渡しにQueryPropertyAttributeを使っている方は要注意。トリミングやNative AOTとの互換性がありません。IQueryAttributableインターフェースに切り替えることで、型安全性が向上し、意図しないデータ漏洩のリスクも軽減されます。

セキュリティチェックリスト

最後に、本番リリース前に確認しておきたいチェックリストをまとめました。一つでも「いいえ」があれば、リリース前に対応することを強くおすすめします。

  • データ保存:機密データはSecureStorageに保存しているか
  • 通信:すべてのAPI通信がHTTPSで行われているか
  • 認証:OAuth 2.0 + PKCEフローを使用しているか
  • トークン管理:アクセストークンの有効期限を適切に管理しているか
  • 証明書:重要なAPIに対して証明書ピンニングを実装しているか
  • APIキー:クライアントサイドにAPIキーを埋め込んでいないか
  • ログ:本番ビルドで機密情報をログに出力していないか
  • 難読化:リリースビルドでコードの難読化を適用しているか
  • バックアップ:Androidの自動バックアップから機密データを除外しているか
  • 入力検証:ユーザー入力をサーバーサイドで検証しているか

よくある質問(FAQ)

.NET MAUIのSecureStorageにはどのくらいのデータを保存できますか?

容量の明示的な制限はありませんが、少量のテキストデータ(トークンやパスワードなど)の保存を目的として設計されています。大量のデータを入れるとパフォーマンスに影響が出ます。ちなみにWindowsでは、設定名の長さは最大255文字、各設定は最大8KB、複合設定は最大64KBという制限があります。大きなデータにはSQLiteなどのローカルDBを使いましょう。

.NET MAUIアプリでAPIキーを安全に管理するにはどうすればよいですか?

アプリバイナリにAPIキーを含めるのは安全ではありません。おすすめは、自社のバックエンドサーバーを中間層として使い、APIキーをサーバーサイド(環境変数やAzure Key Vault)で管理する方法です。モバイルアプリはユーザー認証済みの状態で自社APIを呼び出し、サードパーティAPIへのアクセスはサーバーに任せる構成にしてください。

.NET MAUIで証明書ピンニングを実装する必要がありますか?

金融、医療、政府関連など機密性の高いデータを扱うアプリでは強く推奨します。一般的なアプリでも、決済処理やログイン認証などの重要なAPIエンドポイントにはピンニングを検討すべきです。ただし、証明書の更新時にアプリ更新も必要になる場合があるので、運用計画は事前に立てておきましょう。

.NET MAUIの生体認証でPINへのフォールバックは設定できますか?

はい、できます。Plugin.Maui.BiometricAuthenticationRequestAllowPasswordAuth = trueに設定すれば、生体認証に失敗した場合にデバイスのPINやパスワードでの認証にフォールバックします。生体認証を使えないユーザーもいるので、このフォールバックは必ず用意しておくのがベストプラクティスです。

OAuth 2.0のPKCEとは何ですか?なぜモバイルアプリで重要なのですか?

PKCE(Proof Key for Code Exchange、「ピクシー」と読みます)は、OAuth 2.0の認証コードフローを強化するセキュリティ拡張です。モバイルアプリはパブリッククライアントなので、クライアントシークレットを安全に保存できません。PKCEを使えば、認証コードが傍受されても攻撃者がそれをアクセストークンに交換することを防げます。仕組みとしては、クライアントがランダムなコードベリファイヤを生成し、そのハッシュ(コードチャレンジ)を認証リクエストに含めます。トークン交換時に元のコードベリファイヤを提示して、正当なクライアントであることを証明するわけです。

著者について Sofia Marchetti

Sofia is a mobile platform engineer with eleven years across native iOS, Xamarin, and now .NET MAUI. She spent three years at Spotify in Stockholm working on internal tooling for the mobile build infrastructure, then joined a fintech in Milan where she leads the mobile foundations team responsible for a MAUI app that handles around 2 million monthly active users across iOS and Android. Most of what she writes about lives in the build and release layer: deterministic builds, fastlane integration for MAUI, code signing on macOS runners, MAUI .NET 9 upgrade postmortems, and benchmarking startup time on cheap Android hardware. She co-organizes the Milano .NET meetup and gave a talk at NDC Oslo 2025 on shrinking a MAUI Android APK from 84 MB to 31 MB without losing features.