跳至主要內容

為你的 Android (Kotlin/Java) 應用程式新增驗證 (Authentication)

本指南將向你展示如何將 Logto 整合到你的 Android 應用程式中。

提示:

先決條件​

  • 一個 Logto Cloud 帳戶或 自託管 Logto。
  • 已建立的 Logto 原生應用程式。
  • 一個 Kotlin Android 應用程式專案。

安裝​

備註:

Logto Android SDK 支援的最低 Android API 等級為 24。

Logto Android SDK 有兩個主要版本:

本指南涵蓋兩個版本。在下方的標籤頁中選擇你的版本,此選擇將在整個指南中保持同步。

在安裝 Logto Android SDK 之前,請確保在 Gradle 專案的建置檔案中將 mavenCentral() 添加到你的倉庫配置中:

settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}

將 Logto Android SDK 添加到你的相依項目中:

v3 以 3.0.0-beta 預發行版本形式發布直至正式版。請使用最新的預發行版本作為版本號:

build.gradle.kts
dependencies {
implementation("io.logto.sdk:android:3.0.0-beta")
}

由於 SDK 需要網路存取,你需要在 AndroidManifest.xml 檔案中添加以下權限:

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

<!-- 添加網路權限 -->
<uses-permission android:name="android.permission.INTERNET" />

<!-- 其他配置... -->
</manifest>

整合​

初始化 LogtoClient​

建立一個 LogtoViewModel.kt 並在此 ViewModel 中初始化 LogtoClient:

LogtoViewModel.kt
//...with other 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 {
// 從 extras 中獲取 Application 物件
val application = checkNotNull(extras[APPLICATION_KEY])
return LogtoViewModel(application) as T
}
}
}
}

接著,為你的 MainActivity.kt 建立一個 LogtoViewModel:

MainActivity.kt
//...with other imports
class MainActivity : AppCompatActivity() {
private val logtoViewModel: LogtoViewModel by viewModels { LogtoViewModel.Factory }
//...其他程式碼
}

配置重定向 URI​

在進入細節之前,這裡先快速說明一下終端使用者的體驗。登入流程可簡化如下:

  1. 你的應用程式呼叫登入方法。
  2. 使用者被重新導向至 Logto 登入頁面。對於原生應用程式,會開啟系統瀏覽器。
  3. 使用者登入後,會被重新導向回你的應用程式(設定為 redirect URI)。

關於基於重導的登入​

  1. 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
  2. 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。

欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析。


讓我們切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI io.logto.android://io.logto.sample/callback,然後點擊「儲存變更」。

Logto Console 中的重定向 URI

在 Android 中,重定向 URI 遵循以下模式:$(LOGTO_REDIRECT_SCHEME)://$(YOUR_APP_PACKAGE)/callback:

  • LOGTO_REDIRECT_SCHEME 應為反向域格式的自定義方案。
  • YOUR_APP_PACKAGE 是你的應用程式包名。

假設你將 io.logto.android 作為自定義 LOGTO_REDIRECT_SCHEME,而 io.logto.sample 是你的應用程式包名,則重定向 URI 應為 io.logto.android://io.logto.sample/callback。

在 v3 中,登入體驗在 Custom Tab(系統瀏覽器)中開啟,重定向透過 OS 層級的 intent filter 路由回你的應用程式。你需要在應用程式的建置檔案中使用 logtoRedirectScheme manifest placeholder 宣告重定向 URI 的 scheme:

build.gradle.kts
android {
defaultConfig {
manifestPlaceholders["logtoRedirectScheme"] = "io.logto.android"
}
}

此外,v3 透過 Android 的 intent filter 比對來強制執行重定向 URI 模式,因此偏離模式的重定向 URI 永遠不會被傳遞到你的應用程式:

  • scheme 必須等於 logtoRedirectScheme manifest placeholder。
  • host 必須是你的 applicationId。
  • path 必須是 /callback。

請將 scheme 和 host 保持小寫,因為 intent filter 比對區分大小寫,而瀏覽器會將 scheme 轉為小寫。

