Použití přihlášení s Apple in Xamarin.Forms

Přihlášení pomocí Apple je určené pro všechny nové aplikace v iOSu 13, které používají ověřovací služby třetích stran. Podrobnosti implementace mezi iOSem a Androidem jsou poměrně odlišné. Tato příručka vás provede tím, jak to můžete udělat dnes v Xamarin.Forms.

V této příručce a ukázce se ke zpracování přihlášení pomocí Apple používají konkrétní služby platformy:

  • Android s využitím obecné webové služby, která komunikuje se službou Azure Functions s OpenID/OpenAuth
  • iOS používá nativní rozhraní API pro ověřování v iOSu 13 a vrátí se do obecné webové služby pro iOS 12 a níže.

Ukázkový tok přihlášení Apple

Tato ukázka nabízí názornou implementaci pro získání apple přihlášení, aby fungovalo ve vaší Xamarin.Forms aplikaci.

K pomoci s tokem ověřování používáme dvě funkce Azure Functions:

  1. applesignin_auth - Vygeneruje autorizační adresu URL přihlášení Apple a přesměruje na ni. Děláme to na straně serveru místo mobilní aplikace, abychom je mohli uložit do mezipaměti state a ověřit, když servery Apple posílají zpětné volání.
  2. applesignin_callback - Zpracovává zpětné volání POST od Společnosti Apple a bezpečně vyměňuje autorizační kód pro přístupový token a token ID. Nakonec se přesměruje zpět do schématu identifikátoru URI aplikace a předá tokeny v fragmentu adresy URL.

Mobilní aplikace se zaregistruje pro zpracování vlastního schématu identifikátorů URI, které jsme vybrali (v tomto případě xamarinformsapplesignin://), aby applesignin_callback funkce mohl tokeny předávat zpět do ní.

Když uživatel spustí ověřování, dojde k následujícím krokům:

  1. Mobilní aplikace vygeneruje nonce hodnotu a state předá je do applesignin_auth funkce Azure.
  2. Funkce applesignin_auth Azure vygeneruje autorizační adresu URL apple pro přihlášení (pomocí zadané state adresy a nonce) a přesměruje na ni prohlížeč mobilních aplikací.
  3. Uživatel bezpečně zadá přihlašovací údaje na autorizační stránce Apple Sign In hostované na serverech Společnosti Apple.
  4. Jakmile se tok Přihlášení Apple dokončí na serverech Společnosti Apple, Apple Přesměruje na redirect_uriapplesignin_callback funkci Azure.
  5. Požadavek od společnosti Apple odeslaný do applesignin_callback funkce se ověří, aby se zajistilo, že je vrácena správná state odpověď a že deklarace identity tokenu ID jsou platné.
  6. Funkce applesignin_callback Azure vymění code odesílané společností Apple za přístupový token, obnovovací token a token ID (který obsahuje deklarace identity týkající se ID uživatele, jména a e-mailu).
  7. Funkce applesignin_callback Azure nakonec přesměruje zpět do schématu identifikátoru URI aplikace (xamarinformsapplesignin://) připojující fragment identifikátoru URI s tokeny (např. xamarinformsapplesignin://#access_token=...&refresh_token=...&id_token=...).
  8. Mobilní aplikace parsuje fragment identifikátoru URI a AppleAccount ověří, že nonce přijatá deklarace identity odpovídá nonce vygenerovanému na začátku toku.
  9. Mobilní aplikace je teď ověřená.

Azure Functions

Tato ukázka používá Azure Functions. Alternativně může řešení ASP.NET Core Controller nebo podobné webového serveru poskytovat stejné funkce.

Konfigurace

Při používání Azure Functions je potřeba nakonfigurovat několik nastavení aplikace:

  • APPLE_SIGNIN_KEY_ID - Tohle je tvoje KeyId dřívější.
  • APPLE_SIGNIN_TEAM_ID- Obvykle se jedná o ID vašeho týmu, které najdete ve vašem profilu členství.
  • APPLE_SIGNIN_SERVER_ID: Jedná se o ServerId předchozí verzi. Nejedná se o ID sady prostředků aplikace, ale o identifikátorID služeb, které jste vytvořili.
  • APPLE_SIGNIN_APP_CALLBACK_URI – Jedná se o vlastní schéma identifikátorů URI, pomocí kterého chcete přesměrovat zpět do aplikace. V této ukázce xamarinformsapplesignin:// se používá.
  • APPLE_SIGNIN_REDIRECT_URI – Adresa URL pro přesměrování, kterou nastavíte při vytváření ID služeb v části Konfigurace přihlášení Apple. Pokud chcete testovat, může to vypadat nějak takto: http://local.test:7071/api/applesignin_callback
  • APPLE_SIGNIN_P8_KEY – Textový obsah .p8 souboru se všemi odstraněnými novými \n spojnicemi, takže je to jeden dlouhý řetězec.

Bezpečnostní aspekty

Nikdy neukládejte klíč P8 uvnitř kódu aplikace. Kód aplikace se snadno stáhne a rozebere.

Také se považuje za špatný postup použití WebView k hostování ověřovacího toku a k zachycení událostí navigace adresy URL za účelem získání autorizačního kódu. V současné době neexistuje žádný plně zabezpečený způsob, jak zpracovat přihlášení s Apple na zařízeních bez iOS13+ bez hostování kódu na serveru pro zpracování výměny tokenů. Doporučujeme hostovat kód pro generování autorizační adresy URL na serveru, abyste mohli uložit stav do mezipaměti a ověřit ho, když Apple vydá zpětné volání POST na váš server.

Multiplatformní přihlašovací služba

Xamarin.Forms Pomocí DependencyService můžete vytvořit samostatné ověřovací služby, které používají služby platformy v iOSu, a obecnou webovou službu pro Android a další platformy bez iOS založené na sdíleném rozhraní.

public interface IAppleSignInService
{
    bool Callback(string url);

    Task<AppleAccount> SignInAsync();
}

V iOSu se používají nativní rozhraní 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

Příznak __IOS__13 kompilace slouží k poskytování podpory pro iOS 13 i starších verzí, které se přecházely do obecné webové služby.

V Androidu se používá obecná webová služba se službou Azure Functions:

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;
    }
}

Shrnutí

Tento článek popisuje kroky potřebné k nastavení přihlášení pomocí Apple pro použití ve vašich Xamarin.Forms aplikacích.