Compartilhar via


Utilizar o início de sessão com a Apple in Xamarin.Forms

Entrar com a Apple é para todos os novos aplicativos no iOS 13 que usam serviços de autenticação de terceiros. Os detalhes de implementação entre iOS e Android são bem diferentes. Este guia explica como você pode fazer isso hoje no Xamarin.Forms.

Neste guia e exemplo, serviços de plataforma específicos são usados para lidar com o login com a Apple:

  • Android usando um serviço Web genérico conversando com o Azure Functions com OpenID/OpenAuth
  • O iOS usa a API nativa para autenticação no iOS 13 e retorna para um serviço Web genérico para iOS 12 e versões inferiores

Um exemplo de fluxo de login da Apple

Este exemplo oferece uma implementação opinativa para fazer com que o Apple Sign In funcione em seu Xamarin.Forms aplicativo.

Usamos duas Funções do Azure para ajudar com o fluxo de autenticação:

  1. applesignin_auth - Gera o URL de autorização de login da Apple e redireciona para ele. Fazemos isso no lado do servidor, em vez do aplicativo móvel, para que possamos armazenar em cache e state validá-lo quando os servidores da Apple enviarem um retorno de chamada.
  2. applesignin_callback - Manipula o retorno de chamada POST da Apple e troca com segurança o código de autorização por um Access Token e ID Token. Finalmente, ele redireciona de volta para o Esquema de URI do aplicativo, passando de volta os tokens em um fragmento de URL.

O aplicativo móvel se registra para lidar com o esquema de URI personalizado que selecionamos (neste caso xamarinformsapplesignin://) para que a applesignin_callback função possa retransmitir os tokens de volta para ele.

Quando o usuário inicia a autenticação, as seguintes etapas acontecem:

  1. O aplicativo móvel gera um nonce valor e state e os passa para a applesignin_auth função do Azure.
  2. A applesignin_auth função do Azure gera uma URL de Autorização de Entrada da Apple (usando o fornecido state e nonce) e redireciona o navegador do aplicativo móvel para ele.
  3. O usuário insere suas credenciais com segurança na página de autorização de login da Apple hospedada nos servidores da Apple.
  4. Depois que o fluxo de login da Apple terminar nos servidores da Apple, a Apple redireciona para o redirect_uri que será a applesignin_callback função do Azure.
  5. A solicitação da Apple enviada para a applesignin_callback função é validada para garantir que o correto state seja retornado e que as declarações de ID Token sejam válidas.
  6. A applesignin_callback função do Azure troca o postado code pela Apple por um Token de Acesso, Token de Atualização e Token de ID (que contém declarações sobre a ID do Usuário, o Nome e o Email).
  7. A applesignin_callback função do Azure finalmente redireciona de volta para o esquema de URI do aplicativo (xamarinformsapplesignin://) anexando um fragmento de URI com os Tokens (por exemplo, xamarinformsapplesignin://#access_token=...&refresh_token=...&id_token=...).
  8. O aplicativo móvel analisa o Fragmento de URI em um AppleAccount e valida a nonce declaração recebida corresponde à nonce gerada no início do fluxo.
  9. O aplicativo móvel agora está autenticado!

Azure Functions

Este exemplo usa o Azure Functions. Como alternativa, um ASP.NET Core Controller ou uma solução de servidor Web semelhante pode fornecer a mesma funcionalidade.

Configuração

Várias configurações de aplicativo precisam ser definidas ao usar o Azure Functions:

  • APPLE_SIGNIN_KEY_ID - Este é o seu KeyId de antes.
  • APPLE_SIGNIN_TEAM_ID - Este é geralmente o seu ID de Equipe encontrado em seu Perfil de Membro
  • APPLE_SIGNIN_SERVER_ID: Este é o ServerId de antes. Não é o ID do Pacote de Aplicativos, mas sim o Identificador do ID de Serviços que você criou.
  • APPLE_SIGNIN_APP_CALLBACK_URI - Este é o esquema de URI personalizado com o qual você deseja redirecionar de volta para seu aplicativo. Neste exemplo xamarinformsapplesignin:// é usado.
  • APPLE_SIGNIN_REDIRECT_URI- O URL de redirecionamento que você configura ao criar seu ID de Serviços na seção Configuração de Login da Apple. Para testar, pode ser algo como: http://local.test:7071/api/applesignin_callback
  • APPLE_SIGNIN_P8_KEY - O conteúdo de texto do seu .p8 arquivo, com todas as \n novas linhas removidas para que seja uma cadeia de caracteres longa

Considerações de segurança

Nunca armazene sua chave P8 dentro do código do aplicativo. O código do aplicativo é fácil de baixar e desmontar.

Também é considerada uma prática ruim usar um para hospedar o fluxo de WebView autenticação e interceptar eventos de navegação de URL para obter o código de autorização. No momento, não há nenhuma maneira totalmente segura de lidar com o login com a Apple em dispositivos que não sejam iOS13+ sem hospedar algum código em um servidor para lidar com a troca de tokens. Recomendamos hospedar o código de geração de URL de autorização em um servidor para que você possa armazenar em cache o estado e validá-lo quando a Apple emitir um retorno de chamada POST para o servidor.

Um serviço de login entre plataformas

Usando o Xamarin.Forms DependencyService, você pode criar serviços de autenticação separados que usam os serviços de plataforma no iOS e um serviço Web genérico para Android e outras plataformas não iOS com base em uma interface compartilhada.

public interface IAppleSignInService
{
    bool Callback(string url);

    Task<AppleAccount> SignInAsync();
}

No iOS, as APIs nativas são usadas:

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

O sinalizador __IOS__13 de compilação é usado para fornecer suporte para o iOS 13, bem como versões herdadas que fallback para o serviço Web genérico.

No Android, o serviço Web genérico com o Azure Functions é usado:

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

Resumo

Este artigo descreveu os passos necessários para configurar o Iniciar sessão com a Apple para utilização nas suas Xamarin.Forms aplicações.