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.
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 ).
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:
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
Na tela abaixo, clique no sinal de mais:
Escolha Services IDs
e continue:
Preencha a descrição e o identificador. Anote esse identificador pois vamos usar ele mais pra frente.
Finalize registrando o aplicativo.
Agora vamos criar o certificado para a autenticação.
No menu inicial clique em Keys
:
Coloque um nome para a chave, e marque a opção Sign in with Apple
e depois clique no botão Configure
.
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 ).
Agora que já terminamos de configurar a chave, vamos seguir na tela que estávamos:
Agora confirme o registro na tela final:
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.
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
.
Escolha apple, e depois vamos adicionar as configurações solicitadas.
Service ID
é o Service ID identifier que anotamos antes.
Team ID
é o código que aparece no topo do console de desenvolvedor da apple:
ID da chave
é o Key ID
que apareceu na etapa de download da chave:
E agora a na chave privada, vamos abrir aquele arquivo que baixamos, e copiar o conteúdo inteiro dentro do campo:
Agora copie a url de retorno para a próxima etapa.
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
:
Escolha o serviço que criamos, e na tela de detalhes escolha o botão Configure
:
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:
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';
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);
}
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);
}
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)});
});
}
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>
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 .
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();
- incluir o webview no corpo da nossa tela:
body: Webview(
_controller,
permissionRequested: _onPermissionRequested,
),
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,
),
);
}
}
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
});
}
...
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(() {});
});
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"
);
''');
}
});
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();');
Com isso ao executar o aplicativo já irá aparecer a tela de autenticação:
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();');
}
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();');
}
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']);
}
});
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:
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,
),
);
}
}
Top comments (0)