DEV Community

Filipe Nanclarez
Filipe Nanclarez

Posted on

Sign in with Apple no Windows com Flutter

Hoje resolvi encarar um desafio que está me consumindo energia a alguns meses já.

Estou criando um aplicativo multiplataforma, e preciso autenticar meus usuários. Uma forma bem conveniente de fazer isso é através de provedores de autenticação conhecidos, como google, facebook, twitter. Isso é conveniente para o usuário, pois não precisa ficar digitando e-mail e senha e lembrando deles depois.

O problema é que a apple exige que se o aplicativo tiver qualquer provedor de autenticação, então ele também deve ter a opção de autenticar pela apple.

Ok, então é simples, vamos começar pela autenticação da apple. Aí foi onde o problema começou a ficar mais complexo.

O suporte para desktop foi lançado a pouco tempo pelo equipe do flutter. Com isso alguns parceiros começaram a lançar seus pacotes tambem. Um pacote que estava muito promissor era o da Invertase. Eles estão trabalhando no suporte completo para do flutterfire para desktop.

Eu aguardei pois sabia que logo logo eles iriam colocar a mão nesse problema, e foi exatamente o que aconteceu. Mas para minha surpresa, eles pularam a parte mais difícil. Eles fizeram a autenticação funcionar em MacOs mas não em Windows. Segundo eles não é prioridade para eles fazer isso agora.

Isso me fez voltar nesse problema, e resolver isso de uma vez por todas. Depois de pesquisar vários pacotes no pub.dev e não achar nenhum que faça isso (se vc achar me conte por favor), eu resolvi adotar um recurso técnico alternativo.

Resolvi fazer uma página web que faça a autenticação e apenas me retorne o e-mail do usuário. E com isso eu faço o resto que preciso no meu aplicativo. Não é a solução mais elegante do mundo, mas me lembro de uma frase que diz 'antes feito que perfeito'. Melhor fazer o possível agora, e futuramente quando for preciso melhorar esse recurso, estudo novamente uma solução mais elaborada.

Então vamos lá.

Configurando o servidor web

Primeiro de tudo, precisamos de um servidor web funcional. Vou usar o mesmo que criamos no meu artigo sobre couchbase. Primeiro vamos instalar o apache.

sudo yum install nano httpd -y

Com isso se acessarmos o servidor já teremos a página do apache abrindo.

Image description

Agora vamos criar uma pagina para fazer a autenticação.

Para manter o foco, vamos usar o WinSCP, e só colocar no servidor os arquivos que precisamos. Então vamos conectar no winSCP.

Depois de conectar, vamos colocar o arquivo index.html dentro da pasta /var/www/html. Ao fazer isso, você deve receber um erro de permissão. Esse diretório só pode ser acessado com usuário com permissões administrativas. Para simplificar, vamos configurar o WinSCP para executar o sudo logo após efetuar o login.

Para isso, vamos alterar a configuração do winSCP com esse comando sudo /usr/libexec/openssh/sftp-server ( lembrando que esse procedimento é específico para o CentOS ).

Image description

Agora podemos fazer uma página html simples só para testar o servidor. Vamos criar um arquivo com o seguinte conteúdo e salvar como /var/www/html/index.html

<html>
    teste
</html>
Enter fullscreen mode Exit fullscreen mode

Com isso devemos conseguir ver o seguinte resultado:

Image description

Configurando a autenticação no console de desenvolvedor da apple

Agora antes de começarmos o código para a autenticação, vamos criar as configurações necessárias no console de desenvolvedor da apple.

Para isso estou seguindo esse artigo

Então primeiro vamos fazer login no portal de desenvolvedor da Apple..

Nessa página, vamos em Certificates, IDs & Profiles

Image description

Na tela abaixo, clique no sinal de mais:

Image description

Escolha Services IDs e continue:

Image description

Preencha a descrição e o identificador. Anote esse identificador pois vamos usar ele mais pra frente.

