Skip to content

Commit

Permalink
[WebAuthn] Add Conditional UI creds to TTF bottom sheet
Browse files Browse the repository at this point in the history
This is the second CL for adding Touch-to-Fill integration for WebAuthn
Conditional UI. With this change, TouchToFillBridge takes a WebAuthn
credential list and any entries in it are populated in the list
on the sheet.

This still adds the field to non-WebAuthn credential sheet items.
Specific WebAuthn elements will be added later.

This feature is guarded by
--enable-features=WebAuthnticationConditionalUI.

Bug: 1318942
Change-Id: I9b2ae44425a03d2887d167521b5525279cacf49e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3614251
Reviewed-by: Friedrich Horschig <fhorschig@chromium.org>
Commit-Queue: Ken Buchanan <kenrb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1001336}
  • Loading branch information
kenrb authored and Chromium LUCI CQ committed May 10, 2022
1 parent 72f43ac commit 3d4e272
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ private static void insertWebAuthnCredential(
}

@CalledByNative
private void showCredentials(
GURL url, boolean isOriginSecure, Credential[] credentials, boolean submitCredential) {
mTouchToFillComponent.showCredentials(
url, isOriginSecure, Arrays.asList(credentials), submitCredential);
private void showCredentials(GURL url, boolean isOriginSecure, Credential[] credentials,
WebAuthnCredential[] webAuthnCredentials, boolean submitCredential) {
mTouchToFillComponent.showCredentials(url, isOriginSecure, Arrays.asList(credentials),
Arrays.asList(webAuthnCredentials), submitCredential);
}

@Override
Expand All @@ -96,6 +96,13 @@ public void onCredentialSelected(Credential credential) {
}
}

@Override
public void onWebAuthnCredentialSelected(WebAuthnCredential credential) {
if (mNativeView != 0) {
TouchToFillBridgeJni.get().onWebAuthnCredentialSelected(mNativeView, credential);
}
}

