Adicionar autenticação ao seu aplicativo Flutter
Este tutorial mostrará como integrar o Logto ao seu aplicativo Flutter.
- O pacote SDK está disponível no pub.dev e no repositório GitHub do Logto.
- O projeto de exemplo é construído usando Flutter material. Você pode encontrá-lo no pub.dev e em nosso repositório GitHub.
- O SDK é compatível apenas com as plataformas Android e iOS.
- O SDK v1.x é compatível com Dart 2.x. Para o SDK v2.x, você precisa atualizar sua versão do Dart para 3.x ou superior.
Pré-requisitos
- Uma conta Logto Cloud ou um Logto auto-hospedado.
- Um aplicativo nativo Logto criado.
- Um ambiente de desenvolvimento Flutter ou Dart.
Instalação
- pub.dev
- GitHub
Você pode instalar o logto_dart_sdk package
diretamente usando o gerenciador de pacotes pub. Execute o seguinte comando no diretório raiz do seu projeto:
flutter pub get logto_dart_sdk
Se preferir criar sua própria versão do SDK, você pode clonar o repositório diretamente do GitHub.
git clone https://github.com/logto-io/dart
Módulos
O logto_dart_sdk
inclui dois módulos principais:
-
logto_core.dart Este módulo principal fornece as funções e interfaces básicas para o Logto SDK.
-
logto_client.dart Este módulo cliente oferece uma classe cliente Logto de alto nível para interagir com o servidor Logto.
Dependências e configurações
Este SDK possui as seguintes dependências, algumas requerem configurações adicionais:
flutter_secure_storage
Usamos flutter_secure_storage para implementar o armazenamento seguro de tokens persistente e multiplataforma.
- Keychain é usado para iOS
- Criptografia AES é usada para Android.
Configurar versão do Android
Defina o android:minSdkVersion para 18 no arquivo android/app/build.gradle do seu projeto.
android {
...
defaultConfig {
...
minSdkVersion 18
...
}
}
Desativar backup automático
Por padrão, o Android pode fazer backup de dados no Google Drive automaticamente. Isso pode causar a exceção java.security.InvalidKeyException:Failed to unwrap key
.
Para evitar isso, você pode desativar o backup automático para seu aplicativo ou excluir sharedprefs
do FlutterSecureStorage
.
-
Para desativar o backup automático, vá para o arquivo de manifesto do seu aplicativo e defina os atributos
android:allowBackup
eandroid:fullBackupContent
comofalse
.AndroidManifest.xml<manifest ... >
...
<application
android:allowBackup="false"
android:fullBackupContent="false"
...
>
...
</application>
</manifest> -
Excluir
sharedprefs
doFlutterSecureStorage
.Se você precisar manter o
android:fullBackupContent
para seu aplicativo em vez de desativá-lo, pode excluir o diretóriosharedprefs
do backup. Veja mais detalhes na documentação do Android.No seu arquivo AndroidManifest.xml, adicione o atributo android:fullBackupContent ao elemento
<application>
, como mostrado no exemplo a seguir. Este atributo aponta para um arquivo XML que contém regras de backup.AndroidManifest.xml<application ...
android:fullBackupContent="@xml/backup_rules">
</application>Crie um arquivo XML chamado
@xml/backup_rules
no diretóriores/xml/
. Neste arquivo, adicione regras com os elementos<include>
e<exclude>
. O exemplo a seguir faz backup de todas as preferências compartilhadas, exceto device.xml:@xml/backup_rules<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>
Por favor, verifique flutter_secure_storage para mais detalhes.
flutter_web_auth
flutter_web_auth é usado por trás do SDK flutter do Logto. Dependemos de sua interface de interação baseada em webview para autenticar usuários.
Este plugin usa ASWebAuthenticationSession
no iOS 12+ e macOS 10.15+, SFAuthenticationSession
no iOS 11, Chrome Custom Tabs
no Android e abre uma nova janela na Web.
Registrar a URL de callback no Android
Para capturar a URL de callback da página de login do Logto, você precisará registrar seu redirectUri de login no arquivo AndroidManifest.xml
.
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity" android:exported="true">
<intent-filter android:label="flutter_web_auth">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="io.logto"/>
</intent-filter>
</activity>
http.dart
Como o SDK precisa fazer requisições de rede, você precisará passar um cliente HTTP para o SDK. Você pode usar o http.Client
padrão do http.dart ou criar seu próprio http.Client
com configurações personalizadas.
import 'package:http/http.dart' as http;
Integração
Iniciar LogtoClient
Importe o pacote logto_dart_sdk
e inicialize a instância LogtoClient
na raiz do seu aplicativo.
import 'package:logto_dart_sdk/logto_dart_sdk.dart';
import 'package:http/http.dart' as http;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Logto Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late LogtoClient logtoClient;
void render() {
// mudança de estado
}
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>"
);
void _init() {
logtoClient = LogtoClient(
config: logtoConfig,
httpClient: http.Client(), // Cliente http opcional
);
render();
}
void initState() {
super.initState();
_init();
}
// ...
}
Implementar login
Antes de mergulharmos nos detalhes, aqui está uma visão geral rápida da experiência do usuário final. O processo de login pode ser simplificado da seguinte forma:
- Seu aplicativo invoca o método de login.
- O usuário é redirecionado para a página de login do Logto. Para aplicativos nativos, o navegador do sistema é aberto.
- O usuário faz login e é redirecionado de volta para o seu aplicativo (configurado como o URI de redirecionamento).
Sobre o login baseado em redirecionamento
- Este processo de autenticação segue o protocolo OpenID Connect (OIDC), e o Logto aplica medidas de segurança rigorosas para proteger o login do usuário.
- Se você tiver vários aplicativos, pode usar o mesmo provedor de identidade (Logto). Uma vez que o usuário faz login em um aplicativo, o Logto completará automaticamente o processo de login quando o usuário acessar outro aplicativo.
Para saber mais sobre a lógica e os benefícios do login baseado em redirecionamento, veja Experiência de login do Logto explicada.
Antes de começar, você precisa adicionar um URI de redirecionamento no Admin Console para o seu aplicativo.
Vamos mudar para a página de detalhes do Aplicativo no Logto Console. Adicione um URI de redirecionamento io.logto://callback
e clique em "Salvar alterações".
- Para iOS, o esquema de URI de redirecionamento não importa realmente, pois a classe
ASWebAuthenticationSession
ouvirá o URI de redirecionamento independentemente de estar registrado ou não. - Para Android, o esquema de URI de redirecionamento deve ser registrado no arquivo
AndroidManifest.xml
.
Após o URI de redirecionamento ser configurado, adicionamos um botão de login à sua página, que chamará a API logtoClient.signIn
para invocar o fluxo de login do Logto:
class _MyHomePageState extends State<MyHomePage> {
// ...
final redirectUri = 'io.logto://callback';
Widget build(BuildContext context) {
// ...
Widget signInButton = TextButton(
onPressed: () async {
await logtoClient.signIn(redirectUri);
render();
},
child: const Text('Sign In'),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
signInButton,
],
),
),
);
}
}
Implementar logout
Agora vamos adicionar um botão de logout na página principal para que os usuários possam sair do seu aplicativo.
class _MyHomePageState extends State<MyHomePage> {
// ...
Widget build(BuildContext context) {
// ...
Widget signOutButton = TextButton(
onPressed: () async {
await logtoClient.signOut();
render();
},
child: const Text('Sign Out'),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
signInButton,
signOutButton,
],
),
),
);
}
}
Lidar com o status de autenticação
O SDK do Logto fornece um método assíncrono para verificar o status de autenticação. O método é logtoClient.isAuthenticated
. O método retorna um valor booleano, true
se o usuário estiver autenticado, caso contrário, false
.
No exemplo, renderizamos condicionalmente os botões de login e logout com base no status de autenticação. Agora vamos atualizar o método render
em nosso Widget para lidar com a mudança de estado:
class _MyHomePageState extends State<MyHomePage> {
// ...
bool? isAuthenticated = false;
void render() {
setState(() async {
isAuthenticated = await logtoClient.isAuthenticated;
});
}
Widget build(BuildContext context) {
// ...
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
isAuthenticated == true ? signOutButton : signInButton,
],
),
),
);
}
}
Ponto de verificação: Teste seu aplicativo
Agora, você pode testar seu aplicativo:
- Execute seu aplicativo, você verá o botão de login.
- Clique no botão de login, o SDK iniciará o processo de login e redirecionará você para a página de login do Logto.
- Após fazer login, você será redirecionado de volta para o seu aplicativo e verá o botão de logout.
- Clique no botão de logout para limpar o armazenamento local e sair.
Obter informações do usuário
Exibir informações do usuário
Para exibir as informações do usuário, você pode usar o getter logtoClient.idTokenClaims
. Por exemplo, em um aplicativo Flutter:
class _MyHomePageState extends State<MyHomePage> {
// ...
Widget build(BuildContext context) {
// ...
Widget getUserInfoButton = TextButton(
onPressed: () async {
final userClaims = await logtoClient.idTokenClaims;
print(userInfo);
},
child: const Text('Obter informações do usuário'),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('Meu Aplicativo de Demonstração'),
isAuthenticated == true ? signOutButton : signInButton,
isAuthenticated == true ? getUserInfoButton : const SizedBox.shrink(),
],
),
),
);
}
}
Solicitar reivindicações adicionais
Você pode perceber que algumas informações do usuário estão faltando no objeto retornado de client.idTokenClaims
. Isso ocorre porque OAuth 2.0 e OpenID Connect (OIDC) são projetados para seguir o princípio do menor privilégio (PoLP), e o Logto é construído com base nesses padrões.
Por padrão, reivindicações limitadas são retornadas. Se você precisar de mais informações, pode solicitar escopos adicionais para acessar mais reivindicações.
Uma "reivindicação (Claim)" é uma afirmação feita sobre um sujeito; um "escopo (Scope)" é um grupo de reivindicações. No caso atual, uma reivindicação é uma informação sobre o usuário.
Aqui está um exemplo não normativo da relação escopo - reivindicação:
A reivindicação "sub" significa "sujeito (Subject)", que é o identificador único do usuário (ou seja, ID do usuário).
O Logto SDK sempre solicitará três escopos: openid
, profile
e offline_access
.
Para solicitar escopos adicionais, você pode passá-los para o objeto LogtoConfig
. Por exemplo:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: ["email", "phone"],
);
Também fornecemos um enum LogtoUserScope
embutido no pacote SDK para ajudá-lo a usar os escopos (Scopes) predefinidos.
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: [LogtoUserScope.email.value, LogtoUserScope.phone.value],
);
Reivindicações que precisam de solicitações de rede
Para evitar o inchaço do Token de ID (ID token), algumas reivindicações requerem solicitações de rede para serem buscadas. Por exemplo, a reivindicação custom_data
não está incluída no objeto do usuário, mesmo que seja solicitada nos escopos. Para acessar essas reivindicações, você pode usar o método logtoClient.getUserInfo()
:
class _MyHomePageState extends State<MyHomePage> {
// ...
Widget build(BuildContext context) {
// ...
Widget getUserInfoButton = TextButton(
onPressed: () async {
final userInfo = await logtoClient.getUserInfo();
print(userInfo);
},
child: const Text('Obter informações do usuário'),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('Meu Aplicativo de Demonstração'),
isAuthenticated == true ? signOutButton : signInButton,
isAuthenticated == true ? getUserInfoButton : const SizedBox.shrink(),
],
),
),
);
}
}
Escopos e reivindicações
Logto usa as convenções de escopos e reivindicações do OIDC para definir os escopos e reivindicações para recuperar informações do usuário do Token de ID e do endpoint userinfo do OIDC. Tanto "escopo" quanto "reivindicação" são termos das especificações OAuth 2.0 e OpenID Connect (OIDC).
Aqui está a lista de Escopos (Scopes) suportados e as Reivindicações (Claims) correspondentes:
openid
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
sub | string | O identificador único do usuário | Não |
profile
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
name | string | O nome completo do usuário | Não |
username | string | O nome de usuário do usuário | Não |
picture | string | URL da foto de perfil do Usuário Final. Este URL DEVE referir-se a um arquivo de imagem (por exemplo, um arquivo de imagem PNG, JPEG ou GIF), em vez de uma página da Web contendo uma imagem. Note que este URL DEVE referenciar especificamente uma foto de perfil do Usuário Final adequada para exibição ao descrever o Usuário Final, em vez de uma foto arbitrária tirada pelo Usuário Final. | Não |
created_at | number | Hora em que o Usuário Final foi criado. O tempo é representado como o número de milissegundos desde a época Unix (1970-01-01T00:00:00Z). | Não |
updated_at | number | Hora em que as informações do Usuário Final foram atualizadas pela última vez. O tempo é representado como o número de milissegundos desde a época Unix (1970-01-01T00:00:00Z). | Não |
Outras reivindicações padrão incluem family_name
, given_name
, middle_name
, nickname
, preferred_username
, profile
, website
, gender
, birthdate
, zoneinfo
e locale
também serão incluídas no escopo profile
sem a necessidade de solicitar o endpoint userinfo. Uma diferença em relação às reivindicações acima é que essas reivindicações só serão retornadas quando seus valores não estiverem vazios, enquanto as reivindicações acima retornarão null
se os valores estiverem vazios.
Ao contrário das reivindicações padrão, as reivindicações created_at
e updated_at
estão usando milissegundos em vez de segundos.
email
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
string | O endereço de email do usuário | Não | |
email_verified | boolean | Se o endereço de email foi verificado | Não |
phone
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
phone_number | string | O número de telefone do usuário | Não |
phone_number_verified | boolean | Se o número de telefone foi verificado | Não |
address
Por favor, consulte o OpenID Connect Core 1.0 para os detalhes da reivindicação de endereço.
custom_data
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
custom_data | object | Os dados personalizados do usuário | Sim |
identities
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
identities | object | As identidades vinculadas do usuário | Sim |
sso_identities | array | As identidades SSO vinculadas do usuário | Sim |
urn:logto:scope:organizations
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
organizations | string[] | Os IDs das organizações às quais o usuário pertence | Não |
organization_data | object[] | Os dados das organizações às quais o usuário pertence | Sim |
urn:logto:scope:organization_roles
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
organization_roles | string[] | Os papéis da organização aos quais o usuário pertence com o formato <organization_id>:<role_name> | Não |
Considerando o desempenho e o tamanho dos dados, se "Precisa de userinfo?" for "Sim", isso significa que a reivindicação não aparecerá no Token de ID, mas será retornada na resposta do endpoint userinfo.
Recursos de API e organizações
Recomendamos ler 🔐 Controle de Acesso Baseado em Papel (RBAC) primeiro para entender os conceitos básicos do RBAC do Logto e como configurar corretamente os recursos de API.
Configurar cliente Logto
Depois de configurar os recursos de API, você pode adicioná-los ao configurar o Logto em seu aplicativo:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
// Add your API resources
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
);
Cada recurso de API tem suas próprias permissões (escopos).
Por exemplo, o recurso https://shopping.your-app.com/api
tem as permissões shopping:read
e shopping:write
, e o recurso https://store.your-app.com/api
tem as permissões store:read
e store:write
.
Para solicitar essas permissões, você pode adicioná-las ao configurar o Logto em seu aplicativo:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
// Add your API resources' scopes
scopes: ["shopping:read", "shopping:write", "store:read", "store:write"]
);
Você pode notar que os escopos são definidos separadamente dos recursos de API. Isso ocorre porque Resource Indicators for OAuth 2.0 especifica que os escopos finais para a solicitação serão o produto cartesiano de todos os escopos em todos os serviços de destino.
Assim, no caso acima, os escopos podem ser simplificados a partir da definição no Logto, ambos os recursos de API podem ter escopos read
e write
sem o prefixo. Então, na configuração do Logto:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
// Escopos compartilhados por todos os recursos
scopes: ["read", "write"]
);
Para cada recurso de API, ele solicitará os escopos read
e write
.
Não há problema em solicitar escopos que não estão definidos nos recursos de API. Por exemplo, você pode solicitar o escopo email
mesmo que os recursos de API não tenham o escopo email
disponível. Escopos indisponíveis serão ignorados com segurança.
Após o login bem-sucedido, o Logto emitirá os escopos apropriados para os recursos de API de acordo com os papéis do usuário.
Buscar token de acesso para o recurso de API
Para buscar o token de acesso para um recurso de API específico, você pode usar o método getAccessToken
:
Future<AccessToken?> getAccessToken(String resource) async {
var token = await logtoClient.getAccessToken(resource: resource);
return token;
}
Este método retornará um token de acesso JWT que pode ser usado para acessar o recurso de API quando o usuário tiver as permissões relacionadas. Se o token de acesso em cache atual tiver expirado, este método tentará automaticamente usar um token de atualização para obter um novo token de acesso.
Buscar token de acesso para organizações
Assim como os recursos de API, você também pode solicitar um token de acesso para organizações. Isso é útil quando você precisa acessar recursos que são definidos usando o escopo da organização em vez do escopo do recurso de API.
Se organização é um conceito novo para você, por favor, leia 🏢 Organizações (Multi-tenancy) para começar.
Você precisa adicionar o escopo LogtoUserScope.Organizations
ao configurar o cliente Logto:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: [LogtoUserScopes.organizations.value]
);
Uma vez que o usuário esteja autenticado, você pode buscar o token de organização para o usuário:
// IDs de organizações válidas para o usuário podem ser encontradas na reivindicação do Token de ID `organizations`.
Future<AccessToken?> getOrganizationAccessToken(String organizationId) async {
var token = await logtoClient.getOrganizationToken(organizationId);
return token;
}