次の方法で共有


Xamarin.Forms で Apple でのサインインを使用する

Apple でのサインインは、サードパーティの認証サービスを使用する iOS 13 上のすべての新しいアプリケーション用です。 iOS と Android の実装の詳細は大きく異なります。 このガイドでは、Xamarin.Forms で現在これを行う方法について説明します。

このガイドとサンプルでは、特定のプラットフォーム サービスを使用して Apple でのサインインを処理します。

  • OpenID/OpenAuth を使用して Azure Functions と通信する汎用 Web サービスを使用する Android
  • iOS では iOS 13 での認証にネイティブ API を使用し、iOS 12 以下では汎用 Web サービスにフォールバックします

サンプルの Apple サインイン フロー

このサンプルでは、Apple サインインを Xamarin.Forms アプリで機能させるために、厳格な実装を提供します。

認証フローに役立つように、次の 2 つの Azure Functions を使用します。

  1. applesignin_auth - Apple サインイン認可 URL を生成し、それにリダイレクトします。 これは、モバイル アプリではなくサーバー側で行うので、Apple のサーバーからコールバックを送信するときに、state をキャッシュして検証できます。
  2. applesignin_callback - Apple からの POST コールバックを処理し、アクセス トークンと ID トークンの認可コードを安全に交換します。 最後に、アプリの URI スキームにリダイレクトされ、URL フラグメント内のトークンが返されます。

