PasskeyBase
K

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).

← All SDKs


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
// 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.

MainActivity.kt
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.

MyApp.kt
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.

PasskeyRegistrationManager.kt
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.

PasskeyAuthManager.kt
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.

OAuthLoginManager.kt
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.

Token storage
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.

Management API
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.

Enrollment
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.

Compose components
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.

Error handling
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}")
}
ExceptionDescription
NetworkErrorConnection failed, DNS resolution error, or timeout.
ServerErrorServer returned an HTTP error (4xx or 5xx).
UserCancelledUser dismissed the credential picker dialog.
CredentialNotFoundNo matching passkey found on the device.
TokenExpiredAccess token or enrollment token has expired.
InvalidStateOAuth state parameter mismatch (potential CSRF).
ProviderNotAvailableGoogle Play Services not available or outdated.

Next steps