Skip to content

Commit 816df68

Browse files
nsatragnoshs96c
authored andcommitted
Add support to debug virtual authenticators (#7842)
Add support for: * `addCredential` * `getCredentials`, * `removeCredential` * `removeAllCredentials` * `setUserVerified`, `
1 parent 81552aa commit 816df68

File tree

8 files changed

+521
-29
lines changed

8 files changed

+521
-29
lines changed

java/client/src/org/openqa/selenium/remote/DriverCommand.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,4 +323,9 @@ static CommandPayload SET_CURRENT_WINDOW_SIZE(Dimension targetSize) {
323323
// http://w3c.github.io/webauthn#sctn-automation
324324
String ADD_VIRTUAL_AUTHENTICATOR = "addVirtualAuthenticator";
325325
String REMOVE_VIRTUAL_AUTHENTICATOR = "removeVirtualAuthenticator";
326+
String ADD_CREDENTIAL = "addCredential";
327+
String GET_CREDENTIALS = "getCredentials";
328+
String REMOVE_CREDENTIAL = "removeCredential";
329+
String REMOVE_ALL_CREDENTIALS = "removeAllCredentials";
330+
String SET_USER_VERIFIED = "setUserVerified";
326331
}

java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,13 @@
6767
import org.openqa.selenium.logging.Logs;
6868
import org.openqa.selenium.logging.NeedsLocalLogs;
6969
import org.openqa.selenium.remote.internal.WebElementToJsonConverter;
70+
import org.openqa.selenium.virtualauthenticator.Credential;
7071
import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
7172
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticator;
7273
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
7374

7475
import java.net.URL;
76+
import java.util.Base64;
7577
import java.util.Collection;
7678
import java.util.Collections;
7779
import java.util.Date;
@@ -669,7 +671,7 @@ public Mouse getMouse() {
669671
public VirtualAuthenticator addVirtualAuthenticator(VirtualAuthenticatorOptions options) {
670672
String authenticatorId = (String)
671673
execute(DriverCommand.ADD_VIRTUAL_AUTHENTICATOR, options.toMap()).getValue();
672-
return new VirtualAuthenticator(authenticatorId);
674+
return new RemoteVirtualAuthenticator(authenticatorId);
673675
}
674676

675677
@Override
@@ -1058,6 +1060,57 @@ public void sendKeys(String keysToSend) {
10581060
}
10591061
}
10601062

1063+
private class RemoteVirtualAuthenticator implements VirtualAuthenticator {
1064+
private final String id;
1065+
1066+
public RemoteVirtualAuthenticator(final String id) {
1067+
this.id = Objects.requireNonNull(id);
1068+
}
1069+
1070+
@Override
1071+
public String getId() {
1072+
return id;
1073+
}
1074+
1075+
@Override
1076+
public void addCredential(Credential credential) {
1077+
execute(DriverCommand.ADD_CREDENTIAL,
1078+
new ImmutableMap.Builder<String, Object>()
1079+
.putAll(credential.toMap())
1080+
.put("authenticatorId", id)
1081+
.build());
1082+
}
1083+
1084+
@Override
1085+
public List<Credential> getCredentials() {
1086+
List<Map<String, Object>> response = (List<Map<String, Object>>)
1087+
execute(DriverCommand.GET_CREDENTIALS, ImmutableMap.of("authenticatorId", id)).getValue();
1088+
return response.stream().map(Credential::fromMap).collect(Collectors.toList());
1089+
}
1090+
1091+
@Override
1092+
public void removeCredential(byte[] credentialId) {
1093+
removeCredential(Base64.getUrlEncoder().encodeToString(credentialId));
1094+
}
1095+
1096+
@Override
1097+
public void removeCredential(String credentialId) {
1098+
execute(DriverCommand.REMOVE_CREDENTIAL,
1099+
ImmutableMap.of("authenticatorId", id, "credentialId", credentialId)).getValue();
1100+
}
1101+
1102+
@Override
1103+
public void removeAllCredentials() {
1104+
execute(DriverCommand.REMOVE_ALL_CREDENTIALS, ImmutableMap.of("authenticatorId", id));
1105+
}
1106+
1107+
@Override
1108+
public void setUserVerified(boolean verified) {
1109+
execute(DriverCommand.SET_USER_VERIFIED,
1110+
ImmutableMap.of("authenticatorId", id, "isUserVerified", verified));
1111+
}
1112+
}
1113+
10611114
public enum When {
10621115
BEFORE,
10631116
AFTER,

java/client/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static java.nio.charset.StandardCharsets.UTF_8;
2727
import static org.openqa.selenium.json.Json.MAP_TYPE;
2828
import static org.openqa.selenium.remote.DriverCommand.ADD_COOKIE;
29+
import static org.openqa.selenium.remote.DriverCommand.ADD_CREDENTIAL;
2930
import static org.openqa.selenium.remote.DriverCommand.ADD_VIRTUAL_AUTHENTICATOR;
3031
import static org.openqa.selenium.remote.DriverCommand.CLEAR_ELEMENT;
3132
import static org.openqa.selenium.remote.DriverCommand.CLICK_ELEMENT;
@@ -47,6 +48,7 @@
4748
import static org.openqa.selenium.remote.DriverCommand.GET_CAPABILITIES;
4849
import static org.openqa.selenium.remote.DriverCommand.GET_CONTEXT_HANDLES;
4950
import static org.openqa.selenium.remote.DriverCommand.GET_COOKIE;
51+
import static org.openqa.selenium.remote.DriverCommand.GET_CREDENTIALS;
5052
import static org.openqa.selenium.remote.DriverCommand.GET_CURRENT_CONTEXT_HANDLE;
5153
import static org.openqa.selenium.remote.DriverCommand.GET_CURRENT_URL;
5254
import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_LOCATION;
@@ -77,6 +79,8 @@
7779
import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION;
7880
import static org.openqa.selenium.remote.DriverCommand.QUIT;
7981
import static org.openqa.selenium.remote.DriverCommand.REFRESH;
82+
import static org.openqa.selenium.remote.DriverCommand.REMOVE_ALL_CREDENTIALS;
83+
import static org.openqa.selenium.remote.DriverCommand.REMOVE_CREDENTIAL;
8084
import static org.openqa.selenium.remote.DriverCommand.REMOVE_VIRTUAL_AUTHENTICATOR;
8185
import static org.openqa.selenium.remote.DriverCommand.SCREENSHOT;
8286
import static org.openqa.selenium.remote.DriverCommand.SEND_KEYS_TO_ELEMENT;
@@ -88,6 +92,7 @@
8892
import static org.openqa.selenium.remote.DriverCommand.SET_SCREEN_ROTATION;
8993
import static org.openqa.selenium.remote.DriverCommand.SET_SCRIPT_TIMEOUT;
9094
import static org.openqa.selenium.remote.DriverCommand.SET_TIMEOUT;
95+
import static org.openqa.selenium.remote.DriverCommand.SET_USER_VERIFIED;
9196
import static org.openqa.selenium.remote.DriverCommand.STATUS;
9297
import static org.openqa.selenium.remote.DriverCommand.SWITCH_TO_CONTEXT;
9398
import static org.openqa.selenium.remote.DriverCommand.SWITCH_TO_FRAME;
@@ -221,6 +226,16 @@ public AbstractHttpCommandCodec() {
221226
defineCommand(ADD_VIRTUAL_AUTHENTICATOR, post("/session/:sessionId/webauthn/authenticator"));
222227
defineCommand(REMOVE_VIRTUAL_AUTHENTICATOR,
223228
delete("/session/:sessionId/webauthn/authenticator/:authenticatorId"));
229+
defineCommand(ADD_CREDENTIAL,
230+
post("/session/:sessionId/webauthn/authenticator/:authenticatorId/credential"));
231+
defineCommand(GET_CREDENTIALS,
232+
get("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials"));
233+
defineCommand(REMOVE_CREDENTIAL,
234+
delete("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId"));
235+
defineCommand(REMOVE_ALL_CREDENTIALS,
236+
delete("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials"));
237+
defineCommand(SET_USER_VERIFIED,
238+
post("/session/:sessionId/webauthn/authenticator/:authenticatorId/uv"));
224239
}
225240

226241
@Override
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.virtualauthenticator;
19+
20+
import java.security.spec.PKCS8EncodedKeySpec;
21+
import java.util.Base64;
22+
import java.util.Collections;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import java.util.Objects;
26+
27+
/**
28+
* A credential stored in a virtual authenticator.
29+
* @see https://w3c.github.io/webauthn/#credential-parameters
30+
*/
31+
public class Credential {
32+
33+
private final byte[] id;
34+
private final boolean isResidentCredential;
35+
private final String rpId;
36+
private final PKCS8EncodedKeySpec privateKey;
37+
private final byte[] userHandle;
38+
private final int signCount;
39+
40+
/**
41+
* Creates a non resident (i.e. stateless) credential.
42+
*/
43+
public static Credential createNonResidentCredential(byte[] id, String rpId,
44+
PKCS8EncodedKeySpec privateKey, int signCount) {
45+
return new Credential(id, /*isResidentCredential=*/false, Objects.requireNonNull(rpId),
46+
privateKey, /*userHandle=*/null, signCount);
47+
}
48+
49+
/**
50+
* Creates a resident (i.e. stateful) credential.
51+
*/
52+
public static Credential createResidentCredential(byte[] id, String rpId,
53+
PKCS8EncodedKeySpec privateKey, byte[] userHandle, int signCount) {
54+
return new Credential(id, /*isResidentCredential=*/true, Objects.requireNonNull(rpId),
55+
privateKey, Objects.requireNonNull(userHandle), signCount);
56+
}
57+
58+
/**
59+
* Creates a credential from a map.
60+
*/
61+
public static Credential fromMap(Map<String, Object> map) {
62+
Base64.Decoder decoder = Base64.getUrlDecoder();
63+
return new Credential(decoder.decode((String) map.get("credentialId")),
64+
(boolean) map.get("isResidentCredential"),
65+
(String) map.get("rpId"),
66+
new PKCS8EncodedKeySpec(decoder.decode((String) map.get("privateKey"))),
67+
map.get("userHandle") == null ? null : decoder.decode((String) map.get("userHandle")),
68+
((Long) map.get("signCount")).intValue());
69+
}
70+
71+
private Credential(byte[] id, boolean isResidentCredential, String rpId,
72+
PKCS8EncodedKeySpec privateKey, byte[] userHandle, int signCount) {
73+
this.id = Objects.requireNonNull(id);
74+
this.isResidentCredential = isResidentCredential;
75+
this.rpId = rpId;
76+
this.privateKey = Objects.requireNonNull(privateKey);
77+
this.userHandle = userHandle;
78+
this.signCount = signCount;
79+
}
80+
81+
public byte[] getId() {
82+
return id;
83+
}
84+
85+
public boolean isResidentCredential() {
86+
return isResidentCredential;
87+
}
88+
89+
public String getRpId() {
90+
return rpId;
91+
}
92+
93+
public PKCS8EncodedKeySpec getPrivateKey() {
94+
return privateKey;
95+
}
96+
97+
public byte[] getUserHandle() {
98+
return userHandle;
99+
}
100+
101+
public int getSignCount() {
102+
return signCount;
103+
}
104+
105+
public Map<String, Object> toMap() {
106+
Base64.Encoder encoder = Base64.getUrlEncoder();
107+
Map<String, Object> map = new HashMap<String, Object>();
108+
map.put("credentialId", encoder.encodeToString(id));
109+
map.put("isResidentCredential", isResidentCredential);
110+
map.put("rpId", rpId);
111+
map.put("privateKey", encoder.encodeToString(privateKey.getEncoded()));
112+
map.put("signCount", signCount);
113+
if (userHandle != null) {
114+
map.put("userHandle", encoder.encodeToString(userHandle));
115+
}
116+
return Collections.unmodifiableMap(map);
117+
}
118+
}

java/client/src/org/openqa/selenium/virtualauthenticator/HasVirtualAuthenticator.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@
2424
* Interface implemented by each driver that allows access to the virtual authenticator API.
2525
*/
2626
public interface HasVirtualAuthenticator {
27+
/**
28+
* Adds a virtual authenticator with the given options.
29+
* @return the new virtual authenticator.
30+
*/
2731
public VirtualAuthenticator addVirtualAuthenticator(VirtualAuthenticatorOptions options);
2832

33+
/**
34+
* Removes a previously added virtual authenticator. The authenticator is no
35+
* longer valid after removal, so no methods may be called.
36+
*/
2937
public void removeVirtualAuthenticator(VirtualAuthenticator authenticator);
3038
}

java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticator.java

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,53 @@
1717

1818
package org.openqa.selenium.virtualauthenticator;
1919

20+
import org.openqa.selenium.virtualauthenticator.Credential;
21+
22+
import java.util.List;
2023
import java.util.Objects;
2124

2225
/**
2326
* Represents a virtual authenticator.
2427
*/
25-
public class VirtualAuthenticator {
28+
public interface VirtualAuthenticator {
29+
30+
/**
31+
* @return the authenticator unique identifier.
32+
*/
33+
public String getId();
34+
35+
/**
36+
* Injects a credential into the authenticator.
37+
*/
38+
public void addCredential(Credential credential);
39+
40+
/**
41+
* @return the list of credentials owned by the authenticator.
42+
*/
43+
public List<Credential> getCredentials();
44+
45+
/**
46+
* Removes a credential from the authenticator.
47+
* @param credentialId the ID of the credential to be removed.
48+
*/
49+
public void removeCredential(byte[] credentialId);
2650

27-
private final String id;
51+
/**
52+
* Removes a credential from the authenticator.
53+
* @param credentialId the ID of the credential to be removed as a base64url
54+
* string.
55+
*/
56+
public void removeCredential(String credentialId);
2857

29-
public VirtualAuthenticator(final String id) {
30-
this.id = Objects.requireNonNull(id);
31-
}
58+
/**
59+
* Removes all the credentials from the authenticator.
60+
*/
61+
public void removeAllCredentials();
3262

33-
public String getId() {
34-
return id;
35-
}
63+
/**
64+
* Sets whether the authenticator will simulate success or fail on user verification.
65+
* @param verified true if the authenticator will pass user verification,
66+
* false otherwise.
67+
*/
68+
public void setUserVerified(boolean verified);
3669
}

java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorOptions.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717

1818
package org.openqa.selenium.virtualauthenticator;
1919

20+
import java.util.Collections;
2021
import java.util.HashMap;
2122
import java.util.Map;
2223

2324
/**
2425
* Options for the creation of virtual authenticators.
25-
* @see http://w3c.github.io/webauthn/#sctn-automation
26+
* @see https://w3c.github.io/webauthn/#sctn-automation
2627
*/
2728
public class VirtualAuthenticatorOptions {
2829

@@ -90,13 +91,13 @@ public VirtualAuthenticatorOptions setIsUserVerified(boolean isUserVerified) {
9091
}
9192

9293
public Map<String, Object> toMap() {
93-
HashMap<String, Object> map = new HashMap();
94+
Map<String, Object> map = new HashMap<String, Object>();
9495
map.put("protocol", protocol.id);
9596
map.put("transport", transport.id);
9697
map.put("hasResidentKey", hasResidentKey);
9798
map.put("hasUserVerification", hasUserVerification);
9899
map.put("isUserConsenting", isUserConsenting);
99100
map.put("isUserVerified", isUserVerified);
100-
return map;
101+
return Collections.unmodifiableMap(map);
101102
}
102103
}

0 commit comments

Comments
 (0)