Sign in with Apple no Windows com Flutter
Tags: apple, auth, flutter, windows
Published: 2022-05-02 21:17:38

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](https://developer.apple.com/app-store/review/guidelines/#sign-in-with-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](https://invertase.io/blog/announcing-flutterfire-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](https://github.com/invertase/flutterfire_desktop/issues/68). Segundo eles [não é prioridade para eles fazer isso agora](https://github.com/invertase/flutter_desktop_webview_auth/issues/18).

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1k25tbqfowra7gv77wid.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qiyjvyryx5nupesbftj2.png)

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>
```

Com isso devemos conseguir ver o seguinte resultado:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38hzektghe94z3pszhxf.png)

#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](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple)

Então primeiro vamos fazer login no [portal de desenvolvedor da Apple.](https://developer.apple.com/account).

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/71yyvppotu9umm3hh45q.png)

Na tela abaixo, clique no sinal de mais:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/37ejsyelmjmzsn394i2d.png)

Escolha `Services IDs` e continue:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6csxbddyy56d4jckk9q.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l48dzpcb0mp0ycs10z7h.png)
   
Finalize registrando o aplicativo.

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

No menu inicial clique em `Keys`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j9e8id088xhh3hdsek7.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9rmcqk63zzzhkjixpctw.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/akeq46rhkz66lg6429j8.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zclz1qgsa06ng9855nuw.png)

Agora confirme o registro na tela final:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l0dxu0dwstdkuybab6es.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0tvwzfkyhe2rdq9t7ox2.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4dpafq0puv5wwzxz10l.png)

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

`Service ID` é o Service ID identifier que anotamos antes.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ov07ox9l3bvx4hud99i9.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qiyfe393myc98ofa48ks.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xg1eeg5y96sloh9girdr.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2tj026b2dj07rs5qdhay.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bkqedyni36ilf7hdb3et.png)

#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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/16cyrf2gcs5bbrsf6jjz.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nnis7m0nsmvfh8poc7as.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ayavbl56731gv1krxbop.png)

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](https://firebase.google.com/docs/web/alt-setup), para importar os pacotes vamos usar o seguinte código:

```javascript
	// 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';
```

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:

```javascript
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);	
}
```
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.

```javascript
// 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);
}
```

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:

```javascript
// 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)});

	});
	
}
```

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

```html
<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>
```  
 
#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](https://pub.dev/packages/webview_windows) e depois abrir ele no Visual Studio Code.

```shell
flutter create --platforms=windows auth_apple_no_windows
cd auth_apple_no_windows
flutter pub add webview_windows
code .
```

Agora vamos: 
- [remover todos os comentários](https://miajimyu.com/en/docs/flutter/flutter-remove-all-comments-in-starter-app/) 
- remover a função de incremento que veio no exemplo padrão
- remover o float button 
- declarar um `controller` para nosso webview 
```dart 
final _controller = WebviewController();
```
- incluir o webview no corpo da nossa tela:
```dart
      body: Webview(
        _controller,
        permissionRequested: _onPermissionRequested,
      ),  
```

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

```dart
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,
      ),
    );
  }
}

```

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](https://www.flutterbeads.com/call-method-after-build-in-flutter/), também conhecida como `addPostFrameCallback`. Vamos adicionar a chamada dessa função de retorno no método `initState` do ciclo de vida da tela:

```dart
  ...
  final _controller = WebviewController();

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

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

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

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

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:

```dart
      _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"
          );
          ''');
        }
      });
```

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.

```dart
await controller.executeScript('sign();');
```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g7i78drx5cx9nc4s08ot.png)

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:

```dart
var firstRun = true;
...
if (firstRun) {
  firstRun = false;
  await _controller.executeScript('sign();');
}
```

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`:

```dart
var firstRun = true;
...
if (firstRun) {
  firstRun = false;
  await _controller.executeScript('sign();');
}else{
  await controller.executeScript('getResult();');
}
```

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:

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

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

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tp3qianq48waqzro49ee.png)
 
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](https://firebase.flutter.dev/docs/auth/manage-users/#re-authenticate-a-user). 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](https://www.back4app.com) 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](https://docs.parseplatform.org/cloudcode/guide/#cloud-functions) da plataforma deles.

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

Até a próxima 😉

```dart
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,
      ),
    );
  }
}

```