Kimlik Bilgisi Yöneticisi'ni Web Görünümü ile entegre etme

Bu dokümanda, Kimlik Bilgisi Yöneticisi API'sinin Web Görünümü kullanan bir Android uygulamasıyla nasıl entegre edileceği açıklanmaktadır.

Genel bakış

Entegrasyon işlemine başlamadan önce yerel Android kodu, uygulamanızın kimlik doğrulamasını yöneten Web Görünümü 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)

  1. Arka uç, ilk kayıt JSON'unu oluşturur ve bunu Web Görünümü'nde oluşturulan web sayfasına gönderir.
  2. Web sayfası yeni kimlik bilgilerini kaydetmek için navigator.credentials.create() kullanır. Yerleştirilen JavaScript'i, isteği Android uygulamasına göndermek amacıyla sonraki bir adımda bu yöntemi geçersiz kılmak için kullanacaksınız.
  3. Android uygulaması, kimlik bilgisi isteği oluşturmak ve bunu createCredential için kullanmak üzere Credential Manager API'yi kullanır.
  4. Kimlik Bilgisi Yöneticisi API'si, ortak anahtar kimlik bilgisini uygulamayla paylaşır.
  5. Uygulama, yerleştirilen JavaScript'in yanıtları ayrıştırabilmesi için ortak anahtar kimlik bilgisini web sayfasına geri gönderir.
  6. Web sayfası, ortak anahtarı arka uca göndererek ortak anahtarı doğrular ve kaydeder.
Geçiş anahtarı kayıt akışını gösteren grafik
Şekil 1. Geçiş anahtarı kayıt akışı.

Kimlik doğrulama (geçiş anahtarı al)

  1. Arka uç, kimlik bilgisini almak için kimlik doğrulama JSON'ı oluşturur ve bunu Web Görünümü istemcisinde oluşturulan web sayfasına gönderir.
  2. Web sayfası navigator.credentials.get kullanıyor. İsteği Android uygulamasına yönlendirmek amacıyla bu yöntemi geçersiz kılmak için yerleştirilen JavaScript'i kullanın.
  3. Uygulama, getCredential çağrısı yaparak Credential Manager API'sini kullanarak kimlik bilgisini alır.
  4. Kimlik Bilgisi Yöneticisi API'si, kimlik bilgisini uygulamaya döndürür.
  5. Uygulama, özel anahtarın dijital imzasını alır ve yerleştirilen JavaScript'in yanıtları ayrıştırabilmesi için bunu web sayfasına gönderir.
  6. Ardından web sayfası, dijital imzayı ortak anahtarla doğrulayan sunucuya gönderir.
Geçiş anahtarı kimlik doğrulama akışını gösteren grafik
Şekil 1. Geçiş anahtarı kimlik doğrulama akışı.

Aynı akış, şifreler veya birleştirilmiş kimlik sistemleri için de kullanılabilir.

Ön koşullar

Credential Manager API'yi kullanmak için Kimlik Bilgisi Yöneticisi kılavuzunun önkoşullar bölümünde açıklanan adımları tamamlayın ve aşağıdakileri yaptığınızdan emin olun:

JavaScript iletişimi

Web Görünümünde 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 Web Görünümü'ne özel JavaScript kodu ekleyin. Bu işlem, web içeriğinin davranışını değiştirmenize ve yerel Android koduyla etkileşimde bulunmanıza olanak tanır.

JavaScript yerleştirme

Aşağıdaki JavaScript kodu, Web Görünümü ile Android uygulaması arasındaki iletişimi oluşturur. 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 işleyici oluşturma

JavaScript ile iletişimi işleyen bir PasskeyWebListener sınıfı oluşturun. Bu sınıf, WebViewCompat.WebMessageListener öğesinden devralmalıdır. Bu sınıf, JavaScript'ten mesajlar alır ve Android uygulamasında gerekli işlemleri gerçekleştirir.

Kotlin

// The class talking to Javascript should inherit:
class PasskeyWebListener(
    private val activity: Activity,
    private val coroutineScope: CoroutineScope,
    private val credentialManagerHandler: CredentialManagerHandler
) : WebViewCompat.WebMessageListener

// ... Implementation details

Java

// The class talking to Javascript should inherit:
class PasskeyWebListener implements WebViewCompat.WebMessageListener {

  // Implementation details
  private Activity activity;

  // Handles get/create methods meant for Java:
  private CredentialManagerHandler credentialManagerHandler;