モバイル アプリでは、選択したカスタム URI スキーム (この場合 xamarinformsapplesignin://) を処理するためにそれ自体を登録するため、applesignin_callback 関数からトークンをリレーできます。

ユーザーが認証を開始すると、次の手順が実行されます。

  1. モバイル アプリでは、nonce および state 値が生成され、applesignin_auth Azure 関数に渡されます。
  2. applesignin_auth Azure 関数では、Apple サインイン認可 URL (指定された statenonce を使用) を生成し、モバイル アプリ ブラウザーをそれにリダイレクトします。
  3. ユーザーは、Apple のサーバーでホストされている Apple サインイン認可ページで、資格情報を安全に入力します。
  4. Apple のサーバーで Apple サインイン フローが完了すると、Apple では applesignin_callback Azure 関数になる redirect_uri にリダイレクトします。
  5. applesignin_callback 関数に送信された Apple からの要求は、正しい state が返されるようにし、ID トークン要求が有効であることを確認するために検証されます。
  6. applesignin_callback Azure 関数では、アクセス トークン更新トークンID トークン (ユーザー ID、名前、メールに関する要求を含む) について、Apple によって投稿された code を交換します。
  7. applesignin_callback Azure 関数は、最終的にアプリの URI スキーム (xamarinformsapplesignin://) にリダイレクトされ、トークン (例: xamarinformsapplesignin://#access_token=...&refresh_token=...&id_token=...) に URI フラグメントが追加されます。
  8. モバイル アプリでは URI フラグメントを AppleAccount に解析して、受信した nonce 要求がフローの開始時に生成された nonce と一致することを検証します。
  9. これでモバイル アプリが認証されました。

Azure Functions

このサンプルでは、Azure Functions を使用します。 または、ASP.NET Core コントローラーや同様の Web サーバー ソリューションで同じ機能を提供することもできます。

構成

Azure Functions を使用する場合は、いくつかのアプリ設定を構成する必要があります。

  • APPLE_SIGNIN_KEY_ID - これは以前の KeyId です。
  • APPLE_SIGNIN_TEAM_ID - これは通常、メンバーシップ プロファイル にある "チーム ID" です
  • APPLE_SIGNIN_SERVER_ID: これは以前の ServerId です。 これはアプリ "バンドル ID" "ではなく"、作成した "サービス ID" の "識別子" です。
  • APPLE_SIGNIN_APP_CALLBACK_URI - これは、アプリにリダイレクトするカスタム URI スキームです。 このサンプルでは、xamarinformsapplesignin:// が使用されます。
  • APPLE_SIGNIN_REDIRECT_URI - "Apple サインイン" 構成セクションで "サービス ID" を作成するときに設定する "リダイレクト URL"。 テストするには、http://local.test:7071/api/applesignin_callback のように表示されます。
  • APPLE_SIGNIN_P8_KEY - すべての \n 改行が削除され、1 つの長い文字列になった .p8 ファイルのテキスト コンテンツ

セキュリティに関する考慮事項

アプリケーション コード内に P8 キーを保存しないでください。 アプリケーション コードは、簡単にダウンロードして逆アセンブルできます。

認証フローをホストするために WebView を使用し、URL ナビゲーション イベントをインターセプトして認可コードを取得することも、不適切な方法と見なされます。 現時点では、トークン交換を処理するコードをサーバーでホストせずに、iOS13 以降以外のデバイスで Apple でのサインインを処理する完全に安全な方法はありません。 Apple でサーバーに POST コールバックを発行したときに状態をキャッシュして検証できるように、サーバーで認可 URL 生成コードをホストすることをお勧めします。

クロスプラットフォーム サインイン サービス

Xamarin.Forms DependencyService を使用すると、iOS 上のプラットフォーム サービスを使用する個別の認証サービスと、共有インターフェイスに基づいて Android やその他の iOS 以外のプラットフォーム用の汎用 Web サービスを作成できます。

public interface IAppleSignInService
{
    bool Callback(string url);

    Task<AppleAccount> SignInAsync();
}

iOS では、ネイティブ API が使用されます。

public class AppleSignInServiceiOS : IAppleSignInService
{
#if __IOS__13
    AuthManager authManager;
#endif

    bool Is13 => UIDevice.CurrentDevice.CheckSystemVersion(13, 0);
    WebAppleSignInService webSignInService;

    public AppleSignInServiceiOS()
    {
        if (!Is13)
            webSignInService = new WebAppleSignInService();
    }

    public async Task<AppleAccount> SignInAsync()
    {
        // Fallback to web for older iOS versions
        if (!Is13)
            return await webSignInService.SignInAsync();

        AppleAccount appleAccount = default;

#if __IOS__13
        var provider = new ASAuthorizationAppleIdProvider();
        var req = provider.CreateRequest();

        authManager = new AuthManager(UIApplication.SharedApplication.KeyWindow);

        req.RequestedScopes = new[] { ASAuthorizationScope.FullName, ASAuthorizationScope.Email };
        var controller = new ASAuthorizationController(new[] { req });

        controller.Delegate = authManager;
        controller.PresentationContextProvider = authManager;

        controller.PerformRequests();

        var creds = await authManager.Credentials;

        if (creds == null)
            return null;

        appleAccount = new AppleAccount();
        appleAccount.IdToken = JwtToken.Decode(new NSString(creds.IdentityToken, NSStringEncoding.UTF8).ToString());
        appleAccount.Email = creds.Email;
        appleAccount.UserId = creds.User;
        appleAccount.Name = NSPersonNameComponentsFormatter.GetLocalizedString(creds.FullName, NSPersonNameComponentsFormatterStyle.Default, NSPersonNameComponentsFormatterOptions.Phonetic);
        appleAccount.RealUserStatus = creds.RealUserStatus.ToString();
#endif

        return appleAccount;
    }

    public bool Callback(string url) => true;
}

#if __IOS__13
class AuthManager : NSObject, IASAuthorizationControllerDelegate, IASAuthorizationControllerPresentationContextProviding
{
    public Task<ASAuthorizationAppleIdCredential> Credentials
        => tcsCredential?.Task;

    TaskCompletionSource<ASAuthorizationAppleIdCredential> tcsCredential;

    UIWindow presentingAnchor;

    public AuthManager(UIWindow presentingWindow)
    {
        tcsCredential = new TaskCompletionSource<ASAuthorizationAppleIdCredential>();
        presentingAnchor = presentingWindow;
    }

    public UIWindow GetPresentationAnchor(ASAuthorizationController controller)
        => presentingAnchor;

    [Export("authorizationController:didCompleteWithAuthorization:")]
    public void DidComplete(ASAuthorizationController controller, ASAuthorization authorization)
    {
        var creds = authorization.GetCredential<ASAuthorizationAppleIdCredential>();
        tcsCredential?.TrySetResult(creds);
    }

    [Export("authorizationController:didCompleteWithError:")]
    public void DidComplete(ASAuthorizationController controller, NSError error)
        => tcsCredential?.TrySetException(new Exception(error.LocalizedDescription));
}
#endif

コンパイル フラグ __IOS__13 は、iOS 13 と、汎用 Web サービスにフォールバックするレガシ バージョンのサポートを提供するために使用されます。

Android では、Azure Functions を使用した汎用 Web サービスが使用されます。

public class WebAppleSignInService : IAppleSignInService
{
    // IMPORTANT: This is what you register each native platform's url handler to be
    public const string CallbackUriScheme = "xamarinformsapplesignin";
    public const string InitialAuthUrl = "http://local.test:7071/api/applesignin_auth";

    string currentState;
    string currentNonce;

    TaskCompletionSource<AppleAccount> tcsAccount = null;

    public bool Callback(string url)
    {
        // Only handle the url with our callback uri scheme
        if (!url.StartsWith(CallbackUriScheme + "://"))
            return false;

        // Ensure we have a task waiting
        if (tcsAccount != null && !tcsAccount.Task.IsCompleted)
        {
            try
            {
                // Parse the account from the url the app opened with
                var account = AppleAccount.FromUrl(url);

                // IMPORTANT: Validate the nonce returned is the same as our originating request!!
                if (!account.IdToken.Nonce.Equals(currentNonce))
                    tcsAccount.TrySetException(new InvalidOperationException("Invalid or non-matching nonce returned"));

                // Set our account result
                tcsAccount.TrySetResult(account);
            }
            catch (Exception ex)
            {
                tcsAccount.TrySetException(ex);
            }
        }

        tcsAccount.TrySetResult(null);
        return false;
    }

    public async Task<AppleAccount> SignInAsync()
    {
        tcsAccount = new TaskCompletionSource<AppleAccount>();

        // Generate state and nonce which the server will use to initial the auth
        // with Apple.  The nonce should flow all the way back to us when our function
        // redirects to our app
        currentState = Util.GenerateState();
        currentNonce = Util.GenerateNonce();

        // Start the auth request on our function (which will redirect to apple)
        // inside a browser (either SFSafariViewController, Chrome Custom Tabs, or native browser)
        await Xamarin.Essentials.Browser.OpenAsync($"{InitialAuthUrl}?&state={currentState}&nonce={currentNonce}",
            Xamarin.Essentials.BrowserLaunchMode.SystemPreferred);

        return await tcsAccount.Task;
    }
}

まとめ

この記事では、Xamarin.Forms アプリケーションで使用するように Apple でのサインインを設定するために必要な手順について説明しました。