Kotlin / Android SDK
com.passkeybase.android
Native Android SDK built on theCredentialManager API, with Jetpack Compose components, coroutine support, and encrypted token storage. Supports Android API 28+ (Android 9).
Installation
Add the SDK to your app module's build.gradle.kts. The SDK depends on the AndroidX Credentials library for passkey operations.
// build.gradle.kts (app module)
dependencies {
implementation("com.passkeybase:android:1.0.0")
// Required peer dependencies
implementation("androidx.credentials:credentials:1.3.0")
implementation("androidx.credentials:credentials-play-services-auth:1.3.0")
}
// Also add the Maven repository in settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://maven.passkeybase.com") }
}
}Requirements: Android API 28+ (Android 9), Kotlin 1.9+, and Google Play Services for the CredentialManager API. You must also configure yourassetlinks.jsonfor the Digital Asset Links association.
Quick Start with Jetpack Compose
Initialize the client in your Activity and wrap your Compose tree in aPasskeyBaseProvider.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.passkeybase.android.PasskeyBaseClient
import com.passkeybase.android.compose.PasskeyBaseProvider
class MainActivity : ComponentActivity() {
private val passkeyBase = PasskeyBaseClient(
clientId = "pk_live_abc123",
domain = "myapp.auth.ing"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PasskeyBaseProvider(client = passkeyBase) {
MyApp()
}
}
}
}Access authentication state anywhere in the Compose tree usingLocalAuthState.
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.passkeybase.android.compose.LocalAuthState
@Composable
fun MyApp() {
val authState by LocalAuthState.current.collectAsState()
when {
authState.isLoading -> {
LoadingScreen()
}
authState.isAuthenticated -> {
HomeScreen(
user = authState.user!!,
onLogout = { authState.logout() }
)
}
else -> {
LoginScreen()
}
}
}Native Passkey Registration
Register a new passkey using Android'sCredentialManager API. This presents the system dialog for biometric verification and passkey creation.
import android.content.Context
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CredentialManager
import com.passkeybase.android.PasskeyBaseClient
class PasskeyRegistrationManager(
private val context: Context,
private val client: PasskeyBaseClient
) {
private val credentialManager = CredentialManager.create(context)
suspend fun register(displayName: String) {
// 1. Get registration options from PasskeyBase
val options = client.getRegistrationOptions(
displayName = displayName
)
// 2. Create the credential request
val request = CreatePublicKeyCredentialRequest(
requestJson = options.toJson()
)
// 3. Present the system dialog
val result = credentialManager.createCredential(
context = context as android.app.Activity,
request = request
)
// 4. Send the attestation to PasskeyBase
client.completeRegistration(
credential = result
)
}
}Digital Asset Links: You must host an assetlinks.jsonfile at https://myapp.auth.ing/.well-known/assetlinks.jsonthat includes your app's SHA-256 certificate fingerprint. This is configured automatically in the dashboard when you add your Android app.
Native Passkey Authentication
Authenticate a user with an existing passkey. The system will prompt for biometric verification and return the signed assertion.
import android.content.Context
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetPublicKeyCredentialOption
import com.passkeybase.android.PasskeyBaseClient
import com.passkeybase.android.AuthResult
class PasskeyAuthManager(
private val context: Context,
private val client: PasskeyBaseClient
) {
private val credentialManager = CredentialManager.create(context)
suspend fun authenticate(): AuthResult {
// 1. Get authentication options from PasskeyBase
val options = client.getAuthenticationOptions()
// 2. Build the credential request
val publicKeyOption = GetPublicKeyCredentialOption(
requestJson = options.toJson()
)
val request = GetCredentialRequest(
listOf(publicKeyOption)
)
// 3. Present the system dialog
val result = credentialManager.getCredential(
context = context as android.app.Activity,
request = request
)
// 4. Verify the assertion with PasskeyBase
return client.completeAuthentication(
credential = result.credential
)
}
}OAuth 2.0 Login
For browser-based login, use Chrome Custom Tabs to open the PasskeyBase hosted login page. This provides a seamless in-app browser experience with PKCE support.
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import com.passkeybase.android.PasskeyBaseClient
import com.passkeybase.android.AuthResult
class OAuthLoginManager(
private val context: Context,
private val client: PasskeyBaseClient
) {
fun launchLogin() {
// Build the authorization URL with PKCE
val (authUrl, codeVerifier) = client.buildAuthorizationUrl(
redirectUri = "myapp://callback",
scopes = listOf("openid", "profile")
)
// Store the code verifier for the callback
client.storeCodeVerifier(codeVerifier)
// Open in Custom Tabs for a seamless experience
val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
customTabsIntent.launchUrl(context, Uri.parse(authUrl))
}
// Call this from your Activity that handles the callback URI
suspend fun handleCallback(callbackUri: Uri): AuthResult {
return client.handleCallback(
uri = callbackUri
)
}
}
// In your AndroidManifest.xml, register the callback Activity:
// <activity android:name=".CallbackActivity"
// android:exported="true">
// <intent-filter>
// <action android:name="android.intent.action.VIEW" />
// <category android:name="android.intent.category.DEFAULT" />
// <category android:name="android.intent.category.BROWSABLE" />
// <data android:scheme="myapp" android:host="callback" />
// </intent-filter>
// </activity>Token Storage (EncryptedSharedPreferences)
The SDK stores tokens usingEncryptedSharedPreferencesfrom the AndroidX Security library. Tokens are encrypted with AES-256-GCM using keys stored in the Android Keystore.
import com.passkeybase.android.storage.SecureTokenStore
// The SDK uses EncryptedSharedPreferences by default.
// Tokens are encrypted with AES-256-GCM using Android Keystore.
// Access the default token store
val tokenStore = SecureTokenStore(context)
// Store tokens after authentication
tokenStore.save(
accessToken = result.accessToken,
idToken = result.idToken,
refreshToken = result.refreshToken,
expiresAt = result.expiresAt
)
// Retrieve the current access token
val accessToken = tokenStore.getAccessToken()
if (accessToken != null) {
// Use with OkHttp, Retrofit, Ktor, etc.
val request = Request.Builder()
.url(apiUrl)
.addHeader("Authorization", "Bearer $accessToken")
.build()
}
// Check if tokens are still valid
val isValid = tokenStore.hasValidTokens()
// Clear all stored tokens on logout
tokenStore.clear()Management API
The Management API lets you manage users, passkeys, and OAuth clients programmatically. All methods are suspend functions compatible with Kotlin coroutines.
Security: The Management API key should only be used server-side or in trusted environments. Never ship the API key in your production Android app.
import com.passkeybase.android.management.PasskeyBaseManagement
val management = PasskeyBaseManagement(
domain = "myapp.auth.ing",
apiKey = "mgmt_key_abc123" // Keep this server-side!
)
// List users
val users = management.listUsers(page = 1, perPage = 20)
users.data.forEach { user ->
println("${user.displayName} - ${user.passkeyCount} passkeys")
}
// Get a specific user
val user = management.getUser(id = "usr_abc123")
// Delete a user and all their passkeys
management.deleteUser(id = "usr_abc123")
// List passkeys for a user
val passkeys = management.listPasskeys(userId = "usr_abc123")
// Delete a specific passkey
management.deletePasskey(
userId = "usr_abc123",
passkeyId = "pk_xyz789"
)
// List OAuth clients
val clients = management.listClients()
// Create a new OAuth client
val newClient = management.createClient(
name = "My Android App",
redirectUris = listOf("myapp://callback")
)Enrollment Tokens
Enroll passkeys for users who are already authenticated through your existing auth system. Your backend creates an enrollment token via theManagement API, and the Android SDK uses it to register a passkey on the device.
import com.passkeybase.android.PasskeyBaseClient
import com.passkeybase.android.enrollment.PasskeyEnrollmentManager
// 1. Request an enrollment token from your backend
val response = httpClient.post("/api/enrollment-token") {
header("Authorization", "Bearer $sessionToken")
}
val enrollToken = response.body<EnrollmentResponse>().token
// 2. Enroll a passkey using the token
val enrollmentManager = PasskeyEnrollmentManager(
context = activity,
client = passkeyBase
)
enrollmentManager.enrollPasskey(
token = enrollToken,
displayName = "Pixel 8 Pro"
)
// The user's device passkey is now linked to their account.
// Future logins will recognize this passkey.Compose Components
The SDK includes pre-built Jetpack Compose components for common authentication patterns.
import androidx.compose.runtime.Composable
import com.passkeybase.android.compose.*
// Ready-made Compose components
@Composable
fun LoginScreen() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// One-tap passkey login button
PasskeyLoginButton(
onSuccess = { result ->
// Navigate to home
},
onError = { error ->
// Show error
}
)
Spacer(modifier = Modifier.height(16.dp))
// OAuth login button (opens Custom Tabs)
OAuthLoginButton(
text = "Sign in with PasskeyBase",
redirectUri = "myapp://callback",
onSuccess = { result ->
// Navigate to home
}
)
}
}
// Auth-gated content
@Composable
fun ProtectedScreen() {
RequireAuth(
fallback = { LoginScreen() }
) { user ->
// Only rendered when authenticated
Text("Welcome, ${user.displayName}")
}
}PasskeyLoginButton
One-tap passkey authentication button. Handles the full CredentialManager flow and calls your success/error callbacks.
OAuthLoginButton
Opens the PasskeyBase login page in Custom Tabs. Handles PKCE, callback, and token exchange automatically.
RequireAuth
Composable wrapper that only renders its content when the user is authenticated. Shows a fallback (e.g., login screen) otherwise.
Error Handling
The SDK defines a sealed classPasskeyBaseExceptionwith typed subclasses for every error condition.
import com.passkeybase.android.PasskeyBaseException
try {
val result = authManager.authenticate()
handleSuccess(result)
} catch (e: PasskeyBaseException.NetworkError) {
// Connection failed, timeout, etc.
showSnackbar("Network error: ${e.message}")
} catch (e: PasskeyBaseException.ServerError) {
// Server returned an error (4xx, 5xx)
showSnackbar("Server error ${e.status}: ${e.message}")
} catch (e: PasskeyBaseException.UserCancelled) {
// User dismissed the credential picker
// No action needed
} catch (e: PasskeyBaseException.CredentialNotFound) {
// No matching passkey on this device
showSnackbar("No passkey found. Please register first.")
} catch (e: PasskeyBaseException.TokenExpired) {
// Access token or enrollment token expired
refreshTokens()
} catch (e: PasskeyBaseException.InvalidState) {
// OAuth state mismatch (possible CSRF)
showSnackbar("Authentication failed. Please try again.")
} catch (e: PasskeyBaseException.ProviderNotAvailable) {
// Google Play Services not available
showSnackbar("Please update Google Play Services.")
} catch (e: Exception) {
showSnackbar("Unexpected error: ${e.message}")
}| Exception | Description |
|---|---|
NetworkError | Connection failed, DNS resolution error, or timeout. |
ServerError | Server returned an HTTP error (4xx or 5xx). |
UserCancelled | User dismissed the credential picker dialog. |
CredentialNotFound | No matching passkey found on the device. |
TokenExpired | Access token or enrollment token has expired. |
InvalidState | OAuth state parameter mismatch (potential CSRF). |
ProviderNotAvailable | Google Play Services not available or outdated. |
Next steps
- OAuth Flow -- understand the authentication flow in detail
- Enrollment -- add passkeys to existing auth systems
- API Reference -- full Management API documentation
- Swift / iOS SDK -- building for both platforms?
- Dart / Flutter SDK -- cross-platform mobile alternative