Bu belgede, Credential Manager API'nin WebView kullanan bir Android uygulamasıyla nasıl entegre edileceği açıklanmaktadır.
Genel Bakış
Entegrasyon sürecine başlamadan önce, yerel Android kodu, uygulamanızın kimlik doğrulamasını yöneten bir WebView içinde oluşturulan bir web bileşeni ve arka uç arasındaki iletişim akışını anlamanız önemlidir. Akış, kayıt (kimlik bilgileri oluşturma) ve kimlik doğrulama (mevcut kimlik bilgilerini alma) işlemlerini içerir.
Kayıt (geçiş anahtarı oluşturma)
- Arka uç, ilk kayıt JSON'unu oluşturur ve WebView'da oluşturulan web sayfasına gönderir.
- Web sayfası, yeni kimlik bilgilerini kaydetmek için
navigator.credentials.create()
kullanıyor. İsteği Android uygulamasına göndermek için bu yöntemi daha sonraki bir adımda geçersiz kılmak üzere yerleştirilen JavaScript'i kullanacaksınız. - Android uygulaması, kimlik bilgisi isteğini oluşturmak ve
createCredential
için kullanmak üzere Credential Manager API'yi kullanır. - Kimlik Bilgisi Yöneticisi API'si, ortak anahtar kimlik bilgisini uygulamayla paylaşır.
- Uygulama, eklenen JavaScript'in yanıtları ayrıştırabilmesi için ortak anahtar kimlik bilgisini web sayfasına geri gönderir.
- Web sayfası, ortak anahtarı arka uca gönderir. Arka uç, ortak anahtarı doğrulayıp kaydeder.

Kimlik doğrulama (geçiş anahtarı edinme)
- Arka uç, kimlik bilgisini almak için kimlik doğrulama JSON'u oluşturur ve bunu WebView istemcisinde oluşturulan web sayfasına gönderir.
- Web sayfası
navigator.credentials.get
kullanıyor. İsteği Android uygulamasına yönlendirmek için bu yöntemi geçersiz kılmak üzere yerleştirilmiş JavaScript'i kullanın. - Uygulama,
getCredential
çağrısı yaparak Kimlik Bilgileri Yöneticisi API'sini kullanarak kimlik bilgisini alır. - Kimlik Bilgisi Yöneticisi API'si, kimlik bilgisini uygulamaya döndürür.
- Uygulama, özel anahtarın dijital imzasını alır ve eklenen JavaScript'in yanıtları ayrıştırabilmesi için web sayfasına gönderir.
- Ardından web sayfası, ortak anahtarla dijital imzayı doğrulayan sunucuya gönderir.

