Skip to content

Add support to debug virtual authenticators #7842

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Dec 11, 2019
5 changes: 5 additions & 0 deletions java/client/src/org/openqa/selenium/remote/DriverCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,9 @@ static CommandPayload SET_CURRENT_WINDOW_SIZE(Dimension targetSize) {
// http://w3c.github.io/webauthn#sctn-automation
String ADD_VIRTUAL_AUTHENTICATOR = "addVirtualAuthenticator";
String REMOVE_VIRTUAL_AUTHENTICATOR = "removeVirtualAuthenticator";
String ADD_CREDENTIAL = "addCredential";
String GET_CREDENTIALS = "getCredentials";
String REMOVE_CREDENTIAL = "removeCredential";
String REMOVE_ALL_CREDENTIALS = "removeAllCredentials";
String SET_USER_VERIFIED = "setUserVerified";
}
55 changes: 54 additions & 1 deletion java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@
import org.openqa.selenium.logging.Logs;
import org.openqa.selenium.logging.NeedsLocalLogs;
import org.openqa.selenium.remote.internal.WebElementToJsonConverter;
import org.openqa.selenium.virtualauthenticator.Credential;
import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticator;
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;

import java.net.URL;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
Expand Down Expand Up @@ -669,7 +671,7 @@ public Mouse getMouse() {
public VirtualAuthenticator addVirtualAuthenticator(VirtualAuthenticatorOptions options) {
String authenticatorId = (String)
execute(DriverCommand.ADD_VIRTUAL_AUTHENTICATOR, options.toMap()).getValue();
return new VirtualAuthenticator(authenticatorId);
return new RemoteVirtualAuthenticator(authenticatorId);
}

@Override
Expand Down Expand Up @@ -1058,6 +1060,57 @@ public void sendKeys(String keysToSend) {
}
}

private class RemoteVirtualAuthenticator implements VirtualAuthenticator {
private final String id;

public RemoteVirtualAuthenticator(final String id) {
this.id = Objects.requireNonNull(id);
}

@Override
public String getId() {
return id;
}

@Override
public void addCredential(Credential credential) {
execute(DriverCommand.ADD_CREDENTIAL,
new ImmutableMap.Builder<String, Object>()
.putAll(credential.toMap())
.put("authenticatorId", id)
.build());
}

@Override
public List<Credential> getCredentials() {
List<Map<String, Object>> response = (List<Map<String, Object>>)
execute(DriverCommand.GET_CREDENTIALS, ImmutableMap.of("authenticatorId", id)).getValue();
return response.stream().map(Credential::fromMap).collect(Collectors.toList());
}

@Override
public void removeCredential(byte[] credentialId) {
removeCredential(Base64.getUrlEncoder().encodeToString(credentialId));
}

@Override
public void removeCredential(String credentialId) {
execute(DriverCommand.REMOVE_CREDENTIAL,
ImmutableMap.of("authenticatorId", id, "credentialId", credentialId)).getValue();
}

@Override
public void removeAllCredentials() {
execute(DriverCommand.REMOVE_ALL_CREDENTIALS, ImmutableMap.of("authenticatorId", id));
}

@Override
public void setUserVerified(boolean verified) {
execute(DriverCommand.SET_USER_VERIFIED,
ImmutableMap.of("authenticatorId", id, "isUserVerified", verified));
}
}