要使用 Android App Links(你擁有的網域上的 https 重定向 URI)代替自定義 scheme:

  1. 在 https://your.domain/.well-known/assetlinks.json 托管 Digital Asset Links 檔案,宣告你的應用程式 ID 和簽名憑證的 SHA-256 指紋。使用 Play App Signing 發布時,可以在 Play Console 的設定 > 應用程式簽名下找到發布指紋。該檔案必須以 Content-Type: application/json 和 HTTP 200(無重定向)提供。

  2. 在 AndroidManifest.xml 檔案中,為 SDK 的重定向接收者 activity io.logto.sdk.android.auth.logto.LogtoRedirectReceiverActivity 宣告 App Links intent filter。如果你完全不使用自定義 scheme,可以使用 tools:node="removeAll" 移除 SDK 的內建 filter,此時就不再需要 logtoRedirectScheme manifest placeholder:

    AndroidManifest.xml
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application>
    <activity android:name="io.logto.sdk.android.auth.logto.LogtoRedirectReceiverActivity">
    <!-- 省略此行以同時保留自定義 scheme 重定向。 -->
    <intent-filter tools:node="removeAll" />
    <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="your.domain" android:path="/callback" />
    </intent-filter>
    </activity>
    </application>
    </manifest>
  3. 在 Logto Console 的應用程式詳細資料頁面中,將 https://your.domain/callback 新增為重定向 URI(如果用於登出,也新增為登出後重定向 URI),並將其傳遞給 signIn / signOut。

請注意,callback 現在是你網域上的真實 URL,因此請在那裡提供一個備用頁面(例如,「返回應用程式」按鈕),供不會在伺服器重定向時啟動 App Links 的瀏覽器使用。按鈕只需連結到當前 URL(例如,將 href 設為 window.location.href):授權參數在查詢字串中,使用者點擊可讓相同的 URL 再次嘗試路由到你的應用程式。在 Android 12+ 上,未驗證的網域永遠不會開啟應用程式,因此損壞的 assetlinks.json 會靜默失敗。你可以使用 adb shell pm get-app-links <applicationId> 檢查驗證狀態。

實作登入與登出​

備註:

在呼叫 logtoClient.signIn 之前,請確保已在管理控制台中正確配置了 Redirect URI。

你可以使用 logtoClient.signIn 來讓使用者登入,並使用 logtoClient.signOut 來讓使用者登出。

在 v3 中,logtoClient.signOut 執行完整的登出:清除本地憑證、撤銷重新整理權杖,並透過在瀏覽器中開啟結束工作階段端點來結束 Logto 工作階段。瀏覽器然後透過登出後重新導向 URI 導航回你的應用程式。在使用之前,切換到 Logto Console 的應用程式詳細資料頁面,新增登出後重新導向 URI io.logto.android://io.logto.sample/callback 並點擊「儲存變更」。登出後重新導向 URI 遵循與重新導向 URI 相同的模式,其 scheme 也必須與 logtoRedirectScheme manifest placeholder 匹配。

例如,在 Android 應用程式中:

LogtoModelView.kt
//...with other imports
class LogtoViewModel(application: Application) : AndroidViewModel(application) {
// ...other codes

// 新增一個 live data 來觀察驗證 (Authentication) 狀態
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) }
// 更新 live data
_authenticated.postValue(logtoClient.isAuthenticated)
}
}

fun signOut(context: Activity) {
logtoClient.signOut(context, "io.logto.android://io.logto.sample/callback") { logtoException ->
logtoException?.let { println(it) }
// 更新 live data
_authenticated.postValue(logtoClient.isAuthenticated)
}
}
}

然後在你的 activity 中呼叫 signIn 和 signOut 方法:

MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//...other codes

// 假設你的佈局中有一個 id 為 "sign_in_button" 的按鈕
val signInButton = findViewById<Button>(R.id.sign_in_button)
signInButton.setOnClickListener {
logtoViewModel.signIn(this)
}

// 假設你的佈局中有一個 id 為 "sign_out_button" 的按鈕
val signOutButton = findViewById<Button>(R.id.sign_out_button)
signOutButton.setOnClickListener {
if (logtoViewModel.authenticated) { // 檢查使用者是否已驗證 (Authenticated)
logtoViewModel.signOut(this)
}
}