Aynı akış, şifreler veya birleşik kimlik sistemleri için de kullanılabilir.
Ön koşullar
Credential Manager API'yi kullanmak için Credential Manager kılavuzunun ön koşullar bölümünde belirtilen adımları tamamlayın ve aşağıdakileri yaptığınızdan emin olun:
- Gerekli bağımlılıkları ekleyin.
- ProGuard dosyasındaki sınıfları koruyun.
- Digital Asset Links desteği eklendi.
JavaScript iletişimi
Bir WebView'da JavaScript'in ve yerel Android kodunun birbirleriyle iletişim kurmasına izin vermek için iki ortam arasında mesaj göndermeniz ve istekleri işlemeniz gerekir. Bunu yapmak için bir WebView'a özel JavaScript kodu ekleyin. Bu sayede web içeriğinin davranışını değiştirebilir ve yerel Android koduyla etkileşimde bulunabilirsiniz.
JavaScript ekleme
Aşağıdaki JavaScript kodu, WebView ile Android uygulaması arasında iletişim kurar. Bu kod, daha önce açıklanan kayıt ve kimlik doğrulama akışları için WebAuthn API tarafından kullanılan navigator.credentials.create()
ve navigator.credentials.get()
yöntemlerini geçersiz kılar.
Uygulamanızda bu JavaScript kodunun küçültülmüş sürümünü kullanın.
Geçiş anahtarları için bir dinleyici oluşturma
JavaScript ile iletişimi işleyen bir PasskeyWebListener
sınıfı oluşturun. Bu sınıf, WebViewCompat.WebMessageListener
sınıfından devralınmalıdır. Bu sınıf, JavaScript'ten mesaj alır ve Android uygulamasında gerekli işlemleri gerçekleştirir.
Aşağıdaki bölümlerde PasskeyWebListener
sınıfının yapısı ve isteklerin/yanıtların işlenmesi açıklanmaktadır.
Kimlik doğrulama isteğini işleme
WebAuthn navigator.credentials.create()
veya navigator.credentials.get()
işlemleri için istekleri işlemek üzere, JavaScript kodu Android uygulamasına mesaj gönderdiğinde PasskeyWebListener
sınıfının onPostMessage
yöntemi çağrılır:
// The class talking to Javascript should inherit:
class PasskeyWebListener(
private val activity: Activity,
private val coroutineScope: CoroutineScope,
private val credentialManagerHandler: CredentialManagerHandler
) : WebViewCompat.WebMessageListener {
/** havePendingRequest is true if there is an outstanding WebAuthn request.
There is only ever one request outstanding at a time. */
private var havePendingRequest = false
/** pendingRequestIsDoomed is true if the WebView has navigated since
starting a request. The FIDO module cannot be canceled, but the response
will never be delivered in this case. */
private var pendingRequestIsDoomed = false
/** replyChannel is the port that the page is listening for a response on.
It is valid if havePendingRequest is true. */
private var replyChannel: ReplyChannel? = null
/**
* Called by the page during a WebAuthn request.
*
* @param view Creates the WebView.
* @param message The message sent from the client using injected JavaScript.
* @param sourceOrigin The origin of the HTTPS request. Should not be null.
* @param isMainFrame Should be set to true. Embedded frames are not
supported.
* @param replyProxy Passed in by JavaScript. Allows replying when wrapped in
the Channel.
* @return The message response.
*/
@UiThread
override fun onPostMessage(
view: WebView,
message: WebMessageCompat,
sourceOrigin: Uri,
isMainFrame: Boolean,
replyProxy: JavaScriptReplyProxy,
) {
val messageData = message.data ?: return
onRequest(
messageData,
sourceOrigin,
isMainFrame,
JavaScriptReplyChannel(replyProxy)
)
}
private fun onRequest(
msg: String,
sourceOrigin: Uri,
isMainFrame: Boolean,
reply: ReplyChannel,
) {
msg?.let {
val jsonObj = JSONObject(msg);
val type = jsonObj.getString(TYPE_KEY)
val message = jsonObj.getString(REQUEST_KEY)
if (havePendingRequest) {
postErrorMessage(reply, "The request already in progress", type)
return
}
replyChannel = reply
if (!isMainFrame) {
reportFailure("Requests from subframes are not supported", type)
return
}
val originScheme = sourceOrigin.scheme
if (originScheme == null || originScheme.lowercase() != "https") {
reportFailure("WebAuthn not permitted for current URL", type)
return
}
// Verify that origin belongs to your website,
// it's because the unknown origin may gain credential info.
// if (isUnknownOrigin(originScheme)) {
// return
// }
havePendingRequest = true
pendingRequestIsDoomed = false
// Use a temporary "replyCurrent" variable to send the data back, while
// resetting the main "replyChannel" variable to null so it’s ready for
// the next request.
val replyCurrent = replyChannel
if (replyCurrent == null) {
Log.i(TAG, "The reply channel was null, cannot continue")
return;
}
when (type) {
CREATE_UNIQUE_KEY ->
this.coroutineScope.launch {
handleCreateFlow(credentialManagerHandler, message, replyCurrent)
}
GET_UNIQUE_KEY -> this.coroutineScope.launch {
handleGetFlow(credentialManagerHandler, message, replyCurrent)
}
else -> Log.i(TAG, "Incorrect request json")
}
}
}
private suspend fun handleCreateFlow(
credentialManagerHandler: CredentialManagerHandler,
message: String,
reply: ReplyChannel,
) {
try {
havePendingRequest = false
pendingRequestIsDoomed = false
val response = credentialManagerHandler.createPasskey(message)
val successArray = ArrayList<Any>();
successArray.add("success");
successArray.add(JSONObject(response.registrationResponseJson));
successArray.add(CREATE_UNIQUE_KEY);
reply.send(JSONArray(successArray).toString())
replyChannel = null // setting initial replyChannel for the next request
} catch (e: CreateCredentialException) {
reportFailure(
"Error: ${e.errorMessage} w type: ${e.type} w obj: $e",
CREATE_UNIQUE_KEY
)
} catch (t: Throwable) {
reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY)
}
}
companion object {
/** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */
const val INTERFACE_NAME = "__webauthn_interface__"
const val TYPE_KEY = "type"
const val REQUEST_KEY = "request"
const val CREATE_UNIQUE_KEY = "create"
const val GET_UNIQUE_KEY = "get"
/** INJECTED_VAL is the minified version of the JavaScript code described at this class
* heading. The non minified form is found at credmanweb/javascript/encode.js.*/
const val INJECTED_VAL = """
var __webauthn_interface__,__webauthn_hooks__;!function(e){console.log("In the hook."),__webauthn_interface__.addEventListener("message",function e(n){var r=JSON.parse(n.data),t=r[2];"get"===t?o(r):"create"===t?u(r):console.log("Incorrect response format for reply")});var n=null,r=null,t=null,a=null;function o(e){if(null!==n&&null!==t){if("success"!=e[0]){var r=t;n=null,t=null,r(new DOMException(e[1],"NotAllowedError"));return}var a=i(e[1]),o=n;n=null,t=null,o(a)}}function l(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),function(e){return e.charCodeAt(0)}).buffer}function s(e){return btoa(Array.from(new Uint8Array(e),function(e){return String.fromCharCode(e)}).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function u(e){if(null===r||null===a){console.log("Here: "+r+" and reject: "+a);return}if(console.log("Output back: "+e),"success"!=e[0]){var n=a;r=null,a=null,n(new DOMException(e[1],"NotAllowedError"));return}var t=i(e[1]),o=r;r=null,a=null,o(t)}function i(e){return console.log("Here is the response from credential manager: "+e),e.rawId=l(e.rawId),e.response.clientDataJSON=l(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=l(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=l(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=l(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=l(e.response.userHandle)),e.getClientExtensionResults=function e(){return{}},e}e.create=function n(t){if(!("publicKey"in t))return e.originalCreateFunction(t);var o=new Promise(function(e,n){r=e,a=n}),l=t.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}if(l.hasOwnProperty("user")&&l.user.hasOwnProperty("id")){var i=s(l.user.id);l.user.id=i}var c=JSON.stringify({type:"create",request:l});return __webauthn_interface__.postMessage(c),o},e.get=function r(a){if(!("publicKey"in a))return e.originalGetFunction(a);var o=new Promise(function(e,r){n=e,t=r}),l=a.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}var i=JSON.stringify({type:"get",request:l});return __webauthn_interface__.postMessage(i),o},e.onReplyGet=o,e.CM_base64url_decode=l,e.CM_base64url_encode=s,e.onReplyCreate=u}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!1)};
"""
}
handleCreateFlow
ve handleGetFlow
için GitHub'daki örneğe bakın.
Yanıtı işleme
Yerel uygulamadan web sayfasına gönderilen yanıtları işlemek için JavaScriptReplyProxy
öğesini JavaScriptReplyChannel
içine ekleyin.
// The setup for the reply channel allows communication with JavaScript.
private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) :
ReplyChannel {
override fun send(message: String?) {
try {
reply.postMessage(message!!)
} catch (t: Throwable) {
Log.i(TAG, "Reply failure due to: " + t.message);
}
}
}
// ReplyChannel is the interface where replies to the embedded site are
// sent. This allows for testing since AndroidX bans mocking its objects.
interface ReplyChannel {
fun send(message: String?)
}
Yerel uygulamadaki hataları yakalayıp JavaScript tarafına geri gönderdiğinizden emin olun.
WebView ile entegrasyon
Bu bölümde, WebView entegrasyonunuzu nasıl ayarlayacağınız açıklanmaktadır.
WebView'ı başlatma
Android uygulamanızın etkinliğinde bir WebView
başlatın ve eşlik eden bir WebViewClient
ayarlayın. WebViewClient
, WebView
içine yerleştirilen JavaScript koduyla iletişimi yönetir.
WebView'ı ayarlayın ve Credential Manager'ı çağırın:
val credentialManagerHandler = CredentialManagerHandler(this)
setContent {
val coroutineScope = rememberCoroutineScope()
AndroidView(factory = {
WebView(it).apply {
settings.javaScriptEnabled = true
// Test URL:
val url = "https://passkeys-codelab.glitch.me/"
val listenerSupported = WebViewFeature.isFeatureSupported(
WebViewFeature.WEB_MESSAGE_LISTENER
)
if (listenerSupported) {
// Inject local JavaScript that calls Credential Manager.
hookWebAuthnWithListener(
this, this@WebViewMainActivity,
coroutineScope, credentialManagerHandler
)
} else {
// Fallback routine for unsupported API levels.
}
loadUrl(url)
}
}
)
}
Yeni bir WebView istemci nesnesi oluşturun ve web sayfasına JavaScript yerleştirin:
val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler)
val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null)
}
}
webView.webViewClient = webViewClient
Web mesajı işleyici ayarlama
JavaScript ile Android uygulaması arasında mesaj gönderilmesine izin vermek için WebViewCompat.addWebMessageListener
yöntemiyle bir web mesajı dinleyicisi ayarlayın.
val rules = setOf("*")
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(webView, PasskeyWebListener.INTERFACE_NAME,
rules, passkeyWebListener)
}
Web entegrasyonu
Web entegrasyonu oluşturmayı öğrenmek için Şifresiz giriş için geçiş anahtarı oluşturma ve Formu otomatik doldurma özelliğiyle geçiş anahtarıyla oturum açma başlıklı makaleleri inceleyin.
Test ve dağıtım
Android uygulaması, web sayfası ve arka uç arasında doğru iletişimin sağlanması için kontrollü bir ortamda tüm akışı kapsamlı bir şekilde test edin.
Entegre çözümü üretime dağıtın. Arka ucun, gelen kayıt ve kimlik doğrulama isteklerini işleyebildiğinden emin olun. Arka uç kodu, kayıt (oluşturma) ve kimlik doğrulama (alma) işlemleri için ilk JSON'u oluşturmalıdır. Ayrıca, web sayfasından alınan yanıtların doğrulanması ve onaylanması da gerekir.
Uygulamanın kullanıcı deneyimi önerilerine uygun olduğunu doğrulayın.
Önemli notlar
navigator.credentials.create()
venavigator.credentials.get()
işlemlerini işlemek için sağlanan JavaScript kodunu kullanın.PasskeyWebListener
sınıfı, Android uygulaması ile WebView'daki JavaScript kodu arasındaki köprüdür. Mesaj iletme, iletişim ve gerekli işlemlerin yürütülmesini sağlar.- Sağlanan kod snippet'lerini projenizin yapısına, adlandırma kurallarına ve sahip olabileceğiniz özel gereksinimlere uyacak şekilde uyarlayın.
- Yerel uygulama tarafındaki hataları yakalayıp JavaScript tarafına geri gönderin.
Bu kılavuzu uygulayarak ve Credential Manager API'yi WebView kullanan Android uygulamanıza entegre ederek, kullanıcılarınızın kimlik bilgilerini etkili bir şekilde yönetirken onlara güvenli ve sorunsuz bir geçiş anahtarı etkinleştirilmiş oturum açma deneyimi sunabilirsiniz.