chromium/components/webauthn/android/java/src/org/chromium/components/webauthn/cred_man/CredManHelper.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.webauthn.cred_man;

import static org.chromium.components.webauthn.WebauthnModeProvider.is;

import android.content.Context;
import android.credentials.CreateCredentialException;
import android.credentials.CreateCredentialRequest;
import android.credentials.CreateCredentialResponse;
import android.credentials.CredentialManager;
import android.credentials.GetCredentialException;
import android.credentials.GetCredentialRequest;
import android.credentials.GetCredentialResponse;
import android.credentials.PrepareGetCredentialResponse;
import android.os.Build;
import android.os.Bundle;
import android.os.OutcomeReceiver;
import android.os.SystemClock;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.blink.mojom.AuthenticatorStatus;
import org.chromium.blink.mojom.GetAssertionAuthenticatorResponse;
import org.chromium.blink.mojom.MakeCredentialAuthenticatorResponse;
import org.chromium.blink.mojom.PublicKeyCredentialCreationOptions;
import org.chromium.blink.mojom.PublicKeyCredentialRequestOptions;
import org.chromium.components.webauthn.AuthenticationContextProvider;
import org.chromium.components.webauthn.Barrier;
import org.chromium.components.webauthn.Fido2CredentialRequest.ConditionalUiState;
import org.chromium.components.webauthn.Fido2CredentialRequestJni;
import org.chromium.components.webauthn.GetAssertionResponseCallback;
import org.chromium.components.webauthn.MakeCredentialResponseCallback;
import org.chromium.components.webauthn.WebauthnBrowserBridge;
import org.chromium.components.webauthn.WebauthnMode;
import org.chromium.components.webauthn.WebauthnModeProvider;
import org.chromium.components.webauthn.cred_man.CredManMetricsHelper.CredManCreateRequestEnum;
import org.chromium.components.webauthn.cred_man.CredManMetricsHelper.CredManGetRequestEnum;
import org.chromium.components.webauthn.cred_man.CredManMetricsHelper.CredManPrepareRequestEnum;
import org.chromium.content_public.browser.RenderFrameHost;

import java.nio.ByteBuffer;

public class CredManHelper {
    // These two values are formed differently because they come from the
    // Jetpack library, not the framework.
    @VisibleForTesting
    public static final String CRED_MAN_EXCEPTION_CREATE_CREDENTIAL_TYPE_INVALID_STATE_ERROR =
            "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_DOM_EXCEPTION/androidx.credentials.TYPE_INVALID_STATE_ERROR";

    protected static final String CRED_MAN_PREFIX = "androidx.credentials.";
    protected static final String TYPE_PASSKEY = CRED_MAN_PREFIX + "TYPE_PUBLIC_KEY_CREDENTIAL";

    private static final String TAG = "CredManHelper";

    private Callback<Integer> mErrorCallback;
    private Barrier mBarrier;
    private boolean mPlayServicesAvailable;
    private boolean mRequestPasswords;
    private final AuthenticationContextProvider mAuthenticationContextProvider;
    private final WebauthnBrowserBridge.Provider mBridgeProvider;
    private byte[] mClientDataJson;
    private ConditionalUiState mConditionalUiState = ConditionalUiState.NONE;
    private CredManRequestDecorator mCredManRequestDecorator;
    private CredManMetricsHelper mMetricsHelper;
    private Runnable mNoCredentialsFallback;

    public CredManHelper(
            AuthenticationContextProvider authenticationContextProvider,
            WebauthnBrowserBridge.Provider bridgeProvider,
            boolean playServicesAvailable) {
        mMetricsHelper = new CredManMetricsHelper();
        mAuthenticationContextProvider = authenticationContextProvider;
        mBridgeProvider = bridgeProvider;
        mPlayServicesAvailable = playServicesAvailable;
        mCredManRequestDecorator =
                WebauthnModeProvider.getInstance()
                        .getCredManRequestDecorator(authenticationContextProvider.getWebContents());
    }