public enum When {
BEFORE,
AFTER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.openqa.selenium.json.Json.MAP_TYPE;
import static org.openqa.selenium.remote.DriverCommand.ADD_COOKIE;
import static org.openqa.selenium.remote.DriverCommand.ADD_CREDENTIAL;
import static org.openqa.selenium.remote.DriverCommand.ADD_VIRTUAL_AUTHENTICATOR;
import static org.openqa.selenium.remote.DriverCommand.CLEAR_ELEMENT;
import static org.openqa.selenium.remote.DriverCommand.CLICK_ELEMENT;
Expand All @@ -47,6 +48,7 @@
import static org.openqa.selenium.remote.DriverCommand.GET_CAPABILITIES;
import static org.openqa.selenium.remote.DriverCommand.GET_CONTEXT_HANDLES;
import static org.openqa.selenium.remote.DriverCommand.GET_COOKIE;
import static org.openqa.selenium.remote.DriverCommand.GET_CREDENTIALS;
import static org.openqa.selenium.remote.DriverCommand.GET_CURRENT_CONTEXT_HANDLE;
import static org.openqa.selenium.remote.DriverCommand.GET_CURRENT_URL;
import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_LOCATION;
Expand Down Expand Up @@ -77,6 +79,8 @@
import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION;
import static org.openqa.selenium.remote.DriverCommand.QUIT;
import static org.openqa.selenium.remote.DriverCommand.REFRESH;
import static org.openqa.selenium.remote.DriverCommand.REMOVE_ALL_CREDENTIALS;
import static org.openqa.selenium.remote.DriverCommand.REMOVE_CREDENTIAL;
import static org.openqa.selenium.remote.DriverCommand.REMOVE_VIRTUAL_AUTHENTICATOR;
import static org.openqa.selenium.remote.DriverCommand.SCREENSHOT;
import static org.openqa.selenium.remote.DriverCommand.SEND_KEYS_TO_ELEMENT;
Expand All @@ -88,6 +92,7 @@
import static org.openqa.selenium.remote.DriverCommand.SET_SCREEN_ROTATION;
import static org.openqa.selenium.remote.DriverCommand.SET_SCRIPT_TIMEOUT;
import static org.openqa.selenium.remote.DriverCommand.SET_TIMEOUT;
import static org.openqa.selenium.remote.DriverCommand.SET_USER_VERIFIED;
import static org.openqa.selenium.remote.DriverCommand.STATUS;
import static org.openqa.selenium.remote.DriverCommand.SWITCH_TO_CONTEXT;
import static org.openqa.selenium.remote.DriverCommand.SWITCH_TO_FRAME;
Expand Down Expand Up @@ -221,6 +226,16 @@ public AbstractHttpCommandCodec() {
defineCommand(ADD_VIRTUAL_AUTHENTICATOR, post("/session/:sessionId/webauthn/authenticator"));
defineCommand(REMOVE_VIRTUAL_AUTHENTICATOR,
delete("/session/:sessionId/webauthn/authenticator/:authenticatorId"));
defineCommand(ADD_CREDENTIAL,
post("/session/:sessionId/webauthn/authenticator/:authenticatorId/credential"));
defineCommand(GET_CREDENTIALS,
get("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials"));
defineCommand(REMOVE_CREDENTIAL,
delete("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId"));
defineCommand(REMOVE_ALL_CREDENTIALS,
delete("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials"));
defineCommand(SET_USER_VERIFIED,
post("/session/:sessionId/webauthn/authenticator/:authenticatorId/uv"));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.virtualauthenticator;

import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
* A credential stored in a virtual authenticator.
* @see https://w3c.github.io/webauthn/#credential-parameters
*/
public class Credential {

private final byte[] id;
private final boolean isResidentCredential;
private final String rpId;
private final PKCS8EncodedKeySpec privateKey;
private final byte[] userHandle;
private final int signCount;

/**
* Creates a non resident (i.e. stateless) credential.
*/
public static Credential createNonResidentCredential(byte[] id, String rpId,
PKCS8EncodedKeySpec privateKey, int signCount) {
return new Credential(id, /*isResidentCredential=*/false, Objects.requireNonNull(rpId),
privateKey, /*userHandle=*/null, signCount);
}

/**
* Creates a resident (i.e. stateful) credential.
*/
public static Credential createResidentCredential(byte[] id, String rpId,
PKCS8EncodedKeySpec privateKey, byte[] userHandle, int signCount) {
return new Credential(id, /*isResidentCredential=*/true, Objects.requireNonNull(rpId),
privateKey, Objects.requireNonNull(userHandle), signCount);
}

/**
* Creates a credential from a map.
*/
public static Credential fromMap(Map<String, Object> map) {
Base64.Decoder decoder = Base64.getUrlDecoder();
return new Credential(decoder.decode((String) map.get("credentialId")),
(boolean) map.get("isResidentCredential"),
(String) map.get("rpId"),
new PKCS8EncodedKeySpec(decoder.decode((String) map.get("privateKey"))),
map.get("userHandle") == null ? null : decoder.decode((String) map.get("userHandle")),
((Long) map.get("signCount")).intValue());
}

private Credential(byte[] id, boolean isResidentCredential, String rpId,
PKCS8EncodedKeySpec privateKey, byte[] userHandle, int signCount) {
this.id = Objects.requireNonNull(id);
this.isResidentCredential = isResidentCredential;
this.rpId = rpId;
this.privateKey = Objects.requireNonNull(privateKey);
this.userHandle = userHandle;
this.signCount = signCount;
}

public byte[] getId() {
return id;
}

public boolean isResidentCredential() {
return isResidentCredential;
}

public String getRpId() {
return rpId;
}

public PKCS8EncodedKeySpec getPrivateKey() {
return privateKey;
}

public byte[] getUserHandle() {
return userHandle;
}

public int getSignCount() {
return signCount;
}

public Map<String, Object> toMap() {
Base64.Encoder encoder = Base64.getUrlEncoder();
Map<String, Object> map = new HashMap<String, Object>();
map.put("credentialId", encoder.encodeToString(id));
map.put("isResidentCredential", isResidentCredential);
map.put("rpId", rpId);
map.put("privateKey", encoder.encodeToString(privateKey.getEncoded()));
map.put("signCount", signCount);
if (userHandle != null) {
map.put("userHandle", encoder.encodeToString(userHandle));
}
return Collections.unmodifiableMap(map);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@
* Interface implemented by each driver that allows access to the virtual authenticator API.
*/
public interface HasVirtualAuthenticator {
/**
* Adds a virtual authenticator with the given options.
* @return the new virtual authenticator.
*/
public VirtualAuthenticator addVirtualAuthenticator(VirtualAuthenticatorOptions options);

/**
* Removes a previously added virtual authenticator. The authenticator is no
* longer valid after removal, so no methods may be called.
*/
public void removeVirtualAuthenticator(VirtualAuthenticator authenticator);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,53 @@

package org.openqa.selenium.virtualauthenticator;

import org.openqa.selenium.virtualauthenticator.Credential;

import java.util.List;
import java.util.Objects;

/**
* Represents a virtual authenticator.
*/
public class VirtualAuthenticator {
public interface VirtualAuthenticator {

/**
* @return the authenticator unique identifier.
*/
public String getId();

/**
* Injects a credential into the authenticator.
*/
public void addCredential(Credential credential);

/**
* @return the list of credentials owned by the authenticator.
*/
public List<Credential> getCredentials();

/**
* Removes a credential from the authenticator.
* @param credentialId the ID of the credential to be removed.
*/
public void removeCredential(byte[] credentialId);

private final String id;
/**
* Removes a credential from the authenticator.
* @param credentialId the ID of the credential to be removed as a base64url
* string.
*/
public void removeCredential(String credentialId);

public VirtualAuthenticator(final String id) {
this.id = Objects.requireNonNull(id);
}
/**
* Removes all the credentials from the authenticator.
*/
public void removeAllCredentials();

public String getId() {
return id;
}
/**
* Sets whether the authenticator will simulate success or fail on user verification.
* @param verified true if the authenticator will pass user verification,
* false otherwise.
*/
public void setUserVerified(boolean verified);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@

package org.openqa.selenium.virtualauthenticator;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* Options for the creation of virtual authenticators.
* @see http://w3c.github.io/webauthn/#sctn-automation
* @see https://w3c.github.io/webauthn/#sctn-automation
*/
public class VirtualAuthenticatorOptions {

Expand Down Expand Up @@ -90,13 +91,13 @@ public VirtualAuthenticatorOptions setIsUserVerified(boolean isUserVerified) {
}

public Map<String, Object> toMap() {
HashMap<String, Object> map = new HashMap();
Map<String, Object> map = new HashMap<String, Object>();
map.put("protocol", protocol.id);
map.put("transport", transport.id);
map.put("hasResidentKey", hasResidentKey);
map.put("hasUserVerification", hasUserVerification);
map.put("isUserConsenting", isUserConsenting);
map.put("isUserVerified", isUserVerified);
return map;
return Collections.unmodifiableMap(map);
}
}
Loading