diff --git a/dotnet/src/webdriver/VirtualAuth/Credential.cs b/dotnet/src/webdriver/VirtualAuth/Credential.cs index 170677b12e035..57dbcbe76ff45 100644 --- a/dotnet/src/webdriver/VirtualAuth/Credential.cs +++ b/dotnet/src/webdriver/VirtualAuth/Credential.cs @@ -18,31 +18,30 @@ // using OpenQA.Selenium.Internal; +using System; using System.Collections.Generic; +#nullable enable + namespace OpenQA.Selenium.VirtualAuth { /// /// A credential stored in a virtual authenticator. - /// Refer https://w3c.github.io/webauthn/#credential-parameters + /// Refer /// - public class Credential + public sealed class Credential { private readonly byte[] id; - private readonly bool isResidentCredential; - private readonly string rpId; - private readonly string privateKey; - private readonly byte[] userHandle; - private readonly int signCount; + private readonly byte[]? userHandle; - private Credential(byte[] id, bool isResidentCredential, string rpId, string privateKey, byte[] userHandle, int signCount) + private Credential(byte[] id, bool isResidentCredential, string rpId, string privateKey, byte[]? userHandle, int signCount) { - this.id = id; - this.isResidentCredential = isResidentCredential; - this.rpId = rpId; - this.privateKey = privateKey; + this.id = id ?? throw new ArgumentNullException(nameof(id)); + this.IsResidentCredential = isResidentCredential; + this.RpId = rpId ?? throw new ArgumentNullException(nameof(rpId)); + this.PrivateKey = privateKey ?? throw new ArgumentNullException(nameof(privateKey)); this.userHandle = userHandle; - this.signCount = signCount; + this.SignCount = signCount; } /// @@ -53,6 +52,7 @@ private Credential(byte[] id, bool isResidentCredential, string rpId, string pri /// The private Key for the credentials. /// The signature counter for the credentials. /// The created instance of the Credential class. + /// If , , or are . public static Credential CreateNonResidentCredential(byte[] id, string rpId, string privateKey, int signCount) { return new Credential(id, false, rpId, privateKey, null, signCount); @@ -67,6 +67,7 @@ public static Credential CreateNonResidentCredential(byte[] id, string rpId, str /// The user handle associated to the credential. /// The signature counter for the credentials. /// The created instance of the Credential class. + /// If , , or are . public static Credential CreateResidentCredential(byte[] id, string rpId, string privateKey, byte[] userHandle, int signCount) { return new Credential(id, true, rpId, privateKey, userHandle, signCount); @@ -75,50 +76,32 @@ public static Credential CreateResidentCredential(byte[] id, string rpId, string /// /// Gets the byte array of the ID of the credential. /// - public byte[] Id - { - get { return (byte[])id.Clone(); } - } + public byte[] Id => (byte[])id.Clone(); /// /// Gets a value indicating whether this Credential is a resident credential. /// - public bool IsResidentCredential - { - get { return this.isResidentCredential; } - } + public bool IsResidentCredential { get; } /// /// Gets the ID of the relying party of this credential. /// - public string RpId - { - get { return this.rpId; } - } + public string RpId { get; } /// /// Gets the private key of the credential. /// - public string PrivateKey - { - get { return this.privateKey; } - } + public string PrivateKey { get; } /// /// Gets the user handle of the credential. /// - public byte[] UserHandle - { - get { return userHandle == null ? null : (byte[])userHandle.Clone(); } - } + public byte[]? UserHandle => (byte[]?)userHandle?.Clone(); /// /// Gets the signature counter associated to the public key credential source. /// - public int SignCount - { - get { return this.signCount; } - } + public int SignCount { get; } /// /// Creates a Credential instance from a dictionary of values. @@ -127,13 +110,14 @@ public int SignCount /// The created instance of the Credential. public static Credential FromDictionary(Dictionary dictionary) { - return new Credential( - Base64UrlEncoder.DecodeBytes((string)dictionary["credentialId"]), - (bool)dictionary["isResidentCredential"], - dictionary.ContainsKey("rpId") ? (string)dictionary["rpId"] : null, - (string)dictionary["privateKey"], - dictionary.ContainsKey("userHandle") ? Base64UrlEncoder.DecodeBytes((string)dictionary["userHandle"]) : null, - (int)((long)dictionary["signCount"])); + byte[] id = Base64UrlEncoder.DecodeBytes((string)dictionary["credentialId"]); + bool isResidentCredential = (bool)dictionary["isResidentCredential"]; + string? rpId = dictionary.TryGetValue("rpId", out object? r) ? (string)r : null; + string privateKey = (string)dictionary["privateKey"]; + byte[]? userHandle = dictionary.TryGetValue("userHandle", out object? u) ? Base64UrlEncoder.DecodeBytes((string)u) : null; + int signCount = (int)(long)dictionary["signCount"]; + + return new Credential(id, isResidentCredential, rpId, privateKey, userHandle, signCount); } /// @@ -145,11 +129,11 @@ public Dictionary ToDictionary() Dictionary toReturn = new Dictionary(); toReturn["credentialId"] = Base64UrlEncoder.Encode(this.id); - toReturn["isResidentCredential"] = this.isResidentCredential; - toReturn["rpId"] = this.rpId; - toReturn["privateKey"] = this.privateKey; - toReturn["signCount"] = this.signCount; - if (this.userHandle != null) + toReturn["isResidentCredential"] = this.IsResidentCredential; + toReturn["rpId"] = this.RpId; + toReturn["privateKey"] = this.PrivateKey; + toReturn["signCount"] = this.SignCount; + if (this.userHandle is not null) { toReturn["userHandle"] = Base64UrlEncoder.Encode(this.userHandle); } diff --git a/dotnet/src/webdriver/VirtualAuth/IHasVirtualAuthenticator.cs b/dotnet/src/webdriver/VirtualAuth/IHasVirtualAuthenticator.cs index 92619ded68acb..2d8616416f90e 100644 --- a/dotnet/src/webdriver/VirtualAuth/IHasVirtualAuthenticator.cs +++ b/dotnet/src/webdriver/VirtualAuth/IHasVirtualAuthenticator.cs @@ -17,8 +17,11 @@ // under the License. // +using System; using System.Collections.Generic; +#nullable enable + namespace OpenQA.Selenium.VirtualAuth { /// @@ -31,18 +34,23 @@ public interface IHasVirtualAuthenticator /// /// The VirtualAuthenticatorOptions to use in creating the authenticator. /// The ID of the added virtual authenticator. + /// If is . string AddVirtualAuthenticator(VirtualAuthenticatorOptions options); /// /// Removes a virtual authenticator. /// /// The ID of the virtual authenticator to remove. + /// If is . + /// If the specified virtual authenticator does not exist. void RemoveVirtualAuthenticator(string id); /// /// Adds a credential to the virtual authenticator. /// /// The credential to add to the authenticator. + /// If is . + /// If a Virtual Authenticator has not been added yet. void AddCredential(Credential credential); /// @@ -55,23 +63,29 @@ public interface IHasVirtualAuthenticator /// Removes a credential from the virtual authenticator. /// /// A byte array representing the ID of the credential to remove. + /// If is . + /// If a Virtual Authenticator has not been added yet. void RemoveCredential(byte[] credentialId); /// /// Removes a credential from the virtual authenticator. /// /// A string representing the ID of the credential to remove. + /// If is . + /// If a Virtual Authenticator has not been added yet. void RemoveCredential(string credentialId); /// /// Removes all credentials registered to this virtual authenticator. /// + /// If a Virtual Authenticator has not been added yet. void RemoveAllCredentials(); /// /// Sets whether or not a user is verified in this virtual authenticator. /// /// if the user is verified; otherwise . + /// If a Virtual Authenticator has not been added yet. void SetUserVerified(bool verified); } } diff --git a/dotnet/src/webdriver/VirtualAuth/VirtualAuthenticatorOptions.cs b/dotnet/src/webdriver/VirtualAuth/VirtualAuthenticatorOptions.cs index 6e118df89e037..502d1368f9d0f 100644 --- a/dotnet/src/webdriver/VirtualAuth/VirtualAuthenticatorOptions.cs +++ b/dotnet/src/webdriver/VirtualAuth/VirtualAuthenticatorOptions.cs @@ -20,6 +20,8 @@ using System; using System.Collections.Generic; +#nullable enable + namespace OpenQA.Selenium.VirtualAuth { /// @@ -78,10 +80,13 @@ public static class Transport private bool isUserVerified = false; /// - /// Sets the protocol the Virtual Authenticator speaks + /// Sets the Client to Authenticator Protocol (CTAP) this Virtual Authenticator speaks. /// - /// Valid protocol value - /// VirtualAuthenticatorOptions + /// The CTAP protocol identifier. + /// This options instance for chaining. + /// Valid protocols are available on the type. + /// If is not a supported protocol value. + /// public VirtualAuthenticatorOptions SetProtocol(string protocol) { if (string.Equals(Protocol.CTAP2, protocol) || string.Equals(Protocol.U2F, protocol)) @@ -92,15 +97,19 @@ public VirtualAuthenticatorOptions SetProtocol(string protocol) else { throw new ArgumentException("Enter a valid protocol value." + - "Refer to https://www.w3.org/TR/webauthn-2/#sctn-automation-virtual-authenticators for supported protocols."); + "Refer to https://www.w3.org/TR/webauthn-2/#sctn-automation-virtual-authenticators for supported protocols."); } } /// - /// Sets the transport authenticator needs to implement to communicate with clients + /// Sets the Authenticator Transport this Virtual Authenticator needs to implement, to communicate with clients. /// - /// Valid transport value - /// VirtualAuthenticatorOptions + /// Valid transport value. + /// + /// This options instance for chaining. + /// Valid protocols are available on the type. + /// If is not a supported transport value. + /// public VirtualAuthenticatorOptions SetTransport(string transport) { if (Transport.BLE == transport || Transport.INTERNAL == transport || Transport.NFC == transport || Transport.USB == transport) @@ -111,16 +120,15 @@ public VirtualAuthenticatorOptions SetTransport(string transport) else { throw new ArgumentException("Enter a valid transport value." + - "Refer to https://www.w3.org/TR/webauthn-2/#enum-transport for supported transport values."); + "Refer to https://www.w3.org/TR/webauthn-2/#enum-transport for supported transport values."); } } /// - /// If set to true the authenticator will support client-side discoverable credentials. - /// Refer https://w3c.github.io/webauthn/#client-side-discoverable-credential + /// If set to , the authenticator will support Client-side discoverable Credentials. /// - /// boolean value to set - /// VirtualAuthenticatorOptions + /// Whether authenticator will support client-side discoverable credentials. + /// This options instance for chaining. public VirtualAuthenticatorOptions SetHasResidentKey(bool hasResidentKey) { this.hasResidentKey = hasResidentKey; @@ -128,11 +136,10 @@ public VirtualAuthenticatorOptions SetHasResidentKey(bool hasResidentKey) } /// - /// If set to true, the authenticator supports user verification. - /// Refer https://w3c.github.io/webauthn/#user-verification. + /// If set to , the authenticator will support User Verification. /// - /// boolean value to set - /// + /// Whether the authenticator supports user verification. + /// This options instance for chaining. public VirtualAuthenticatorOptions SetHasUserVerification(bool hasUserVerification) { this.hasUserVerification = hasUserVerification; @@ -140,11 +147,10 @@ public VirtualAuthenticatorOptions SetHasUserVerification(bool hasUserVerificati } /// - /// If set to true, a user consent will always be granted. - /// Refer https://w3c.github.io/webauthn/#user-consent + /// If set to , a User Consent will always be granted. /// - /// boolean value to set - /// VirtualAuthenticatorOptions + /// Whether a user consent will always be granted. + /// This options instance for chaining. public VirtualAuthenticatorOptions SetIsUserConsenting(bool isUserConsenting) { this.isUserConsenting = isUserConsenting; @@ -152,11 +158,10 @@ public VirtualAuthenticatorOptions SetIsUserConsenting(bool isUserConsenting) } /// - /// If set to true, User Verification will always succeed. - /// Refer https://w3c.github.io/webauthn/#user-verification + /// If set to , User Verification will always succeed. /// - /// boolean value to set - /// VirtualAuthenticatorOptions + /// Whether User Verification will always succeed. + /// This options instance for chaining. public VirtualAuthenticatorOptions SetIsUserVerified(bool isUserVerified) { this.isUserVerified = isUserVerified; @@ -164,7 +169,7 @@ public VirtualAuthenticatorOptions SetIsUserVerified(bool isUserVerified) } /// - /// Serializes this set of options to a dictionary of key-value pairs. + /// Serializes this set of options into a dictionary of key-value pairs. /// /// The dictionary containing the values of this set of options. public Dictionary ToDictionary() diff --git a/dotnet/src/webdriver/WebDriver.cs b/dotnet/src/webdriver/WebDriver.cs index d9960ea97b235..9bcca8b28f8b3 100644 --- a/dotnet/src/webdriver/WebDriver.cs +++ b/dotnet/src/webdriver/WebDriver.cs @@ -1036,44 +1036,68 @@ private object ParseJavaScriptReturnValue(object responseValue) return returnValue; } +#nullable enable + /// /// Creates a Virtual Authenticator. /// - /// VirtualAuthenticator Options (https://w3c.github.io/webauthn/#sctn-automation-virtual-authenticators) + /// Virtual Authenticator Options. /// Authenticator id as string + /// If is . public string AddVirtualAuthenticator(VirtualAuthenticatorOptions options) { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + Response commandResponse = this.Execute(DriverCommand.AddVirtualAuthenticator, options.ToDictionary()); - string id = commandResponse.Value.ToString(); + string id = (string)commandResponse.Value!; this.AuthenticatorId = id; - return this.AuthenticatorId; + return id; } /// /// Removes the Virtual Authenticator /// - /// Id as string that uniquely identifies a Virtual Authenticator + /// Id as string that uniquely identifies a Virtual Authenticator. + /// If is . public void RemoveVirtualAuthenticator(string authenticatorId) { + if (authenticatorId is null) + { + throw new ArgumentNullException(nameof(authenticatorId)); + } + Dictionary parameters = new Dictionary(); - parameters.Add("authenticatorId", this.AuthenticatorId); + parameters.Add("authenticatorId", authenticatorId); + this.Execute(DriverCommand.RemoveVirtualAuthenticator, parameters); this.AuthenticatorId = null; } /// - /// Gets the virtual authenticator ID for this WebDriver instance. + /// Gets the cached virtual authenticator ID, or if no authenticator ID is set. /// - public string AuthenticatorId { get; private set; } + public string? AuthenticatorId { get; private set; } /// /// Add a credential to the Virtual Authenticator/ /// /// The credential to be stored in the Virtual Authenticator + /// If is . + /// If a Virtual Authenticator has not been added yet. public void AddCredential(Credential credential) { + if (credential is null) + { + throw new ArgumentNullException(nameof(credential)); + } + + string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations"); + Dictionary parameters = new Dictionary(credential.ToDictionary()); - parameters.Add("authenticatorId", this.AuthenticatorId); + parameters.Add("authenticatorId", authenticatorId); this.Execute(driverCommandToExecute: DriverCommand.AddCredential, parameters); } @@ -1082,18 +1106,25 @@ public void AddCredential(Credential credential) /// Retrieves all the credentials stored in the Virtual Authenticator /// /// List of credentials + /// If a Virtual Authenticator has not been added yet. public List GetCredentials() { + string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations"); + Dictionary parameters = new Dictionary(); - parameters.Add("authenticatorId", this.AuthenticatorId); + parameters.Add("authenticatorId", authenticatorId); - object[] commandResponse = (object[])this.Execute(driverCommandToExecute: DriverCommand.GetCredentials, parameters).Value; + Response getCredentialsResponse = this.Execute(driverCommandToExecute: DriverCommand.GetCredentials, parameters); - List credentials = new List(); + if (getCredentialsResponse.Value is not object?[] credentialsList) + { + throw new WebDriverException($"Get credentials call succeeded, but the response was not a list of credentials: {getCredentialsResponse.Value}"); + } - foreach (object dictionary in commandResponse) + List credentials = new List(credentialsList.Length); + foreach (object? dictionary in credentialsList) { - Credential credential = Credential.FromDictionary((Dictionary)dictionary); + Credential credential = Credential.FromDictionary((Dictionary)dictionary!); credentials.Add(credential); } @@ -1104,6 +1135,8 @@ public List GetCredentials() /// Removes the credential identified by the credentialId from the Virtual Authenticator. /// /// The id as byte array that uniquely identifies a credential + /// If is . + /// If a Virtual Authenticator has not been added yet. public void RemoveCredential(byte[] credentialId) { RemoveCredential(Base64UrlEncoder.Encode(credentialId)); @@ -1113,10 +1146,19 @@ public void RemoveCredential(byte[] credentialId) /// Removes the credential identified by the credentialId from the Virtual Authenticator. /// /// The id as string that uniquely identifies a credential + /// If is . + /// If a Virtual Authenticator has not been added yet. public void RemoveCredential(string credentialId) { + if (credentialId is null) + { + throw new ArgumentNullException(nameof(credentialId)); + } + + string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations"); + Dictionary parameters = new Dictionary(); - parameters.Add("authenticatorId", this.AuthenticatorId); + parameters.Add("authenticatorId", authenticatorId); parameters.Add("credentialId", credentialId); this.Execute(driverCommandToExecute: DriverCommand.RemoveCredential, parameters); @@ -1125,10 +1167,13 @@ public void RemoveCredential(string credentialId) /// /// Removes all the credentials stored in the Virtual Authenticator. /// + /// If a Virtual Authenticator has not been added yet. public void RemoveAllCredentials() { + string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations"); + Dictionary parameters = new Dictionary(); - parameters.Add("authenticatorId", this.AuthenticatorId); + parameters.Add("authenticatorId", authenticatorId); this.Execute(driverCommandToExecute: DriverCommand.RemoveAllCredentials, parameters); } @@ -1139,8 +1184,10 @@ public void RemoveAllCredentials() /// The boolean value representing value to be set public void SetUserVerified(bool verified) { + string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations"); + Dictionary parameters = new Dictionary(); - parameters.Add("authenticatorId", this.AuthenticatorId); + parameters.Add("authenticatorId", authenticatorId); parameters.Add("isUserVerified", verified); this.Execute(driverCommandToExecute: DriverCommand.SetUserVerified, parameters); diff --git a/dotnet/test/common/VirtualAuthn/VirtualAuthenticatorTest.cs b/dotnet/test/common/VirtualAuthn/VirtualAuthenticatorTest.cs index e6c16e11b673a..c46d541dcb548 100644 --- a/dotnet/test/common/VirtualAuthn/VirtualAuthenticatorTest.cs +++ b/dotnet/test/common/VirtualAuthn/VirtualAuthenticatorTest.cs @@ -194,6 +194,26 @@ public void ShouldRemoveAuthenticator() Assert.IsNull(webDriver.AuthenticatorId); } + [Test] + [NeedsFreshDriver(IsCreatedAfterTest = true)] + [IgnoreBrowser(Selenium.Browser.IE, "IE does not support Virtual Authenticator")] + [IgnoreBrowser(Selenium.Browser.Firefox, "Firefox does not support Virtual Authenticator")] + [IgnoreBrowser(Selenium.Browser.Safari, "Safari does not support Virtual Authenticator")] + public void ShouldSupportMultipleVirtualAuthenticatorsAtOnce() + { + VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions(); + + string authenticatorId1 = webDriver.AddVirtualAuthenticator(options); + Assert.That(webDriver.AuthenticatorId, Is.EqualTo(authenticatorId1)); + + string authenticatorId2 = webDriver.AddVirtualAuthenticator(options); + + webDriver.RemoveVirtualAuthenticator(authenticatorId1); + webDriver.RemoveVirtualAuthenticator(authenticatorId2); + + Assert.IsNull(webDriver.AuthenticatorId); + } + [Test] [NeedsFreshDriver(IsCreatedAfterTest = true)] [IgnoreBrowser(Selenium.Browser.IE, "IE does not support Virtual Authenticator")] @@ -302,7 +322,9 @@ public void AddResidentCredentialNotSupportedWhenAuthenticatorUsesU2FProtocol() byte[] userHandle = { 1 }; Credential credential = Credential.CreateResidentCredential( credentialId, "localhost", base64EncodedEC256PK, userHandle, /*signCount=*/0); - Assert.Throws(() => webDriver.AddCredential(credential)); + Assert.That( + () => webDriver.AddCredential(credential), + Throws.TypeOf().With.Message.Contains("The Authenticator does not support Resident Credentials.")); } [Test] @@ -492,5 +514,37 @@ public void testSetUserVerified() Assert.True(error.StartsWith("NotAllowedError")); } + + [Test] + [NeedsFreshDriver(IsCreatedAfterTest = true)] + [IgnoreBrowser(Selenium.Browser.IE, "IE does not support Virtual Authenticator")] + [IgnoreBrowser(Selenium.Browser.Firefox, "Firefox does not support Virtual Authenticator")] + [IgnoreBrowser(Selenium.Browser.Safari, "Safari does not support Virtual Authenticator")] + public void ShouldThrowOnInvalidArguments() + { + Assert.That( + () => webDriver.AddVirtualAuthenticator(null), + Throws.ArgumentNullException); + + Assert.That( + () => webDriver.RemoveVirtualAuthenticator(null), + Throws.ArgumentNullException); + + Assert.That( + () => webDriver.AddCredential(null), + Throws.ArgumentNullException); + + Assert.That( + () => webDriver.RemoveCredential((byte[])null), + Throws.ArgumentNullException); + + Assert.That( + () => webDriver.RemoveCredential((string)null), + Throws.ArgumentNullException); + + Assert.That( + () => webDriver.RemoveVirtualAuthenticator("non-existant"), + Throws.TypeOf()); + } } }