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:
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 questate
se puede almacenar en caché y validar cuando los servidores de Apple envíen una devolución de llamada.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:
- La aplicación móvil genera un valor
nonce
ystate
, y los pasa a la función de Azureapplesignin_auth
. - La función de Azure
applesignin_auth
genera una dirección URL de autorización de inicio de sesión de Apple (mediante los valoresstate
ynonce
proporcionados) y redirige el explorador de la aplicación móvil a ella. - 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.
- 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 Azureapplesignin_callback
. - La solicitud de Apple enviada a la función
applesignin_callback
se valida para asegurarse de que se devuelve el valorstate
correcto y que las notificaciones del token de identificador son válidas. - La función de Azure
applesignin_callback
intercambia el valorcode
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). - 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=...
). - La aplicación móvil analiza el fragmento de URI en una instancia de
AppleAccount
y valida que la notificaciónnonce
recibida coincide con el valornonce
generado al principio del flujo. - 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 valorKeyId
anterior.APPLE_SIGNIN_TEAM_ID
: suele ser el identificador de equipo que se encuentra en el perfil de pertenenciaAPPLE_SIGNIN_SERVER_ID
: es el valorServerId
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 utilizaxamarinformsapplesignin://
.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.