• 16.08.2026 09:04:02
  • Admin Admin

Android eğitimi kapsamında, Credential Manager ve WebAuthn kullanarak parola tabanlı oturum açmayı passkey akışına geçirmenin Android istemci, backend ve yayınlama ayrıntılarını inceliyoruz.

Android'de Passkey Geçişi: Credential Manager ile Sağlam Kimlik Doğrulama

Android eğitimi için doğru sınır: Passkey istemcide değil, WebAuthn sunucusunda doğrulanır

Bir android eğitim veya kotlin eğitimi projesinde sık yapılan hata, passkey sonucunu uygulamanın imzaladığı bir JWT gibi değerlendirmektir. Android tarafındaki authenticationResponseJson, imzalı WebAuthn assertion verisidir; gerçek güven kararı backend'de verilir. Sunucu önce kullanıcıya ve RP ID'ye bağlı, tek kullanımlık bir challenge üretir; istemciden dönen clientDataJSON.challenge değerini bununla karşılaştırır, ardından authenticator imzasını kaydedilmiş public key ile doğrular. Challenge'ı Redis'te 5 dakika TTL ile saklamak, hem tekrar oynatma saldırısını hem de aynı kullanıcının paralel giriş denemelerinde yanlış challenge eşleşmesini engeller.

// Backend'in /auth/passkey/options endpoint'inden gelen challenge,
// kullanıcıya ve akış kimliğine bağlı olmalıdır.
data class PasskeyOptions(
    val challenge: String, // Base64URL; rastgele en az 32 byte
    val rpId: String,
    val allowCredentials: List<CredentialDescriptor>
)

// Sunucu tarafı pseudo-code
val challenge = secureRandomBytes(32)
redis.setex("webauthn:$flowId", 300, challenge.base64Url())
return assertionOptions(challenge, rpId = "example.com", userCredentials)

Credential Manager yalnızca Android cihazındaki sağlayıcılarla konuşur; credential public key kaydı, sayaç politikası ve kullanıcı doğrulama tercihi sunucudaki WebAuthn kütüphanesinde kalmalıdır. Java ekosisteminde webauthn4j, Node.js tarafında @simplewebauthn/server bu doğrulama için kullanılan gerçek seçeneklerdir. Özellikle signCount alanını mutlak bir güvenlik sinyali saymayın: bazı platform authenticator'lar sıfır veya artmayan sayaç döndürebilir. Sıfır olmayan sayaç geriye gidiyorsa hesabı sessizce kilitlemek yerine riski işaretleyip ek doğrulama istemek daha güvenli bir operasyonel politikadır.

Kotlin kursu uygulamasında Credential Manager ile assertion alma

İstemci bağımlılığında AndroidX Credential Manager çekirdeğini ve Play services köprüsünü birlikte ekleyin; köprü olmadığı durumda belirli cihaz sağlayıcıları görünmeyebilir. android studio eğitimi laboratuvarlarında bu bağımlılığı yalnızca debug flavor'a koymak yaygın ama hatalıdır: gerçek kullanıcıdaki passkey sağlayıcısı release APK/AAB içinde de aynı entegrasyonu gerektirir.

dependencies {
    implementation("androidx.credentials:credentials:<uyumlu-surum>")
    implementation("androidx.credentials:credentials-play-services-auth:<uyumlu-surum>")
}

suspend fun getPasskey(
    context: Context,
    requestJson: String
): String {
    val request = GetCredentialRequest(
        listOf(GetPublicKeyCredentialOption(requestJson))
    )
    val result = CredentialManager.create(context)
        .getCredential(context, request)

    val credential = result.credential as? PublicKeyCredential
        ?: error("Beklenen public-key credential gelmedi")
    return credential.authenticationResponseJson
}

Bu suspend çağrıyı Application context'i ile başlatmayın. Sağlayıcı seçici ekranı ve biyometrik kullanıcı etkileşimi bir Activity penceresine ihtiyaç duyar; Activity kapanırken coroutine iptal edilmezse eski ekranın sonucu yeni ekrana yazılabilir. ViewModel'de sonucu state'e taşırken isteğe ait bir attemptId üretin ve backend yanıtı geldiğinde hâlâ aktif denemeyle eşleştiğini kontrol edin. Ayrıca GetCredentialCancellationException kullanıcı iptalidir; bunu 'giriş başarısız' analitik olayı olarak saymak dönüşüm metriklerini yapay biçimde düşürür.

class LoginViewModel : ViewModel() {
    private var activeAttempt: String? = null

    fun loginWithPasskey(activity: Activity, optionsJson: String) = viewModelScope.launch {
        val attemptId = UUID.randomUUID().toString()
        activeAttempt = attemptId
        try {
            val assertion = getPasskey(activity, optionsJson)
            val session = api.verifyPasskey(assertion, attemptId)
            if (activeAttempt == attemptId) _state.value = LoginState.Success(session)
        } catch (_: GetCredentialCancellationException) {
            if (activeAttempt == attemptId) _state.value = LoginState.Idle
        }
    }
}

Jetpack Compose ve Android MVVM mimarisi ile tek seferlik giriş olayını yönetmek

jetpack compose içinde credential seçicisini composable gövdesinden doğrudan çağırmak hatalıdır: recomposition aynı yan etkiyi tekrar başlatabilir. android mvvm mimarisi için pratik ayrım şudur: ViewModel yalnızca StartPasskeyLogin effect'i üretir; Activity bu effect'i toplar ve Credential Manager çağrısını yapar. Effect akışı için Channel kullanmak, ekrana geri dönüldüğünde StateFlow'daki eski event'in yeniden tüketilmesini önler.