Image description

Finalize registrando o aplicativo.

Agora vamos criar o certificado para a autenticação.

No menu inicial clique em Keys:

Image description

Coloque um nome para a chave, e marque a opção Sign in with Apple e depois clique no botão Configure.

Image description

Na tela de configuração, escolha o app ( no meu caso, o app já existia, então estou só fazendo a configuração para um app existente ).

Image description

Agora que já terminamos de configurar a chave, vamos seguir na tela que estávamos:

Image description

Agora confirme o registro na tela final:

Image description

ATENÇÃO!

Nessa tela abaixo, anote o ID da chave, pois vamos usar depois. Aqui nós vamos fazer o download do arquivo, que só pode ser feito uma vez, então ESCOLHA BEM ONDE SALVAR ESSE ARQUIVO.

Image description

Embora esteja com a extensão p8 esse é um arquivo de texto. Vamos usar ele na mais à frente.

Depois de baixar o arquivo, pode finalizar no botão Done.

Configurando autenticação no console do Firebase

Agora vamos no console do firebase e dentro de Authentication vamos na aba Sign-in method e depois em Adicionar novo fornecedor.

Image description

Escolha apple, e depois vamos adicionar as configurações solicitadas.

Service ID é o Service ID identifier que anotamos antes.

Image description

Team ID é o código que aparece no topo do console de desenvolvedor da apple:

Image description

ID da chave é o Key ID que apareceu na etapa de download da chave:

Image description

E agora a na chave privada, vamos abrir aquele arquivo que baixamos, e copiar o conteúdo inteiro dentro do campo:

Image description

Agora copie a url de retorno para a próxima etapa.

Image description

Configurando a url de retorno

Volte no console de desenvolvedor da apple, e na aba Identifiers clique no menu App IDs à direita e escolha Services IDs:

Image description

Escolha o serviço que criamos, e na tela de detalhes escolha o botão Configure:

Image description

Escolha o seu app no combo Primary App ID e preencha seu domínio e a url de retorno que acabamos de copiar do console do firebase:

Image description

Com isso podemos voltar para o código para fazer a autenticação.

Authenticando com apple via firebase com javascript

Como o que queremos é apenas uma página que será chamada dentro de um webview, não iremos instalar o sdk via npm, nem fazer nenhuma configuração mais elaborada. Vamos apenas chamar os pacotes do firebase diretamente no nosso script e disparar o fluxo de autenticação.

Mas vamos precisar tomar cuidado, para não expor os dados da nosso projeto do firebase no nosso script, pois essa página será pública então vamos encapsular isso usando funções.

Seguindo a documentação do firebase, para importar os pacotes vamos usar o seguinte código:

    // Import the functions you need from the SDKs you need
    import { initializeApp } from "https://www.gstatic.com/firebasejs/9.7.0/firebase-app.js";
    import { getAnalytics } from "https://www.gstatic.com/firebasejs/9.7.0/firebase-analytics.js";

    // TODO: Add SDKs for Firebase products that you want to use
    // https://firebase.google.com/docs/web/setup#available-libraries
    import { OAuthProvider, getAuth, signInWithRedirect, getRedirectResult, signOut  } from 'https://www.gstatic.com/firebasejs/9.7.0/firebase-auth.js';
Enter fullscreen mode Exit fullscreen mode

Depois disso, precisamos inicializar o firebase com as variáveis do nosso projeto. Vamos criar uma função e passar essas variáveis como parâmetros, assim elas não ficam expostas. Como o import dos pacotes só pode ser feito dentro de módulos, e funções dentro de módulos não ficam disponíveis para acesso externo, vamos expor a função jogando ela dentro da variável global window.

Dentro da nossa função, passamos os parâmetros para uma variável, e depois chamamos a inicialização do firebase:

