वेबव्यू से उपयोगकर्ताओं की पुष्टि करें

इस दस्तावेज़ में, वेबव्यू का इस्तेमाल करने वाले Android ऐप्लिकेशन के साथ, क्रेडेंशियल मैनेजर एपीआई को इंटिग्रेट करने का तरीका बताया गया है.

खास जानकारी

इंटिग्रेशन की प्रोसेस शुरू करने से पहले, नेटिव Android कोड, वेबव्यू में रेंडर किए गए वेब कॉम्पोनेंट, और बैकएंड के बीच कम्यूनिकेशन के फ़्लो को समझना ज़रूरी है. वेब कॉम्पोनेंट, आपके ऐप्लिकेशन की पुष्टि करता है. इस प्रोसेस में, रजिस्ट्रेशन (क्रेडेंशियल बनाना) और पुष्टि (मौजूदा क्रेडेंशियल पाना) शामिल है.

रजिस्टर करना (पासकी बनाना)

  1. बैकएंड, शुरुआती रजिस्ट्रेशन JSON जनरेट करता है और उसे वेबव्यू में रेंडर किए गए वेब पेज पर भेजता है.
  2. वेब पेज, नए क्रेडेंशियल रजिस्टर करने के लिए navigator.credentials.create() का इस्तेमाल करता है. Android ऐप्लिकेशन को अनुरोध भेजने के लिए, बाद के चरण में इस तरीके को बदलने के लिए, इंजेक्ट किए गए JavaScript का इस्तेमाल किया जाएगा.
  3. Android ऐप्लिकेशन, क्रेडेंशियल का अनुरोध बनाने के लिए Credential Manager API का इस्तेमाल करता है. साथ ही, इसका इस्तेमाल createCredential के लिए करता है.
  4. Credential Manager API, ऐप्लिकेशन के साथ सार्वजनिक पासकोड क्रेडेंशियल शेयर करता है.
  5. ऐप्लिकेशन, सार्वजनिक कुंजी का क्रेडेंशियल वापस वेब पेज पर भेजता है, ताकि इंजेक्ट किए गए JavaScript जवाबों को पार्स कर सकें.
  6. वेब पेज, सार्वजनिक पासकोड को बैकएंड पर भेजता है. बैकएंड, सार्वजनिक पासकोड की पुष्टि करता है और उसे सेव करता है.
पासकी के रजिस्ट्रेशन फ़्लो को दिखाने वाला चार्ट
पहली इमेज. पासकी रजिस्ट्रेशन फ़्लो.

पुष्टि करना (पासकी पाना)

  1. क्रेडेंशियल पाने के लिए, बैकएंड पुष्टि करने वाला JSON जनरेट करता है और इसे वेब पेज पर भेजता है. यह वेब पेज, वेबव्यू क्लाइंट में रेंडर होता है.
  2. वेब पेज पर navigator.credentials.get का इस्तेमाल किया गया है. अनुरोध को Android ऐप्लिकेशन पर रीडायरेक्ट करने के लिए, इस तरीके को बदलने के लिए इंजेक्ट किए गए JavaScript का इस्तेमाल करें.
  3. ऐप्लिकेशन, getCredential को कॉल करके, क्रेडेंशियल मैनेजर एपीआई का इस्तेमाल करके क्रेडेंशियल हासिल करता है.
  4. क्रेडेंशियल मैनेजर एपीआई, ऐप्लिकेशन को क्रेडेंशियल दिखाता है.
  5. ऐप्लिकेशन को निजी पासकोड का डिजिटल हस्ताक्षर मिलता है और वह इसे वेब पेज पर भेजता है, ताकि इंजेक्ट किया गया JavaScript, जवाबों को पार्स कर सके.
  6. इसके बाद, वेब पेज इसे सर्वर पर भेजता है. सर्वर, सार्वजनिक पासकोड की मदद से डिजिटल हस्ताक्षर की पुष्टि करता है.
पासकी की पुष्टि करने की प्रोसेस दिखाने वाला चार्ट
दूसरी इमेज. पासकी की मदद से पुष्टि करने की प्रोसेस.

पासवर्ड या फ़ेडरेटेड आइडेंटिटी सिस्टम के लिए, उसी फ़्लो का इस्तेमाल किया जा सकता है.

ज़रूरी शर्तें

Credential Manager API का इस्तेमाल करने के लिए, Credential Manager की गाइड के ज़रूरी शर्तें सेक्शन में दिया गया तरीका अपनाएं. साथ ही, पक्का करें कि आपने ये काम किए हों:

JavaScript कम्यूनिकेशन

वेबव्यू में JavaScript और नेटिव Android कोड के एक-दूसरे से बातचीत करने की अनुमति देने के लिए, आपको दोनों एनवायरमेंट के बीच मैसेज भेजने और अनुरोधों को मैनेज करने की ज़रूरत होगी. ऐसा करने के लिए, किसी वेबव्यू में कस्टम JavaScript कोड इंजेक्ट करें. इससे, वेब कॉन्टेंट के व्यवहार में बदलाव किया जा सकता है और नेटिव Android कोड के साथ इंटरैक्ट किया जा सकता है.

JavaScript इंजेक्शन

यहां दिया गया JavaScript कोड, वेबव्यू और Android ऐप्लिकेशन के बीच कम्यूनिकेशन की सुविधा देता है. यह navigator.credentials.create() और navigator.credentials.get() तरीकों को बदल देता है. इन तरीकों का इस्तेमाल, रजिस्टर करने और पुष्टि करने के लिए, पहले बताए गए WebAuthn API करता है.