// 觀察驗證 (Authentication) 狀態以更新 UI
logtoViewModel.authenticated.observe(this) { authenticated ->
if (authenticated) {
// 使用者已驗證 (Authenticated)
signInButton.visibility = View.GONE
signOutButton.visibility = View.VISIBLE
} else {
// 使用者未驗證 (Authenticated)
signInButton.visibility = View.VISIBLE
signOutButton.visibility = View.GONE
}
}

}
}
備註:
  • 你也可以不帶登出後重新導向 URI 呼叫 logtoClient.signOut(context)。這種情況下不需要 Console 配置:瀏覽器會顯示 Logto 登出頁面,使用者透過手動關閉返回應用程式。
  • 如果沒有 UI context 可用,你可以呼叫 logtoClient.clearCredentials 來清除本地憑證並撤銷重新整理權杖。請注意,這會保留瀏覽器中的 Logto 工作階段,因此下次 signIn 可能會透過該工作階段靜默登入使用者。

檢查點:測試你的應用程式​

現在,你可以測試你的應用程式:

  1. 執行你的應用程式,你會看到登入按鈕。
  2. 點擊登入按鈕,SDK 會初始化登入流程並將你重定向到 Logto 登入頁面。
  3. 登入後,你將被重定向回應用程式並看到登出按鈕。
  4. 點擊登出按鈕以清除權杖存儲並登出。

獲取使用者資訊​

顯示使用者資訊​

要顯示使用者的資訊,你可以使用 logtoClient.getIdTokenClaims() 方法。例如,你可以在 ViewModel 中獲取使用者資訊,然後在你的活動中顯示:

LogtoModelView.kt
class LogtoViewModel(application: Application) : AndroidViewModel(application) {
// ...其他代碼

// 添加一個 live data 來觀察 ID 權杖 (ID token) 宣告 (Claims)
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)
}
}
}
MainActivity.kt
//...與其他匯入
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//...其他代碼

// 假設你的佈局中有一個 ID 為 `user_info_text_view` 的文字視圖
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 {
""
}
}
}
}

請求額外的宣告 (Claims)​

你可能會發現從 logtoClient.getIdTokenClaims() 返回的物件中缺少一些使用者資訊。這是因為 OAuth 2.0 和 OpenID Connect (OIDC) 的設計遵循最小權限原則 (PoLP, Principle of Least Privilege),而 Logto 是基於這些標準構建的。

預設情況下,僅返回有限的宣告 (Claims)。如果你需要更多資訊,可以請求額外的權限範圍 (Scopes) 以存取更多宣告。

資訊:

「宣告 (Claim)」是對主體所做的斷言;「權限範圍 (Scope)」是一組宣告。在目前的情況下,宣告是關於使用者的一部分資訊。

以下是權限範圍與宣告關係的非規範性範例:

提示:

「sub」宣告表示「主體 (Subject)」,即使用者的唯一識別符(例如使用者 ID)。

Logto SDK 將始終請求三個權限範圍:openid、profile 和 offline_access。

要請求額外的權限範圍 (Scopes),你可以將權限範圍傳遞給 LogtoConfig 物件。例如:

LogtoViewModel.kt
private val logtoConfig = LogtoConfig(
// ...其他配置
scopes = listOf("email", "phone"), // 或 `listOf(UserScope.EMAIL, UserScope.PHONE)`
)

然後你可以在 logtoClient.getIdTokenClaims() 的返回值中訪問額外的宣告 (Claims):

logtoClient.getIdTokenClaims { logtoException, idTokenClaims ->
println("IdTokenClaims:$idTokenClaims")
}
// 現在你可以訪問額外的宣告 `claims.email`、`claims.phone` 等。

需要網路請求的宣告 (Claims)​

為了防止 ID 權杖 (ID token) 膨脹,某些宣告 (Claims) 需要透過網路請求來獲取。例如,即使在權限範圍 (Scopes) 中請求了 custom_data 宣告,它也不會包含在使用者物件中。要存取這些宣告,你可以使用 logtoClient.fetchUserInfo() 方法:

LogtoViewModel.kt
logtoClient.fetchUserInfo {_, userInfoResponse ->
println("UserInfoResponse:$userInfoResponse")
}
// 現在你可以訪問宣告 `userInfo.custom_data`
此方法將透過請求 userinfo 端點來獲取使用者資訊。要了解更多可用的權限範圍 (Scopes) 和宣告 (Claims),請參閱 權限範圍 (Scopes) 和宣告 (Claims) 部分。

權限範圍 (Scopes) 與宣告 (Claims)​