window.initApp = function initApp(apiKey,authDomain,databaseURL,projectId,storageBucket,messagingSenderId,appId,measurementId){
    //Your web app's Firebase configuration
    // For Firebase JS SDK v7.20.0 and later, measurementId is optional
    const firebaseConfig = {
        apiKey: apiKey,
        authDomain: authDomain,
        databaseURL: databaseURL,
        projectId: projectId,
        storageBucket: storageBucket,
        messagingSenderId: messagingSenderId,
        appId: appId,
        measurementId: measurementId
    };

    const app = initializeApp(firebaseConfig);
    const analytics = getAnalytics(app);    
}
Enter fullscreen mode Exit fullscreen mode

Agora que já temos o firebase inicializado, vamos disparar o fluxo de autenticação. Para isso também vamos criar uma função para podemos disparar manualmente em nosso aplicativo flutter.

// Start redirect auth flow
window.sign = async function (){
    let signOutResult = await signOut(auth);
    console.log('sign-out result:');
    console.log(signOutResult);

    // start auth with redirect
    signInWithRedirect(auth, provider);
}
Enter fullscreen mode Exit fullscreen mode

Isso vai fazer a página abrir a tela de autenticação da apple. Depois que o usuário se autenticar, o fluxo vai chamar novamente nossa página. Mas agora, vamos precisar pegar o resultado da autenticação. Então para isso vamos criar outra função, que vai pegar essas informações recebidas após o fluxo de autenticação retornar.

Para enviar uma informação do nosso código javascript para o aplicativo, vamos usar esse código:

window.chrome.webview.postMessage({chave:valor})

Depois veremos como receber isso do lado do flutter.

Então nossa função para receber o retorno do fluxo de autenticação ficou assim:

// Get result from redirect auth flow
window.getResult = function (){
    getRedirectResult(auth) 
    .then((result) => {

        if (!result) return;

        window.chrome.webview.postMessage({"result":JSON.stringify(result)});

    })
    .catch((error) => {
        console.log('erro');
        console.log(error);
        window.chrome.webview.postMessage({"erro ao capturar credencial":JSON.stringify(error)});

    });

}
Enter fullscreen mode Exit fullscreen mode

Adicionando algumas variáveis de controle, um detalhe aqui outro ali, temos o código completo abaixo:

<html>
    <body>
        <div></div>
        <script type="module">
            let auth, provider;

            // Import the functions you need from the SDKs you need
            import { initializeApp } from "https://www.gstatic.com/firebasejs/9.7.0/firebase-app.js";
            import { getAnalytics } from "https://www.gstatic.com/firebasejs/9.7.0/firebase-analytics.js";

            // TODO: Add SDKs for Firebase products that you want to use
            // https://firebase.google.com/docs/web/setup#available-libraries
            import { OAuthProvider, getAuth, signInWithRedirect, getRedirectResult, signOut  } from 'https://www.gstatic.com/firebasejs/9.7.0/firebase-auth.js';

            // Initialize Firebase
            window.initApp = function initApp(apiKey,authDomain,databaseURL,projectId,storageBucket,messagingSenderId,appId,measurementId){
                //Your web app's Firebase configuration
                // For Firebase JS SDK v7.20.0 and later, measurementId is optional
                const firebaseConfig = {
                    apiKey: apiKey,
                    authDomain: authDomain,
                    databaseURL: databaseURL,
                    projectId: projectId,
                    storageBucket: storageBucket,
                    messagingSenderId: messagingSenderId,
                    appId: appId,
                    measurementId: measurementId
                };

                const app = initializeApp(firebaseConfig);
                const analytics = getAnalytics(app);

                provider = new OAuthProvider('apple.com');
                auth = getAuth();

                console.log('firebase init successfully');
            }

            // Start redirect auth flow
            window.sign = async function (){
                let signOutResult = await signOut(auth);
                console.log('sign-out result:');
                console.log(signOutResult);

                // start auth with redirect
                signInWithRedirect(auth, provider);
            }

            // Get result from redirect auth flow
            window.getResult = function (){
                getRedirectResult(auth) 
                .then((result) => {

                    if (!result) return;

                    window.chrome.webview.postMessage({"result":JSON.stringify(result)});

                })
                .catch((error) => {
                    console.log('erro');
                    console.log(error);
                    window.chrome.webview.postMessage({"erro ao capturar credencial":JSON.stringify(error)});

                });

            }
        </script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Interagindo com o webview no flutter