अपने ऐप्लिकेशन में इस JavaScript कोड के छोटे किए गए वर्शन का इस्तेमाल करें.

पासकी के लिए एक लिसनर बनाना

ऐसी PasskeyWebListener क्लास सेट अप करें जो JavaScript के साथ कम्यूनिकेशन मैनेज करती हो. इस क्लास को WebViewCompat.WebMessageListener से इनहेरिट करना चाहिए. इस क्लास को JavaScript से मैसेज मिलते हैं और यह Android ऐप्लिकेशन में ज़रूरी कार्रवाइयां करती है.

नीचे दिए गए सेक्शन में, PasskeyWebListener क्लास के स्ट्रक्चर के साथ-साथ अनुरोधों और जवाबों को मैनेज करने के बारे में बताया गया है.

पुष्टि करने के अनुरोध को मैनेज करना

WebAuthn navigator.credentials.create() या navigator.credentials.get() ऑपरेशन के अनुरोधों को मैनेज करने के लिए, PasskeyWebListener क्लास के onPostMessage तरीके को तब कॉल किया जाता है, जब JavaScript कोड, Android ऐप्लिकेशन को मैसेज भेजता है:

// 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 और handleGetFlow के लिए, GitHub पर दिया गया उदाहरण देखें.

जवाब मैनेज करना

नेटिव ऐप्लिकेशन से वेब पेज पर भेजे गए रिस्पॉन्स को मैनेज करने के लिए, JavaScriptReplyChannel में JavaScriptReplyProxy जोड़ें.

// 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?)
}

नेटिव ऐप्लिकेशन से होने वाली सभी गड़बड़ियों को पक्का तौर पर पकड़ें और उन्हें JavaScript साइड पर भेजें.

वेबव्यू के साथ इंटिग्रेट करना

इस सेक्शन में, वेबव्यू इंटिग्रेशन सेट अप करने का तरीका बताया गया है.

वेबव्यू को शुरू करना

अपने Android ऐप्लिकेशन की गतिविधि में, WebView को शुरू करें और उससे जुड़ा WebViewClient सेट अप करें. WebViewClient, WebView में इंजेक्ट किए गए JavaScript कोड के साथ कम्यूनिकेशन मैनेज करता है.

वेबव्यू सेट अप करें और क्रेडेंशियल मैनेजर को कॉल करें:

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

नया वेबव्यू क्लाइंट ऑब्जेक्ट बनाएं और वेब पेज में JavaScript इंजेक्ट करें:

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

वेब मैसेज लिसनर सेट अप करना

JavaScript और Android ऐप्लिकेशन के बीच मैसेज पोस्ट करने की अनुमति देने के लिए, WebViewCompat.addWebMessageListener तरीके का इस्तेमाल करके वेब मैसेज लिसनर सेट अप करें.

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

वेब इंटिग्रेशन

वेब इंटिग्रेशन चेकआउट बनाने का तरीका जानने के लिए, बिना पासवर्ड के लॉगिन करने के लिए पासकी बनाएं और फ़ॉर्म में अपने-आप जानकारी भरने की सुविधा की मदद से पासकी से साइन इन करें.

जांच करना और डिप्लॉय करना

Android ऐप्लिकेशन, वेब पेज, और बैकएंड के बीच सही कम्यूनिकेशन पक्का करने के लिए, पूरे फ़्लो की जांच कंट्रोल किए गए माहौल में करें.

इंटिग्रेट किए गए समाधान को प्रोडक्शन में डिप्लॉय करें. साथ ही, यह पक्का करें कि बैकएंड, आने वाले रजिस्ट्रेशन और पुष्टि के अनुरोधों को मैनेज कर सके. बैकएंड कोड को रजिस्ट्रेशन (बनाना) और पुष्टि (पाना) की प्रोसेस के लिए, शुरुआती JSON जनरेट करना चाहिए. यह वेब पेज से मिले जवाबों की पुष्टि भी करनी चाहिए.

पुष्टि करें कि लागू करने का तरीका, यूज़र एक्सपीरियंस के सुझावों के मुताबिक हो.

अहम जानकारी

  • navigator.credentials.create() और navigator.credentials.get() ऑपरेशन को मैनेज करने के लिए, दिए गए JavaScript कोड का इस्तेमाल करें.
  • PasskeyWebListener क्लास, वेबव्यू में मौजूद Android ऐप्लिकेशन और JavaScript कोड के बीच ब्रिज की तरह काम करती है. यह मैसेज भेजने, बातचीत करने, और ज़रूरी कार्रवाइयों को लागू करने की सुविधा देता है.
  • दिए गए कोड स्निपेट को अपने प्रोजेक्ट के स्ट्रक्चर, नाम रखने के लिए इस्तेमाल होने वाले नियमों, और अपनी ज़रूरतों के मुताबिक बनाएं.
  • नेटिव ऐप्लिकेशन साइड पर गड़बड़ियां पकड़ें और उन्हें JavaScript साइड पर भेजें.

इस गाइड का पालन करके और वेबव्यू का इस्तेमाल करने वाले अपने Android ऐप्लिकेशन में क्रेडेंशियल मैनेजर एपीआई को इंटिग्रेट करके, अपने उपयोगकर्ताओं को पासकी की मदद से सुरक्षित और आसान लॉगिन अनुभव दिया जा सकता है. साथ ही, उनके क्रेडेंशियल को बेहतर तरीके से मैनेज किया जा सकता है.