Logto 採用 OIDC 權限範圍 (Scopes) 與宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 及 OIDC userinfo 端點 取得使用者資訊時的權限範圍與宣告。無論「權限範圍 (Scope)」還是「宣告 (Claim)」,皆為 OAuth 2.0 與 OpenID Connect (OIDC) 規範中的術語。

對於標準 OIDC 宣告 (Claims),其是否包含於 ID 權杖 (ID token) 內,完全取決於所請求的權限範圍 (Scopes)。擴充宣告(如 custom_data 與 organizations)則可透過 自訂 ID 權杖 (Custom ID token) 設定,額外配置於 ID 權杖中。

以下是支援的權限範圍 (Scopes) 及對應的宣告 (Claims) 清單:

標準 OIDC 權限範圍 (Scopes)​

openid(預設)

Claim nameTypeDescription
substring使用者的唯一識別符 (The unique identifier of the user)

profile(預設)

Claim nameTypeDescription
namestring使用者全名 (The full name of the user)
usernamestring使用者名稱 (The username of the user)
picturestring終端使用者大頭貼的 URL。此 URL 必須指向圖片檔案(如 PNG、JPEG 或 GIF),而非包含圖片的網頁。請注意,此 URL 應明確指向適合描述終端使用者的個人照片,而非任意由終端使用者拍攝的照片。(URL of the End-User's profile picture. This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), rather than to a Web page containing an image. Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User.)
created_atnumber終端使用者建立時間。以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。(Time the End-User was created. The time is represented as the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).)
updated_atnumber終端使用者資訊最後更新時間。以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。(Time the End-User's information was last updated. The time is represented as the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).)

其他 標準宣告 (Standard claims) 包含 family_name、given_name、middle_name、nickname、preferred_username、profile、website、gender、birthdate、zoneinfo 及 locale 也會包含在 profile 權限範圍內,無需額外請求 userinfo endpoint。與上表宣告不同的是,這些宣告僅在其值不為空時才會回傳,而上表宣告若值為空則會回傳 null。

備註:

與標準宣告不同,created_at 與 updated_at 宣告使用毫秒而非秒為單位。

email

Claim nameTypeDescription
emailstring使用者的電子郵件地址 (The email address of the user)
email_verifiedboolean電子郵件地址是否已驗證 (Whether the email address has been verified)

phone

Claim nameTypeDescription
phone_numberstring使用者的電話號碼 (The phone number of the user)
phone_number_verifiedboolean電話號碼是否已驗證 (Whether the phone number has been verified)

address

請參閱 OpenID Connect Core 1.0 以瞭解 address 宣告的詳細資訊。

資訊:

標註為 (預設) 的權限範圍 (Scopes) 會由 Logto SDK 自動請求。當請求對應權限範圍時,標準 OIDC 權限範圍下的宣告 (Claims) 會始終包含於 ID 權杖 (ID token) 中,且無法關閉。

擴充權限範圍 (Extended scopes)​

以下權限範圍由 Logto 擴充,會透過 userinfo endpoint 回傳宣告 (Claims)。這些宣告也可透過 Console > Custom JWT 設定直接包含於 ID 權杖 (ID token) 中。詳情請參閱 自訂 ID 權杖 (Custom ID token)。

custom_data

Claim nameTypeDescriptionIncluded in ID token by default
custom_dataobject使用者的自訂資料 (The custom data of the user)

identities

Claim nameTypeDescriptionIncluded in ID token by default
identitiesobject使用者的連結身分 (The linked identities of the user)
sso_identitiesarray使用者的連結 SSO 身分 (The linked SSO identities of the user)

roles

Claim nameTypeDescriptionIncluded in ID token by default
rolesstring[]使用者的角色 (The roles of the user)✅

urn:logto:scope:organizations

Claim nameTypeDescriptionIncluded in ID token by default
organizationsstring[]使用者所屬的組織 ID (The organization IDs the user belongs to)✅
organization_dataobject[]使用者所屬的組織資料 (The organization data the user belongs to)
備註:

這些組織宣告 (Organization claims) 也可在使用 不透明權杖 (Opaque token) 時,透過 userinfo endpoint 取得。然而,不透明權杖無法作為組織權杖 (Organization tokens) 來存取組織專屬資源。詳見 不透明權杖與組織 (Opaque token and organizations)。

urn:logto:scope:organization_roles

Claim nameTypeDescriptionIncluded in ID token by default
organization_rolesstring[]使用者所屬組織角色,格式為 <organization_id>:<role_name> (The organization roles the user belongs to with the format of <organization_id>:<role_name>)✅

API 資源與組織​

我們建議先閱讀 🔐 角色型存取控制 (RBAC, Role-Based Access Control),以瞭解 Logto RBAC 的基本概念以及如何正確設定 API 資源。

配置 Logto 客戶端​

一旦你設定了 API 資源,就可以在應用程式中配置 Logto 時新增它們:

LogtoViewModel.kt
val logtoConfig = LogtoConfig(
//...other configs
resources = listOf("https://shopping.your-app.com/api", "https://store.your-app.com/api"), // 新增 API 資源 (API resources)
)

每個 API 資源都有其自身的權限(權限範圍)。

例如,https://shopping.your-app.com/api 資源具有 shopping:read 和 shopping:write 權限,而 https://store.your-app.com/api 資源具有 store:read 和 store:write 權限。

要請求這些權限,你可以在應用程式中配置 Logto 時新增它們:

LogtoViewModel.kt
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"),
)