Agora vamos para o lado 'dart' da força. kkk

Trocadilhos a parte, vamos iniciar um novo projeto flutter, e vamos adicionar o plugin webview_windows e depois abrir ele no Visual Studio Code.

flutter create --platforms=windows auth_apple_no_windows
cd auth_apple_no_windows
flutter pub add webview_windows
code .
Enter fullscreen mode Exit fullscreen mode

Agora vamos:

  • remover todos os comentários
  • remover a função de incremento que veio no exemplo padrão
  • remover o float button
  • declarar um controller para nosso webview
final _controller = WebviewController();
Enter fullscreen mode Exit fullscreen mode
  • incluir o webview no corpo da nossa tela:
      body: Webview(
        _controller,
        permissionRequested: _onPermissionRequested,
      ),  
Enter fullscreen mode Exit fullscreen mode

O código deverá ficar assim então:

import 'package:flutter/material.dart';
import 'package:webview_windows/webview_windows.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _controller = WebviewController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Webview(
        _controller,
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Agora precisamos fazer o webview carregar nossa página assim que essa tela for carregada. Para isso vamos usar uma função de retorno que é chamada quando o último frame da tela é desenhado, também conhecida como addPostFrameCallback. Vamos adicionar a chamada dessa função de retorno no método initState do ciclo de vida da tela:

  ...
  final _controller = WebviewController();

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance?.addPostFrameCallback((_) {
      // do something

    });
  }
  ...
Enter fullscreen mode Exit fullscreen mode

Agora dentro dessa função, vamos inicializar o controlador e pedir para o webview carregar a página que criamos. :

    WidgetsBinding.instance?.addPostFrameCallback((_) async {
      await _controller.initialize();
      await _controller
          .loadUrl('http://example.com');
      if (!mounted) return;
      setState(() {});
    });
Enter fullscreen mode Exit fullscreen mode

Se você tentar rodar o aplicativo nesse momento verá apenas uma tela em branco. Isso porque não disparamos aquelas funções que criamos no javascript.

Então vamos fazer isso através do método executeScript. Mas só podemos fazer isso, depois que a página já estiver totalmente carregada. Para isso, vamos adicionar um 'ouvinte', que vai ficar monitorando o webview e vai nos avisar a cada mudança que ocorrer nele. A mudança que nos interessa é a LoadingState.navigationCompleted.

Nessa situação, vamos chamar a função initApp() passando os parâmetros para iniciar o firebase:

      _controller.loadingState.listen((event) async {
        if (event == LoadingState.navigationCompleted) {
          // inicializando o firebase
          await _controller.executeScript('''
          initApp(
              "AIzaSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXMA",
              "XXXXXXXXXXX.firebaseapp.com",
              "https://XXXXXXXXX.firebaseio.com",
              "XXXXXXXXXX",
              "XXXXXXXX.appspot.com",
              "96XXXXXXXX19",
              "1:96XXXXXXXXXX19:web:f9XXXXXXXXXXa30XXXXXX6",
              "G-EXXXXXXXX3"
          );
          ''');
        }
      });
Enter fullscreen mode Exit fullscreen mode

Agora que já temos o firebase inicializado, podemos chamar a função que criamos que dispara o login. Podemos fazer isso logo após a inicialização. Não se preocupe, vou postar o código inteiro no final para ficar claro onde exatamente esses pedaços de código estão.

await controller.executeScript('sign();');
Enter fullscreen mode Exit fullscreen mode

Com isso ao executar o aplicativo já irá aparecer a tela de autenticação:

