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 を使用します。
applesignin_auth
- Apple サインイン認可 URL を生成し、それにリダイレクトします。 これは、モバイル アプリではなくサーバー側で行うので、Apple のサーバーからコールバックを送信するときに、state
をキャッシュして検証できます。applesignin_callback
- Apple からの POST コールバックを処理し、アクセス トークンと ID トークンの認可コードを安全に交換します。 最後に、アプリの URI スキームにリダイレクトされ、URL フラグメント内のトークンが返されます。
モバイル アプリでは、選択したカスタム URI スキーム (この場合 xamarinformsapplesignin://
) を処理するためにそれ自体を登録するため、applesignin_callback
関数からトークンをリレーできます。
ユーザーが認証を開始すると、次の手順が実行されます。
- モバイル アプリでは、
nonce
およびstate
値が生成され、applesignin_auth
Azure 関数に渡されます。 applesignin_auth
Azure 関数では、Apple サインイン認可 URL (指定されたstate
とnonce
を使用) を生成し、モバイル アプリ ブラウザーをそれにリダイレクトします。- ユーザーは、Apple のサーバーでホストされている Apple サインイン認可ページで、資格情報を安全に入力します。
- Apple のサーバーで Apple サインイン フローが完了すると、Apple では
applesignin_callback
Azure 関数になるredirect_uri
にリダイレクトします。 applesignin_callback
関数に送信された Apple からの要求は、正しいstate
が返されるようにし、ID トークン要求が有効であることを確認するために検証されます。applesignin_callback
Azure 関数では、アクセス トークン、更新トークン、ID トークン (ユーザー ID、名前、メールに関する要求を含む) について、Apple によって投稿されたcode
を交換します。applesignin_callback
Azure 関数は、最終的にアプリの URI スキーム (xamarinformsapplesignin://
) にリダイレクトされ、トークン (例:xamarinformsapplesignin://#access_token=...&refresh_token=...&id_token=...
) に URI フラグメントが追加されます。- モバイル アプリでは URI フラグメントを
AppleAccount
に解析して、受信したnonce
要求がフローの開始時に生成されたnonce
と一致することを検証します。 - これでモバイル アプリが認証されました。
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 でのサインインを設定するために必要な手順について説明しました。