Adicionar autenticação ao seu aplicativo Android (Kotlin/Java)
Este guia mostrará como integrar o Logto ao seu aplicativo Android.
- O exemplo é baseado no sistema View e no View Model, mas os conceitos são os mesmos ao usar o Jetpack Compose.
- O exemplo está escrito em Kotlin, mas os conceitos são os mesmos para Java.
- Projetos de exemplo em Kotlin e Java estão disponíveis em nosso repositório SDK.
- O vídeo tutorial está disponível em nosso canal do YouTube.
Pré-requisitos
- Uma conta Logto Cloud ou um Logto auto-hospedado.
- Um aplicativo nativo Logto criado.
- Um projeto de aplicativo Android em Kotlin.
Instalação
O nível mínimo de API Android suportado pelo Logto Android SDK é o nível 24.
Antes de instalar o Logto Android SDK, certifique-se de que mavenCentral()
está adicionado à configuração do seu repositório no arquivo de build do projeto Gradle:
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
Adicione o Logto Android SDK às suas dependências:
- Kotlin
- Groovy
dependencies {
implementation("io.logto.sdk:android:1.1.3")
}
dependencies {
implementation 'io.logto.sdk:android:1.1.3'
}
Como o SDK precisa de acesso à internet, você precisa adicionar a seguinte permissão ao seu arquivo AndroidManifest.xml
:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- adicionar permissão de internet -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- outras configurações... -->
</manifest>
Integração
Inicializar LogtoClient
Crie um LogtoViewModel.kt
e inicie o LogtoClient
neste view model:
//...com outros imports
import io.logto.sdk.android.LogtoClient
import io.logto.sdk.android.type.LogtoConfig
class LogtoViewModel(application: Application) : AndroidViewModel(application) {
private val logtoConfig = LogtoConfig(
endpoint = "<your-logto-endpoint>",
appId = "<your-app-id>",
scopes = null,
resources = null,
usingPersistStorage = true,
)
private val logtoClient = LogtoClient(logtoConfig, application)
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
// Obtenha o objeto Application dos extras
val application = checkNotNull(extras[APPLICATION_KEY])
return LogtoViewModel(application) as T
}
}
}
}
em seguida, crie um LogtoViewModel
para o seu MainActivity.kt
:
//...com outros imports
class MainActivity : AppCompatActivity() {
private val logtoViewModel: LogtoViewModel by viewModels { LogtoViewModel.Factory }
//...outros códigos
}
Configurar URI de redirecionamento
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.
Vamos mudar para a página de detalhes do Aplicativo no Logto Console. Adicione um URI de redirecionamento io.logto.android://io.logto.sample/callback
e clique em "Salvar alterações".
No Android, o URI de redirecionamento segue o padrão: $(LOGTO_REDIRECT_SCHEME)://$(YOUR_APP_PACKAGE)/callback
:
- O
LOGTO_REDIRECT_SCHEME
deve ser um esquema personalizado no formato de domínio reverso. - O
YOUR_APP_PACKAGE
é o nome do pacote do seu aplicativo.
Assumindo que você trate io.logto.android
como o esquema personalizado LOGTO_REDIRECT_SCHEME
, e io.logto.sample
seja o nome do pacote do seu aplicativo, o URI de redirecionamento deve ser io.logto.android://io.logto.sample/callback
.
Implementar login e logout
Antes de chamar logtoClient.signIn
, certifique-se de que você configurou corretamente o URI de redirecionamento no Admin Console.
Você pode usar logtoClient.signIn
para autenticar o usuário e logtoClient.signOut
para desconectar o usuário.
Por exemplo, em um aplicativo Android:
//...com outros imports
class LogtoViewModel(application: Application) : AndroidViewModel(application) {
// ...outros códigos
// Adicione um live data para observar o status de autenticação
private val _authenticated = MutableLiveData(logtoClient.isAuthenticated)
val authenticated: LiveData<Boolean>
get() = _authenticated
fun signIn(context: Activity) {
logtoClient.signIn(context, "io.logto.android://io.logto.sample/callback") { logtoException ->
logtoException?.let { println(it) }
// Atualize o live data
_authenticated.postValue(logtoClient.isAuthenticated)
}
}
fun signOut() {
logtoClient.signOut { logtoException ->
logtoException?.let { println(it) }
// Atualize o live data
_authenticated.postValue(logtoClient.isAuthenticated)
}
}
}
Em seguida, chame os métodos signIn
e signOut
na sua atividade:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//...outros códigos
// Suponha que você tenha um botão com id "sign_in_button" no seu layout
val signInButton = findViewById<Button>(R.id.sign_in_button)
signInButton.setOnClickListener {
logtoViewModel.signIn(this)
}
// Suponha que você tenha um botão com id "sign_out_button" no seu layout
val signOutButton = findViewById<Button>(R.id.sign_out_button)
signOutButton.setOnClickListener {
if (logtoViewModel.authenticated) { // Verifique se o usuário está autenticado
logtoViewModel.signOut()
}
}
// Observe o status de autenticação para atualizar a interface do usuário
logtoViewModel.authenticated.observe(this) { authenticated ->
if (authenticated) {
// O usuário está autenticado
signInButton.visibility = View.GONE
signOutButton.visibility = View.VISIBLE
} else {
// O usuário não está autenticado
signInButton.visibility = View.VISIBLE
signOutButton.visibility = View.GONE
}
}
}
}
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 método logtoClient.getIdTokenClaims()
. Por exemplo, você pode obter informações do usuário em um ViewModel e depois exibi-las em sua atividade:
class LogtoViewModel(application: Application) : AndroidViewModel(application) {
// ...outros códigos
// Adicionar um live data para observar as reivindicações do token de ID
private val _idTokenClaims = MutableLiveData<IdTokenClaims>()
val idTokenClaims: LiveData<IdTokenClaims>
get() = _idTokenClaims
fun getIdTokenClaims() {
logtoClient.getIdTokenClaims { logtoException, idTokenClaims ->
logtoException?.let { _logtoException.postValue(it) } ?: _idTokenClaims.postValue(idTokenClaims)
}
}
}
//...com outros imports
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//...outros códigos
// Assumindo que você tem uma View de texto com id `user_info_text_view` em seu layout
val userInfoResponseTextView: TextView = findViewById(R.id.user_info_text_view)
logtoViewModel.userInfoResponse.observe(this) { userInfoResponse ->
userInfoResponseTextView.text = if (userInfoResponse !== null) {
val json = Gson().toJson(userInfoResponse, UserInfoResponse::class.java)
JSONObject(json).toString(2)
} else {
""
}
}
}
}
Solicitar reivindicações adicionais
Você pode perceber que algumas informações do usuário estão faltando no objeto retornado de logtoClient.getIdTokenClaims()
. 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:
private val logtoConfig = LogtoConfig(
// ...outras configurações
scopes = listOf("email", "phone"), // ou `listOf(UserScope.EMAIL, UserScope.PHONE)`
)
Então você pode acessar as reivindicações adicionais no valor de retorno de logtoClient.getIdTokenClaims()
:
logtoClient.getIdTokenClaims { logtoException, idTokenClaims ->
println("IdTokenClaims:$idTokenClaims")
}
// Agora você pode acessar as reivindicações adicionais `claims.email`, `claims.phone`, etc.
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.fetchUserInfo()
:
logtoClient.fetchUserInfo {_, userInfoResponse ->
println("UserInfoResponse:$userInfoResponse")
}
// Agora você pode acessar a reivindicação `userInfo.custom_data`
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:
val logtoConfig = LogtoConfig(
//...other configs
resources = listOf("https://shopping.your-app.com/api", "https://store.your-app.com/api"), // Adicionar recursos de 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:
val logtoConfig = LogtoConfig(
// ..other configs
scopes = listOf("shopping:read", "shopping:write", "store:read", "store:write"),
resources = listOf("https://shopping.your-app.com/api", "https://store.your-app.com/api"),
)
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:
val logtoConfig = LogtoConfig(
// ...other configs
scopes = listOf("read", "write"),
resources = listOf("https://shopping.your-app.com/api", "https://store.your-app.com/api"),
)
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
:
logtoClient.getAccessToken("https://shopping.your-app.com/api") { logtoException, accessToken ->
logtoException?.let { println(it) }
accessToken?.let { println(it) }
}
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 tokens de organização
Se organização é um conceito novo para você, por favor, leia 🏢 Organizações (Multi-tenancy) para começar.
Você precisa adicionar o escopo UserScope.Organizations
ao configurar o cliente Logto:
val logtoConfig = LogtoConfig(
// ...other configs
scopes = listOf(UserScope.Organizations),
)
Uma vez que o usuário esteja autenticado, você pode buscar o token de organização para o usuário:
// Substitua o parâmetro por um ID de organização válido.
// IDs de organização válidos para o usuário podem ser encontrados na reivindicação do token de ID `organizations`.
logtoClient.getOrganizationToken("organization-id") { logtoException, organizationToken ->
logtoException?.let { println(it) }
organizationToken?.let { println(it) }
}
// ou
logtoClient.getOrganizationTokenClaims("organization-id") { logtoException, claims ->
logtoException?.let { println(it) }
claims?.let { println(it) }
}
Recursos de API da organização (Organization API resources)
Para buscar um token de acesso para um recurso de API em uma organização, você pode usar o método getAccessToken
com o recurso de API e o ID da organização como parâmetros:
logtoClient.getAccessToken(
'https://shopping.your-app.com/api',
organizationId
) { logtoException, accessToken ->
println("AccessToken:$accessToken")
}