Image description

Mas se vc tentar autenticar agora, encontrará um problema. Como estamos disparando a autenticação assim que a página é carregada, após o fluxo de autenticação terminar e o sistema do firebase redirecionar a página de volta para nós, nosso sistema vai disparar novamente a função de autenticação gerando um loop infinito.

Para resolver isso de forma simples, vamos criar uma variável de controle que inicia com valor true. Depois de rodarmos a primeira vez mudamos ela para false e verificamos ela ao disparar a função de autenticação:

var firstRun = true;
...
if (firstRun) {
  firstRun = false;
  await _controller.executeScript('sign();');
}
Enter fullscreen mode Exit fullscreen mode

Agora o que precisamos é capturar o retorno do fluxo de autenticação quando o usuário terminar de fazer login. Como isso só acontece na segunda vez que a página é carregada, vamos usar esse mesmo if e chamar a função que captura o resultado no else:

var firstRun = true;
...
if (firstRun) {
  firstRun = false;
  await _controller.executeScript('sign();');
}else{
  await controller.executeScript('getResult();');
}
Enter fullscreen mode Exit fullscreen mode

E para receber o valor que essa função envia para nós, vamos usar outro 'ouvinte' que vai ficar monitorando todas as mensagens que recebermos do webview. Lembra que eu disse que ia mostrar como recebemos no dart a mensagem que enviamos lá no código javascript? É aqui que isso acontece:

_controller.webMessage.listen((event) async {
  if (event['result'] != null) {
    var userData = json.decode(event['result'])['user'];

    debugPrint(userData['uid']);
    debugPrint(userData['email']);
  }
});
Enter fullscreen mode Exit fullscreen mode

E finalmente o resultado tão esperado. Recebemos o id do usuário lá do firebase, e também o endereço de e-mail dele:

Image description

Agora é só usar esses dados da forma que for melhor para o seu projeto.

Talvez você esteja se perguntando como fica a parte de controle da sessão do usuário e logout e onde vai guardar isso. Isso tudo não faz parte do escopo desse artigo. Eu tentei capturar as credenciais e colocar elas dentro de um objeto de autenticação do lado do aplicativo com o método reauthenticateWithCredential conforme essa documentação. Mas o package flutter_desktop_webview_auth da invertase entrou em conflito com o package webview_windows então eu decidi apenas usar os dados do usuário e fazer o meu próprio controle manualmente mesmo.

O pessoal da back4app me sugeriu uma solução parecida com essa que abordamos nesse artigo, mas em vez de usar uma página com javascript, eles indicaram usar o recurso Cloud Functions da plataforma deles.

Vou finalizar deixando o código dart completo conforme prometi.

Até a próxima 😉

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:webview_windows/webview_windows.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _controller = WebviewController();
  var firstRun = true;

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance?.addPostFrameCallback((_) async {
      await _controller.initialize();

      _controller.loadingState.listen((event) async {
        if (event == LoadingState.navigationCompleted) {
          // inicializando o firebase
          await _controller.executeScript('''
          initApp(
              "AIXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXmMA",
              "XXXXXXX.firebaseapp.com",
              "https://XXXXXXX.firebaseio.com",
              "XXXXXXX",
              "XXXXXXX.appspot.com",
              "96XXXXXXX19",
              "1:96XXXXXXX19:web:f9XXXXXXXd6",
              "G-EXXXXXXX3"
          );
        ''');

          if (firstRun) {
            firstRun = false;
            await _controller.executeScript('sign();');
          } else {
            await _controller.executeScript('getResult();');
          }
        }
      });

      _controller.webMessage.listen((event) async {
        if (event['result'] != null) {
          var userData = json.decode(event['result'])['user'];

          debugPrint(userData['uid']);
          debugPrint(userData['email']);
        }
      });

      await _controller
          .loadUrl('http://example.com');

      if (!mounted) return;
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Webview(
        _controller,
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)