sealed interface LoginEffect {
    data class StartPasskeyLogin(val optionsJson: String) : LoginEffect
}

private val effectsChannel = Channel<LoginEffect>(Channel.BUFFERED)
val effects = effectsChannel.receiveAsFlow()

@Composable
fun LoginRoute(viewModel: LoginViewModel) {
    val activity = LocalContext.current as Activity
    LaunchedEffect(Unit) {
        viewModel.effects.collect { effect ->
            if (effect is LoginEffect.StartPasskeyLogin) {
                viewModel.loginWithPasskey(activity, effect.optionsJson)
            }
        }
    }
    LoginScreen(onPasskeyClick = viewModel::requestPasskeyOptions)
}

Bir başka incelik, kullanıcı hesabı henüz cihazda passkey oluşturmamışsa assertion isteğinin credential bulunamadı hatasıyla dönmesidir. Bu durumda otomatik olarak parola ekranına düşmek yerine backend'in seçenek endpoint'inde hesabın passkey durumunu modelleyin ve UI'da 'passkey oluştur' kaydını ayrı bir akış yapın. Kayıt akışında CreatePublicKeyCredentialRequest kullanılır; sunucunun ürettiği creation options içindeki user.id kararlı, opak bir byte dizisi olmalıdır. E-posta adresini bu alana koymak, kullanıcı e-postasını değiştirdiğinde credential ilişkisinin bozulmasına yol açar.

Digital Asset Links, test matrisi ve Play Store yayınlama kontrolü

Passkey'in Android uygulamanız ile web RP'niz arasında güven ilişkisi kurabilmesi için alan adınızda https://example.com/.well-known/assetlinks.json yayınlayın. Dosyadaki sertifika parmak izi debug değil, dağıtımda kullanılan imzalama sertifikasına ait olmalıdır. Play App Signing kullanılıyorsa bu, yerelde AAB'yi imzalayan upload key değil Google Play'in app signing certificate parmak izidir. Kontrol için Android Studio içinden App Links Assistant'a güvenmek yerine CI'da aşağıdaki endpoint'i indirip JSON şemasını doğrulayın.

curl --fail --silent --show-error   https://example.com/.well-known/assetlinks.json | jq -e '
  any(.[];
    .target.namespace == "android_app" and
    .target.package_name == "com.example.app" and
    (.target.sha256_cert_fingerprints | length > 0)
  )'

play store yayınlama öncesinde internal testing kanalından kurulan paketi, en az bir gerçek cihazda test edin; IDE'den yüklenen debug APK bu sertifika farkını maskeleyebilir. Test matrisine şunları ekleyin: aynı hesap için iki cihazda assertion, credential bulunmayan yeni cihaz, kullanıcı seçiciyi iptal ettiğinde ekran dönüşü ve domain sertifikası/assetlinks erişilemez olduğunda fallback. Backend gözlemlenebilirliği için challenge_issued, assertion_verified, assertion_rejected sayaçlarını ayrı tutun; assertion_rejected etiketlerinde ham assertion veya e-posta saklamayın, yalnızca hata kodu, RP ID ve akış süresi kaydedin.

mobil uygulama geliştirme eğitimi açısından ölçülebilir kabul kriteri belirlemek yararlıdır: internal testte en az 20 başarılı girişte, istemcinin options alma ile session alma arasındaki p50/p95 sürelerini ayrı ölçün; kullanıcı iptali başarı oranına dahil etmeyin. Bu ayrım, sağlayıcı seçici gecikmesi ile backend'deki WebAuthn imza doğrulamasını aynı 'login latency' metriğinde karıştırmanızı engeller.

Sık Sorulan Sorular

Android kursu projesinde Credential Manager neden Application context ile çalışmıyor?

GetCredential çağrısı kullanıcıya credential sağlayıcısı veya biyometrik doğrulama UI'ı gösterebilir; bunun için görünür bir Activity gerekir. Activity referansını kısa ömürlü tutun, ViewModel'e saklamayın ve çağrıyı lifecycleScope/viewModel effect üzerinden ekrana taşıyın.

Kotlin eğitimi sırasında passkey authenticationResponseJson doğrudan JWT olarak kullanılabilir mi?

Hayır. Bu JSON içindeki assertion; challenge, origin, RP ID hash ve authenticator imzası açısından backend'de WebAuthn doğrulamasından geçmelidir. Doğrulama başarılı olduktan sonra sunucunuz kendi kısa ömürlü access token'ını üretmelidir.

Jetpack Compose ile passkey girişinde recomposition iki kez biyometrik pencere açar mı?

Composable gövdesinde getCredential çağrılırsa açabilir. Çağrıyı Channel tabanlı tek seferlik effect ile LaunchedEffect içinde toplayın; UI state değişikliklerini doğrudan credential isteğinin tetikleyicisi yapmayın.

Play Store yayınlama sırasında passkey doğrulaması neden internal testte başarısız olur?

En sık neden assetlinks.json içinde upload key veya debug sertifikası fingerprint'inin bulunmasıdır. Play App Signing kullanılan dağıtımlarda Google Play Console'daki app signing certificate SHA-256 fingerprint'ini assetlinks.json'a ekleyin ve dosyanın HTTPS üzerinden yönlendirmesiz erişildiğini curl ile doğrulayın.

AI / LLM Discovery

Bu makale Opendart Akademi Android eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt