Skip to main content
Passwordless authentication with biometrics or device credentials.

Prerequisites

  • SDK initialized
  • Domain you control (for .well-known files)
  • App package name and SHA-256 certificate fingerprint
  • Android SDK 28+

App Configuration

There are a few steps to configure passkey support. If you have already set up an associated domain for your application, you can skip to Managing Passkeys.

Set Up an Associated Domain

On your webserver, set up this route:
GET https://{{yourdomain}}/.well-known/assetlinks.json
This route should serve a static JSON array containing your app’s package name and SHA-256 certificate fingerprint:
.well-known/assetlinks.json
[{
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.yourapp",
    "sha256_cert_fingerprints": [
      "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
    ]
  }
}]
Replace:
  • com.example.yourapp with your app’s package name
  • The SHA-256 fingerprint with your app’s signing certificate fingerprint

Get Your SHA-256 Certificate Fingerprint

To get your app’s SHA-256 certificate fingerprint:
# For debug keystore
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

# For release keystore
keytool -list -v -keystore /path/to/your/release.keystore -alias your-key-alias
Look for the SHA256 line in the output and copy the fingerprint.
You can validate your asset links configuration using the Google Digital Asset Links Tool. This tool tests your domain against Android’s requirements and helps ensure your passkey setup is properly configured.

SDK Configuration

Configure the appOrigin in your SDK initialization to associate passkeys with your domain:
MainActivity.kt
import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.core.ClientProps

val props = ClientProps(
    environmentId = "your-environment-id",
    appName = "Your App",
    appOrigin = "https://yourdomain.com"  // Replace with your actual domain
)
DynamicSDK.initialize(props, applicationContext, this)
The appOrigin must match your website domain exactly (e.g., https://example.com). This domain will be used to create and associate passkeys, so ensure it’s the same domain where you host the .well-known/assetlinks.json endpoint.

Managing Passkeys

Now that your application has correctly associated your domain, you can register and authenticate using passkeys.

Sign In with Passkey

Use passkeys as the primary authentication method:
import com.dynamic.sdk.android.DynamicSDK

val sdk = DynamicSDK.getInstance()

fun signInWithPasskey() {
    try {
        sdk.auth.passkey.signIn()
        println("Signed in with passkey!")
    } catch (e: Exception) {
        println("Passkey sign-in failed: ${e.message}")
    }
}
When called, this triggers the system passkey prompt where users authenticate with biometrics, device PIN, or pattern.

Register passkey

Register a new passkey for the authenticated user:
sdk.passkeys.registerPasskey()
This will trigger the device’s biometric authentication flow (fingerprint, face recognition, or device PIN).

Get user passkeys

Retrieve all passkeys registered for the current user:
import com.dynamic.sdk.android.Models.UserPasskey

val passkeys: List<UserPasskey> = sdk.passkeys.getPasskeys()

passkeys.forEach { passkey ->
    println("Passkey ID: ${passkey.id}")
    println("Created: ${passkey.createdAt}")
    println("Last used: ${passkey.lastUsedAt}")
    println("Is default: ${passkey.isDefault}")
}

Authenticate with passkey for MFA

Use a passkey to authenticate for MFA purposes:
import com.dynamic.sdk.android.Models.MfaCreateToken

val response = sdk.passkeys.authenticatePasskeyMFA(
    createMfaToken = MfaCreateToken(singleUse = true),
    relatedOriginRpId = null
)

println("Verified: ${response.verified}")

Delete passkey

Remove a passkey from the user’s account:
import com.dynamic.sdk.android.Models.DeletePasskeyRequest

val passkeyId = "passkey-id"
sdk.passkeys.deletePasskey(DeletePasskeyRequest(passkeyId = passkeyId))

Complete passkey management example

Here’s a complete example of passkey management in a ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.UserPasskey
import com.dynamic.sdk.android.Models.DeletePasskeyRequest
import com.dynamic.sdk.android.Models.MfaCreateToken
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class PasskeysViewModel(private val sdk: DynamicSDK) : ViewModel() {
    private val _passkeys = MutableStateFlow<List<UserPasskey>>(emptyList())
    val passkeys: StateFlow<List<UserPasskey>> = _passkeys

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading

    private val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error

    init {
        loadPasskeys()
    }

    fun loadPasskeys() {
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null
            try {
                _passkeys.value = sdk.passkeys.getPasskeys()
            } catch (e: Exception) {
                _error.value = "Failed to load passkeys: ${e.message}"
            }
            _isLoading.value = false
        }
    }

    fun registerPasskey(onSuccess: () -> Unit) {
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null
            try {
                sdk.passkeys.registerPasskey()
                loadPasskeys()
                onSuccess()
            } catch (e: Exception) {
                _error.value = "Failed to register passkey: ${e.message}"
            }
            _isLoading.value = false
        }
    }

    fun authenticateMfa(onSuccess: (String?) -> Unit) {
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null
            try {
                val response = sdk.passkeys.authenticatePasskeyMFA(
                    createMfaToken = MfaCreateToken(singleUse = true),
                    relatedOriginRpId = null
                )
                onSuccess(response.verified.toString())
            } catch (e: Exception) {
                _error.value = "Failed to authenticate: ${e.message}"
            }
            _isLoading.value = false
        }
    }

    fun deletePasskey(passkeyId: String, onSuccess: () -> Unit) {
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null
            try {
                sdk.passkeys.deletePasskey(DeletePasskeyRequest(passkeyId = passkeyId))
                loadPasskeys()
                onSuccess()
            } catch (e: Exception) {
                _error.value = "Failed to delete passkey: ${e.message}"
            }
            _isLoading.value = false
        }
    }
}

Passkey UI example

Display passkeys in a Compose UI:
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun PasskeysScreen(viewModel: PasskeysViewModel) {
    val passkeys by viewModel.passkeys.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()
    val error by viewModel.error.collectAsState()

    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        Button(
            onClick = { viewModel.registerPasskey { /* Success */ } },
            modifier = Modifier.fillMaxWidth()
        ) {
            Icon(Icons.Default.Add, contentDescription = null)
            Spacer(modifier = Modifier.width(8.dp))
            Text("Register Passkey")
        }

        Spacer(modifier = Modifier.height(16.dp))

        error?.let { errorMessage ->
            Text(
                text = errorMessage,
                color = MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.bodySmall
            )
            Spacer(modifier = Modifier.height(8.dp))
        }

        if (isLoading) {
            CircularProgressIndicator()
        } else if (passkeys.isEmpty()) {
            Text("No passkeys registered")
        } else {
            LazyColumn {
                items(passkeys) { passkey ->
                    PasskeyItem(
                        passkey = passkey,
                        onDelete = { 
                            viewModel.deletePasskey(passkey.id) { /* Success */ }
                        },
                        onAuthenticate = {
                            viewModel.authenticateMfa { token ->
                                println("MFA Token: $token")
                            }
                        }
                    )
                }
            }
        }
    }
}

@Composable
fun PasskeyItem(
    passkey: UserPasskey,
    onDelete: () -> Unit,
    onAuthenticate: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 8.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "Passkey",
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = "ID: ${passkey.id.take(8)}...",
                style = MaterialTheme.typography.bodySmall
            )
            Text(
                text = "Created: ${passkey.createdAt.take(10)}",
                style = MaterialTheme.typography.bodySmall
            )
            
            if (passkey.isDefault == true) {
                Text(
                    text = "Default",
                    style = MaterialTheme.typography.labelSmall,
                    color = MaterialTheme.colorScheme.primary
                )
            }

            Spacer(modifier = Modifier.height(8.dp))

            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                Button(onClick = onAuthenticate) {
                    Text("Authenticate")
                }
                OutlinedButton(onClick = onDelete) {
                    Text("Delete")
                }
            }
        }
    }
}

Using Passkeys for MFA

Passkeys can also be used as a second factor for multi-factor authentication. Once you’ve completed the configuration above, see the MFA Guide for details on using passkeys as a 2FA method.

Benefits of Passkeys

  • Passwordless: No need to remember or type passwords
  • Secure: Uses device biometrics or PIN
  • Phishing-resistant: Cannot be stolen or intercepted
  • Fast: Quick authentication with fingerprint or face recognition
  • Cross-platform: Works across devices with Google Password Manager sync

Troubleshooting

Common Issues

Passkey registration fails
  • Verify your asset links configuration at https://yourdomain.com/.well-known/assetlinks.json
  • Check that the JSON contains your correct package name and SHA-256 fingerprint
  • Ensure you’re using the correct signing certificate fingerprint (debug vs release)
  • Validate your asset links using the Google Digital Asset Links Tool
“No passkeys available” error
  • The user may not have any passkeys registered yet
  • Ensure the user is authenticated before attempting passkey sign-in
  • Check that passkeys are supported on the device (Android 9.0+)
Domain mismatch errors
  • Verify appOrigin in SDK configuration matches your asset links domain exactly
  • Check that both use the same protocol (https://)
  • Ensure the domain is accessible and the asset links file is served correctly

What’s Next

Now that you have passkeys configured:
  1. MFA Guide - Use passkeys as a second factor for multi-factor authentication
  2. Authentication - Learn about other authentication methods
  3. Session Management - Manage authenticated sessions