@NativeMethods
interface Natives {
void onCredentialSelected(long nativeTouchToFillViewImpl, Credential credential);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.chrome.browser.touch_to_fill.data.WebAuthnCredential;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.ui.modelutil.PropertyModel;
Expand Down Expand Up @@ -38,8 +39,9 @@ public void initialize(Context context, BottomSheetController sheetController,

@Override
public void showCredentials(GURL url, boolean isOriginSecure, List<Credential> credentials,
boolean triggerSubmission) {
mMediator.showCredentials(url, isOriginSecure, credentials, triggerSubmission);
List<WebAuthnCredential> webAuthnCredentials, boolean triggerSubmission) {
mMediator.showCredentials(
url, isOriginSecure, credentials, webAuthnCredentials, triggerSubmission);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ON_CLICK_MANAGE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.SHEET_ITEMS;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VISIBLE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.WebAuthnCredentialProperties.ON_WEBAUTHN_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.WebAuthnCredentialProperties.WEBAUTHN_CREDENTIAL;

import androidx.annotation.Px;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillComponent.UserAction;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.WebAuthnCredentialProperties;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.chrome.browser.touch_to_fill.data.WebAuthnCredential;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.embedder_support.util.Origin;
Expand Down Expand Up @@ -55,6 +59,7 @@ class TouchToFillMediator {
private LargeIconBridge mLargeIconBridge;
private @Px int mDesiredIconSize;
private List<Credential> mCredentials;
private List<WebAuthnCredential> mWebAuthnCredentials;

void initialize(TouchToFillComponent.Delegate delegate, PropertyModel model,
LargeIconBridge largeIconBridge, @Px int desiredIconSize) {
Expand All @@ -66,7 +71,7 @@ void initialize(TouchToFillComponent.Delegate delegate, PropertyModel model,
}

void showCredentials(GURL url, boolean isOriginSecure, List<Credential> credentials,
boolean triggerSubmission) {
List<WebAuthnCredential> webAuthnCredentials, boolean triggerSubmission) {
assert credentials != null;
mModel.set(ON_CLICK_MANAGE, this::onManagePasswordSelected);

Expand All @@ -90,8 +95,17 @@ void showCredentials(GURL url, boolean isOriginSecure, List<Credential> credenti
for (Credential credential : credentials) {
final PropertyModel model = createModel(credential, triggerSubmission);
sheetItems.add(new ListItem(TouchToFillProperties.ItemType.CREDENTIAL, model));
if (shouldCreateConfirmationButton(credentials, webAuthnCredentials)) {
sheetItems.add(new ListItem(TouchToFillProperties.ItemType.FILL_BUTTON, model));
}
requestIconOrFallbackImage(model, url);
if (shouldCreateConfirmationButton(credentials)) {
}

mWebAuthnCredentials = webAuthnCredentials;
for (WebAuthnCredential credential : webAuthnCredentials) {
final PropertyModel model = createWebAuthnModel(credential);
sheetItems.add(new ListItem(TouchToFillProperties.ItemType.WEBAUTHN_CREDENTIAL, model));
if (shouldCreateConfirmationButton(credentials, webAuthnCredentials)) {
sheetItems.add(new ListItem(TouchToFillProperties.ItemType.FILL_BUTTON, model));
}
}
Expand Down Expand Up @@ -124,21 +138,32 @@ private String getIconOrigin(String credentialOrigin, GURL siteUrl) {
return o != null && !o.uri().isOpaque() ? credentialOrigin : siteUrl.getSpec();
}

private void onSelectedCredential(Credential credential) {
mModel.set(VISIBLE, false);
if (mCredentials.size() > 1) {
private void reportCredentialSelection(int userAction, int index) {
if (mCredentials.size() + mWebAuthnCredentials.size() > 1) {
// We only record this histogram in case multiple credentials were shown to the user.
// Otherwise the single credential case where position should always be 0 will dominate
// the recording.
RecordHistogram.recordCount100Histogram(
UMA_TOUCH_TO_FILL_CREDENTIAL_INDEX, mCredentials.indexOf(credential));
RecordHistogram.recordCount100Histogram(UMA_TOUCH_TO_FILL_CREDENTIAL_INDEX, index);
}

RecordHistogram.recordEnumeratedHistogram(UMA_TOUCH_TO_FILL_USER_ACTION,
UserAction.SELECT_CREDENTIAL, UserAction.MAX_VALUE + 1);
RecordHistogram.recordEnumeratedHistogram(
UMA_TOUCH_TO_FILL_USER_ACTION, userAction, UserAction.MAX_VALUE + 1);
}

private void onSelectedCredential(Credential credential) {
mModel.set(VISIBLE, false);
reportCredentialSelection(UserAction.SELECT_CREDENTIAL, mCredentials.indexOf(credential));
mDelegate.onCredentialSelected(credential);
}

private void onSelectedWebAuthnCredential(WebAuthnCredential credential) {
mModel.set(VISIBLE, false);
// The index assumes WebAuthn credentials are listed after password credentials.
reportCredentialSelection(UserAction.SELECT_WEBAUTHN_CREDENTIAL,
mCredentials.size() + mWebAuthnCredentials.indexOf(credential));
mDelegate.onWebAuthnCredentialSelected(credential);
}

public void onDismissed(@StateChangeReason int reason) {
if (!mModel.get(VISIBLE)) return; // Dismiss only if not dismissed yet.
mModel.set(VISIBLE, false);
Expand All @@ -160,8 +185,9 @@ private void onManagePasswordSelected() {
* @param credentials The available credentials. Show the confirmation for a lone credential.
* @return True if a confirmation button should be shown at the end of the bottom sheet.
*/
private boolean shouldCreateConfirmationButton(List<Credential> credentials) {
return credentials.size() == 1;
private boolean shouldCreateConfirmationButton(
List<Credential> credentials, List<WebAuthnCredential> webauthnCredentials) {
return credentials.size() + webauthnCredentials.size() == 1;
}

private PropertyModel createModel(Credential credential, boolean triggerSubmission) {
Expand All @@ -173,4 +199,11 @@ private PropertyModel createModel(Credential credential, boolean triggerSubmissi
.with(SHOW_SUBMIT_BUTTON, triggerSubmission)
.build();
}

private PropertyModel createWebAuthnModel(WebAuthnCredential credential) {
return new PropertyModel.Builder(WebAuthnCredentialProperties.ALL_KEYS)
.with(WEBAUTHN_CREDENTIAL, credential)
.with(ON_WEBAUTHN_CLICK_LISTENER, this::onSelectedWebAuthnCredential)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import org.chromium.base.Callback;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.chrome.browser.touch_to_fill.data.WebAuthnCredential;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyKey;
Expand Down Expand Up @@ -81,6 +82,22 @@ static class FaviconOrFallback {
private CredentialProperties() {}
}

/**
* Properties for a Web Authentication credential entry in TouchToFill sheet.
*/
static class WebAuthnCredentialProperties {
static final PropertyModel
.ReadableObjectPropertyKey<WebAuthnCredential> WEBAUTHN_CREDENTIAL =
new PropertyModel.ReadableObjectPropertyKey<>("webauthn_credential");
static final PropertyModel.ReadableObjectPropertyKey<Callback<WebAuthnCredential>>
ON_WEBAUTHN_CLICK_LISTENER =
new PropertyModel.ReadableObjectPropertyKey<>("on_webauthn_click_listener");

static final PropertyKey[] ALL_KEYS = {WEBAUTHN_CREDENTIAL, ON_WEBAUTHN_CLICK_LISTENER};

private WebAuthnCredentialProperties() {}
}

/**
* Properties defined here reflect the visible state of the header in the TouchToFill sheet.
*/
Expand All @@ -102,7 +119,8 @@ static class HeaderProperties {
private HeaderProperties() {}
}

@IntDef({ItemType.HEADER, ItemType.CREDENTIAL, ItemType.FILL_BUTTON})
@IntDef({ItemType.HEADER, ItemType.CREDENTIAL, ItemType.WEBAUTHN_CREDENTIAL,
ItemType.FILL_BUTTON})
@Retention(RetentionPolicy.SOURCE)
@interface ItemType {
/**
Expand All @@ -115,10 +133,15 @@ private HeaderProperties() {}
*/
int CREDENTIAL = 2;

/**
* A section containing information about a WebAuthn credential.
*/
int WEBAUTHN_CREDENTIAL = 3;

/**
* The fill button at the end of the sheet that filling more obvious for one suggestion.
*/
int FILL_BUTTON = 3;
int FILL_BUTTON = 4;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ON_CLICK_MANAGE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.SHEET_ITEMS;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VISIBLE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.WebAuthnCredentialProperties.ON_WEBAUTHN_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.WebAuthnCredentialProperties.WEBAUTHN_CREDENTIAL;
import static org.chromium.components.embedder_support.util.UrlUtilities.stripScheme;

import android.content.Context;
Expand All @@ -34,6 +36,7 @@
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ItemType;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.chrome.browser.touch_to_fill.data.WebAuthnCredential;
import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.ui.modelutil.MVCListAdapter;
Expand Down Expand Up @@ -90,6 +93,11 @@ private static TouchToFillViewHolder createViewHolder(
case ItemType.CREDENTIAL:
return new TouchToFillViewHolder(parent, R.layout.touch_to_fill_credential_item,
TouchToFillViewBinder::bindCredentialView);
case ItemType.WEBAUTHN_CREDENTIAL:
// TODO(https://crbug.com/1318942): The specific UI for this is forthcoming, but
// for now it is just filling into the existing credential item layout.
return new TouchToFillViewHolder(parent, R.layout.touch_to_fill_credential_item,
TouchToFillViewBinder::bindWebAuthnCredentialView);
case ItemType.FILL_BUTTON:
return new TouchToFillViewHolder(parent, R.layout.touch_to_fill_fill_button,
TouchToFillViewBinder::bindFillButtonView);
Expand Down Expand Up @@ -154,6 +162,26 @@ private static void bindCredentialView(
}
}

/**
* Called whenever a WebAuthn credential is bound to this view holder.
* @param model The model containing the data for the view
* @param view The view to be bound
* @param propertyKey The key of the property to be bound
*/
private static void bindWebAuthnCredentialView(
PropertyModel model, View view, PropertyKey propertyKey) {
WebAuthnCredential credential = model.get(WEBAUTHN_CREDENTIAL);
if (propertyKey == ON_WEBAUTHN_CLICK_LISTENER) {
view.setOnClickListener(
clickedView -> model.get(ON_WEBAUTHN_CLICK_LISTENER).onResult(credential));
} else if (propertyKey == WEBAUTHN_CREDENTIAL) {
TextView usernameText = view.findViewById(R.id.username);
usernameText.setText(credential.getUsername());
} else {
assert false : "Unhandled update to property:" + propertyKey;
}
}

/**
* Called whenever a fill button for a single credential is bound to this view holder.
* @param model The model containing the data for the view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import androidx.annotation.IntDef;

import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.chrome.browser.touch_to_fill.data.WebAuthnCredential;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.url.GURL;

Expand All @@ -30,13 +31,14 @@ public interface TouchToFillComponent {
* TODO(crbug.com/1013134): Deduplicate the Java and C++ enum.
*/
@IntDef({UserAction.SELECT_CREDENTIAL, UserAction.DISMISS, UserAction.SELECT_MANAGE_PASSWORDS,
UserAction.MAX_VALUE})
UserAction.SELECT_WEBAUTHN_CREDENTIAL, UserAction.MAX_VALUE})
@Retention(RetentionPolicy.SOURCE)
@interface UserAction {
int SELECT_CREDENTIAL = 0;
int DISMISS = 1;
int SELECT_MANAGE_PASSWORDS = 2;
int MAX_VALUE = SELECT_MANAGE_PASSWORDS;
int SELECT_WEBAUTHN_CREDENTIAL = 3;
int MAX_VALUE = SELECT_WEBAUTHN_CREDENTIAL;
}

/**
Expand All @@ -46,9 +48,17 @@ public interface TouchToFillComponent {
interface Delegate {
/**
* Called when the user select one of the credentials shown in the TouchToFillComponent.
* @param credential The selected {@link Credential}.
*/
void onCredentialSelected(Credential credential);

/**
* Called when the user select one of the Web Authentication credentials shown in the
* TouchToFillComponent.
* @param credential The selected {@link WebAuthnCredential}.
*/
void onWebAuthnCredentialSelected(WebAuthnCredential credential);

/**
* Called when the user dismisses the TouchToFillComponent. Not called if a suggestion was
* selected.
Expand Down Expand Up @@ -78,5 +88,5 @@ interface Delegate {
* after filling.
*/
void showCredentials(GURL url, boolean isOriginSecure, List<Credential> credentials,
boolean triggerSubmission);
List<WebAuthnCredential> webauthnCredentials, boolean triggerSubmission);
}

0 comments on commit 3d4e272

Please sign in to comment.