  public PasskeyWebListener(
    Activity activity,
    CredentialManagerHandler credentialManagerHandler
    ) {
    this.activity = activity;
    this.credentialManagerHandler = credentialManagerHandler;
  }

// ... Implementation details
}

PasskeyWebListener içinde, aşağıdaki bölümlerde açıklandığı gibi istekler ve yanıtlar için mantığı uygulayın.

Kimlik doğrulama isteğini işleme

WebAuthn navigator.credentials.create() veya navigator.credentials.get() işlemlerine yönelik istekleri işlemek için JavaScript kodu Android uygulamasına mesaj gönderdiğinde PasskeyWebListener sınıfının onPostMessage yöntemi çağrılır:

Kotlin

class PasskeyWebListener(...)... {
// ...

  /** 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 {
    const val TYPE_KEY = "type"
    const val REQUEST_KEY = "request"
    const val CREATE_UNIQUE_KEY = "create"
    const val GET_UNIQUE_KEY = "get"
  }
}

Java

class PasskeyWebListener implements ... {
// ...

  /**
  * 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
  public void onPostMessage(
    @NonNull WebView view,
    @NonNull WebMessageCompat message,
    @NonNull Uri sourceOrigin,
    Boolean isMainFrame,
    @NonNull JavaScriptReplyProxy replyProxy,
  ) {
      if (messageData == null) {
        return;
    }
    onRequest(
      messageData,
      sourceOrigin,
      isMainFrame,
      JavaScriptReplyChannel(replyProxy)
    )
  }

  private void onRequest(
    String msg,
    Uri sourceOrigin,
    boolean isMainFrame,
    ReplyChannel reply
  ) {
      if (msg != null) {
        try {
          JSONObject jsonObj = new JSONObject(msg);
          String type = jsonObj.getString(TYPE_KEY);
          String message = jsonObj.getString(REQUEST_KEY);

          boolean isCreate = type.equals(CREATE_UNIQUE_KEY);
          boolean isGet = type.equals(GET_UNIQUE_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;
          }
          String originScheme = sourceOrigin.getScheme();
          if (originScheme == null || !originScheme.toLowerCase().equals("https")) {
              reportFailure("WebAuthn not permitted for current URL", type);
              return;
          }

          // Verify that origin belongs to your website,
          // Requests of unknown origin may gain access to 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.

          ReplyChannel replyCurrent = replyChannel;
          if (replyCurrent == null) {
              Log.i(TAG, "The reply channel was null, cannot continue");
              return;
          }

          if (isCreate) {
              handleCreateFlow(credentialManagerHandler, message, replyCurrent));
          } else if (isGet) {
              handleGetFlow(credentialManagerHandler, message, replyCurrent));
          } else {
              Log.i(TAG, "Incorrect request json");
          }
        } catch (JSONException e) {
        e.printStackTrace();
      }
    }
  }
}

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 JavaScriptReplyChannel içine JavaScriptReplyProxy bilgisini ekleyin.

Kotlin

class PasskeyWebListener(...)... {
// ...
  // 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?)
  }
}

Java

class PasskeyWebListener implements ... {
// ...

  // The setup for the reply channel allows communication with JavaScript.
  private static class JavaScriptReplyChannel implements ReplyChannel {
    private final JavaScriptReplyProxy reply;

    JavaScriptReplyChannel(JavaScriptReplyProxy reply) {
      this.reply = reply;
    }

    @Override
    public void send(String message) {
      reply.postMessage(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 {
    void send(String message);
  }
}

Yerel uygulamadaki hataları yakalayıp JavaScript tarafına geri gönderdiğinizden emin olun.

Web Görünümü ile entegre et

Bu bölümde, Web Görünümü entegrasyonunuzu nasıl ayarlayacağınız açıklanmaktadır.

Web Görünümü'nü başlatma

Android uygulamanızın etkinliğinde bir WebView başlatın ve ona eşlik eden WebViewClient oluşturun. WebViewClient, WebView içine yerleştirilen JavaScript koduyla iletişimi gerçekleştirir.

Web Görünümü'nü ayarlayın ve Kimlik Bilgisi Yöneticisi'ni çağırın:

Kotlin

val credentialManagerHandler = CredentialManagerHandler(this)
// ...

AndroidView(factory = {
  WebView(it).apply {
    settings.javaScriptEnabled = true

    // Test URL:
    val url = "https://credman-web-test.glitch.me/"
    val listenerSupported = WebViewFeature.isFeatureSupported(
      WebViewFeature.WEB_MESSAGE_LISTENER
    )
    if (listenerSupported) {
      // Inject local JavaScript that calls Credential Manager.
      hookWebAuthnWithListener(this, this@MainActivity,
      coroutineScope, credentialManagerHandler)
      } else {
        // Fallback routine for unsupported API levels.
      }
      loadUrl(url)
    }
  }
)

Java

// Example shown in the onCreate method of an Activity

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  WebView webView = findViewById(R.id.web_view);
  // Test URL:
  String url = "https://credman-web-test.glitch.me/";
  Boolean listenerSupported = WebViewFeature.isFeatureSupported(
    WebViewFeature.WEB_MESSAGE_LISTENER
  );
  if (listenerSupported) {
    // Inject local JavaScript that calls Credential Manager.
    hookWebAuthnWithListener(webView, this,
      coroutineScope, credentialManagerHandler)
  } else {
    // Fallback routine for unsupported API levels.
  }
  webView.loadUrl(url);
}

Yeni bir WebView istemci nesnesi oluşturun ve web sayfasına JavaScript ekleyin:

Kotlin

// This is an example call into hookWebAuthnWithListener
val passkeyWebListener = PasskeyWebListener(
  activity, coroutineScope, credentialManagerHandler
)

val webViewClient = object : WebViewClient() {
  override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
    super.onPageStarted(view, url, favicon)
    // Handle page load events
    passkeyWebListener.onPageStarted();
    webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null)
  }
}

webView.webViewClient = webViewClient

Java

// This is an example call into hookWebAuthnWithListener
PasskeyWebListener passkeyWebListener = new PasskeyWebListener(
  activity, credentialManagerHandler
)

WebViewClient webiewClient = new WebViewClient() {
  @Override
  public void onPageStarted(WebView view, String url, Bitmap favicon) {
    super.onPageStarted(view, url, favicon);
    // Handle page load events
    passkeyWebListener.onPageStarted();
    webView.evaulateJavascript(PasskeyWebListener.INJECTED_VAL, null);
  }
};

webView.setWebViewClient(webViewClient);

Web mesajı dinleyicisi ayarlama

Mesajların JavaScript ve Android uygulaması arasında yayınlanmasına izin vermek için WebViewCompat.addWebMessageListener yöntemiyle bir web mesaj işleyicisi ayarlayın.

Kotlin

val rules = setOf("*")
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
  WebViewCompat.addWebMessageListener(
    webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener
  )
}

Java

Set<String> rules = new HashSet<>(Arrays.asList("*"));

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şler için geçiş anahtarı oluşturma ve Formları otomatik doldurma aracılığıyla 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 düzgün bir iletişim olduğundan emin olmak için akışın tamamını kontrollü bir ortamda kapsamlı bir şekilde test edin.

Arka ucun gelen kayıt ve kimlik doğrulama isteklerini işleyebildiğinden emin olarak entegre çözümü üretime dağıtın. Arka uç kodu, kayıt (oluşturma) ve kimlik doğrulama (alma) işlemleri için ilk JSON'u oluşturmalıdır. Aynı zamanda web sayfasından alınan yanıtların doğrulanmasını ve doğrulanmasını sağlar.

Uygulamanın kullanıcı deneyimi önerilerine uygun olduğunu doğrulayın.

Önemli notlar

  • navigator.credentials.create() ve navigator.credentials.get() işlemlerini gerçekleştirmek için sağlanan JavaScript kodunu kullanın.
  • PasskeyWebListener sınıfı, Android uygulaması ile Web Görünümü'ndeki JavaScript kodu arasındaki köprüdür. Mesaj iletme, iletişim ve gerekli işlemlerin yürütülmesini yönetir.
  • Sağlanan kod snippet'lerini projenizin yapısına, adlandırma kurallarına ve sahip olabileceğiniz belirli gereksinimlere uyacak şekilde uyarlayın.
  • Yerel uygulama tarafındaki hataları yakalayın ve JavaScript tarafına geri gönderin.

Bu kılavuzu takip ederek ve Credential Manager API'sini WebView kullanan Android uygulamanıza entegre ederek kullanıcılarınıza güvenli ve sorunsuz bir geçiş anahtarı özellikli giriş deneyimi sunarken kimlik bilgilerini etkili bir şekilde yönetebilirsiniz.