    /** Create a credential using the Android 14 CredMan API. */
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    public int startMakeRequest(
            PublicKeyCredentialCreationOptions options,
            String originString,
            @Nullable byte[] clientDataJson,
            byte[] clientDataHash,
            MakeCredentialResponseCallback makeCallback,
            Callback<Integer> errorCallback) {
        mClientDataJson = clientDataJson;
        final String requestAsJson =
                Fido2CredentialRequestJni.get().createOptionsToJson(options.serialize());

        OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> receiver =
                new OutcomeReceiver<>() {
                    @Override
                    public void onError(CreateCredentialException exception) {
                        String errorType = exception.getType();
                        Log.e(
                                TAG,
                                "CredMan CreateCredential call failed: %s",
                                errorType + " (" + exception.getMessage() + ")");
                        if (errorType.equals(CreateCredentialException.TYPE_USER_CANCELED)) {
                            errorCallback.onResult(AuthenticatorStatus.NOT_ALLOWED_ERROR);
                            mMetricsHelper.recordCredManCreateRequestHistogram(
                                    CredManCreateRequestEnum.CANCELLED);
                        } else if (errorType.equals(
                                CRED_MAN_EXCEPTION_CREATE_CREDENTIAL_TYPE_INVALID_STATE_ERROR)) {
                            errorCallback.onResult(AuthenticatorStatus.CREDENTIAL_EXCLUDED);
                            // This is successful from the point of view of the user.
                            mMetricsHelper.recordCredManCreateRequestHistogram(
                                    CredManCreateRequestEnum.SUCCESS);
                        } else {
                            // Includes:
                            //  * CreateCredentialException.TYPE_UNKNOWN
                            //  * CreateCredentialException.TYPE_NO_CREATE_OPTIONS
                            //  * CreateCredentialException.TYPE_INTERRUPTED
                            errorCallback.onResult(AuthenticatorStatus.UNKNOWN_ERROR);
                            mMetricsHelper.recordCredManCreateRequestHistogram(
                                    CredManCreateRequestEnum.FAILURE);
                        }
                    }

                    @Override
                    public void onResult(CreateCredentialResponse createCredentialResponse) {
                        Bundle data;
                        data = createCredentialResponse.getData();
                        String json =
                                data.getString(
                                        CRED_MAN_PREFIX + "BUNDLE_KEY_REGISTRATION_RESPONSE_JSON");
                        byte[] responseSerialized =
                                Fido2CredentialRequestJni.get()
                                        .makeCredentialResponseFromJson(json);
                        if (responseSerialized == null) {
                            Log.e(
                                    TAG,
                                    "Failed to convert response from CredMan to Mojo object: %s",
                                    json);
                            errorCallback.onResult(AuthenticatorStatus.UNKNOWN_ERROR);
                            mMetricsHelper.recordCredManCreateRequestHistogram(
                                    CredManCreateRequestEnum.FAILURE);
                            return;
                        }
                        MakeCredentialAuthenticatorResponse response;
                        try {
                            response =
                                    MakeCredentialAuthenticatorResponse.deserialize(
                                            ByteBuffer.wrap(responseSerialized));
                        } catch (org.chromium.mojo.bindings.DeserializationException e) {
                            logDeserializationException(e);
                            errorCallback.onResult(AuthenticatorStatus.UNKNOWN_ERROR);
                            mMetricsHelper.recordCredManCreateRequestHistogram(
                                    CredManCreateRequestEnum.FAILURE);
                            return;
                        }
                        response.info.clientDataJson = mClientDataJson;
                        response.echoCredProps = options.credProps;
                        makeCallback.onRegisterResponse(AuthenticatorStatus.SUCCESS, response);
                        mMetricsHelper.recordCredManCreateRequestHistogram(
                                CredManCreateRequestEnum.SUCCESS);
                    }
                };

        final CredManCreateCredentialRequestHelper requestHelper =
                new CredManCreateCredentialRequestHelper.Builder(requestAsJson, clientDataHash)
                        .setUserId(options.user.id)
                        .setOrigin(originString)
                        .build();
        final CreateCredentialRequest request =
                requestHelper.getCreateCredentialRequest(mCredManRequestDecorator);
        Context context = mAuthenticationContextProvider.getContext();
        final CredentialManager manager =
                (CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
        manager.createCredential(context, request, null, context.getMainExecutor(), receiver);
        mMetricsHelper.recordCredManCreateRequestHistogram(CredManCreateRequestEnum.SENT_REQUEST);
        return AuthenticatorStatus.SUCCESS;
    }

    /** Queries credential availability using the Android 14 CredMan API. */
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    public void startPrefetchRequest(
            PublicKeyCredentialRequestOptions options,
            String originString,
            @Nullable byte[] clientDataJson,
            byte[] clientDataHash,
            GetAssertionResponseCallback getCallback,
            Callback<Integer> errorCallback,
            Barrier barrier,
            boolean ignoreGpm) {
        long startTimeMs = SystemClock.elapsedRealtime();
        mErrorCallback = errorCallback;
        mBarrier = barrier;

        RenderFrameHost frameHost = mAuthenticationContextProvider.getRenderFrameHost();
        OutcomeReceiver<PrepareGetCredentialResponse, GetCredentialException> receiver =
                new OutcomeReceiver<>() {
                    @Override
                    public void onError(GetCredentialException e) {
                        assert mConditionalUiState != ConditionalUiState.WAITING_FOR_SELECTION;
                        // prepareGetCredential uses getCredentialException, but it cannot be user
                        // cancelled so all errors map to UNKNOWN_ERROR.
                        Log.e(
                                TAG,
                                "CredMan prepareGetCredential call failed: %s",
                                e.getType() + " (" + e.getMessage() + ")");
                        mConditionalUiState = ConditionalUiState.NONE;
                        mBarrier.onCredManFailed(AuthenticatorStatus.UNKNOWN_ERROR);
                        mMetricsHelper.recordCredmanPrepareRequestHistogram(
                                CredManPrepareRequestEnum.FAILURE);
                    }

                    @Override
                    public void onResult(
                            PrepareGetCredentialResponse prepareGetCredentialResponse) {
                        if (mConditionalUiState == ConditionalUiState.CANCEL_PENDING) {
                            // The request was completed synchronously when the cancellation was
                            // received.
                            mConditionalUiState = ConditionalUiState.NONE;
                            mBridgeProvider.getBridge().cleanupCredManRequest(frameHost);
                            return;
                        }
                        if (mConditionalUiState != ConditionalUiState.WAITING_FOR_CREDENTIAL_LIST) {
                            Log.e(
                                    TAG,
                                    "CredMan prepareGetCredential request received a response while"
                                            + " the state is "
                                            + mConditionalUiState
                                            + ". Ignoring the response.");
                            return;
                        }
                        boolean hasPublicKeyCredentials =
                                prepareGetCredentialResponse.hasCredentialResults(TYPE_PASSKEY);
                        boolean hasAuthenticationResults =
                                prepareGetCredentialResponse.hasAuthenticationResults();

                        mConditionalUiState = ConditionalUiState.WAITING_FOR_SELECTION;

                        mBarrier.onCredManSuccessful(
                                () -> {
                                    mBridgeProvider
                                            .getBridge()
                                            .onCredManConditionalRequestPending(
                                                    frameHost,
                                                    hasPublicKeyCredentials
                                                            || hasAuthenticationResults,
                                                    (requestPasswords) -> {
                                                        setRequestPasswords(requestPasswords);
                                                        startGetRequest(
                                                                options,
                                                                originString,
                                                                clientDataJson,
                                                                clientDataHash,
                                                                getCallback,
                                                                errorCallback,
                                                                ignoreGpm);
                                                    });
                                });
                        mMetricsHelper.recordCredmanPrepareRequestHistogram(
                                hasPublicKeyCredentials
                                        ? CredManPrepareRequestEnum.SUCCESS_HAS_RESULTS
                                        : CredManPrepareRequestEnum.SUCCESS_NO_RESULTS);
                        mMetricsHelper.recordCredmanPrepareRequestDuration(
                                SystemClock.elapsedRealtime() - startTimeMs,
                                hasPublicKeyCredentials);
                    }
                };

        mConditionalUiState = ConditionalUiState.WAITING_FOR_CREDENTIAL_LIST;
        final GetCredentialRequest getCredentialRequest =
                buildGetCredentialRequest(
                        options,
                        originString,
                        clientDataHash,
                        /* requestPasswords= */ false,
                        /* preferImmediatelyAvailable= */ false,
                        /* ignoreGpm= */ ignoreGpm);
        if (getCredentialRequest == null) {
            mConditionalUiState = ConditionalUiState.NONE;
            mMetricsHelper.recordCredmanPrepareRequestHistogram(
                    CredManPrepareRequestEnum.COULD_NOT_SEND_REQUEST);
            mBarrier.onCredManFailed(AuthenticatorStatus.NOT_ALLOWED_ERROR);
            return;
        }

        Context context = mAuthenticationContextProvider.getContext();
        final CredentialManager manager =
                (CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
        manager.prepareGetCredential(
                getCredentialRequest, null, context.getMainExecutor(), receiver);
        mMetricsHelper.recordCredmanPrepareRequestHistogram(CredManPrepareRequestEnum.SENT_REQUEST);
    }

    public void setNoCredentialsFallback(Runnable noCredentialsFallback) {
        mNoCredentialsFallback = noCredentialsFallback;
    }

    /** Gets the credential using the Android 14 CredMan API. */
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    public int startGetRequest(
            PublicKeyCredentialRequestOptions options,
            String originString,
            @Nullable byte[] clientDataJson,
            byte[] clientDataHash,
            GetAssertionResponseCallback getCallback,
            Callback<Integer> errorCallback,
            boolean ignoreGpm) {
        mErrorCallback = errorCallback;
        mClientDataJson = clientDataJson;
        RenderFrameHost frameHost = mAuthenticationContextProvider.getRenderFrameHost();

        // The Android 14 APIs have to be called via reflection until Chromium
        // builds with the Android 14 SDK by default.
        OutcomeReceiver<GetCredentialResponse, GetCredentialException> receiver =
                new OutcomeReceiver<>() {
                    @Override
                    public void onError(GetCredentialException getCredentialException) {
                        String errorType = getCredentialException.getType();
                        Log.e(
                                TAG,
                                "CredMan getCredential call failed: %s",
                                errorType + " (" + getCredentialException.getMessage() + ")");
                        notifyBrowserOnCredManClosed(false);
                        if (mConditionalUiState == ConditionalUiState.CANCEL_PENDING) {
                            mConditionalUiState = ConditionalUiState.NONE;
                            mBridgeProvider.getBridge().cleanupCredManRequest(frameHost);
                            mBarrier.onCredManCancelled();
                            return;
                        }
                        if (errorType.equals(GetCredentialException.TYPE_USER_CANCELED)) {
                            if (mConditionalUiState == ConditionalUiState.NONE) {
                                mErrorCallback.onResult(AuthenticatorStatus.NOT_ALLOWED_ERROR);
                            }

                            mMetricsHelper.reportGetCredentialMetrics(
                                    CredManGetRequestEnum.CANCELLED, mConditionalUiState);
                        } else if (errorType.equals(GetCredentialException.TYPE_NO_CREDENTIAL)) {
                            // This was a modal request and no credentials were found.
                            // The UI that CredMan would show in this case is unsuitable
                            // so the request is forwarded to Play Services instead. Play
                            // Services shouldn't find any credentials either, but it
                            // will show a bottomsheet to that effect.
                            assert mConditionalUiState == ConditionalUiState.NONE;
                            assert !options.isConditional;

                            mMetricsHelper.reportGetCredentialMetrics(
                                    CredManGetRequestEnum.NO_CREDENTIAL_FOUND, mConditionalUiState);
                            if (mNoCredentialsFallback != null) {
                                mNoCredentialsFallback.run();
                            } else if (mConditionalUiState == ConditionalUiState.NONE) {
                                mErrorCallback.onResult(AuthenticatorStatus.NOT_ALLOWED_ERROR);
                            }
                        } else {
                            // Includes:
                            //  * GetCredentialException.TYPE_UNKNOWN
                            //  * GetCredentialException.TYPE_NO_CREATE_OPTIONS
                            //  * GetCredentialException.TYPE_INTERRUPTED
                            mErrorCallback.onResult(AuthenticatorStatus.UNKNOWN_ERROR);
                            mMetricsHelper.reportGetCredentialMetrics(
                                    CredManGetRequestEnum.FAILURE, mConditionalUiState);
                        }
                        mConditionalUiState =
                                options.isConditional
                                        ? ConditionalUiState.WAITING_FOR_SELECTION
                                        : ConditionalUiState.NONE;
                    }

                    @Override
                    public void onResult(GetCredentialResponse getCredentialResponse) {
                        if (mConditionalUiState == ConditionalUiState.CANCEL_PENDING) {
                            notifyBrowserOnCredManClosed(false);
                            mConditionalUiState = ConditionalUiState.NONE;
                            mBridgeProvider.getBridge().cleanupCredManRequest(frameHost);
                            mBarrier.onCredManCancelled();
                            return;
                        }
                        Bundle data = getCredentialResponse.getCredential().getData();
                        String type = getCredentialResponse.getCredential().getType();

                        if (!TYPE_PASSKEY.equals(type)) {
                            mBridgeProvider
                                    .getBridge()
                                    .onPasswordCredentialReceived(
                                            frameHost,
                                            data.getString(CRED_MAN_PREFIX + "BUNDLE_KEY_ID"),
                                            data.getString(
                                                    CRED_MAN_PREFIX + "BUNDLE_KEY_PASSWORD"));
                            mMetricsHelper.reportGetCredentialMetrics(
                                    CredManGetRequestEnum.SUCCESS_PASSWORD, mConditionalUiState);
                            return;
                        }

                        String json =
                                data.getString(
                                        CRED_MAN_PREFIX
                                                + "BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON");
                        byte[] responseSerialized =
                                Fido2CredentialRequestJni.get().getCredentialResponseFromJson(json);
                        if (responseSerialized == null) {
                            Log.e(
                                    TAG,
                                    "Failed to convert response from CredMan to Mojo object: %s",
                                    json);
                            mMetricsHelper.reportGetCredentialMetrics(
                                    CredManGetRequestEnum.FAILURE, mConditionalUiState);
                            mConditionalUiState =
                                    options.isConditional
                                            ? ConditionalUiState.WAITING_FOR_SELECTION
                                            : ConditionalUiState.NONE;
                            notifyBrowserOnCredManClosed(false);
                            mErrorCallback.onResult(AuthenticatorStatus.UNKNOWN_ERROR);
                            return;
                        }

                        GetAssertionAuthenticatorResponse response;
                        try {
                            response =
                                    GetAssertionAuthenticatorResponse.deserialize(
                                            ByteBuffer.wrap(responseSerialized));
                        } catch (org.chromium.mojo.bindings.DeserializationException e) {
                            logDeserializationException(e);
                            mMetricsHelper.reportGetCredentialMetrics(
                                    CredManGetRequestEnum.FAILURE, mConditionalUiState);
                            mConditionalUiState =
                                    options.isConditional
                                            ? ConditionalUiState.WAITING_FOR_SELECTION
                                            : ConditionalUiState.NONE;
                            notifyBrowserOnCredManClosed(false);
                            mErrorCallback.onResult(AuthenticatorStatus.UNKNOWN_ERROR);
                            return;
                        }
                        response.info.clientDataJson = mClientDataJson;
                        response.extensions.echoAppidExtension = options.extensions.appid != null;
                        mConditionalUiState =
                                options.isConditional
                                        ? ConditionalUiState.WAITING_FOR_SELECTION
                                        : ConditionalUiState.NONE;
                        notifyBrowserOnCredManClosed(true);
                        mMetricsHelper.reportGetCredentialMetrics(
                                CredManGetRequestEnum.SUCCESS_PASSKEY, mConditionalUiState);
                        if (frameHost != null) {
                            frameHost.notifyWebAuthnAssertionRequestSucceeded();
                        }
                        getCallback.onSignResponse(AuthenticatorStatus.SUCCESS, response);
                    }
                };

        if (mConditionalUiState == ConditionalUiState.WAITING_FOR_CREDENTIAL_LIST) {
            Log.e(TAG, "Received a second credential selection while the first still in progress.");
            mMetricsHelper.reportGetCredentialMetrics(
                    CredManGetRequestEnum.COULD_NOT_SEND_REQUEST, mConditionalUiState);
            return AuthenticatorStatus.NOT_ALLOWED_ERROR;
        }
        mConditionalUiState =
                options.isConditional
                        ? ConditionalUiState.WAITING_FOR_CREDENTIAL_LIST
                        : ConditionalUiState.NONE;
        final GetCredentialRequest getCredentialRequest =
                buildGetCredentialRequest(
                        options,
                        originString,
                        clientDataHash,
                        mRequestPasswords,
                        shouldPreferImmediatelyAvailable(options),
                        ignoreGpm);
        if (getCredentialRequest == null) {
            mMetricsHelper.reportGetCredentialMetrics(
                    CredManGetRequestEnum.COULD_NOT_SEND_REQUEST, mConditionalUiState);
            mConditionalUiState =
                    options.isConditional
                            ? ConditionalUiState.WAITING_FOR_SELECTION
                            : ConditionalUiState.NONE;
            return AuthenticatorStatus.NOT_ALLOWED_ERROR;
        }
        Context context = mAuthenticationContextProvider.getContext();
        final CredentialManager manager =
                (CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
        manager.getCredential(
                context, getCredentialRequest, null, context.getMainExecutor(), receiver);
        mMetricsHelper.reportGetCredentialMetrics(
                CredManGetRequestEnum.SENT_REQUEST, mConditionalUiState);
        return AuthenticatorStatus.SUCCESS;
    }

    public void cancelConditionalGetAssertion() {
        switch (mConditionalUiState) {
            case WAITING_FOR_CREDENTIAL_LIST:
                mConditionalUiState = ConditionalUiState.CANCEL_PENDING;
                mBarrier.onCredManCancelled();
                break;
            case WAITING_FOR_SELECTION:
                mBridgeProvider
                        .getBridge()
                        .cleanupCredManRequest(mAuthenticationContextProvider.getRenderFrameHost());
                mConditionalUiState = ConditionalUiState.NONE;
                mBarrier.onCredManCancelled();
                break;
            default:
                // No action
        }
    }

    public void setMetricsHelperForTesting(CredManMetricsHelper metricsHelper) {
        mMetricsHelper = metricsHelper;
    }

    void setRequestPasswords(boolean requestPasswords) {
        mRequestPasswords = requestPasswords;
    }

    boolean shouldPreferImmediatelyAvailable(PublicKeyCredentialRequestOptions options) {
        // Chrome renders its own UI when there are no credentials when using CredMan. However, this
        // is not true for WebView or Chrome 3rd party PWM mode - there are no other UIs. Thus
        // they never ask CredMan to skip its UI.
        if (is(mAuthenticationContextProvider.getWebContents(), WebauthnMode.CHROME)) {
            return !options.isConditional;
        }
        return false;
    }

    private void notifyBrowserOnCredManClosed(boolean success) {
        if (mBridgeProvider.getBridge() == null) return;
        mBridgeProvider
                .getBridge()
                .onCredManUiClosed(mAuthenticationContextProvider.getRenderFrameHost(), success);
    }

    /**
     * Construct a CredMan request for credentials.
     *
     * @param options The WebAuthn get() call.
     * @param originString The origin that made the WebAuthn request.
     * @param clientDataHash The hash of the ClientDataJSON to be passed to the CredMan API.
     * @param requestPasswords True if password credentials should also be requested.
     * @param preferImmediatelyAvailable True to make the eventual request fail with a
     *     `NO_CREDENTIAL` error if there are no credentials found.
     * @param ignoreGpm True if Google Password Manager should ignore CredMan requests.
     */
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    private GetCredentialRequest buildGetCredentialRequest(
            PublicKeyCredentialRequestOptions options,
            String originString,
            byte[] clientDataHash,
            boolean requestPasswords,
            boolean preferImmediatelyAvailable,
            boolean ignoreGpm) {
        final String requestAsJson =
                Fido2CredentialRequestJni.get().getOptionsToJson(options.serialize());

        boolean hasAllowCredentials =
                options.allowCredentials != null && options.allowCredentials.length != 0;
        CredManGetCredentialRequestHelper helper =
                new CredManGetCredentialRequestHelper.Builder(
                                requestAsJson,
                                clientDataHash,
                                preferImmediatelyAvailable,
                                hasAllowCredentials,
                                requestPasswords)
                        .setOrigin(originString)
                        .setPlayServicesAvailable(mPlayServicesAvailable)
                        .setIgnoreGpm(ignoreGpm)
                        .setRenderFrameHost(mAuthenticationContextProvider.getRenderFrameHost())
                        .build();
        return helper.getGetCredentialRequest(mCredManRequestDecorator);
    }

    private static void logDeserializationException(Throwable e) {
        Log.e(
                TAG,
                "Failed to parse Mojo object. If this is happening in a test, and"
                    + " authenticator.mojom was updated, then you'll need to update the fake Mojo"
                    + " structures in Fido2ApiTestHelper. Robolectric doesn't support JNI calls so"
                    + " the JNI calls to translate from JSON -> serialized Mojo are mocked out and"
                    + " the responses are hard-coded. If the Mojo structure is updated then the"
                    + " responses also need to be updated. Flip `kUpdateRobolectricTests` in"
                    + " `value_conversions_unittest.cc`, run `component_unittests"
                    + " --gtest_filter=\"WebAuthnentication*\"` and it'll print out updated Java"
                    + " literals for `Fido2ApiTestHelper.java`.",
                e);
    }
}