Compartir vía


Uso de Iniciar sesión con Apple en Xamarin.Forms

Iniciar sesión con Apple es para todas las aplicaciones nuevas en iOS 13 que usan servicios de autenticación de terceros. Los detalles de implementación entre iOS y Android son bastante diferentes. En esta guía se explica cómo puede hacerlo hoy en Xamarin.Forms.

En esta guía y en el ejemplo, se usan servicios de plataforma específicos para controlar Iniciar sesión con Apple:

  • Android mediante un servicio web genérico que se comunica con Azure Functions con OpenID/OpenAuth
  • iOS usa la API nativa para la autenticación en iOS 13 y recurre a un servicio web genérico para iOS 12 y versiones posteriores

Flujo de inicio de sesión de Apple de ejemplo

En este ejemplo se ofrece una implementación con opiniones para que Iniciar sesión con Apple funcione en la aplicación de Xamarin.Forms.

Se usan dos instancias de Azure Functions para facilitar el flujo de autenticación:

  1. applesignin_auth: genera la dirección URL de autorización de inicio de sesión de Apple y redirige hacia ella. Esto se hace en el lado del servidor, en lugar de la aplicación móvil, por lo que state se puede almacenar en caché y validar cuando los servidores de Apple envíen una devolución de llamada.
  2. applesignin_callback: controla la devolución de llamada POST de Apple e intercambia de forma segura el código de autorización por un token de acceso y un token de identificador. Por último, vuelve a redirigir al esquema de URI de la aplicación, y pasa los tokens en un fragmento de dirección URL.

La aplicación móvil se registra para controlar el esquema de URI personalizado que se ha seleccionado (en este caso xamarinformsapplesignin://) para que la función applesignin_callback le pueda retransmitir los tokens.

Cuando el usuario inicia la autenticación, se realizan los pasos siguientes:

  1. La aplicación móvil genera un valor nonce y state, y los pasa a la función de Azure applesignin_auth.
  2. La función de Azureapplesignin_auth genera una dirección URL de autorización de inicio de sesión de Apple (mediante los valores state y nonce proporcionados) y redirige el explorador de la aplicación móvil a ella.
  3. El usuario escribe sus credenciales de forma segura en la página de autorización de inicio de sesión de Apple hospedada en los servidores de Apple.
  4. Una vez que finaliza el flujo de inicio de sesión de Apple en los servidores de Apple, Apple redirige a redirect_uri, que será la función de Azure applesignin_callback.
  5. La solicitud de Apple enviada a la función applesignin_callback se valida para asegurarse de que se devuelve el valor state correcto y que las notificaciones del token de identificador son válidas.
  6. La función de Azure applesignin_callback intercambia el valor code publicado por Apple por un token de acceso, un token de actualización y un token de identificador (que contiene notificaciones sobre el identificador de usuario, el nombre y el correo electrónico).
  7. La función de Azure applesignin_callback, por último, vuelve a redirigir al esquema de URI de la aplicación (xamarinformsapplesignin://) y adjunta un fragmento de URI con los tokens (por ejemplo, xamarinformsapplesignin://#access_token=...&refresh_token=...&id_token=...).
  8. La aplicación móvil analiza el fragmento de URI en una instancia de AppleAccount y valida que la notificación nonce recibida coincide con el valor nonce generado al principio del flujo.
  9. La aplicación móvil ya está autenticada.

Funciones de Azure

En este ejemplo se usa Azure Functions. Como alternativa, un controlador de ASP.NET Core o una solución de servidor web similar podría ofrecer la misma funcionalidad.

Configuración

Es necesario configurar varios valores de la aplicación al usar Azure Functions:

  • APPLE_SIGNIN_KEY_ID: es el valor KeyId anterior.
  • APPLE_SIGNIN_TEAM_ID: suele ser el identificador de equipo que se encuentra en el perfil de pertenencia
  • APPLE_SIGNIN_SERVER_ID: es el valor ServerId anterior. No es el identificador de paquete de la aplicación, sino el identificador del Id. de servicios que ha creado.
  • APPLE_SIGNIN_APP_CALLBACK_URI: es el esquema de URI personalizado con el que quiere volver a redirigir a la aplicación. En este ejemplo, se utiliza xamarinformsapplesignin://.
  • APPLE_SIGNIN_REDIRECT_URI: la dirección URL de redireccionamiento que se configura al crear el Id. de servicios en la sección Configuración de inicio de sesión de Apple. Para probarlo, podría tener un aspecto similar al este: http://local.test:7071/api/applesignin_callback
  • APPLE_SIGNIN_P8_KEY: el contenido de texto del archivo .p8, con todas las líneas nuevas \n quitadas, por lo que es una cadena larga

Consideraciones sobre la seguridad

Nunca almacene la clave P8 dentro del código de la aplicación. El código de la aplicación es fácil de descargar y desensamblar.

También se considera un procedimiento incorrecto usar WebView para hospedar el flujo de autenticación y para interceptar eventos de navegación de URL a fin de obtener el código de autorización. Actualmente no hay ninguna manera totalmente segura de controlar el Iniciar sesión con Apple en dispositivos que no son iOS13+ sin hospedar código en un servidor para controlar el intercambio de tokens. Se recomienda hospedar el código de generación de direcciones URL de autorización en un servidor para que pueda almacenar el estado en caché y validarlo cuando Apple emite una devolución de llamada POST al servidor.

Un servicio de inicio de sesión multiplataforma

Con DependencyService de Xamarin.Forms, puede crear servicios de autenticación independientes que usen los servicios de plataforma en iOS y un servicio web genérico para Android y otras plataformas que no sean de iOS basadas en una interfaz compartida.

public interface IAppleSignInService
{
    bool Callback(string url);

    Task<AppleAccount> SignInAsync();
}

En iOS, se usan las API nativas:

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

La marca de compilación __IOS__13 se usa para proporcionar compatibilidad con iOS 13, así como versiones heredadas que recurren al servicio web genérico.

En Android, se usa el servicio web genérico con 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;
    }
}

Resumen

En este artículo se describen los pasos necesarios para configurar Inicio de sesión con Apple para su uso en las aplicaciones de Xamarin.Forms.