你可能會注意到權限範圍是獨立於 API 資源定義的。這是因為 OAuth 2.0 的資源標示符 (Resource Indicators) 指定請求的最終權限範圍將是所有目標服務中所有權限範圍的笛卡兒積。

因此,在上述情況中,權限範圍可以從 Logto 的定義中簡化,兩個 API 資源都可以擁有 read 和 write 權限範圍而不需要前綴。然後,在 Logto 配置中:

LogtoViewModel.kt
val logtoConfig = LogtoConfig(
// ...other configs
scopes = listOf("read", "write"),
resources = listOf("https://shopping.your-app.com/api", "https://store.your-app.com/api"),
)

對於每個 API 資源,它將請求 read 和 write 權限範圍。

備註:

請求未在 API 資源中定義的權限範圍是可以的。例如,即使 API 資源中沒有可用的 email 權限範圍,你也可以請求 email 權限範圍。不可用的權限範圍將被安全地忽略。

成功登入後,Logto 將根據使用者的角色向 API 資源發出適當的權限範圍。

獲取 API 資源的存取權杖 (Access token)​

要獲取特定 API 資源的存取權杖 (Access token),你可以使用 getAccessToken 方法:

LogtoViewModel.kt
logtoClient.getAccessToken("https://shopping.your-app.com/api") { logtoException, accessToken ->
logtoException?.let { println(it) }
accessToken?.let { println(it) }
}

此方法將返回一個 JWT 存取權杖 (Access token),當使用者擁有相關權限時,可以用來存取 API 資源。如果當前快取的存取權杖 (Access token) 已過期,此方法將自動嘗試使用重新整理權杖 (Refresh token) 獲取新的存取權杖 (Access token)。

獲取組織權杖 (Organization tokens)​

如果你對組織 (Organization) 不熟悉,請閱讀 🏢 組織(多租戶,Multi-tenancy) 以開始了解。

在配置 Logto client 時,你需要新增 UserScope.Organizations 權限範圍 (scope):

LogtoViewModel.kt
val logtoConfig = LogtoConfig(
// ...other configs
scopes = listOf(UserScope.Organizations),
)

使用者登入後,你可以為使用者獲取組織權杖 (organization token):

LogtoViewModel.kt
// 將參數替換為有效的組織 ID。
// 使用者的有效組織 ID 可以在 ID 權杖 (ID token) 宣告 (claim) `organizations` 中找到。
logtoClient.getOrganizationToken("organization-id") { logtoException, organizationToken ->
logtoException?.let { println(it) }
organizationToken?.let { println(it) }
}

// 或
logtoClient.getOrganizationTokenClaims("organization-id") { logtoException, claims ->
logtoException?.let { println(it) }
claims?.let { println(it) }
}

組織 API 資源 (Organization API resources)​

要為組織中的 API 資源取得存取權杖 (Access token),可以使用 getAccessToken 方法,並將 API 資源和組織 ID 作為參數:

LogtoViewModel.kt
logtoClient.getAccessToken(
'https://shopping.your-app.com/api',
organizationId
) { logtoException, accessToken ->
println("AccessToken:$accessToken")
}

進一步閱讀​

終端使用者流程:驗證流程、帳號流程與組織流程 (End-user flows: authentication flows, account flows, and organization flows) 設定連接器 (Configure connectors) 授權 (Authorization)