Usare l'accesso con Apple in Xamarin.Forms

Scaricare l'esempio Scaricare l'esempio

L'accesso con Apple è per tutte le nuove applicazioni in iOS 13 che usano servizi di autenticazione di terze parti. I dettagli di implementazione tra iOS e Android sono piuttosto diversi. Questa guida illustra come eseguire questa operazione oggi in Xamarin.Forms .

In questa guida e in questo esempio vengono usati servizi della piattaforma specifici per gestire l'accesso con Apple:

  • Android che usa un servizio Web generico per Funzioni di Azure con OpenID/OpenAuth
  • iOS usa l'API nativa per l'autenticazione in iOS 13 ed è di nuovo in un servizio Web generico per iOS 12 e versioni seguenti

Un flusso di accesso Apple di esempio

Questo esempio offre un'implementazione di opinione per il funzionamento dell'accesso Apple Xamarin.Forms nell'app.

Vengono usate due Funzioni di Azure per facilitare il flusso di autenticazione:

  1. applesignin_auth - Genera l'URL di autorizzazione di accesso di Apple e reindirizza a esso. Questa operazione viene eseguito sul lato server, anziché sull'app per dispositivi mobili, in modo che sia possibile memorizzare nella cache e convalidarla quando i server state apple inviano un callback.
  2. applesignin_callback - Gestisce il callback POST da Apple e scambia in modo sicuro il codice di autorizzazione per un token di accesso e un token ID. Infine, reindirizza nuovamente lo schema URI dell'app, passando di nuovo i token in un frammento di URL.

L'app per dispositivi mobili si registra per gestire lo schema URI personalizzato selezionato (in questo caso ) in modo che la funzione possa inoltrare xamarinformsapplesignin://applesignin_callback nuovamente i token.

Quando l'utente avvia l'autenticazione, si verificano i passaggi seguenti:

  1. L'app per dispositivi nonce mobili genera un valore e e li passa alla funzione di stateapplesignin_auth Azure.
  2. La funzione di Azure genera un URL di autorizzazione di accesso Apple (usando i valori forniti da e ) e reindirizza il applesignin_authstate browser nonce dell'app per dispositivi mobili.
  3. L'utente immette le proprie credenziali in modo sicuro nella pagina di autorizzazione di accesso di Apple ospitata nei server Apple.
  4. Al termine del flusso di accesso apple nei server apple, Apple reindirizza a redirect_uri che sarà la funzione di applesignin_callback Azure.
  5. La richiesta inviata da Apple alla funzione viene convalidata per assicurarsi che sia restituito il valore corretto e che le attestazioni applesignin_callback del token ID siano state valide.
  6. La funzione di Azure scambia l'oggetto inviato da Apple per un token di accesso, un token di aggiornamento e un token ID (che contiene attestazioni relative a ID utente, nome applesignin_callback e posta code elettronica). applesignin_callbackcode
  7. La funzione di Azure reindirizza infine lo schema URI dell'app ( ) aggiungendo un frammento URI con applesignin_callbackxamarinformsapplesignin:// i token (ad esempio xamarinformsapplesignin://#access_token=...&refresh_token=...&id_token=... ).
  8. L'app per dispositivi mobili analizza il frammento URI in un e convalida l'attestazione ricevuta corrisponde a AppleAccountnonce generata nonce all'inizio del flusso.
  9. L'app per dispositivi mobili è ora autenticata.

Funzioni di Azure

Questo esempio usa Funzioni di Azure. In alternativa, una soluzione ASP.NET Core controller o un server Web simile potrebbe offrire la stessa funzionalità.

Configurazione

È necessario configurare diverse impostazioni dell'app quando si usa Funzioni di Azure:

  • APPLE_SIGNIN_KEY_ID - Questa è la versione KeyId precedente.
  • APPLE_SIGNIN_TEAM_ID - Questo è in genere APPLE_SIGNIN_TEAM_ID trovato nel profilo di appartenenza
  • APPLE_SIGNIN_SERVER_ID: si tratta ServerId dell'oggetto di precedente. Non è l'ID bundle dell'app,ma piuttosto l'identificatore dell'ID servizi creato.
  • APPLE_SIGNIN_APP_CALLBACK_URI - Si tratta dello schema URI personalizzato con cui si vuole eseguire il reindirizzamento all'app. In questo esempio xamarinformsapplesignin:// viene usato .
  • APPLE_SIGNIN_REDIRECT_URI - URL APPLE_SIGNIN_REDIRECT_URI che si configura durante la creazione dell'ID dei servizi nella sezione Configurazione di accesso Apple. Per eseguire il test, potrebbe essere simile al seguente: http://local.test:7071/api/applesignin_callback
  • APPLE_SIGNIN_P8_KEY - Contenuto di testo del file, con tutte le righe di nuova riga rimosse in modo che .p8 sia una stringa \n lunga

Considerazioni sulla sicurezza

Non archiviare mai la chiave P8 all'interno del codice dell'applicazione. Il codice dell'applicazione è facile da scaricare e disassemblare.

È anche considerato una procedura non consigliata usare un per ospitare il flusso di autenticazione e intercettare gli eventi di navigazione WebView URL per ottenere il codice di autorizzazione. Attualmente non esiste un modo completamente sicuro per gestire l'accesso con Apple nei dispositivi non iOS13+ senza ospitare codice in un server per gestire lo scambio di token. È consigliabile ospitare il codice di generazione dell'URL di autorizzazione in un server in modo da poter memorizzare nella cache lo stato e convalidarlo quando Apple elava un callback POST al server.

Un servizio di accesso multipiattaforma

Usando DependencyService è possibile creare servizi di autenticazione separati che usano i servizi della piattaforma in iOS e un servizio Web generico per Android e altre piattaforme non iOS basate su Xamarin.Forms un'interfaccia condivisa.

public interface IAppleSignInService
{
    bool Callback(string url);

    Task<AppleAccount> SignInAsync();
}

In iOS vengono usate le API native:

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

Il flag di compilazione viene usato per fornire supporto per iOS 13 e versioni legacy che esereranno __IOS__13 il fallback al servizio Web generico.

In Android viene usato il servizio Web generico con Funzioni di Azure:

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

Riepilogo

Questo articolo descrive i passaggi necessari per configurare l'accesso con Apple per l'uso nelle Xamarin.Forms applicazioni.