diff --git a/Demo/Controller.cs b/Demo/Controller.cs index 867e95fb..a8dcd2a2 100644 --- a/Demo/Controller.cs +++ b/Demo/Controller.cs @@ -38,7 +38,7 @@ public JsonResult MakeCredentialOptions([FromForm] string username, [FromForm] string displayName, [FromForm] string attType, [FromForm] string authType, - [FromForm] bool requireResidentKey, + [FromForm] string residentKey, [FromForm] string userVerification) { try @@ -63,7 +63,7 @@ public JsonResult MakeCredentialOptions([FromForm] string username, // 3. Create options var authenticatorSelection = new AuthenticatorSelection { - RequireResidentKey = requireResidentKey, + ResidentKey = residentKey.ToEnum(), UserVerification = userVerification.ToEnum() }; diff --git a/Demo/Pages/_options.cshtml b/Demo/Pages/_options.cshtml index 7fb80dd7..9c0d4ea0 100644 --- a/Demo/Pages/_options.cshtml +++ b/Demo/Pages/_options.cshtml @@ -49,29 +49,18 @@
- +
- -
-
-
- -
-
+

For register the client will guide the user through setting up user verification if needed to create a discoverable credential. This takes precedence over the setting of userVerification.

diff --git a/Demo/wwwroot/js/custom.register.js b/Demo/wwwroot/js/custom.register.js index dbcb3c07..9a82ff76 100644 --- a/Demo/wwwroot/js/custom.register.js +++ b/Demo/wwwroot/js/custom.register.js @@ -24,7 +24,10 @@ async function handleRegisterSubmit(event) { user_verification = value("#option-userverification"); } - let require_resident_key = value("#option-residentkey"); + let residentKey = ""; + if (value("#option-residentkey") !== "undefined") { + residentKey = value("#option-residentkey"); + } // prepare form post data var data = new FormData(); @@ -33,7 +36,7 @@ async function handleRegisterSubmit(event) { data.append('attType', attestation_type); data.append('authType', authenticator_attachment); data.append('userVerification', user_verification); - data.append('requireResidentKey', require_resident_key); + data.append('residentKey', residentKey); try { makeCredentialOptions = await fetchMakeCredentialOptions(data); diff --git a/Demo/wwwroot/js/mfa.register.js b/Demo/wwwroot/js/mfa.register.js index 5226ea5c..08aa9bfe 100644 --- a/Demo/wwwroot/js/mfa.register.js +++ b/Demo/wwwroot/js/mfa.register.js @@ -17,8 +17,8 @@ async function handleRegisterSubmit(event) { // possible values: preferred, required, discouraged let user_verification = "discouraged"; - // possible values: true,false - let require_resident_key = false; + // possible values: discouraged, preferred, required + let residentKey = "discouraged"; @@ -29,7 +29,7 @@ async function handleRegisterSubmit(event) { data.append('attType', attestation_type); data.append('authType', authenticator_attachment); data.append('userVerification', user_verification); - data.append('requireResidentKey', require_resident_key); + data.append('residentKey', residentKey); // send to server for registering let makeCredentialOptions; diff --git a/Demo/wwwroot/js/passwordless.register.js b/Demo/wwwroot/js/passwordless.register.js index 9869aed4..9644b07a 100644 --- a/Demo/wwwroot/js/passwordless.register.js +++ b/Demo/wwwroot/js/passwordless.register.js @@ -15,8 +15,8 @@ async function handleRegisterSubmit(event) { // possible values: preferred, required, discouraged let user_verification = "discouraged"; - // possible values: true,false - let require_resident_key = "false"; + // possible values: discouraged, preferred, required + let residentKey = "discouraged"; // prepare form post data var data = new FormData(); @@ -25,7 +25,7 @@ async function handleRegisterSubmit(event) { data.append('attType', attestation_type); data.append('authType', authenticator_attachment); data.append('userVerification', user_verification); - data.append('requireResidentKey', require_resident_key); + data.append('residentKey', residentKey); // send to server for registering let makeCredentialOptions; diff --git a/Demo/wwwroot/js/usernameless.register.js b/Demo/wwwroot/js/usernameless.register.js index 25efc4c3..6fe8214a 100644 --- a/Demo/wwwroot/js/usernameless.register.js +++ b/Demo/wwwroot/js/usernameless.register.js @@ -14,9 +14,9 @@ async function handleRegisterSubmit(event) { // possible values: preferred, required, discouraged let user_verification = "discouraged"; - // possible values: true,false + // possible values: discouraged, preferred, required // NOTE: For usernameless scenarios, resident key must be set to true. - let require_resident_key = "true"; + let residentKey = "required"; // prepare form post data var data = new FormData(); @@ -25,7 +25,7 @@ async function handleRegisterSubmit(event) { data.append('attType', attestation_type); data.append('authType', authenticator_attachment); data.append('userVerification', user_verification); - data.append('requireResidentKey', require_resident_key); + data.append('residentKey', residentKey); // send to server for registering let makeCredentialOptions; diff --git a/Src/Fido2.Models/CredentialCreateOptions.cs b/Src/Fido2.Models/CredentialCreateOptions.cs index dc7c35bc..7f240cda 100644 --- a/Src/Fido2.Models/CredentialCreateOptions.cs +++ b/Src/Fido2.Models/CredentialCreateOptions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; @@ -182,6 +183,7 @@ public PublicKeyCredentialRpEntity(string id, string name, string? icon = null) /// /// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements regarding authenticator attributes. + /// https://w3c.github.io/webauthn/#dictionary-authenticatorSelection /// public class AuthenticatorSelection { @@ -192,11 +194,41 @@ public class AuthenticatorSelection [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AuthenticatorAttachment? AuthenticatorAttachment { get; set; } + private ResidentKeyRequirement _residentKey; + /// + /// Specifies the extent to which the Relying Party desires to create a client-side discoverable credential. For historical reasons the naming retains the deprecated “resident” terminology. The value SHOULD be a member of ResidentKeyRequirement but client platforms MUST ignore unknown values, treating an unknown value as if the member does not exist. If no value is given then the effective value is required if requireResidentKey is true or discouraged if it is false or absent. + /// + [JsonPropertyName("residentKey")] + public ResidentKeyRequirement ResidentKey + { + get => _residentKey; + set + { + _residentKey = value; + _requireResidentKey = value switch + { + ResidentKeyRequirement.Required => true, + ResidentKeyRequirement.Preferred or ResidentKeyRequirement.Discouraged => false, + _ => throw new NotImplementedException(), + }; + } + } + + private bool _requireResidentKey; /// /// This member describes the Relying Parties' requirements regarding resident credentials. If the parameter is set to true, the authenticator MUST create a client-side-resident public key credential source when creating a public key credential. /// + [Obsolete("Use property ResidentKey.")] [JsonPropertyName("requireResidentKey")] - public bool RequireResidentKey { get; set; } + public bool RequireResidentKey + { + get => _requireResidentKey; + set + { + _requireResidentKey = value; + _residentKey = value ? ResidentKeyRequirement.Required : ResidentKeyRequirement.Discouraged; + } + } /// /// This member describes the Relying Party's requirements regarding user verification for the create() operation. Eligible authenticators are filtered to only those capable of satisfying this requirement. @@ -207,7 +239,7 @@ public class AuthenticatorSelection public static AuthenticatorSelection Default => new AuthenticatorSelection { AuthenticatorAttachment = null, - RequireResidentKey = false, + ResidentKey = ResidentKeyRequirement.Discouraged, UserVerification = UserVerificationRequirement.Preferred }; } diff --git a/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs b/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs new file mode 100644 index 00000000..9ddb8d39 --- /dev/null +++ b/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Fido2NetLib.Objects +{ + /// + /// This enumeration’s values describe the Relying Party's requirements for client-side discoverable credentials (formerly known as resident credentials or resident keys). + /// https://w3c.github.io/webauthn/#enum-residentKeyRequirement + /// + [JsonConverter(typeof(FidoEnumConverter))] + public enum ResidentKeyRequirement + { + /// + /// The Relying Party requires a client-side discoverable credential. The client MUST return an error if a client-side discoverable credential cannot be created. + /// + [EnumMember(Value = "required")] + Required, + + /// + /// The Relying Party strongly prefers creating a client-side discoverable credential, but will accept a server-side credential. The client and authenticator SHOULD create a discoverable credential if possible. For example, the client SHOULD guide the user through setting up user verification if needed to create a discoverable credential. This takes precedence over the setting of userVerification. + /// + [EnumMember(Value = "preferred")] + Preferred, + + /// + /// The Relying Party prefers creating a server-side credential, but will accept a client-side discoverable credential. The client and authenticator SHOULD create a server-side credential if possible. + /// + [EnumMember(Value = "discouraged")] + Discouraged + } +} diff --git a/Test/Attestation/Apple.cs b/Test/Attestation/Apple.cs index 1ed3bc96..94848fae 100644 --- a/Test/Attestation/Apple.cs +++ b/Test/Attestation/Apple.cs @@ -239,7 +239,7 @@ public async Task TestApplePublicKeyMismatch() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = _challenge, diff --git a/Test/AuthenticatorResponse.cs b/Test/AuthenticatorResponse.cs index 4e537ff4..989d2433 100644 --- a/Test/AuthenticatorResponse.cs +++ b/Test/AuthenticatorResponse.cs @@ -80,7 +80,7 @@ public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin) AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -184,7 +184,7 @@ public void TestAuthenticatorOriginsFail(string origin, string expectedOrigin) AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -383,7 +383,7 @@ public void TestAuthenticatorAttestationResponseInvalidType() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -454,7 +454,7 @@ public void TestAuthenticatorAttestationResponseInvalidRawId(byte[] value) AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -523,7 +523,7 @@ public void TestAuthenticatorAttestationResponseInvalidRawType() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -600,7 +600,7 @@ public void TestAuthenticatorAttestationResponseRpidMismatch() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -679,7 +679,7 @@ public async Task TestAuthenticatorAttestationResponseNotUserPresent() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -758,7 +758,7 @@ public void TestAuthenticatorAttestationResponseNoAttestedCredentialData() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -836,7 +836,7 @@ public void TestAuthenticatorAttestationResponseUnknownAttestationType() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -914,7 +914,7 @@ public void TestAuthenticatorAttestationResponseNotUniqueCredId() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = challenge, @@ -991,7 +991,7 @@ public void TestAuthenticatorAttestationResponseUVRequired() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Required, }, Challenge = challenge, diff --git a/Test/Fido2Tests.cs b/Test/Fido2Tests.cs index aaeb95f2..d03c2d04 100644 --- a/Test/Fido2Tests.cs +++ b/Test/Fido2Tests.cs @@ -203,7 +203,7 @@ public Attestation() AuthenticatorSelection = new AuthenticatorSelection { AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, - RequireResidentKey = true, + ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Discouraged, }, Challenge = _challenge,